UNPKG

safeer-pdf-generator

Version:

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

526 lines (415 loc) 17.4 kB
# safeer-pdf-generator [![npm version](https://badge.fury.io/js/safeer-pdf-generator.svg)](https://badge.fury.io/js/safeer-pdf-generator) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Downloads](https://img.shields.io/npm/dm/safeer-pdf-generator.svg)](https://www.npmjs.com/package/safeer-pdf-generator) A powerful, framework-agnostic TypeScript library for generating PDF reports with chunking, merging, S3 upload, email delivery, lifecycle events, and webhook dispatch. ## ✨ Features - 🚀 **High Performance** — Concurrent chunk processing with memory optimization - 📊 **3 Built-in Templates** — `default`, `simple`, and `modern` with full customization - 🎨 **Custom Templates** — Inline functions, CSS overrides, or register your own - 🔧 **Framework Agnostic** — Works with Express, NestJS, Fastify, or standalone - ☁️ **S3 & S3-Compatible** — Direct upload to AWS S3, Cloudflare R2, MinIO, Backblaze B2, etc. - 📁 **Local File Output** — Write PDFs to disk alongside S3/email/webhook - 📧 **Email Delivery** — Send PDFs as attachments or download links - 🔀 **PDF Operations** — Merge, split, and manipulate existing PDFs - 🌐 **BYOB Browser** — Bring Your Own Browser for connection pooling - 📡 **Lifecycle Events** — Typed event emitters for every stage (incl. per-chunk progress) - 🔔 **Webhook Dispatcher** — Auto-POST to your backend with HMAC signing - 💾 **Memory Efficient** — Handle large datasets (100k+ records) - 🛡️ **TypeScript** — Full type safety and IntelliSense support ## 📦 Installation ### Prerequisites This package requires Chromium for PDF generation: ```bash # Install Chromium sudo apt-get install chromium-browser # Or set custom path export PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser ``` ### Package Installation ```bash npm install safeer-pdf-generator # Optional peer dependencies npm install @aws-sdk/client-s3 nodemailer ``` ## 🚀 Quick Start ```typescript import { generatePdf } from 'safeer-pdf-generator'; const result = await generatePdf({ title: 'Sales Report', data: [ { name: 'John Doe', sales: 1200, region: 'North' }, { name: 'Jane Smith', sales: 1500, region: 'South' }, ], columns: [ { key: 'name', title: 'Name', dataIndex: 'name' }, { key: 'sales', title: 'Sales', dataIndex: 'sales' }, { key: 'region', title: 'Region', dataIndex: 'region' }, ], }); console.log(`PDF: ${result.fileName} (${result.pageCount} pages, ${result.durationMs}ms)`); ``` ## 🎨 Custom Templates ### Built-in Templates Three templates are available out of the box: ```typescript // Modern template (gradient header, card-style info) await generatePdf({ ...options, template: 'modern' }); // Default template (classic table with company header) await generatePdf({ ...options, template: 'default' }); // Simple template (minimal, lightweight) await generatePdf({ ...options, template: 'simple' }); ``` ### Custom CSS & HTML Injection Override styles and add HTML blocks using any built-in template: ```typescript const result = await generatePdf({ title: 'Styled Report', data, columns, template: 'modern', customCss: ` .modern-table th { background: linear-gradient(135deg, #7c3aed, #2563eb) !important; color: #fff !important; } `, customHtml: { beforeTable: '<div style="padding: 12px; background: #f5f3ff;">Q4 2025 Summary</div>', afterTable: '<div style="font-size: 11px; color: #64748b;">Confidential</div>', }, }); ``` ### Inline Template Function Full control over the generated HTML: ```typescript const result = await generatePdf({ title: 'Custom Report', data, columns, template: (params) => { const { title, data, columns } = params; const html = ` <!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>${title}</title></head> <body> <h1>${title}</h1> <table> <thead><tr>${columns.map(c => `<th>${c.title}</th>`).join('')}</tr></thead> <tbody> ${data.map(row => `<tr>${columns.map(c => `<td>${row[c.dataIndex] ?? '—'}</td>`).join('')}</tr>` ).join('')} </tbody> </table> </body> </html> `; return { html, header: '', footer: '' }; }, }); ``` ### Registered Templates Register a reusable template globally: ```typescript import { registerTemplate, generatePdf } from 'safeer-pdf-generator'; registerTemplate('invoice', (params) => { const { title, data, columns } = params; // ... return { html, header, footer } }); // Use it by name anywhere await generatePdf({ ...options, template: 'invoice' }); ``` ## 🌐 BYOB (Bring Your Own Browser) Reuse a shared Puppeteer browser instance across multiple generations — saves memory and startup time: ```typescript import puppeteer from 'puppeteer'; import { generatePdf } from 'safeer-pdf-generator'; const browser = await puppeteer.launch({ headless: true }); // Generate multiple PDFs using the same browser for (const report of reports) { await generatePdf({ ...report, puppeteer: { browserInstance: browser }, }); } // You manage the browser lifecycle await browser.close(); ``` > **Note:** BYOB requires `safeer-pdf-generator >= 1.3.3`. On 1.3.2 and > earlier, passing a Browser instance via `puppeteer.browserInstance` > throws `browser.newPage is not a function`. Upgrade or pin > `^1.3.3`. ## 📡 Lifecycle Events Listen for typed events during PDF generation: ```typescript import { generatePdf, PdfEventEmitter } from 'safeer-pdf-generator'; const events = new PdfEventEmitter(); events.onEvent('generation:started', ({ title, rowCount }) => { console.log(`Generating: ${title} (${rowCount} rows)`); }); // Per-chunk progress — fires once per chunk during chunked generation events.onEvent('chunk:processed', ({ chunkIndex, totalChunks, sizeBytes }) => { const pct = (((chunkIndex + 1) / totalChunks) * 100).toFixed(0); console.log(`Chunk ${chunkIndex + 1}/${totalChunks} done (${sizeBytes} bytes) — ${pct}%`); }); events.onEvent('generation:complete', ({ result }) => { console.log(`Done: ${result.pageCount} pages in ${result.durationMs}ms`); }); events.onEvent('s3:upload:complete', ({ url }) => { notifyClient(url); }); events.onEvent('file:saved', ({ path, sizeBytes }) => { console.log(`Wrote ${sizeBytes} bytes to ${path}`); }); events.onEvent('error', ({ error, phase }) => { alertOps(`PDF failed at ${phase}: ${error.message}`); }); await generatePdf({ ...options, events }); ``` **Available events:** | Event | Payload | When | |---|---|---| | `generation:started` | `{ title, rowCount, timestamp }` | Generation begins | | `chunk:processed` | `{ chunkIndex, totalChunks, sizeBytes }` | Each chunk finishes rendering (chunked path only) | | `generation:complete` | `{ result }` | Final PDF assembled | | `s3:upload:complete` | `{ url, key, requestPayload? }` | S3 upload finishes | | `file:saved` | `{ path, sizeBytes }` | Local file written (when `localFs` is set) | | `email:sent` | `{ to, messageId? }` | Email accepted by SMTP server | | `error` | `{ error, phase, stack? }` | Any phase fails | > **Note:** `chunk:processed` requires `>= 1.3.4`. `file:saved` requires `>= 1.3.6`. On earlier versions these events were declared in the type map but never fired. ## 🔔 Webhook Dispatcher Automatically POST to your backend when a PDF is ready. Includes HMAC-SHA256 signing and retry logic: ```typescript await generatePdf({ ...options, webhook: { url: 'https://api.example.com/webhooks/pdf-ready', secret: process.env.WEBHOOK_SECRET, // HMAC-SHA256 signing metadata: { tenantId: 'acme', userId: 'u-123' }, // Pass-through data timeoutMs: 10000, }, }); // Your endpoint receives: // POST { s3Url, title, fileName, sizeBytes, pageCount, durationMs, metadata } // Headers: { X-Webhook-Signature: "sha256=abc123..." } ``` ## ☁️ S3 Upload & 📧 Email ```typescript const result = await generatePdf({ title: 'Monthly Report', data: salesData, columns: [ { key: 'name', title: 'Sales Rep', dataIndex: 'name' }, { key: 'sales', title: 'Sales', dataIndex: 'sales' }, ], s3: { bucket: 'my-pdfs', region: 'us-east-1', accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, email: { to: 'user@example.com', attachmentMode: 'link', smtp: { host: 'smtp.gmail.com', port: 587, secure: false, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }, }, }, }); console.log(`Uploaded: ${result.s3?.url}`); ``` ### S3-Compatible Storage (R2, MinIO, B2, Spaces, Wasabi) Set the `endpoint` field. The package uses `@aws-sdk/client-s3` under the hood, so any S3-compatible provider works: ```typescript // Cloudflare R2 s3: { bucket: 'reports', region: 'auto', endpoint: 'https://<account-id>.r2.cloudflarestorage.com', accessKeyId: process.env.R2_ACCESS_KEY_ID, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY, } // MinIO (self-hosted) s3: { bucket: 'reports', region: 'us-east-1', endpoint: 'http://minio.internal:9000', accessKeyId: process.env.MINIO_ACCESS_KEY, secretAccessKey: process.env.MINIO_SECRET_KEY, } // Backblaze B2 s3: { bucket: 'reports', region: 'us-west-002', endpoint: 'https://s3.us-west-002.backblazeb2.com', accessKeyId: process.env.B2_KEY_ID, secretAccessKey: process.env.B2_APP_KEY, } ``` ## 📁 Local File Output Write the generated PDF to local disk. Composes with `s3`, `email`, and `webhook` — every destination runs on the same call: ```typescript import { generatePdf } from 'safeer-pdf-generator'; const result = await generatePdf({ title: 'Sales Report', data, columns, localFs: { path: '/var/reports', // directory; created if missing // filename: 'custom.pdf', // optional override; defaults to result.fileName // createDir: true, // mkdir -p (default true) // overwrite: true, // when false, throws if target exists }, }); console.log(`Saved: ${result.localFs?.path}`); // /var/reports/Sales-Report-2026-05-17-...pdf ``` Combine destinations freely — they all run, all populate the result, all emit their own lifecycle event: ```typescript await generatePdf({ title, data, columns, localFs: { path: '/var/archive' }, // local archive copy s3: { bucket: 'cdn', region: 'us-east-1' }, // primary distribution email: { to: 'finance@acme.com', ... }, // notify stakeholders webhook: { url: 'https://api.acme.com/hook' }, // notify backend }); ``` > **Security note:** The `localFs.path` value is used verbatim with `fs.writeFile`. The library does not sandbox it. If the path comes from untrusted input (e.g., an HTTP request body), validate it in your code before passing it in. > > **Requires `>= 1.3.6`.** Earlier versions had `return: 'file'` and `outputPath` fields declared in the type, but neither was implemented — they are now marked `@deprecated` in favor of `localFs`. ## 🏗️ Framework Examples ### Express.js ```typescript import express from 'express'; import { generatePdf } from 'safeer-pdf-generator'; const app = express(); app.post('/generate-report', async (req, res) => { try { const result = await generatePdf({ title: req.body.title, data: req.body.data, columns: req.body.columns, }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="${result.fileName}"`); res.send(result.buffer); } catch (error) { res.status(500).json({ error: 'PDF generation failed' }); } }); ``` ### NestJS ```typescript import { Injectable } from '@nestjs/common'; import { generatePdf, PdfEventEmitter } from 'safeer-pdf-generator'; @Injectable() export class PdfService { async createReport(data: any[], title: string) { const events = new PdfEventEmitter(); events.onEvent('generation:complete', ({ result }) => { this.logger.log(`PDF ready: ${result.fileName}`); }); return generatePdf({ title, data, columns: [ { key: 'id', title: 'ID', dataIndex: 'id' }, { key: 'name', title: 'Name', dataIndex: 'name' }, ], template: 'modern', events, }); } } ``` ## 📊 Large Dataset Handling ```typescript import { generatePdf } from 'safeer-pdf-generator'; const result = await generatePdf({ title: 'Large Report', data: largeDataset, // 100,000+ records columns, chunking: { enabled: true, chunkSize: 500, // Rows per chunk maxConcurrency: 4, // Parallel chunk processing }, }); ``` ## 🔀 PDF Merging ```typescript import { mergePdfs } from 'safeer-pdf-generator'; const merged = await mergePdfs({ buffers: [buffer1, buffer2, buffer3], }); ``` ## 🔧 Configuration Reference ### `PdfGenerationOptions` | Option | Type | Default | Description | |---|---|---|---| | `title` | `string` | **required** | Report title | | `data` | `any[]` | **required** | Array of data rows | | `columns` | `ColumnDefinition[]` | **required** | Column definitions | | `template` | `string \| Function` | `'default'` | Template name or compiler function | | `customCss` | `string` | `undefined` | CSS injected into the template | | `customHtml` | `{ beforeTable?, afterTable? }` | `undefined` | HTML injected around the table | | `locale` | `string` | `'en'` | Locale (`'en'`, `'ar'` for RTL) | | `userInfo` | `UserInfo` | `undefined` | User info shown in header | | `infoSection` | `InfoSection[]` | `undefined` | Key-value info cards | | `chunking` | `ChunkingOptions` | `{ enabled: true, chunkSize: 100 }` | Chunk processing config | | `s3` | `S3UploadConfig \| false` | `false` | S3 / S3-compatible upload configuration | | `localFs` | `LocalFsConfig \| false` | `false` | Write PDF to local disk (since 1.3.6) | | `email` | `EmailSendConfig \| false` | `false` | Email delivery configuration | | `webhook` | `WebhookConfig \| false` | `false` | Webhook dispatch configuration | | `events` | `PdfEventEmitter` | `undefined` | Lifecycle event emitter | | `puppeteer` | `PuppeteerOptions` | `{ headless: true }` | Puppeteer launch options | | `puppeteer.browserInstance` | `Browser` | `undefined` | External browser (BYOB) | | `hooks` | `HooksConfig` | `undefined` | Sync lifecycle hooks | | `logging` | `LoggingAdapter` | `noOpLogger` | Logger implementation | | `timeoutMs` | `number` | `300000` | Global timeout (ms) | ### `ColumnDefinition` | Field | Type | Description | |---|---|---| | `key` | `string` | Unique column identifier | | `title` | `string` | Column header text | | `dataIndex` | `string` | Property path in data objects (supports nested: `'address.city'`) | | `type` | `'image' \| 'boolean' \| 'text' \| 'link'` | Optional column type for custom rendering | ### `WebhookConfig` | Field | Type | Default | Description | |---|---|---|---| | `url` | `string` | **required** | Endpoint URL | | `secret` | `string` | `undefined` | HMAC-SHA256 secret | | `metadata` | `Record<string, any>` | `undefined` | Pass-through data | | `timeoutMs` | `number` | `10000` | Request timeout | ### `LocalFsConfig` | Field | Type | Default | Description | |---|---|---|---| | `path` | `string` | **required** | Destination directory. Relative paths resolve from `process.cwd()`. | | `filename` | `string` | `result.fileName` | Override the auto-generated filename. | | `createDir` | `boolean` | `true` | Create parent directories recursively if missing. | | `overwrite` | `boolean` | `true` | When `false`, throws `LocalFsError` if the target exists. | The corresponding result field: `result.localFs = { path: string, sizeBytes: number }` (absolute path to the written file). ## 🌟 Why Choose safeer-pdf-generator? - **Production Ready** — Used in enterprise applications - **Memory Efficient** — Handles massive datasets without memory issues - **Developer Friendly** — Full TypeScript support with IntelliSense - **Framework Agnostic** — Works with any Node.js framework - **Full Featured** — Templates, S3, email, webhooks, events — all in one package - **Active Maintenance** — Regular updates and community support ## 📚 Examples | Example | Description | |---|---| | [simple-usage.js](examples/simple-usage.js) | Basic PDF generation | | [custom-template.js](examples/custom-template.js) | Custom CSS, inline templates, and registry | | [byob-and-events.js](examples/byob-and-events.js) | Browser reuse + lifecycle events | | [local-fs-output.js](examples/local-fs-output.js) | Local disk output + `chunk:processed` progress events | | [webhook-integration.js](examples/webhook-integration.js) | Webhook dispatch with HMAC signing | | [express-app/](examples/express-app/) | Full Express.js integration | | [nestjs-simplified/](examples/nestjs-simplified/) | NestJS service example | ## 📄 License MIT © [Safeersoft](https://github.com/Safeersoft) ## 🔗 Links - [NPM Package](https://www.npmjs.com/package/safeer-pdf-generator) - [GitHub Repository](https://github.com/Safeersoft/pdf-reporter) - [Issues & Support](https://github.com/Safeersoft/pdf-reporter/issues)