UNPKG

safeer-pdf-generator

Version:

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

212 lines 8.99 kB
import { validatePdfOptions, validateS3Config, validateEmailConfig } from '../utils/guards.js'; import { defaultPdfOptions, mergeDefaults } from '../config/defaults.js'; import { PdfEngine } from '../core/PdfEngine.js'; import { MergeService } from '../core/MergeService.js'; import { ChunkCoordinator } from '../core/ChunkCoordinator.js'; import { resolveTemplate } from '../templates/registry.js'; import { createS3Uploader } from '../integrations/s3/S3Uploader.js'; import { createEmailSender } from '../integrations/email/EmailSender.js'; import { PdfGenerationError } from '../core/errors.js'; import { Timer } from '../utils/timing.js'; /** * Generate PDF from data and options */ export async function generatePdf(options) { const timer = new Timer(); const startTime = Date.now(); // Merge with defaults and validate const mergedOptions = mergeDefaults(defaultPdfOptions, options); validatePdfOptions(mergedOptions); // Ensure required fields are present after validation if (!mergedOptions.title || !mergedOptions.data || !mergedOptions.columns) { throw new 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(validatedOptions.puppeteer, validatedOptions.pdf, logger); const mergeService = new MergeService(logger); const chunkCoordinator = new ChunkCoordinator(logger, validatedOptions.hooks); try { // Resolve template const templateCompiler = 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...'); validateS3Config(validatedOptions.s3); const s3Uploader = 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...'); validateEmailConfig(validatedOptions.email); const emailSender = 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 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) */ export 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 */ export async function generateOptimizedPdf(options) { const dataLength = options.data.length; const optimalChunking = 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 */ export 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.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