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 {
|
||||||
import type { Response } from 'express';
|
Controller,
|
||||||
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Logger,
|
||||||
|
StreamableFile,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { FullAccessGuard } from '../auth/full-access.guard';
|
import { FullAccessGuard } from '../auth/full-access.guard';
|
||||||
import { ExportService } from './export.service';
|
import { ExportService } from './export.service';
|
||||||
@@ -7,19 +13,26 @@ import { ExportService } from './export.service';
|
|||||||
@Controller('export')
|
@Controller('export')
|
||||||
@UseGuards(JwtAuthGuard, FullAccessGuard)
|
@UseGuards(JwtAuthGuard, FullAccessGuard)
|
||||||
export class ExportController {
|
export class ExportController {
|
||||||
|
private readonly logger = new Logger(ExportController.name);
|
||||||
|
|
||||||
constructor(private readonly exportService: ExportService) {}
|
constructor(private readonly exportService: ExportService) {}
|
||||||
|
|
||||||
@Get('projects.xlsx')
|
@Get(['projects', 'projects.xlsx'])
|
||||||
async projectsXlsx(@Res() res: Response) {
|
async projectsXlsx(): Promise<StreamableFile> {
|
||||||
|
try {
|
||||||
const { buffer, filename } = await this.exportService.buildWorkbook();
|
const { buffer, filename } = await this.exportService.buildWorkbook();
|
||||||
res.setHeader(
|
return new StreamableFile(buffer, {
|
||||||
'Content-Type',
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
'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 { Injectable, Logger } from '@nestjs/common';
|
||||||
import * as ExcelJS from 'exceljs';
|
import { Workbook, type Worksheet } from 'exceljs';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
formatIranDate,
|
formatIranDate,
|
||||||
formatIranDateTime,
|
formatIranDateTime,
|
||||||
iranDaysUntil,
|
iranDaysUntil,
|
||||||
|
iranYmd,
|
||||||
} from '../common/iran-time.util';
|
} from '../common/iran-time.util';
|
||||||
|
|
||||||
const PRODUCT_LABEL: Record<string, string> = {
|
const PRODUCT_LABEL: Record<string, string> = {
|
||||||
@@ -15,9 +16,12 @@ const PRODUCT_LABEL: Record<string, string> = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportService {
|
export class ExportService {
|
||||||
|
private readonly logger = new Logger(ExportService.name);
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async buildWorkbook(): Promise<{ buffer: Buffer; filename: string }> {
|
async buildWorkbook(): Promise<{ buffer: Buffer; filename: string }> {
|
||||||
|
this.logger.log('Building projects Excel export…');
|
||||||
const tenants = await this.prisma.tenant.findMany({
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||||
include: {
|
include: {
|
||||||
@@ -28,7 +32,7 @@ export class ExportService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new Workbook();
|
||||||
workbook.creator = 'SuperAdmin Abshode';
|
workbook.creator = 'SuperAdmin Abshode';
|
||||||
workbook.created = new Date();
|
workbook.created = new Date();
|
||||||
|
|
||||||
@@ -39,15 +43,18 @@ export class ExportService {
|
|||||||
this.addSmsSheet(workbook, tenants);
|
this.addSmsSheet(workbook, tenants);
|
||||||
|
|
||||||
const arrayBuffer = await workbook.xlsx.writeBuffer();
|
const arrayBuffer = await workbook.xlsx.writeBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer as ArrayBuffer);
|
||||||
const stamp = formatIranDate(new Date()).replace(/\//g, '-');
|
const stamp = iranYmd(new Date());
|
||||||
|
this.logger.log(
|
||||||
|
`Excel export ready: ${tenants.length} tenants, ${buffer.length} bytes`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
buffer,
|
buffer,
|
||||||
filename: `superadmin-export-${stamp}.xlsx`,
|
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('پروژهها', {
|
const sheet = workbook.addWorksheet('پروژهها', {
|
||||||
views: [{ rightToLeft: true }],
|
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('سرورها', {
|
const sheet = workbook.addWorksheet('سرورها', {
|
||||||
views: [{ rightToLeft: true }],
|
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('افزونهها', {
|
const sheet = workbook.addWorksheet('افزونهها', {
|
||||||
views: [{ rightToLeft: true }],
|
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('پشتیبانی', {
|
const sheet = workbook.addWorksheet('پشتیبانی', {
|
||||||
views: [{ rightToLeft: true }],
|
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('پیامک', {
|
const sheet = workbook.addWorksheet('پیامک', {
|
||||||
views: [{ rightToLeft: true }],
|
views: [{ rightToLeft: true }],
|
||||||
});
|
});
|
||||||
@@ -310,7 +317,7 @@ export class ExportService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private styleHeader(sheet: ExcelJS.Worksheet) {
|
private styleHeader(sheet: Worksheet) {
|
||||||
const row = sheet.getRow(1);
|
const row = sheet.getRow(1);
|
||||||
row.font = { bold: true };
|
row.font = { bold: true };
|
||||||
row.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
row.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user