From 926da0cb35a8f440e3d7089e10d86e9a3209cd3b Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 10 Jan 2025 06:02:42 +0700 Subject: [PATCH 01/12] feat(excel generator): init excel generator plugin --- src/api-docs/openAPIDocumentGenerator.ts | 2 + .../excelGenerator/excelGeneratorModel.ts | 17 +++ .../excelGenerator/excelGeneratorRouter.ts | 121 ++++++++++++++++++ src/server.ts | 2 + 4 files changed, 142 insertions(+) create mode 100644 src/routes/excelGenerator/excelGeneratorModel.ts create mode 100644 src/routes/excelGenerator/excelGeneratorRouter.ts diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index 5e7da6c..40a6788 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -1,5 +1,6 @@ import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { excelGeneratorRegistry } from '@/routes/excelGenerator/excelGeneratorRouter'; import { healthCheckRegistry } from '@/routes/healthCheck/healthCheckRouter'; import { powerpointGeneratorRegistry } from '@/routes/powerpointGenerator/powerpointGeneratorRouter'; import { articleReaderRegistry } from '@/routes/webPageReader/webPageReaderRouter'; @@ -13,6 +14,7 @@ export function generateOpenAPIDocument() { articleReaderRegistry, powerpointGeneratorRegistry, wordGeneratorRegistry, + excelGeneratorRegistry, ]); const generator = new OpenApiGeneratorV3(registry.definitions); diff --git a/src/routes/excelGenerator/excelGeneratorModel.ts b/src/routes/excelGenerator/excelGeneratorModel.ts new file mode 100644 index 0000000..642a1e9 --- /dev/null +++ b/src/routes/excelGenerator/excelGeneratorModel.ts @@ -0,0 +1,17 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +// Define Word Generator Response Schema +export type ExcelGeneratorResponse = z.infer; +export const ExcelGeneratorResponseSchema = z.object({ + downloadUrl: z.string().openapi({ + description: 'The file path where the generated Word document is saved.', + }), +}); + +// Request Body Schema +export const ExcelGeneratorRequestBodySchema = z.object({}); + +export type ExcelGeneratorRequestBody = z.infer; diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts new file mode 100644 index 0000000..d5e90e9 --- /dev/null +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -0,0 +1,121 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import express, { Request, Response, Router } from 'express'; +import fs from 'fs'; +import { StatusCodes } from 'http-status-codes'; +import cron from 'node-cron'; +import path from 'path'; + +import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import { handleServiceResponse } from '@/common/utils/httpHandlers'; + +import { ExcelGeneratorRequestBodySchema, ExcelGeneratorResponseSchema } from './excelGeneratorModel'; +export const COMPRESS = true; +export const excelGeneratorRegistry = new OpenAPIRegistry(); +excelGeneratorRegistry.register('ExcelGenerator', ExcelGeneratorResponseSchema); +excelGeneratorRegistry.registerPath({ + method: 'post', + path: '/excel-generator/generate', + tags: ['Excel Generator'], + request: { + body: createApiRequestBody(ExcelGeneratorRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(ExcelGeneratorResponseSchema, 'Success'), +}); + +// Create folder to contains generated files +const exportsDir = path.join(__dirname, '../../..', 'excel-exports'); + +// Ensure the exports directory exists +if (!fs.existsSync(exportsDir)) { + fs.mkdirSync(exportsDir, { recursive: true }); +} + +// Cron job to delete files older than 1 hour +cron.schedule('0 * * * *', () => { + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + // Read the files in the exports directory + fs.readdir(exportsDir, (err, files) => { + if (err) { + console.error(`Error reading directory ${exportsDir}:`, err); + return; + } + + files.forEach((file) => { + const filePath = path.join(exportsDir, file); + fs.stat(filePath, (err, stats) => { + if (err) { + console.error(`Error getting stats for file ${filePath}:`, err); + return; + } + + // Check if the file is older than 1 hour + if (now - stats.mtime.getTime() > oneHour) { + fs.unlink(filePath, (err) => { + if (err) { + console.error(`Error deleting file ${filePath}:`, err); + } else { + console.log(`Deleted file: ${filePath}`); + } + }); + } + }); + }); + }); +}); + +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; + +async function execGenExcelFuncs() { + // TOOD: Implement code logic here + const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.docx`; + return fileName; +} + +export const excelGeneratorRouter: Router = (() => { + const router = express.Router(); + // Static route for downloading files + router.use('/downloads', express.static(exportsDir)); + + router.post('/generate', async (_req: Request, res: Response) => { + const { sheets = [] } = _req.body; // TODO: extract excel config object from request body + if (!sheets.length) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Sheets data is required!', + 'Please make sure you have sent the excel sheets content generated from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const fileName = await execGenExcelFuncs(); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'File generated successfully', + { + downloadUrl: `${serverUrl}/excel-generator/downloads/${fileName}`, + }, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't generate excel file.`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; +})(); diff --git a/src/server.ts b/src/server.ts index a569651..0da49de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,6 +10,7 @@ import rateLimiter from '@/common/middleware/rateLimiter'; import requestLogger from '@/common/middleware/requestLogger'; import { healthCheckRouter } from '@/routes/healthCheck/healthCheckRouter'; +import { excelGeneratorRouter } from './routes/excelGenerator/excelGeneratorRouter'; import { powerpointGeneratorRouter } from './routes/powerpointGenerator/powerpointGeneratorRouter'; import { webPageReaderRouter } from './routes/webPageReader/webPageReaderRouter'; import { wordGeneratorRouter } from './routes/wordGenerator/wordGeneratorRouter'; @@ -40,6 +41,7 @@ app.use('/youtube-transcript', youtubeTranscriptRouter); app.use('/web-page-reader', webPageReaderRouter); app.use('/powerpoint-generator', powerpointGeneratorRouter); app.use('/word-generator', wordGeneratorRouter); +app.use('/excel-generator', excelGeneratorRouter); // Swagger UI app.use(openAPIRouter); From c720ba4f31950b7b6ad1f382a333c246842cf088 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Tue, 14 Jan 2025 07:51:06 +0700 Subject: [PATCH 02/12] feat(excel generator): add mapping function --- package-lock.json | 104 ++++++++++++++++++ package.json | 1 + .../excelGenerator/excelGeneratorRouter.ts | 83 +++++++++++++- 3 files changed, 182 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73af284..310b4f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", + "xlsx": "^0.18.5", "youtube-transcript": "^1.1.0", "zod": "^3.22.4" }, @@ -2295,6 +2296,15 @@ "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", "dev": true }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2783,6 +2793,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -3049,6 +3072,15 @@ "node": ">=0.8" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3548,6 +3580,18 @@ "typescript": ">=4" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5055,6 +5099,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9444,6 +9497,18 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -11682,6 +11747,24 @@ "node": ">=6" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11841,6 +11924,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/package.json b/package.json index 9cde457..2d6ee2f 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", + "xlsx": "^0.18.5", "youtube-transcript": "^1.1.0", "zod": "^3.22.4" }, diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index d5e90e9..c7b4d34 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -4,6 +4,7 @@ import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; +import XLSX from 'xlsx'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; @@ -68,9 +69,79 @@ cron.schedule('0 * * * *', () => { const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; -async function execGenExcelFuncs() { - // TOOD: Implement code logic here - const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.docx`; +interface Table { + startCell: string; + rows: string[][]; + columns?: string[]; + sorting?: { column: string; order: 'asc' | 'desc' }; + formulas?: { column: string; formula: string }[]; + filtering?: { column: string; criteria: string }[]; + skipHeader?: boolean; +} + +interface SheetData { + sheetName: string; + tables: Table[]; +} + +export function execGenExcelFuncs(sheetsData: SheetData[]): string { + const workbook = XLSX.utils.book_new(); + + sheetsData.forEach(({ sheetName, tables }) => { + const worksheet = XLSX.utils.aoa_to_sheet([]); + + tables.forEach(({ startCell, rows, columns, skipHeader, sorting, formulas, filtering }) => { + const decodedCell = XLSX.utils.decode_cell(startCell); + const startRow = decodedCell.r; // Row index (0-based) + const startCol = decodedCell.c; // Column index (0-based) + + let rowIndex = 0; // Reset rowIndex for each table + + // Add column headers if not skipped + if (!skipHeader && columns) { + XLSX.utils.sheet_add_aoa(worksheet, [columns], { origin: { c: startCol, r: startRow + rowIndex } }); + rowIndex++; // Increment row index after adding headers + } + + // Add rows + XLSX.utils.sheet_add_aoa(worksheet, rows, { origin: { c: startCol, r: startRow + rowIndex } }); + rowIndex += rows.length; // Increment row index by the number of rows added + + // Apply sorting + if (sorting) { + const columnIndex = sorting.column.charCodeAt(0) - 65; // Convert 'A' to 0, 'B' to 1, etc. + rows.sort((a, b) => + sorting.order === 'asc' + ? a[columnIndex].localeCompare(b[columnIndex]) + : b[columnIndex].localeCompare(a[columnIndex]) + ); + } + + // Apply formulas + if (formulas) { + formulas.forEach(({ column, formula }) => { + const colIndex = XLSX.utils.decode_col(column); + rows.forEach((_, rowIdx) => { + const cellRef = XLSX.utils.encode_cell({ c: colIndex, r: startRow + rowIdx + (skipHeader ? 0 : 1) }); // Adjust row for header + worksheet[cellRef] = { t: 'n', f: formula }; + }); + }); + } + + // Apply filtering (not natively supported in XLSX; requires client-side configuration) + if (filtering) { + console.warn('Filtering is not implemented in this version.'); + } + }); + + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; + const filePath = path.join(exportsDir, fileName); + + XLSX.writeFile(workbook, filePath); + return fileName; } @@ -80,8 +151,8 @@ export const excelGeneratorRouter: Router = (() => { router.use('/downloads', express.static(exportsDir)); router.post('/generate', async (_req: Request, res: Response) => { - const { sheets = [] } = _req.body; // TODO: extract excel config object from request body - if (!sheets.length) { + const { sheetsData } = _req.body; // TODO: extract excel config object from request body + if (!sheetsData.length) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, '[Validation Error] Sheets data is required!', @@ -92,7 +163,7 @@ export const excelGeneratorRouter: Router = (() => { } try { - const fileName = await execGenExcelFuncs(); + const fileName = execGenExcelFuncs(sheetsData); const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'File generated successfully', From 99bc6a70019206cb4b46f117c486dcdce5c6f3f1 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 15 Jan 2025 00:46:50 +0700 Subject: [PATCH 03/12] feat(excel generator): use exceljs, handle multiple tables with number, currency, percent formats --- package-lock.json | 791 +++++++++++++++--- package.json | 2 +- .../excelGenerator/excelGeneratorRouter.ts | 200 ++++- 3 files changed, 837 insertions(+), 156 deletions(-) diff --git a/package-lock.json b/package-lock.json index 310b4f7..d77d13d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "docx": "^9.1.0", "dotenv": "^16.4.5", "envalid": "^8.0.0", + "exceljs": "^4.4.0", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "fs": "^0.0.1-security", @@ -32,7 +33,6 @@ "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", - "xlsx": "^0.18.5", "youtube-transcript": "^1.1.0", "zod": "^3.22.4" }, @@ -883,6 +883,43 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2296,15 +2333,6 @@ "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", "dev": true }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2409,6 +2437,110 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2462,6 +2594,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -2497,8 +2634,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2534,17 +2670,41 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2624,7 +2784,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2645,7 +2804,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -2665,12 +2823,36 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2793,19 +2975,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -2824,6 +2993,17 @@ "node": ">=4" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -3072,15 +3252,6 @@ "node": ">=0.8" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3144,11 +3315,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "2.0.0", @@ -3592,6 +3776,18 @@ "node": ">=0.8" } }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3685,6 +3881,11 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -4042,6 +4243,41 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4094,7 +4330,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -4668,6 +4903,44 @@ "node": ">=0.8.x" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/exceljs/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -4807,6 +5080,18 @@ "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", "dev": true }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5099,15 +5384,6 @@ "node": ">= 0.6" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -5121,11 +5397,15 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -5141,6 +5421,73 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5494,8 +5841,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -5842,7 +6188,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -6689,6 +7034,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6772,6 +7155,11 @@ } } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "node_modules/listr2": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", @@ -6847,17 +7235,55 @@ "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "node_modules/lodash.isstring": { "version": "4.0.1", @@ -6865,6 +7291,11 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -6901,11 +7332,15 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, "node_modules/lodash.uniqby": { "version": "4.7.0", @@ -7250,7 +7685,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7264,6 +7698,17 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mlly": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", @@ -7400,6 +7845,14 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -7496,7 +7949,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -7845,7 +8297,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8643,7 +9094,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8653,6 +9103,25 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -9497,18 +9966,6 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -9864,6 +10321,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -10100,6 +10572,14 @@ "node": ">=18" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -10813,6 +11293,50 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-notifier": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", @@ -11747,24 +12271,6 @@ "node": ">=6" } }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11889,8 +12395,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.18.0", @@ -11924,27 +12429,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, - "bin": { - "xlsx": "bin/xlsx.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -12082,6 +12566,79 @@ "node": ">=18.0.0" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index 2d6ee2f..42db61f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "docx": "^9.1.0", "dotenv": "^16.4.5", "envalid": "^8.0.0", + "exceljs": "^4.4.0", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "fs": "^0.0.1-security", @@ -41,7 +42,6 @@ "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", - "xlsx": "^0.18.5", "youtube-transcript": "^1.1.0", "zod": "^3.22.4" }, diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index c7b4d34..5245330 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -1,10 +1,10 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import * as ExcelJS from 'exceljs'; import express, { Request, Response, Router } from 'express'; import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; -import XLSX from 'xlsx'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; @@ -69,43 +69,169 @@ cron.schedule('0 * * * *', () => { const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; -interface Table { - startCell: string; - rows: string[][]; - columns?: string[]; - sorting?: { column: string; order: 'asc' | 'desc' }; - formulas?: { column: string; formula: string }[]; - filtering?: { column: string; criteria: string }[]; - skipHeader?: boolean; -} - interface SheetData { sheetName: string; - tables: Table[]; + tables: { + title: string; + startCell: string; + rows: any[][]; + columns: { name: string; type: string; format: string }[]; // types that have format, number, percent, currency + skipHeader?: boolean; + sorting?: { column: string; order: 'asc' | 'desc' }; + filtering?: boolean; + }[]; +} + +// Helper function to convert column letter (e.g., 'A') to column index (e.g., 1) +function columnLetterToNumber(letter: string): number { + let column = 0; + for (let i = 0; i < letter.length; i++) { + column = column * 26 + letter.charCodeAt(i) - 'A'.charCodeAt(0) + 1; + } + return column; +} + +// Helper function to auto-fit column widths based on content +function autoFitColumns( + worksheet: ExcelJS.Worksheet, + startRow: number, + rows: any[], + numColumns: number, + startCol: number +): void { + for (let colIdx = 0; colIdx < numColumns; colIdx++) { + let maxLength = 0; + + // Check the max length of the content in the column + rows.forEach((row) => { + const cellValue = row[colIdx]; + if (cellValue != null) { + const cellLength = String(cellValue).length; + maxLength = Math.max(maxLength, cellLength); + } + }); + + // Account for the header row + const headerCell = worksheet.getCell(startRow, startCol + colIdx).value; + if (headerCell != null) { + const headerLength = String(headerCell).length; + maxLength = Math.max(maxLength, headerLength); + } + + // Set the column width + worksheet.getColumn(startCol + colIdx).width = maxLength + 2; // Adding some padding + } } export function execGenExcelFuncs(sheetsData: SheetData[]): string { - const workbook = XLSX.utils.book_new(); + const workbook = new ExcelJS.Workbook(); sheetsData.forEach(({ sheetName, tables }) => { - const worksheet = XLSX.utils.aoa_to_sheet([]); + const worksheet = workbook.addWorksheet(sheetName); - tables.forEach(({ startCell, rows, columns, skipHeader, sorting, formulas, filtering }) => { - const decodedCell = XLSX.utils.decode_cell(startCell); - const startRow = decodedCell.r; // Row index (0-based) - const startCol = decodedCell.c; // Column index (0-based) + tables.forEach(({ startCell, title, rows, columns, skipHeader, sorting, filtering }) => { + const startCol = columnLetterToNumber(startCell[0]); // Convert column letter to index (e.g., 'A' -> 1) + const startRow = parseInt(startCell.slice(1)); // Extract the row number (e.g., 'A1' -> 1) + let rowIndex = startRow; // Set the initial row index to startRow for each table - let rowIndex = 0; // Reset rowIndex for each table + // Add table name row + if (title) { + worksheet.getCell(rowIndex, startCol).value = title; + worksheet.mergeCells(rowIndex, startCol, rowIndex, startCol + columns.length - 1); + worksheet.getCell(rowIndex, startCol).alignment = { horizontal: 'center', vertical: 'middle' }; + rowIndex++; // Move to the next row + } // Add column headers if not skipped if (!skipHeader && columns) { - XLSX.utils.sheet_add_aoa(worksheet, [columns], { origin: { c: startCol, r: startRow + rowIndex } }); + columns.forEach((col, colIdx) => { + worksheet.getCell(rowIndex, startCol + colIdx).value = col.name; + }); rowIndex++; // Increment row index after adding headers } - // Add rows - XLSX.utils.sheet_add_aoa(worksheet, rows, { origin: { c: startCol, r: startRow + rowIndex } }); - rowIndex += rows.length; // Increment row index by the number of rows added + // Map headers to types + const columnTypes = columns?.map((col: any) => col.type) || []; + const columnFormats = + columns?.map((col: any) => { + let format = undefined; + switch (col.type) { + case 'number': + format = col.format || undefined; + break; + case 'percent': + format = col.format || '0.00%'; // Default to percentage format + break; + case 'currency': + format = col.format || '$#,##0'; // Default to currency format + break; + case 'date': + format = col.format || undefined; + break; + } + return format; + }) || []; + + // Add rows with data types + rows.forEach((row) => { + row.forEach((value, colIdx) => { + const cellType = columnTypes[colIdx]; + const format = columnFormats[colIdx]; + let cellValue: any = value != null ? value : ''; // Handle empty/null values + + // Check if the value is a formula + if (typeof value === 'object' && value.f) { + const formulaCell: any = { formula: value.f }; // Handle formula + if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { + formulaCell.style = { numFmt: format }; // Apply number format + } + worksheet.getCell(rowIndex, startCol + colIdx).value = formulaCell; + } else if (value != null) { + // Assign cell type based on the header definition + switch (cellType) { + case 'number': { + cellValue = !isNaN(Number(value)) ? Math.round(Number(value)) : value; + worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || '0'; + break; + } + case 'boolean': { + cellValue = Boolean(value); + worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + break; + } + case 'date': { + const parsedDate = new Date(value); + cellValue = !isNaN(parsedDate.getTime()) ? parsedDate : value; + worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || 'yyyy-mm-dd'; + break; + } + case 'percent': { + cellValue = !isNaN(Number(value)) ? Number(value) : value; + worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || '0.00%'; + break; + } + case 'currency': { + cellValue = !isNaN(Number(value)) ? Number(value) : value; + worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || '$#,##0'; + break; + } + case 'string': + default: { + cellValue = String(value); + worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + break; + } + } + } else { + worksheet.getCell(rowIndex, startCol + colIdx).value = ''; // Handle empty value + } + }); + rowIndex++; // Move to the next row + }); // Apply sorting if (sorting) { @@ -117,30 +243,28 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { ); } - // Apply formulas - if (formulas) { - formulas.forEach(({ column, formula }) => { - const colIndex = XLSX.utils.decode_col(column); - rows.forEach((_, rowIdx) => { - const cellRef = XLSX.utils.encode_cell({ c: colIndex, r: startRow + rowIdx + (skipHeader ? 0 : 1) }); // Adjust row for header - worksheet[cellRef] = { t: 'n', f: formula }; - }); - }); - } - - // Apply filtering (not natively supported in XLSX; requires client-side configuration) + // Apply filtering (not natively supported in ExcelJS; requires client-side configuration) if (filtering) { console.warn('Filtering is not implemented in this version.'); } - }); - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + // Auto-fit column widths + autoFitColumns(worksheet, startRow, rows, columns.length, startCol); + }); }); + // Write the workbook to a file const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; const filePath = path.join(exportsDir, fileName); - XLSX.writeFile(workbook, filePath); + workbook.xlsx + .writeFile(filePath) + .then(() => { + console.log('File has been written to', filePath); + }) + .catch((err) => { + console.error('Error writing Excel file', err); + }); return fileName; } From 6e7638b83033791876b4a2e921b617a030ef38ed Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 15 Jan 2025 09:37:05 +0700 Subject: [PATCH 04/12] feat(excel generator): support adding styles as border, alignment, and font weight --- .../excelGenerator/excelGeneratorRouter.ts | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index 5245330..d9df641 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -136,16 +136,34 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { // Add table name row if (title) { - worksheet.getCell(rowIndex, startCol).value = title; + const startCell = worksheet.getCell(rowIndex, startCol); + startCell.value = title; worksheet.mergeCells(rowIndex, startCol, rowIndex, startCol + columns.length - 1); - worksheet.getCell(rowIndex, startCol).alignment = { horizontal: 'center', vertical: 'middle' }; + startCell.alignment = { horizontal: 'center', vertical: 'middle' }; + startCell.font = { bold: true }; + // Apply borders to the header cell + startCell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' }, + }; rowIndex++; // Move to the next row } // Add column headers if not skipped if (!skipHeader && columns) { columns.forEach((col, colIdx) => { - worksheet.getCell(rowIndex, startCol + colIdx).value = col.name; + const cell = worksheet.getCell(rowIndex, startCol + colIdx); + cell.value = col.name; + cell.font = { bold: true }; // Apply bold font + // Apply borders to the header cell + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' }, + }; }); rowIndex++; // Increment row index after adding headers } @@ -178,57 +196,65 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { const cellType = columnTypes[colIdx]; const format = columnFormats[colIdx]; let cellValue: any = value != null ? value : ''; // Handle empty/null values - + const cell = worksheet.getCell(rowIndex, startCol + colIdx); // Check if the value is a formula - if (typeof value === 'object' && value.f) { - const formulaCell: any = { formula: value.f }; // Handle formula + if (typeof value === 'object' && value.formula) { + const formulaCell: any = { formula: value.formula }; // Handle formula if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { - formulaCell.style = { numFmt: format }; // Apply number format + cell.numFmt = format; // Apply number format } - worksheet.getCell(rowIndex, startCol + colIdx).value = formulaCell; + cell.value = formulaCell; } else if (value != null) { // Assign cell type based on the header definition switch (cellType) { case 'number': { cellValue = !isNaN(Number(value)) ? Math.round(Number(value)) : value; - worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; - worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || '0'; + cell.value = cellValue; + cell.numFmt = format || '0'; break; } case 'boolean': { cellValue = Boolean(value); - worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + cell.value = cellValue; break; } case 'date': { const parsedDate = new Date(value); cellValue = !isNaN(parsedDate.getTime()) ? parsedDate : value; - worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; - worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || 'yyyy-mm-dd'; + cell.value = cellValue; + cell.numFmt = format || 'yyyy-mm-dd'; break; } case 'percent': { cellValue = !isNaN(Number(value)) ? Number(value) : value; - worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; - worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || '0.00%'; + cell.value = cellValue; + cell.numFmt = format || '0.00%'; break; } case 'currency': { cellValue = !isNaN(Number(value)) ? Number(value) : value; - worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; - worksheet.getCell(rowIndex, startCol + colIdx).numFmt = format || '$#,##0'; + cell.value = cellValue; + cell.numFmt = format || '$#,##0'; break; } case 'string': default: { cellValue = String(value); - worksheet.getCell(rowIndex, startCol + colIdx).value = cellValue; + cell.value = cellValue; break; } } } else { - worksheet.getCell(rowIndex, startCol + colIdx).value = ''; // Handle empty value + cell.value = ''; // Handle empty value } + + // Apply borders to the cell + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' }, + }; }); rowIndex++; // Move to the next row }); From 06fc09f1eaecc21a39b4b1c6887a4b985c3a639e Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 15 Jan 2025 11:00:41 +0700 Subject: [PATCH 05/12] feat(excel generator): refactor code cell value --- .../excelGenerator/excelGeneratorRouter.ts | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index d9df641..49c941f 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -156,6 +156,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { columns.forEach((col, colIdx) => { const cell = worksheet.getCell(rowIndex, startCol + colIdx); cell.value = col.name; + cell.alignment = { horizontal: 'center', vertical: 'middle' }; cell.font = { bold: true }; // Apply bold font // Apply borders to the header cell cell.border = { @@ -198,54 +199,52 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { let cellValue: any = value != null ? value : ''; // Handle empty/null values const cell = worksheet.getCell(rowIndex, startCol + colIdx); // Check if the value is a formula - if (typeof value === 'object' && value.formula) { - const formulaCell: any = { formula: value.formula }; // Handle formula + if (typeof cellValue === 'object' && cellValue.formula) { + const formulaCell: any = { formula: cellValue.formula }; // Handle formula if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { cell.numFmt = format; // Apply number format } cell.value = formulaCell; - } else if (value != null) { + } else { // Assign cell type based on the header definition switch (cellType) { case 'number': { - cellValue = !isNaN(Number(value)) ? Math.round(Number(value)) : value; + cellValue = !isNaN(Number(cellValue)) ? Math.round(Number(cellValue)) : cellValue; cell.value = cellValue; cell.numFmt = format || '0'; break; } case 'boolean': { - cellValue = Boolean(value); + cellValue = Boolean(cellValue); cell.value = cellValue; break; } case 'date': { - const parsedDate = new Date(value); - cellValue = !isNaN(parsedDate.getTime()) ? parsedDate : value; + const parsedDate = new Date(cellValue); + cellValue = !isNaN(parsedDate.getTime()) ? parsedDate : cellValue; cell.value = cellValue; cell.numFmt = format || 'yyyy-mm-dd'; break; } case 'percent': { - cellValue = !isNaN(Number(value)) ? Number(value) : value; + cellValue = !isNaN(Number(cellValue)) ? Number(cellValue) : cellValue; cell.value = cellValue; cell.numFmt = format || '0.00%'; break; } case 'currency': { - cellValue = !isNaN(Number(value)) ? Number(value) : value; + cellValue = !isNaN(Number(cellValue)) ? Number(cellValue) : cellValue; cell.value = cellValue; cell.numFmt = format || '$#,##0'; break; } case 'string': default: { - cellValue = String(value); + cellValue = String(cellValue); cell.value = cellValue; break; } } - } else { - cell.value = ''; // Handle empty value } // Apply borders to the cell @@ -261,12 +260,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { // Apply sorting if (sorting) { - const columnIndex = sorting.column.charCodeAt(0) - 65; // Convert 'A' to 0, 'B' to 1, etc. - rows.sort((a, b) => - sorting.order === 'asc' - ? a[columnIndex].localeCompare(b[columnIndex]) - : b[columnIndex].localeCompare(a[columnIndex]) - ); + console.warn('Sorting is not implemented in this version.'); } // Apply filtering (not natively supported in ExcelJS; requires client-side configuration) From 4155f1c329bfaf061bafdf9fae0903b791450db1 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Wed, 15 Jan 2025 14:49:21 +0700 Subject: [PATCH 06/12] feat(excel generator): add excel configs --- .../excelGenerator/excelGeneratorRouter.ts | 125 ++++++++++++------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index 49c941f..4bd2143 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -76,12 +76,30 @@ interface SheetData { startCell: string; rows: any[][]; columns: { name: string; type: string; format: string }[]; // types that have format, number, percent, currency - skipHeader?: boolean; - sorting?: { column: string; order: 'asc' | 'desc' }; - filtering?: boolean; + skipHeader: boolean; }[]; } +interface ExcelConfig { + fontFamily: string; + titleFontSize: number; + headerFontSize: number; + fontSize: number; + autoFilter: boolean; + borderStyle: ExcelJS.BorderStyle; // thin, double, dashed, thick + wrapText: boolean; +} + +const DEFAULT_EXCEL_CONFIGS: ExcelConfig = { + fontFamily: 'Calibri', + titleFontSize: 16, + headerFontSize: 11, + fontSize: 11, + autoFilter: false, + wrapText: false, + borderStyle: 'thin', +}; + // Helper function to convert column letter (e.g., 'A') to column index (e.g., 1) function columnLetterToNumber(letter: string): number { let column = 0; @@ -123,13 +141,45 @@ function autoFitColumns( } } -export function execGenExcelFuncs(sheetsData: SheetData[]): string { +export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelConfig): string { const workbook = new ExcelJS.Workbook(); + const borderConfigs = { + top: { style: excelConfigs.borderStyle }, + left: { style: excelConfigs.borderStyle }, + bottom: { style: excelConfigs.borderStyle }, + right: { style: excelConfigs.borderStyle }, + }; + const titleAlignmentConfigs: any = { + horizontal: 'center', + vertical: 'middle', + wrapText: excelConfigs.wrapText, + }; + const titleFontConfigs: any = { + name: excelConfigs.fontFamily, + bold: true, + size: excelConfigs.titleFontSize, + }; + const headerAligmentConfigs: any = { + wrapText: excelConfigs.wrapText, + horizontal: 'center', + vertical: 'middle', + }; + const headerFontConfigs: any = { + name: excelConfigs.fontFamily, + bold: true, + size: excelConfigs.headerFontSize, + }; + const cellAlignmentConfigs: any = { + wrapText: excelConfigs.wrapText, + }; + const cellFontConfigs: any = { + name: excelConfigs.fontFamily, + size: excelConfigs.fontSize, + }; sheetsData.forEach(({ sheetName, tables }) => { const worksheet = workbook.addWorksheet(sheetName); - - tables.forEach(({ startCell, title, rows, columns, skipHeader, sorting, filtering }) => { + tables.forEach(({ startCell, title, rows, columns, skipHeader }) => { const startCol = columnLetterToNumber(startCell[0]); // Convert column letter to index (e.g., 'A' -> 1) const startRow = parseInt(startCell.slice(1)); // Extract the row number (e.g., 'A1' -> 1) let rowIndex = startRow; // Set the initial row index to startRow for each table @@ -139,15 +189,9 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { const startCell = worksheet.getCell(rowIndex, startCol); startCell.value = title; worksheet.mergeCells(rowIndex, startCol, rowIndex, startCol + columns.length - 1); - startCell.alignment = { horizontal: 'center', vertical: 'middle' }; - startCell.font = { bold: true }; - // Apply borders to the header cell - startCell.border = { - top: { style: 'thin' }, - left: { style: 'thin' }, - bottom: { style: 'thin' }, - right: { style: 'thin' }, - }; + startCell.alignment = titleAlignmentConfigs; + startCell.font = titleFontConfigs; + startCell.border = borderConfigs; rowIndex++; // Move to the next row } @@ -156,15 +200,9 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { columns.forEach((col, colIdx) => { const cell = worksheet.getCell(rowIndex, startCol + colIdx); cell.value = col.name; - cell.alignment = { horizontal: 'center', vertical: 'middle' }; - cell.font = { bold: true }; // Apply bold font - // Apply borders to the header cell - cell.border = { - top: { style: 'thin' }, - left: { style: 'thin' }, - bottom: { style: 'thin' }, - right: { style: 'thin' }, - }; + cell.alignment = headerAligmentConfigs; + cell.font = headerFontConfigs; + cell.border = borderConfigs; }); rowIndex++; // Increment row index after adding headers } @@ -247,25 +285,21 @@ export function execGenExcelFuncs(sheetsData: SheetData[]): string { } } - // Apply borders to the cell - cell.border = { - top: { style: 'thin' }, - left: { style: 'thin' }, - bottom: { style: 'thin' }, - right: { style: 'thin' }, - }; + // Apply styles to the cell + cell.font = cellFontConfigs; + cell.border = borderConfigs; + cell.alignment = cellAlignmentConfigs; }); rowIndex++; // Move to the next row }); - // Apply sorting - if (sorting) { - console.warn('Sorting is not implemented in this version.'); - } - - // Apply filtering (not natively supported in ExcelJS; requires client-side configuration) - if (filtering) { - console.warn('Filtering is not implemented in this version.'); + // Apply auto-filter + if (excelConfigs.autoFilter) { + const lastCol = startCol + columns.length - 1; // Calculate the last column + worksheet.autoFilter = { + from: { row: startRow + 1, column: startCol }, // Start from header row + to: { row: rowIndex - 1, column: lastCol }, // End at the last row of data + }; } // Auto-fit column widths @@ -295,7 +329,7 @@ export const excelGeneratorRouter: Router = (() => { router.use('/downloads', express.static(exportsDir)); router.post('/generate', async (_req: Request, res: Response) => { - const { sheetsData } = _req.body; // TODO: extract excel config object from request body + const { sheetsData, excelConfigs } = _req.body; // TODO: extract excel config object from request body if (!sheetsData.length) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, @@ -307,7 +341,16 @@ export const excelGeneratorRouter: Router = (() => { } try { - const fileName = execGenExcelFuncs(sheetsData); + const fileName = execGenExcelFuncs(sheetsData, { + fontFamily: excelConfigs.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, + titleFontSize: excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.titleFontSize, + headerFontSize: excelConfigs.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, + fontSize: excelConfigs.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, + autoFilter: excelConfigs.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, + borderStyle: excelConfigs.borderStyle ?? DEFAULT_EXCEL_CONFIGS.borderStyle, + wrapText: excelConfigs.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, + }); + const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'File generated successfully', From 46f01aab2706a40536c12c64895c364f98dc998a Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Thu, 16 Jan 2025 08:34:38 +0700 Subject: [PATCH 07/12] feat(excel generator): update user settings fields --- .../excelGenerator/excelGeneratorRouter.ts | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index 4bd2143..8b79025 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -82,22 +82,24 @@ interface SheetData { interface ExcelConfig { fontFamily: string; - titleFontSize: number; + tableTitleFontSize: number; headerFontSize: number; fontSize: number; + autoFitColumnWidth: boolean; autoFilter: boolean; - borderStyle: ExcelJS.BorderStyle; // thin, double, dashed, thick + borderStyle: ExcelJS.BorderStyle | null; // thin, double, dashed, thick wrapText: boolean; } const DEFAULT_EXCEL_CONFIGS: ExcelConfig = { fontFamily: 'Calibri', - titleFontSize: 16, + tableTitleFontSize: 13, headerFontSize: 11, fontSize: 11, + autoFitColumnWidth: true, autoFilter: false, wrapText: false, - borderStyle: 'thin', + borderStyle: null, }; // Helper function to convert column letter (e.g., 'A') to column index (e.g., 1) @@ -143,12 +145,14 @@ function autoFitColumns( export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelConfig): string { const workbook = new ExcelJS.Workbook(); - const borderConfigs = { - top: { style: excelConfigs.borderStyle }, - left: { style: excelConfigs.borderStyle }, - bottom: { style: excelConfigs.borderStyle }, - right: { style: excelConfigs.borderStyle }, - }; + const borderConfigs = excelConfigs.borderStyle + ? { + top: { style: excelConfigs.borderStyle }, + left: { style: excelConfigs.borderStyle }, + bottom: { style: excelConfigs.borderStyle }, + right: { style: excelConfigs.borderStyle }, + } + : {}; const titleAlignmentConfigs: any = { horizontal: 'center', vertical: 'middle', @@ -157,7 +161,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo const titleFontConfigs: any = { name: excelConfigs.fontFamily, bold: true, - size: excelConfigs.titleFontSize, + size: excelConfigs.tableTitleFontSize, }; const headerAligmentConfigs: any = { wrapText: excelConfigs.wrapText, @@ -303,7 +307,9 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo } // Auto-fit column widths - autoFitColumns(worksheet, startRow, rows, columns.length, startCol); + if (excelConfigs.autoFitColumnWidth) { + autoFitColumns(worksheet, startRow, rows, columns.length, startCol); + } }); }); @@ -343,12 +349,16 @@ export const excelGeneratorRouter: Router = (() => { try { const fileName = execGenExcelFuncs(sheetsData, { fontFamily: excelConfigs.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, - titleFontSize: excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.titleFontSize, + tableTitleFontSize: excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, headerFontSize: excelConfigs.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, fontSize: excelConfigs.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, autoFilter: excelConfigs.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, - borderStyle: excelConfigs.borderStyle ?? DEFAULT_EXCEL_CONFIGS.borderStyle, + borderStyle: + excelConfigs.borderStyle || excelConfigs.borderStyle !== 'none' + ? excelConfigs.borderStyle + : DEFAULT_EXCEL_CONFIGS.borderStyle, wrapText: excelConfigs.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, + autoFitColumnWidth: excelConfigs.autoFitColumnWidth ?? DEFAULT_EXCEL_CONFIGS.autoFitColumnWidth, }); const serviceResponse = new ServiceResponse( From 68c5f509e0b31ffa266cf772503d4efcbd06a3a6 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Thu, 16 Jan 2025 12:03:06 +0700 Subject: [PATCH 08/12] feat(excel generator): handle empty object without formula --- src/routes/excelGenerator/excelGeneratorRouter.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index 8b79025..a262aaa 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -241,12 +241,16 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo let cellValue: any = value != null ? value : ''; // Handle empty/null values const cell = worksheet.getCell(rowIndex, startCol + colIdx); // Check if the value is a formula - if (typeof cellValue === 'object' && cellValue.formula) { - const formulaCell: any = { formula: cellValue.formula }; // Handle formula - if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { - cell.numFmt = format; // Apply number format + if (typeof cellValue === 'object') { + if (cellValue.formula) { + const formulaCell: any = { formula: cellValue.formula }; // Handle formula + if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { + cell.numFmt = format; // Apply number format + } + cell.value = formulaCell; + } else { + cell.value = ''; } - cell.value = formulaCell; } else { // Assign cell type based on the header definition switch (cellType) { From c10b2de25f15e238e7b9a2d01cc0ef749868d15e Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Thu, 16 Jan 2025 12:15:27 +0700 Subject: [PATCH 09/12] feat(excel generator): handle empty string formula --- src/routes/excelGenerator/excelGeneratorRouter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index a262aaa..c7d15cb 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -242,7 +242,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo const cell = worksheet.getCell(rowIndex, startCol + colIdx); // Check if the value is a formula if (typeof cellValue === 'object') { - if (cellValue.formula) { + if (cellValue.formula && cellValue.formula !== '') { const formulaCell: any = { formula: cellValue.formula }; // Handle formula if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { cell.numFmt = format; // Apply number format From 99d0a1f0d53680262d87e46994420035c2afbad2 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 17 Jan 2025 11:24:35 +0700 Subject: [PATCH 10/12] feat(excel generator): handle empty rows or columns data --- .../excelGenerator/excelGeneratorRouter.ts | 4 +- src/routes/excelGenerator/simple.json | 100 +++++++++++++++++ src/routes/excelGenerator/test.json | 101 ++++++++++++++++++ 3 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 src/routes/excelGenerator/simple.json create mode 100644 src/routes/excelGenerator/test.json diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index c7d15cb..ff526d4 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -183,7 +183,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo sheetsData.forEach(({ sheetName, tables }) => { const worksheet = workbook.addWorksheet(sheetName); - tables.forEach(({ startCell, title, rows, columns, skipHeader }) => { + tables.forEach(({ startCell, title, rows = [], columns = [], skipHeader }) => { const startCol = columnLetterToNumber(startCell[0]); // Convert column letter to index (e.g., 'A' -> 1) const startRow = parseInt(startCell.slice(1)); // Extract the row number (e.g., 'A1' -> 1) let rowIndex = startRow; // Set the initial row index to startRow for each table @@ -212,7 +212,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo } // Map headers to types - const columnTypes = columns?.map((col: any) => col.type) || []; + const columnTypes = columns.map((col: any) => col.type) || []; const columnFormats = columns?.map((col: any) => { let format = undefined; diff --git a/src/routes/excelGenerator/simple.json b/src/routes/excelGenerator/simple.json new file mode 100644 index 0000000..d070227 --- /dev/null +++ b/src/routes/excelGenerator/simple.json @@ -0,0 +1,100 @@ +{ + "name": "generate_excel_file", + "parameters": { + "type": "object", + "required": ["sheetsData"], + "properties": { + "sheetsData": { + "type": "array", + "items": { + "type": "object", + "required": ["sheetName", "tables"], + "properties": { + "tables": { + "type": "array", + "items": { + "type": "object", + "required": ["startCell", "columns", "rows"], + "properties": { + "rows": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "type": "string", + "description": "A static value (string or number) to be placed directly in the cell. No calculations or references to other cells." + }, + { + "type": "object", + "properties": { + "formula": { + "type": "string", + "description": "A formula to be applied to the cell (not begin with '='). It must reference other cells or perform calculations. It must not include static values as string or number. Ensure that any cell ranges used in the formula start from the first data row (excluding the header and title). For example, if the table starts at A1 with a title in row 1 and header in row 2, a valid formula would be 'SUM(A3:A10)', summing data from rows 3 to 10. Avoid creating circular references where a formula directly or indirectly references its own cell." + } + }, + "required": ["formula"] + } + ], + "description": "Each row item can contain either a static value (string or number) or a formula. Make sure its length matches the defined columns length. Avoid formulas that reference the cell they are in or lead to circular dependencies." + }, + "description": "Array of rows in the table, where each row contains either static values or formulas for the cells. If the value is static, it should be represented as a single string. If the value is a formula, it should be represented as an object with a 'formula' property." + }, + "title": { + "type": "string", + "example": "Q1 Sales Data", + "description": "The title of the table, which will be displayed in the first row." + }, + "columns": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "type"], + "properties": { + "name": { + "type": "string", + "example": "Product Name", + "description": "The name of the column." + }, + "type": { + "enum": ["string", "number", "boolean", "percent", "currency", "date"], + "type": "string", + "example": "string", + "description": "The data type of the column." + }, + "format": { + "type": "string", + "example": "$#,##0", + "description": "The format of the column (e.g., '0.00%', '$#,##0', etc.)." + } + } + }, + "description": "The list of columns in the table, each with a name, type, and optional format." + }, + "startCell": { + "type": "string", + "example": "A1", + "description": "The starting cell (e.g., 'A1') where the table will begin." + }, + "skipHeader": { + "type": "boolean", + "example": false, + "description": "Whether to skip the header row for this table." + } + } + }, + "description": "The tables to include in the sheet." + }, + "sheetName": { + "type": "string", + "example": "Sales Data", + "description": "The name of the sheet to be created in the Excel file." + } + } + }, + "description": "An array of sheet data, where each sheet contains a name and an array of tables to generate." + } + } + }, + "description": "Generate an Excel file (.xlsx) and return the URL for downloading it. If the download URL is not present in the response, inform the user that the Excel file could not be generated. Prompt the user to verify that the correct plugin server URL is being used." +} diff --git a/src/routes/excelGenerator/test.json b/src/routes/excelGenerator/test.json new file mode 100644 index 0000000..4bc36d1 --- /dev/null +++ b/src/routes/excelGenerator/test.json @@ -0,0 +1,101 @@ +{ + "name": "generate_excel_file", + "parameters": { + "type": "object", + "required": ["sheetsData"], + "properties": { + "sheetsData": { + "type": "array", + "items": { + "type": "object", + "required": ["sheetName", "tables"], + "properties": { + "tables": { + "type": "array", + "items": { + "type": "object", + "required": ["startCell", "columns", "rows"], + "properties": { + "rows": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "type": "string", + "description": "A static value (string or number) to be placed directly in the cell. No calculations or references to other cells." + }, + { + "type": "object", + "properties": { + "formula": { + "type": "string", + "description": "A formula to be applied to the cell (not begin with '='). It must not include static values as string or number. Ensure that any cell ranges used in the formula must not include cells of table header and table title. For example, if the table starts at A1 with a title in row 1 and header in row 2, a valid formula would be 'SUM(A3:A10)', summing data from rows 3 to 10. Avoid creating circular references where a formula directly or indirectly references its own cell." + } + }, + "required": ["formula"] + } + ], + "description": "Each row item can contain either a static value (string or number) or a formula. Make sure it's length same as defined columns length." + }, + "required": ["rows"], + "description": "Array of rows in the table, where each row contains either static values or formulas for the cells. If the value is static, it should be represented as a single string. If the value is a formula, it should be represented as an object with a 'formula' property." + }, + "title": { + "type": "string", + "example": "Q1 Sales Data", + "description": "The title of the table, which will be displayed in the first row." + }, + "columns": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "type"], + "properties": { + "name": { + "type": "string", + "example": "Product Name", + "description": "The name of the column." + }, + "type": { + "enum": ["string", "number", "boolean", "percent", "currency", "date"], + "type": "string", + "example": "string", + "description": "The data type of the column." + }, + "format": { + "type": "string", + "example": "$#,##0", + "description": "The format of the column (e.g., '0.00%', '$#,##0', etc.)." + } + } + }, + "description": "The list of columns in the table, each with a name, type, and optional format." + }, + "startCell": { + "type": "string", + "example": "A1", + "description": "The starting cell (e.g., 'A1') where the table will begin." + }, + "skipHeader": { + "type": "boolean", + "example": false, + "description": "Whether to skip the header row for this table." + } + } + }, + "description": "The tables to include in the sheet." + }, + "sheetName": { + "type": "string", + "example": "Sales Data", + "description": "The name of the sheet to be created in the Excel file." + } + } + }, + "description": "An array of sheet data, where each sheet contains a name and an array of tables to generate." + } + } + }, + "description": "Generate an Excel file (.xlsx) and return the URL for downloading it. If the download URL is not present in the response, inform the user that the Excel file could not be generated. Prompt the user to verify that the correct plugin server URL is being used." +} From 82019e5ea1ef8665b136380471cbbb4812462187 Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 17 Jan 2025 11:25:10 +0700 Subject: [PATCH 11/12] feat(excel generator): remove tmp files --- src/routes/excelGenerator/simple.json | 100 ------------------------- src/routes/excelGenerator/test.json | 101 -------------------------- 2 files changed, 201 deletions(-) delete mode 100644 src/routes/excelGenerator/simple.json delete mode 100644 src/routes/excelGenerator/test.json diff --git a/src/routes/excelGenerator/simple.json b/src/routes/excelGenerator/simple.json deleted file mode 100644 index d070227..0000000 --- a/src/routes/excelGenerator/simple.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "name": "generate_excel_file", - "parameters": { - "type": "object", - "required": ["sheetsData"], - "properties": { - "sheetsData": { - "type": "array", - "items": { - "type": "object", - "required": ["sheetName", "tables"], - "properties": { - "tables": { - "type": "array", - "items": { - "type": "object", - "required": ["startCell", "columns", "rows"], - "properties": { - "rows": { - "type": "array", - "items": { - "type": "object", - "oneOf": [ - { - "type": "string", - "description": "A static value (string or number) to be placed directly in the cell. No calculations or references to other cells." - }, - { - "type": "object", - "properties": { - "formula": { - "type": "string", - "description": "A formula to be applied to the cell (not begin with '='). It must reference other cells or perform calculations. It must not include static values as string or number. Ensure that any cell ranges used in the formula start from the first data row (excluding the header and title). For example, if the table starts at A1 with a title in row 1 and header in row 2, a valid formula would be 'SUM(A3:A10)', summing data from rows 3 to 10. Avoid creating circular references where a formula directly or indirectly references its own cell." - } - }, - "required": ["formula"] - } - ], - "description": "Each row item can contain either a static value (string or number) or a formula. Make sure its length matches the defined columns length. Avoid formulas that reference the cell they are in or lead to circular dependencies." - }, - "description": "Array of rows in the table, where each row contains either static values or formulas for the cells. If the value is static, it should be represented as a single string. If the value is a formula, it should be represented as an object with a 'formula' property." - }, - "title": { - "type": "string", - "example": "Q1 Sales Data", - "description": "The title of the table, which will be displayed in the first row." - }, - "columns": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "type"], - "properties": { - "name": { - "type": "string", - "example": "Product Name", - "description": "The name of the column." - }, - "type": { - "enum": ["string", "number", "boolean", "percent", "currency", "date"], - "type": "string", - "example": "string", - "description": "The data type of the column." - }, - "format": { - "type": "string", - "example": "$#,##0", - "description": "The format of the column (e.g., '0.00%', '$#,##0', etc.)." - } - } - }, - "description": "The list of columns in the table, each with a name, type, and optional format." - }, - "startCell": { - "type": "string", - "example": "A1", - "description": "The starting cell (e.g., 'A1') where the table will begin." - }, - "skipHeader": { - "type": "boolean", - "example": false, - "description": "Whether to skip the header row for this table." - } - } - }, - "description": "The tables to include in the sheet." - }, - "sheetName": { - "type": "string", - "example": "Sales Data", - "description": "The name of the sheet to be created in the Excel file." - } - } - }, - "description": "An array of sheet data, where each sheet contains a name and an array of tables to generate." - } - } - }, - "description": "Generate an Excel file (.xlsx) and return the URL for downloading it. If the download URL is not present in the response, inform the user that the Excel file could not be generated. Prompt the user to verify that the correct plugin server URL is being used." -} diff --git a/src/routes/excelGenerator/test.json b/src/routes/excelGenerator/test.json deleted file mode 100644 index 4bc36d1..0000000 --- a/src/routes/excelGenerator/test.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "name": "generate_excel_file", - "parameters": { - "type": "object", - "required": ["sheetsData"], - "properties": { - "sheetsData": { - "type": "array", - "items": { - "type": "object", - "required": ["sheetName", "tables"], - "properties": { - "tables": { - "type": "array", - "items": { - "type": "object", - "required": ["startCell", "columns", "rows"], - "properties": { - "rows": { - "type": "array", - "items": { - "type": "object", - "oneOf": [ - { - "type": "string", - "description": "A static value (string or number) to be placed directly in the cell. No calculations or references to other cells." - }, - { - "type": "object", - "properties": { - "formula": { - "type": "string", - "description": "A formula to be applied to the cell (not begin with '='). It must not include static values as string or number. Ensure that any cell ranges used in the formula must not include cells of table header and table title. For example, if the table starts at A1 with a title in row 1 and header in row 2, a valid formula would be 'SUM(A3:A10)', summing data from rows 3 to 10. Avoid creating circular references where a formula directly or indirectly references its own cell." - } - }, - "required": ["formula"] - } - ], - "description": "Each row item can contain either a static value (string or number) or a formula. Make sure it's length same as defined columns length." - }, - "required": ["rows"], - "description": "Array of rows in the table, where each row contains either static values or formulas for the cells. If the value is static, it should be represented as a single string. If the value is a formula, it should be represented as an object with a 'formula' property." - }, - "title": { - "type": "string", - "example": "Q1 Sales Data", - "description": "The title of the table, which will be displayed in the first row." - }, - "columns": { - "type": "array", - "items": { - "type": "object", - "required": ["name", "type"], - "properties": { - "name": { - "type": "string", - "example": "Product Name", - "description": "The name of the column." - }, - "type": { - "enum": ["string", "number", "boolean", "percent", "currency", "date"], - "type": "string", - "example": "string", - "description": "The data type of the column." - }, - "format": { - "type": "string", - "example": "$#,##0", - "description": "The format of the column (e.g., '0.00%', '$#,##0', etc.)." - } - } - }, - "description": "The list of columns in the table, each with a name, type, and optional format." - }, - "startCell": { - "type": "string", - "example": "A1", - "description": "The starting cell (e.g., 'A1') where the table will begin." - }, - "skipHeader": { - "type": "boolean", - "example": false, - "description": "Whether to skip the header row for this table." - } - } - }, - "description": "The tables to include in the sheet." - }, - "sheetName": { - "type": "string", - "example": "Sales Data", - "description": "The name of the sheet to be created in the Excel file." - } - } - }, - "description": "An array of sheet data, where each sheet contains a name and an array of tables to generate." - } - } - }, - "description": "Generate an Excel file (.xlsx) and return the URL for downloading it. If the download URL is not present in the response, inform the user that the Excel file could not be generated. Prompt the user to verify that the correct plugin server URL is being used." -} From 4054b4140120aca69e2d20f153e17f85292784ad Mon Sep 17 00:00:00 2001 From: dat-devuap Date: Fri, 17 Jan 2025 12:28:15 +0700 Subject: [PATCH 12/12] feat(excel generator): refactor cell object to have type and value --- .../excelGenerator/excelGeneratorRouter.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index ff526d4..8695c7d 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -74,7 +74,10 @@ interface SheetData { tables: { title: string; startCell: string; - rows: any[][]; + rows: { + type: string; // static_value or formula, + value: string; + }[][]; columns: { name: string; type: string; format: string }[]; // types that have format, number, percent, currency skipHeader: boolean; }[]; @@ -234,26 +237,23 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo }) || []; // Add rows with data types - rows.forEach((row) => { - row.forEach((value, colIdx) => { - const cellType = columnTypes[colIdx]; + rows.forEach((rowData) => { + rowData.forEach((cellData, colIdx) => { + const { type = 'static_value', value } = cellData; + const valueType = columnTypes[colIdx]; const format = columnFormats[colIdx]; let cellValue: any = value != null ? value : ''; // Handle empty/null values const cell = worksheet.getCell(rowIndex, startCol + colIdx); // Check if the value is a formula - if (typeof cellValue === 'object') { - if (cellValue.formula && cellValue.formula !== '') { - const formulaCell: any = { formula: cellValue.formula }; // Handle formula - if (cellType === 'percent' || cellType === 'currency' || cellType === 'number' || cellType === 'date') { - cell.numFmt = format; // Apply number format - } - cell.value = formulaCell; - } else { - cell.value = ''; + if (type == 'formula') { + const formulaCell: any = { formula: cellValue }; // Handle formula + if (valueType === 'percent' || valueType === 'currency' || valueType === 'number' || valueType === 'date') { + cell.numFmt = format; // Apply number format } + cell.value = formulaCell; } else { // Assign cell type based on the header definition - switch (cellType) { + switch (valueType) { case 'number': { cellValue = !isNaN(Number(cellValue)) ? Math.round(Number(cellValue)) : cellValue; cell.value = cellValue;