UNPKG

safeer-pdf-generator

Version:

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

1,038 lines (943 loc) 32 kB
/** * Advanced Express.js PDF Generation API using @safeersoft/@safeersoft/pdf-reporter * * Features: * - RESTful PDF generation endpoints * - Template management * - Performance monitoring * - Error handling middleware * - Request validation * - Async job processing * - File caching * - Health checks * - Metrics collection */ const express = require('express'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); const { v4: uuidv4 } = require('uuid'); const { promisify } = require('util'); const fs = require('fs'); const path = require('path'); const { generatePdf, generateOptimizedPdf, mergePdfs, splitPdf, extractPages, getPdfInfo, analyzePdfs, validatePdfs, registerTemplate, consoleLogger, estimatePdfGeneration, } = require('@safeersoft/@safeersoft/pdf-reporter'); const app = express(); const port = process.env.PORT || 3000; // Middleware setup app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes', }, }); app.use('/api/', limiter); // File storage setup const UPLOAD_DIR = path.join(__dirname, 'uploads'); const OUTPUT_DIR = path.join(__dirname, 'output'); const CACHE_DIR = path.join(__dirname, 'cache'); [UPLOAD_DIR, OUTPUT_DIR, CACHE_DIR].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // In-memory job store (use Redis in production) const jobs = new Map(); const jobResults = new Map(); // Metrics collection const metrics = { requests: 0, successful: 0, failed: 0, totalProcessingTime: 0, averageFileSize: 0, templates: new Set(), }; // Advanced sample data generator const generateSampleData = (count = 100, type = 'customers') => { const data = []; if (type === 'customers') { for (let i = 1; i <= count; i++) { data.push({ id: i, name: `Customer ${String(i).padStart(4, '0')}`, email: `customer${i}@example.com`, amount: Math.round(Math.random() * 10000 * 100) / 100, date: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000) .toISOString() .split('T')[0], status: ['Active', 'Inactive', 'Pending', 'Suspended'][Math.floor(Math.random() * 4)], region: ['North America', 'Europe', 'Asia Pacific', 'Latin America'][ Math.floor(Math.random() * 4) ], tier: ['Bronze', 'Silver', 'Gold', 'Platinum'][Math.floor(Math.random() * 4)], lastActivity: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000) .toISOString() .split('T')[0], }); } } else if (type === 'sales') { for (let i = 1; i <= count; i++) { data.push({ id: i, product: `Product ${String(i).padStart(3, '0')}`, category: ['Electronics', 'Clothing', 'Books', 'Home & Garden'][ Math.floor(Math.random() * 4) ], price: Math.round(Math.random() * 500 * 100) / 100, quantity: Math.floor(Math.random() * 100) + 1, revenue: 0, // Will be calculated salesperson: `Sales Rep ${Math.floor(Math.random() * 20) + 1}`, date: new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000) .toISOString() .split('T')[0], }); data[i - 1].revenue = Math.round(data[i - 1].price * data[i - 1].quantity * 100) / 100; } } return data; }; // Column definitions for different report types const columnDefinitions = { customers: [ { key: 'id', title: 'ID', dataIndex: 'id', flex: 1 }, { key: 'name', title: 'Customer Name', dataIndex: 'name', flex: 3 }, { key: 'email', title: 'Email', dataIndex: 'email', flex: 3 }, { key: 'amount', title: 'Amount ($)', dataIndex: 'amount', flex: 2, type: 'currency' }, { key: 'date', title: 'Registration Date', dataIndex: 'date', flex: 2 }, { key: 'status', title: 'Status', dataIndex: 'status', flex: 2 }, { key: 'region', title: 'Region', dataIndex: 'region', flex: 2 }, { key: 'tier', title: 'Tier', dataIndex: 'tier', flex: 1 }, ], sales: [ { key: 'id', title: 'Sale ID', dataIndex: 'id', flex: 1 }, { key: 'product', title: 'Product', dataIndex: 'product', flex: 3 }, { key: 'category', title: 'Category', dataIndex: 'category', flex: 2 }, { key: 'price', title: 'Unit Price', dataIndex: 'price', flex: 2, type: 'currency' }, { key: 'quantity', title: 'Qty', dataIndex: 'quantity', flex: 1 }, { key: 'revenue', title: 'Revenue', dataIndex: 'revenue', flex: 2, type: 'currency' }, { key: 'salesperson', title: 'Sales Rep', dataIndex: 'salesperson', flex: 2 }, { key: 'date', title: 'Sale Date', dataIndex: 'date', flex: 2 }, ], }; // Register advanced templates registerTemplate('modern-business', params => { const { title, data, columns, userInfo, translationFn: t = k => k } = params; const formatValue = (value, type) => { if (type === 'currency' && typeof value === 'number') { return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2 })}`; } return value; }; const getStatusColor = status => { const colors = { Active: '#28a745', Inactive: '#6c757d', Pending: '#ffc107', Suspended: '#dc3545', }; return colors[status] || '#007bff'; }; const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${title}</title> <style> * { box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; color: #333; line-height: 1.6; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.1); } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; text-align: center; position: relative; overflow: hidden; } .header::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px); background-size: 30px 30px; animation: float 20s infinite linear; } @keyframes float { 0% { transform: translate(0, 0); } 100% { transform: translate(-30px, -30px); } } .header h1 { margin: 0; font-size: 2.5em; font-weight: 700; position: relative; z-index: 1; } .header p { margin: 10px 0 0; font-size: 1.1em; opacity: 0.9; position: relative; z-index: 1; } .content { padding: 40px; } .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; } .metric { background: linear-gradient(135deg, #f8f9fa, #e9ecef); padding: 25px; border-radius: 12px; text-align: center; border: 1px solid #dee2e6; transition: transform 0.3s ease; } .metric:hover { transform: translateY(-5px); } .metric-value { font-size: 2.2em; font-weight: 700; margin-bottom: 8px; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .metric-label { color: #666; font-weight: 500; } table { width: 100%; border-collapse: collapse; background: white; border-radius: 12px; overflow: hidden; margin: 20px 0; box-shadow: 0 4px 20px rgba(0,0,0,0.08); } th { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 18px 12px; text-align: left; font-weight: 600; font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.5px; } td { padding: 15px 12px; border-bottom: 1px solid #f1f3f4; } tr:hover { background-color: #f8f9fa; } tr:last-child td { border-bottom: none; } .status-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.85em; font-weight: 600; color: white; text-align: center; } .summary { background: linear-gradient(135deg, #f8f9fa, #e9ecef); padding: 30px; border-radius: 12px; margin-top: 40px; border: 1px solid #dee2e6; } .summary h3 { margin: 0 0 20px; color: #495057; font-weight: 600; display: flex; align-items: center; } .summary h3::before { content: '📈'; margin-right: 10px; font-size: 1.3em; } </style> </head> <body> <div class="container"> <div class="header"> <h1>${t(title)}</h1> <p>Generated on ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', })}</p> </div> <div class="content"> ${ userInfo?.companyName ? ` <div style="text-align: center; margin-bottom: 40px;"> <h2 style="color: #667eea; margin: 0;">${userInfo.companyName}</h2> ${userInfo.name ? `<p style="margin: 5px 0; color: #666;">Prepared by: ${userInfo.name}</p>` : ''} </div> ` : '' } <div class="metrics"> <div class="metric"> <div class="metric-value">${data.length.toLocaleString()}</div> <div class="metric-label">Total Records</div> </div> <div class="metric"> <div class="metric-value">${data.filter(item => item.status === 'Active').length}</div> <div class="metric-label">Active Items</div> </div> <div class="metric"> <div class="metric-value">${ data.length > 0 && data[0].amount ? formatValue( data.reduce((sum, item) => sum + (item.amount || item.revenue || 0), 0) / data.length, 'currency' ) : 'N/A' }</div> <div class="metric-label">Average Value</div> </div> <div class="metric"> <div class="metric-value">${new Set(data.map(item => item.region || item.category)).size}</div> <div class="metric-label">Categories</div> </div> </div> <table> <thead> <tr> ${columns.map(col => `<th>${col.title}</th>`).join('')} </tr> </thead> <tbody> ${data .slice(0, 50) .map( (item, index) => ` <tr> ${columns .map(col => { const value = item[col.dataIndex]; if (col.dataIndex === 'status') { return `<td><span class="status-badge" style="background-color: ${getStatusColor(value)}">${value}</span></td>`; } return `<td>${formatValue(value, col.type)}</td>`; }) .join('')} </tr> ` ) .join('')} </tbody> </table> ${ data.length > 50 ? ` <p style="text-align: center; color: #666; font-style: italic;"> Showing first 50 of ${data.length.toLocaleString()} records </p> ` : '' } <div class="summary"> <h3>Report Summary</h3> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;"> <div> <strong>Dataset Size:</strong> ${data.length.toLocaleString()} records<br> <strong>Generated:</strong> ${new Date().toLocaleString()}<br> <strong>Format:</strong> PDF Report </div> <div> <strong>Processing:</strong> Optimized rendering<br> <strong>Quality:</strong> High resolution<br> <strong>Status:</strong> ✅ Complete </div> </div> </div> </div> </div> </body> </html> `; return { html, header: `<div style="text-align: center; padding: 10px; background: #667eea; color: white; font-weight: 600;">${title} - Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>`, footer: `<div style="text-align: center; padding: 8px; font-size: 10px; color: #666; border-top: 1px solid #eee;">Generated by Safeersoft PDF Reporter | ${new Date().toLocaleDateString()}</div>`, }; }); // Middleware for error handling const asyncHandler = fn => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; // Middleware for request validation const validatePdfRequest = (req, res, next) => { const { title, data, columns } = req.body; if (!title || typeof title !== 'string' || title.trim().length === 0) { return res.status(400).json({ error: 'Invalid title: must be a non-empty string', code: 'VALIDATION_ERROR', }); } if (!Array.isArray(data)) { return res.status(400).json({ error: 'Invalid data: must be an array', code: 'VALIDATION_ERROR', }); } if (!Array.isArray(columns) || columns.length === 0) { return res.status(400).json({ error: 'Invalid columns: must be a non-empty array', code: 'VALIDATION_ERROR', }); } next(); }; // Routes // Health check app.get('/', (req, res) => { res.json({ service: 'Safeersoft PDF Reporter API', version: '1.0.1', status: 'healthy', uptime: process.uptime(), memory: process.memoryUsage(), endpoints: { 'GET /': 'API information', 'GET /api/health': 'Detailed health check', 'GET /api/metrics': 'Service metrics', 'POST /api/generate': 'Generate PDF from data', 'POST /api/generate/async': 'Generate PDF asynchronously', 'POST /api/estimate': 'Estimate PDF generation performance', 'POST /api/merge': 'Merge multiple PDFs', 'POST /api/split': 'Split PDF into pages', 'POST /api/extract': 'Extract specific pages', 'POST /api/analyze': 'Analyze PDF properties', 'GET /api/samples/:type': 'Get sample data', 'GET /api/job/:id': 'Get async job status', }, documentation: 'https://github.com/HassanHamdanDev/@safeersoft/@safeersoft/pdf-reporter#api', }); }); // Detailed health check app.get( '/api/health', asyncHandler(async (req, res) => { const health = { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), environment: process.env.NODE_ENV || 'development', version: process.version, metrics: { requests: metrics.requests, successRate: metrics.requests > 0 ? ((metrics.successful / metrics.requests) * 100).toFixed(2) + '%' : '0%', averageProcessingTime: metrics.requests > 0 ? (metrics.totalProcessingTime / metrics.requests).toFixed(0) + 'ms' : '0ms', }, storage: { uploadDir: fs.existsSync(UPLOAD_DIR), outputDir: fs.existsSync(OUTPUT_DIR), cacheDir: fs.existsSync(CACHE_DIR), }, }; res.json(health); }) ); // Service metrics app.get('/api/metrics', (req, res) => { res.json({ requests: metrics.requests, successful: metrics.successful, failed: metrics.failed, successRate: metrics.requests > 0 ? ((metrics.successful / metrics.requests) * 100).toFixed(2) + '%' : '0%', averageProcessingTime: metrics.requests > 0 ? (metrics.totalProcessingTime / metrics.requests).toFixed(0) + 'ms' : '0ms', averageFileSize: metrics.averageFileSize > 0 ? (metrics.averageFileSize / 1024).toFixed(2) + 'KB' : '0KB', templatesUsed: Array.from(metrics.templates), uptime: process.uptime(), memory: process.memoryUsage(), activeJobs: jobs.size, completedJobs: jobResults.size, }); }); // Get sample data app.get('/api/samples/:type?', (req, res) => { const { type = 'customers' } = req.params; const { count = 100 } = req.query; const parsedCount = Math.min(parseInt(count, 10) || 100, 1000); // Limit to 1000 records try { const data = generateSampleData(parsedCount, type); const columns = columnDefinitions[type] || columnDefinitions.customers; res.json({ type, count: parsedCount, data, columns, totalRecords: data.length, sampleQuery: { url: `/api/generate`, method: 'POST', body: { title: `${type.charAt(0).toUpperCase() + type.slice(1)} Report`, data, columns, options: { template: 'modern-business', format: 'A4', orientation: 'portrait', }, }, }, }); } catch (error) { res.status(500).json({ error: 'Failed to generate sample data', message: error.message, code: 'SAMPLE_GENERATION_ERROR', }); } }); // Generate PDF app.post( '/api/generate', validatePdfRequest, asyncHandler(async (req, res) => { const startTime = Date.now(); metrics.requests++; try { const { title, data, columns, options = {}, userInfo = {}, template = 'modern-business', } = req.body; // Track template usage metrics.templates.add(template); // Generate PDF const pdfBuffer = await generatePdf({ title, data, columns, options: { template, format: options.format || 'A4', orientation: options.orientation || 'portrait', quality: options.quality || 'high', ...options, }, userInfo, logger: consoleLogger, }); const processingTime = Date.now() - startTime; metrics.totalProcessingTime += processingTime; metrics.successful++; // Update average file size if (metrics.averageFileSize === 0) { metrics.averageFileSize = pdfBuffer.length; } else { metrics.averageFileSize = (metrics.averageFileSize + pdfBuffer.length) / 2; } // Set headers for PDF download res.setHeader('Content-Type', 'application/pdf'); res.setHeader( 'Content-Disposition', `attachment; filename="${title.replace(/[^a-zA-Z0-9]/g, '_')}.pdf"` ); res.setHeader('Content-Length', pdfBuffer.length); res.setHeader('X-Processing-Time', `${processingTime}ms`); res.setHeader('X-Records-Count', data.length); res.setHeader('X-Template-Used', template); res.send(pdfBuffer); } catch (error) { metrics.failed++; consoleLogger.error('PDF generation failed:', error); res.status(500).json({ error: 'PDF generation failed', message: error.message, code: 'PDF_GENERATION_ERROR', processingTime: Date.now() - startTime, }); } }) ); // Generate PDF asynchronously app.post( '/api/generate/async', validatePdfRequest, asyncHandler(async (req, res) => { const jobId = uuidv4(); const startTime = Date.now(); // Store job info jobs.set(jobId, { id: jobId, status: 'pending', createdAt: new Date(), estimatedCompletion: null, progress: 0, }); // Start async processing (async () => { try { const { title, data, columns, options = {}, userInfo = {}, template = 'modern-business', } = req.body; // Update job status jobs.set(jobId, { ...jobs.get(jobId), status: 'processing', progress: 25 }); // Estimate processing time const estimation = await estimatePdfGeneration({ data, columns }); jobs.set(jobId, { ...jobs.get(jobId), estimatedCompletion: new Date(Date.now() + estimation.estimatedTime), progress: 50, }); // Generate PDF const pdfBuffer = await generatePdf({ title, data, columns, options: { template, format: options.format || 'A4', orientation: options.orientation || 'portrait', quality: options.quality || 'high', ...options, }, userInfo, logger: consoleLogger, }); // Store result const processingTime = Date.now() - startTime; jobResults.set(jobId, { jobId, pdf: pdfBuffer, metadata: { title, recordCount: data.length, template, processingTime, fileSize: pdfBuffer.length, completedAt: new Date(), }, }); // Update job status jobs.set(jobId, { ...jobs.get(jobId), status: 'completed', progress: 100, completedAt: new Date(), }); } catch (error) { jobs.set(jobId, { ...jobs.get(jobId), status: 'failed', error: error.message, failedAt: new Date(), }); } })(); res.json({ jobId, status: 'accepted', message: 'PDF generation started', checkStatusUrl: `/api/job/${jobId}`, estimatedTime: '30-60 seconds', }); }) ); // Get async job status app.get('/api/job/:id', (req, res) => { const { id } = req.params; const job = jobs.get(id); if (!job) { return res.status(404).json({ error: 'Job not found', code: 'JOB_NOT_FOUND', }); } const response = { ...job }; if (job.status === 'completed') { const result = jobResults.get(id); if (result) { response.downloadUrl = `/api/job/${id}/download`; response.metadata = result.metadata; } } res.json(response); }); // Download completed job result app.get('/api/job/:id/download', (req, res) => { const { id } = req.params; const job = jobs.get(id); const result = jobResults.get(id); if (!job || !result) { return res.status(404).json({ error: 'Job or result not found', code: 'RESULT_NOT_FOUND', }); } if (job.status !== 'completed') { return res.status(400).json({ error: 'Job not completed yet', status: job.status, code: 'JOB_NOT_COMPLETED', }); } // Set headers for PDF download res.setHeader('Content-Type', 'application/pdf'); res.setHeader( 'Content-Disposition', `attachment; filename="${result.metadata.title.replace(/[^a-zA-Z0-9]/g, '_')}.pdf"` ); res.setHeader('Content-Length', result.pdf.length); res.setHeader('X-Job-Id', id); res.setHeader('X-Processing-Time', `${result.metadata.processingTime}ms`); res.send(result.pdf); }); // Estimate PDF generation performance app.post( '/api/estimate', asyncHandler(async (req, res) => { try { const { data, columns, options = {} } = req.body; if (!Array.isArray(data) || !Array.isArray(columns)) { return res.status(400).json({ error: 'Invalid input: data and columns must be arrays', code: 'VALIDATION_ERROR', }); } const estimation = await estimatePdfGeneration({ data, columns, options, }); res.json({ ...estimation, recommendation: estimation.estimatedTime > 30000 ? 'Consider using async generation for better user experience' : 'Synchronous generation recommended', asyncEndpoint: '/api/generate/async', }); } catch (error) { res.status(500).json({ error: 'Estimation failed', message: error.message, code: 'ESTIMATION_ERROR', }); } }) ); // Merge PDFs app.post( '/api/merge', asyncHandler(async (req, res) => { try { const { pdfs, options = {} } = req.body; if (!Array.isArray(pdfs) || pdfs.length < 2) { return res.status(400).json({ error: 'At least 2 PDFs required for merging', code: 'VALIDATION_ERROR', }); } // Convert base64 strings to buffers if needed const pdfBuffers = pdfs.map(pdf => { if (typeof pdf === 'string') { return Buffer.from(pdf, 'base64'); } return Buffer.from(pdf); }); const mergedPdf = await mergePdfs(pdfBuffers, { logger: consoleLogger, ...options, }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'attachment; filename="merged-document.pdf"'); res.setHeader('Content-Length', mergedPdf.length); res.setHeader('X-Source-Count', pdfs.length); res.send(mergedPdf); } catch (error) { res.status(500).json({ error: 'PDF merge failed', message: error.message, code: 'MERGE_ERROR', }); } }) ); // Split PDF app.post( '/api/split', asyncHandler(async (req, res) => { try { const { pdf, options = {} } = req.body; if (!pdf) { return res.status(400).json({ error: 'PDF data required', code: 'VALIDATION_ERROR', }); } const pdfBuffer = typeof pdf === 'string' ? Buffer.from(pdf, 'base64') : Buffer.from(pdf); const pages = await splitPdf(pdfBuffer, { logger: consoleLogger, ...options, }); res.json({ totalPages: pages.length, pages: pages.map((page, index) => ({ pageNumber: index + 1, size: page.length, data: page.toString('base64'), })), message: `PDF split into ${pages.length} pages`, }); } catch (error) { res.status(500).json({ error: 'PDF split failed', message: error.message, code: 'SPLIT_ERROR', }); } }) ); // Extract specific pages app.post( '/api/extract', asyncHandler(async (req, res) => { try { const { pdf, pages, options = {} } = req.body; if (!pdf || !Array.isArray(pages)) { return res.status(400).json({ error: 'PDF data and pages array required', code: 'VALIDATION_ERROR', }); } const pdfBuffer = typeof pdf === 'string' ? Buffer.from(pdf, 'base64') : Buffer.from(pdf); const extractedPdf = await extractPages(pdfBuffer, pages, { logger: consoleLogger, ...options, }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader( 'Content-Disposition', `attachment; filename="extracted-pages-${pages.join('-')}.pdf"` ); res.setHeader('Content-Length', extractedPdf.length); res.setHeader('X-Extracted-Pages', pages.join(',')); res.send(extractedPdf); } catch (error) { res.status(500).json({ error: 'Page extraction failed', message: error.message, code: 'EXTRACTION_ERROR', }); } }) ); // Analyze PDF app.post( '/api/analyze', asyncHandler(async (req, res) => { try { const { pdfs } = req.body; if (!Array.isArray(pdfs) || pdfs.length === 0) { return res.status(400).json({ error: 'PDFs array required', code: 'VALIDATION_ERROR', }); } const pdfBuffers = pdfs.map(pdf => typeof pdf === 'string' ? Buffer.from(pdf, 'base64') : Buffer.from(pdf) ); const analysis = await analyzePdfs(pdfBuffers, { logger: consoleLogger, }); res.json({ analysis, summary: { totalPdfs: pdfs.length, totalPages: analysis.reduce((sum, pdf) => sum + pdf.pageCount, 0), totalSize: analysis.reduce((sum, pdf) => sum + pdf.fileSize, 0), averageFileSize: analysis.reduce((sum, pdf) => sum + pdf.fileSize, 0) / pdfs.length, }, }); } catch (error) { res.status(500).json({ error: 'PDF analysis failed', message: error.message, code: 'ANALYSIS_ERROR', }); } }) ); // Error handling middleware app.use((error, req, res, next) => { console.error('Unhandled error:', error); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong', code: 'INTERNAL_ERROR', timestamp: new Date().toISOString(), }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found', message: `${req.method} ${req.path} is not a valid endpoint`, code: 'NOT_FOUND', availableEndpoints: [ 'GET /', 'GET /api/health', 'GET /api/metrics', 'GET /api/samples/:type', 'POST /api/generate', 'POST /api/generate/async', 'POST /api/estimate', 'POST /api/merge', 'POST /api/split', 'POST /api/extract', 'POST /api/analyze', 'GET /api/job/:id', 'GET /api/job/:id/download', ], }); }); // Cleanup old jobs periodically setInterval( () => { const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours for (const [jobId, job] of jobs.entries()) { const age = now - job.createdAt.getTime(); if (age > maxAge) { jobs.delete(jobId); jobResults.delete(jobId); } } }, 60 * 60 * 1000 ); // Run every hour // Start server app.listen(port, () => { console.log(`🚀 Safeersoft PDF Reporter API running on http://localhost:${port}`); console.log(`📖 API Documentation: http://localhost:${port}/`); console.log(`🏥 Health Check: http://localhost:${port}/api/health`); console.log(`📊 Metrics: http://localhost:${port}/api/metrics`); console.log(`📁 Sample Data: http://localhost:${port}/api/samples/customers`); console.log(''); console.log('Available endpoints:'); console.log(' POST /api/generate - Generate PDF from data'); console.log(' POST /api/generate/async - Generate PDF asynchronously'); console.log(' POST /api/estimate - Estimate generation performance'); console.log(' POST /api/merge - Merge multiple PDFs'); console.log(' POST /api/split - Split PDF into pages'); console.log(' POST /api/extract - Extract specific pages'); console.log(' POST /api/analyze - Analyze PDF properties'); console.log(' GET /api/samples/:type - Get sample data (customers/sales)'); console.log(''); console.log('Example usage:'); console.log(` curl -X GET http://localhost:${port}/api/samples/customers`); console.log(` curl -X POST http://localhost:${port}/api/generate \\`); console.log(' -H "Content-Type: application/json" \\'); console.log(' -d \'{"title":"Test Report","data":[...],"columns":[...]}\''); });