fix: harden Excel export (StreamableFile + clearer errors)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { Controller, Get, Res, UseGuards } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { FullAccessGuard } from '../auth/full-access.guard';
|
||||
import { ExportService } from './export.service';
|
||||
@@ -7,19 +13,26 @@ import { ExportService } from './export.service';
|
||||
@Controller('export')
|
||||
@UseGuards(JwtAuthGuard, FullAccessGuard)
|
||||
export class ExportController {
|
||||
private readonly logger = new Logger(ExportController.name);
|
||||
|
||||
constructor(private readonly exportService: ExportService) {}
|
||||
|
||||
@Get('projects.xlsx')
|
||||
async projectsXlsx(@Res() res: Response) {
|
||||
@Get(['projects', 'projects.xlsx'])
|
||||
async projectsXlsx(): Promise<StreamableFile> {
|
||||
try {
|
||||
const { buffer, filename } = await this.exportService.buildWorkbook();
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
return new StreamableFile(buffer, {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
disposition: `attachment; filename="${filename}"`,
|
||||
length: buffer.length,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.logger.error(`Excel export failed: ${message}`, stack);
|
||||
throw new InternalServerErrorException(
|
||||
`خروجی اکسل ناموفق بود: ${message}`,
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${filename}"; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
||||
);
|
||||
res.send(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Workbook, type Worksheet } from 'exceljs';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
formatIranDate,
|
||||
formatIranDateTime,
|
||||
iranDaysUntil,
|
||||
iranYmd,
|
||||
} from '../common/iran-time.util';
|
||||
|
||||
const PRODUCT_LABEL: Record<string, string> = {
|
||||
@@ -15,9 +16,12 @@ const PRODUCT_LABEL: Record<string, string> = {
|
||||
|
||||
@Injectable()
|
||||
export class ExportService {
|
||||
private readonly logger = new Logger(ExportService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async buildWorkbook(): Promise<{ buffer: Buffer; filename: string }> {
|
||||
this.logger.log('Building projects Excel export…');
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||
include: {
|
||||
@@ -28,7 +32,7 @@ export class ExportService {
|
||||
},
|
||||
});
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const workbook = new Workbook();
|
||||
workbook.creator = 'SuperAdmin Abshode';
|
||||
workbook.created = new Date();
|
||||
|
||||
@@ -39,15 +43,18 @@ export class ExportService {
|
||||
this.addSmsSheet(workbook, tenants);
|
||||
|
||||
const arrayBuffer = await workbook.xlsx.writeBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const stamp = formatIranDate(new Date()).replace(/\//g, '-');
|
||||
const buffer = Buffer.from(arrayBuffer as ArrayBuffer);
|
||||
const stamp = iranYmd(new Date());
|
||||
this.logger.log(
|
||||
`Excel export ready: ${tenants.length} tenants, ${buffer.length} bytes`,
|
||||
);
|
||||
return {
|
||||
buffer,
|
||||
filename: `superadmin-export-${stamp}.xlsx`,
|
||||
};
|
||||
}
|
||||
|
||||
private addProjectsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
private addProjectsSheet(workbook: Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
const sheet = workbook.addWorksheet('پروژهها', {
|
||||
views: [{ rightToLeft: true }],
|
||||
});
|
||||
@@ -115,7 +122,7 @@ export class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private addServersSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
private addServersSheet(workbook: Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
const sheet = workbook.addWorksheet('سرورها', {
|
||||
views: [{ rightToLeft: true }],
|
||||
});
|
||||
@@ -157,7 +164,7 @@ export class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private addPluginsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
private addPluginsSheet(workbook: Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
const sheet = workbook.addWorksheet('افزونهها', {
|
||||
views: [{ rightToLeft: true }],
|
||||
});
|
||||
@@ -203,7 +210,7 @@ export class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private addSupportsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
private addSupportsSheet(workbook: Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
const sheet = workbook.addWorksheet('پشتیبانی', {
|
||||
views: [{ rightToLeft: true }],
|
||||
});
|
||||
@@ -247,7 +254,7 @@ export class ExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private addSmsSheet(workbook: ExcelJS.Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
private addSmsSheet(workbook: Workbook, tenants: Awaited<ReturnType<ExportService['load']>>) {
|
||||
const sheet = workbook.addWorksheet('پیامک', {
|
||||
views: [{ rightToLeft: true }],
|
||||
});
|
||||
@@ -310,7 +317,7 @@ export class ExportService {
|
||||
});
|
||||
}
|
||||
|
||||
private styleHeader(sheet: ExcelJS.Worksheet) {
|
||||
private styleHeader(sheet: Worksheet) {
|
||||
const row = sheet.getRow(1);
|
||||
row.font = { bold: true };
|
||||
row.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||
|
||||
Reference in New Issue
Block a user