UNPKG

safeer-pdf-generator

Version:

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

218 lines 9.46 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 EmailSender_js_1 = require("../integrations/email/EmailSender.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; logger.info(`Starting PDF generation: "${validatedOptions.title}"`); logger.debug(`Data rows: ${validatedOptions.data.length}, Columns: ${validatedOptions.columns.length}`); // 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' }); 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}`); } // 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}`); } const totalTime = timer.elapsed(); logger.info(`PDF generation completed: ${result.pageCount} pages, ` + `${(result.sizeBytes / 1024 / 1024).toFixed(2)}MB, ` + `${totalTime.toFixed(1)}ms`); return result; } catch (error) { const elapsed = timer.elapsed(); logger.error(`PDF generation failed after ${elapsed.toFixed(1)}ms:`, error); 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 (rough) const baseTimePerChunk = 2000; // 2 seconds base const timePerRow = 5; // 5ms per row const estimatedTimeMs = chunks * 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