UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

306 lines 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generatePdf = generatePdf; exports.generateChunkedPdf = generateChunkedPdf; exports.generateOptimizedPdf = generateOptimizedPdf; exports.estimatePdfGeneration = estimatePdfGeneration; const guards_js_1 = require("../utils/guards.js"); const defaults_js_1 = require("../config/defaults.js"); const PdfEngine_js_1 = require("../core/PdfEngine.js"); const MergeService_js_1 = require("../core/MergeService.js"); const ChunkCoordinator_js_1 = require("../core/ChunkCoordinator.js"); const registry_js_1 = require("../templates/registry.js"); const S3Uploader_js_1 = require("../integrations/s3/S3Uploader.js"); const LocalFsWriter_js_1 = require("../integrations/localfs/LocalFsWriter.js"); const EmailSender_js_1 = require("../integrations/email/EmailSender.js"); const WebhookDispatcher_js_1 = require("../integrations/webhook/WebhookDispatcher.js"); const errors_js_1 = require("../core/errors.js"); const timing_js_1 = require("../utils/timing.js"); /** * Generate PDF from data and options */ async function generatePdf(options) { const timer = new timing_js_1.Timer(); const startTime = Date.now(); // Merge with defaults and validate const mergedOptions = (0, defaults_js_1.mergeDefaults)(defaults_js_1.defaultPdfOptions, options); (0, guards_js_1.validatePdfOptions)(mergedOptions); // Ensure required fields are present after validation if (!mergedOptions.title || !mergedOptions.data || !mergedOptions.columns) { throw new errors_js_1.PdfGenerationError('Missing required fields: title, data, and columns must be provided', 'validation'); } // Cast to full options since we've validated required fields const validatedOptions = mergedOptions; const logger = validatedOptions.logging; const events = validatedOptions.events; logger.info(`Starting PDF generation: "${validatedOptions.title}"`); logger.debug(`Data rows: ${validatedOptions.data.length}, Columns: ${validatedOptions.columns.length}`); // Emit generation:started event if (events) { events.emitEvent('generation:started', { title: validatedOptions.title, rowCount: validatedOptions.data.length, timestamp: new Date().toISOString(), }); } // Initialize services const pdfEngine = new PdfEngine_js_1.PdfEngine(validatedOptions.puppeteer, validatedOptions.pdf, logger); const mergeService = new MergeService_js_1.MergeService(logger); const chunkCoordinator = new ChunkCoordinator_js_1.ChunkCoordinator(logger, validatedOptions.hooks); try { // Resolve template const templateCompiler = (0, registry_js_1.resolveTemplate)(validatedOptions.template); timer.mark('template-resolved'); // Process chunks logger.info('Processing data chunks...'); const pdfBuffers = await chunkCoordinator.processChunks(validatedOptions, templateCompiler, pdfEngine); timer.mark('chunks-processed'); // Merge PDFs if multiple chunks let finalBuffer; if (pdfBuffers.length === 1) { finalBuffer = pdfBuffers[0]; logger.debug('Single chunk - no merging needed'); } else { logger.info(`Merging ${pdfBuffers.length} PDF chunks...`); // Execute before merge hook if (validatedOptions.hooks?.beforeMerge) { await validatedOptions.hooks.beforeMerge(pdfBuffers); } finalBuffer = await mergeService.mergePdfs(pdfBuffers); // Execute after merge hook if (validatedOptions.hooks?.afterMerge) { await validatedOptions.hooks.afterMerge(finalBuffer); } } timer.mark('pdf-merged'); // Add page numbers to the final PDF logger.info('Adding page numbers to final PDF...'); finalBuffer = await mergeService.addPageNumbers(finalBuffer, { userInfo: validatedOptions.userInfo, isRTL: validatedOptions.locale === 'ar', footer: validatedOptions.footer, }); timer.mark('page-numbers-added'); // Get PDF info const pdfInfo = await mergeService.getPdfInfo(finalBuffer); timer.mark('pdf-analyzed'); // Generate filename const timestamp = new Date() .toISOString() .replace(/[:.]/g, '-') .replace('T', '-') .replace('Z', ''); const fileName = `${validatedOptions.title.replace(/\s+/g, '-')}-${timestamp}.pdf`; // Prepare result const result = { buffer: finalBuffer, sizeBytes: finalBuffer.length, pageCount: pdfInfo.pages, durationMs: Date.now() - startTime, fileName, metadata: { title: validatedOptions.title, chunkCount: pdfBuffers.length, rows: validatedOptions.data.length, generatedAt: new Date().toISOString(), }, }; // Handle S3 upload if configured if (validatedOptions.s3) { logger.info('Uploading PDF to S3...'); (0, guards_js_1.validateS3Config)(validatedOptions.s3); const s3Uploader = (0, S3Uploader_js_1.createS3Uploader)(validatedOptions.s3, logger); const s3Result = await s3Uploader.upload(finalBuffer, fileName, 'application/pdf'); result.s3 = s3Result; timer.mark('s3-uploaded'); logger.info(`PDF uploaded to S3: ${s3Result.url}`); // Emit s3:upload:complete event if (events) { events.emitEvent('s3:upload:complete', { url: s3Result.url, key: s3Result.key, requestPayload: validatedOptions.webhook ? validatedOptions.webhook.metadata : undefined, }); } } // Handle local filesystem write if configured if (validatedOptions.localFs) { logger.info('Writing PDF to local filesystem...'); (0, guards_js_1.validateLocalFsConfig)(validatedOptions.localFs); const localFsWriter = (0, LocalFsWriter_js_1.createLocalFsWriter)(validatedOptions.localFs, logger); const localFsResult = await localFsWriter.write(finalBuffer, fileName); result.localFs = localFsResult; timer.mark('localfs-written'); logger.info(`PDF written to ${localFsResult.path}`); if (events) { events.emitEvent('file:saved', { path: localFsResult.path, sizeBytes: localFsResult.sizeBytes, }); } } // Handle email sending if configured if (validatedOptions.email) { logger.info('Sending PDF email...'); (0, guards_js_1.validateEmailConfig)(validatedOptions.email); const emailSender = (0, EmailSender_js_1.createEmailSender)(logger); const emailResult = await emailSender.send(validatedOptions.email, finalBuffer, fileName, result.s3?.url); result.email = { sent: emailResult.sent, to: validatedOptions.email.to, messageId: emailResult.messageId, error: emailResult.error, }; timer.mark('email-sent'); logger.info(`Email ${emailResult.sent ? 'sent successfully' : 'failed'}: ${validatedOptions.email.to}`); // Emit email:sent event if (events && emailResult.sent) { events.emitEvent('email:sent', { to: validatedOptions.email.to, messageId: emailResult.messageId, }); } } const totalTime = timer.elapsed(); logger.info(`PDF generation completed: ${result.pageCount} pages, ` + `${(result.sizeBytes / 1024 / 1024).toFixed(2)}MB, ` + `${totalTime.toFixed(1)}ms`); // Emit generation:complete event if (events) { events.emitEvent('generation:complete', { result }); } // Dispatch webhook if configured (non-blocking — failure only logs a warning) if (validatedOptions.webhook) { const webhookConfig = validatedOptions.webhook; const dispatcher = new WebhookDispatcher_js_1.WebhookDispatcher(webhookConfig.url, webhookConfig.secret, logger, webhookConfig.timeoutMs); // Fire and log — do not block the result dispatcher.dispatch({ s3Url: result.s3?.url, s3Key: result.s3?.key, title: result.metadata.title, fileName: result.fileName, sizeBytes: result.sizeBytes, pageCount: result.pageCount, durationMs: result.durationMs, generatedAt: result.metadata.generatedAt, metadata: webhookConfig.metadata, }).then((webhookResult) => { if (webhookResult.success) { logger.info(`Webhook dispatched successfully (${webhookResult.statusCode})`); } else { logger.warn(`Webhook dispatch failed: ${webhookResult.error}`); } }).catch((err) => { logger.warn(`Webhook dispatch error: ${err}`); }); } return result; } catch (error) { const elapsed = timer.elapsed(); logger.error(`PDF generation failed after ${elapsed.toFixed(1)}ms:`, error); // Emit error event if (events) { const err = error instanceof Error ? error : new Error(String(error)); events.emitEvent('error', { error: err, phase: 'generation', stack: err.stack, }); } throw new errors_js_1.PdfGenerationError(`PDF generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'generation', error); } finally { // Clean up resources try { await pdfEngine.close(); } catch (cleanupError) { logger.warn('Error during cleanup:', cleanupError); } } } /** * Generate chunked PDF (alias for generatePdf with chunking enabled) */ async function generateChunkedPdf(options) { const chunkedOptions = { ...options, chunking: { enabled: true, chunkSize: options.chunking?.chunkSize || 100, maxConcurrency: options.chunking?.maxConcurrency || 2, }, }; return generatePdf(chunkedOptions); } /** * Generate PDF with optimal chunking based on data size */ async function generateOptimizedPdf(options) { const dataLength = options.data.length; const optimalChunking = ChunkCoordinator_js_1.ChunkCoordinator.calculateOptimalChunking(dataLength); const optimizedOptions = { ...options, chunking: { ...options.chunking, ...optimalChunking, }, }; if (options.logging) { options.logging.info(`Using optimal chunking: ${optimalChunking.chunkSize} rows/chunk, ` + `${optimalChunking.maxConcurrency} concurrent, ` + `enabled: ${optimalChunking.enabled}`); } return generatePdf(optimizedOptions); } /** * Estimate PDF generation time and resources */ function estimatePdfGeneration(options) { const dataLength = options.data?.length || 0; const chunkSize = options.chunking?.chunkSize || 100; const maxConcurrency = options.chunking?.maxConcurrency || 2; const chunks = Math.ceil(dataLength / chunkSize); const avgRowsPerPage = 50; // Rough estimate const estimatedPages = Math.max(1, Math.ceil(dataLength / avgRowsPerPage)); // Time estimation. Chunks render in parallel pools of `maxConcurrency`, // so the per-chunk overhead scales with the number of parallel batches, // not the total chunk count. const concurrency = Math.max(1, maxConcurrency); const parallelChunkBatches = Math.ceil(chunks / concurrency); const baseTimePerChunk = 800; // warm-browser average const timePerRow = 1; // ~1ms per row on warm Chromium with a typical template const estimatedTimeMs = parallelChunkBatches * baseTimePerChunk + dataLength * timePerRow; // Memory estimation const memoryEstimate = ChunkCoordinator_js_1.ChunkCoordinator.estimateMemoryUsage(dataLength, chunkSize, maxConcurrency); const recommendations = []; if (dataLength > 10000) { recommendations.push('Consider increasing chunk size for large datasets'); } if (dataLength < 100 && chunks > 1) { recommendations.push('Disable chunking for small datasets'); } if (memoryEstimate.estimatedMB > 1000) { recommendations.push('Reduce concurrency or chunk size to lower memory usage'); } if (estimatedTimeMs > 300000) { recommendations.push('Consider processing in smaller batches for very long operations'); } return { estimatedTimeMs, estimatedMemoryMB: memoryEstimate.estimatedMB, estimatedPages, chunking: { chunks, rowsPerChunk: chunkSize, concurrency: maxConcurrency, }, recommendations, }; } //# sourceMappingURL=generatePdf.js.map