safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
212 lines • 8.99 kB
JavaScript
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