diff --git a/README.md b/README.md index b8c01a4..680226a 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,7 @@ key | type | required | description --- | ---- | -------- | ----------- url | string | yes | The URL to generate a PDF from meta | object | | Optional meta data object to send back to the webhook url +doctype | string | | Optional identifier to use special configuration settings per document #### Example @@ -366,6 +367,60 @@ module.exports = { ``` +## Document Level Settings +Optionally, you can specify different settings using a document type identifier for your documents. + +This allows for using document specific webhook, generator options (such as printOptions), or storage configurations. + +```javascript + +module.exports = { + // The settings of the API + api: { + // The port your express.js instance listens to requests from. (default: 3000) + port: 3000, + // Spawn command when a job has been pushed to the API + postPushCommand: ['/home/user/.npm-global/bin/pdf-bot', ['-c', './pdf-bot.config.js', 'shift:all']], + // The token used to validate requests to your API. Not required, but 100% recommended. + token: 'api-token' + }, + + document: { + //a label for configuration to refer to in requests + 'foo_doc': { + storagekey: 's3', + generator: { + printOptions: { + //... special print options here (like margins and headers) + } + }, + //override webhook + webhook: { + // The endpoint to send PDF messages to. + url: 'http://localhost:3000/webhooks/foo_pdf' + } + } + 'bar_doc': { + //use default generator settings + //override webhook + webhook: { + // The endpoint to send PDF messages to. + url: 'http://localhost:3000/webhooks/bar_pdf' + } + } + } + // html-pdf-chrome + generator: { + // Triggers that specify when the PDF should be generated + completionTrigger: new htmlPdf.CompletionTrigger.Timer(1000), // waits for 1 sec + // The port to listen for Chrome (default: 9222) + port: 9222 + } + // ... other options +} +``` + + ## Options ```javascript diff --git a/bin/pdf-bot.js b/bin/pdf-bot.js index 49b8272..2ad2fae 100755 --- a/bin/pdf-bot.js +++ b/bin/pdf-bot.js @@ -39,23 +39,20 @@ var defaultConfig = { }, db: lowDb(), // html-pdf-chrome options - generator: { - - }, + generator: {}, queue: { - generationRetryStrategy: function(job, retries) { + generationRetryStrategy: function (job, retries) { return decaySchedule[retries - 1] ? decaySchedule[retries - 1] : 0 }, generationMaxTries: 5, parallelism: 4, - webhookRetryStrategy: function(job, retries) { + webhookRetryStrategy: function (job, retries) { return decaySchedule[retries - 1] ? decaySchedule[retries - 1] : 0 }, webhookMaxTries: 5, - lowDbOptions: { - - } + lowDbOptions: {} }, + document: {}, storage: { /* 's3': createS3Config({ @@ -93,7 +90,7 @@ program port: port, postPushCommand: apiOptions.postPushCommand, token: apiOptions.token - }).listen(port, function() { + }).listen(port, function () { debug('Listening to port %d', port) }) }) @@ -106,17 +103,17 @@ program function startPrompt() { prompt.start({noHandleSIGINT: true}) prompt.get([ - { - name: 'storagePath', - description: 'Enter a path for storage', - default: path.join(process.cwd(), 'pdf-storage'), - required: true - }, - { - name: 'token', - description: 'An access token for your API', - required: false - }], function (err, result) { + { + name: 'storagePath', + description: 'Enter a path for storage', + default: path.join(process.cwd(), 'pdf-storage'), + required: true + }, + { + name: 'token', + description: 'An access token for your API', + required: false + }], function (err, result) { if (err) { process.exit(0) } @@ -170,7 +167,7 @@ program program .command('db:migrate') - .action(function() { + .action(function () { openConfig() var db = configuration.db(configuration) @@ -186,7 +183,7 @@ program program .command('db:destroy') - .action(function() { + .action(function () { openConfig() var db = configuration.db(configuration) @@ -205,7 +202,7 @@ program process.exit(0) } else { db.destroy() - .then(function() { + .then(function () { console.log('The database has been destroyed.') db.close() process.exit(0) @@ -218,8 +215,7 @@ program program .command('generate [jobID]') .description('Generate PDF for job') - .action(function (jobId, options){ - openConfig() + .action(function (jobId, options) { return queue.getById(jobId) .then(function (job) { @@ -228,7 +224,7 @@ program queue.close() process.exit(1) } - + openConfig(false, job.doctype) return processJob(job, configuration) }) .catch(handleDbError) @@ -244,7 +240,7 @@ program openConfig() return listJobs(queue, options.failed, options.completed, options.limit) - .then(function() { + .then(function () { queue.close() process.exit(0) }) @@ -264,7 +260,7 @@ program console.log('Job not found.') return; } - + openConfig(false, job.doctype) return ping(job, configuration.webhook).then(response => { queue.close() @@ -280,8 +276,8 @@ program program .command('ping:retry-failed') - .action(function() { - openConfig() + .action(function () { + var maxTries = configuration.queue.webhookMaxTries var retryStrategy = configuration.queue.webhookRetryStrategy @@ -292,7 +288,7 @@ program queue.close() process.exit(0) } - + openConfig(false, next.doctype) return ping(next, configuration.webhook).then(function (response) { queue.close() @@ -325,7 +321,7 @@ program colWidths: [40, 40, 50, 20, 20, 20] }); - for(var i in job.pings) { + for (var i in job.pings) { var ping = job.pings[i] table.push([ @@ -367,13 +363,15 @@ program .command('push [url]') .description('Push new job to the queue') .option('-m, --meta [meta]', 'JSON string with meta data. Default: \'{}\'') + .option('-d, --doctype [doctype]', 'Document identifier to use alternate configuration. Default: \'\'') .action(function (url, options) { openConfig() return queue .addToQueue({ url: url, - meta: JSON.parse(options.meta || '{}') + meta: JSON.parse(options.meta || '{}'), + doctype: options.doctype || '' }) .then(function (response) { queue.close() @@ -404,6 +402,8 @@ program queue.close() process.exit(0) } + //have to repull the document specific configuration, if type has changed + if (next.doctype) openConfig(false, next.doctype) return processJob(next, configuration) }) @@ -415,6 +415,11 @@ program .description('Run all unfinished jobs in the queue') .action(function (url) { openConfig() + const defaulttype = '__default' + var docConfigLookup = { + '__default': configuration + } + return queue.isBusy() .then(function (isBusy) { @@ -445,16 +450,26 @@ program console.log('Running chunk %s, %s chunks left', k, chunks.length) var promises = [] - for(var i in chunk) { - promises.push(processJob(chunk[i], clone(configuration), false)) + for (var i in chunk) { + var next = chunk[i] + var doctype = next.doctype ? next.doctype : defaulttype + + if (!docConfigLookup.hasOwnProperty(doctype)) { + openConfig(doctype) + docConfigLookup[doctype] = configuration + } + + configuration = docConfigLookup[defaulttype] + + promises.push(processJob(next, clone(configuration), false)) } Promise.all(promises) - .then(function(){ + .then(function () { return runNextChunk(k + 1) }) - .catch(function(){ - return queue.setIsBusy(false).then(function() { + .catch(function () { + return queue.setIsBusy(false).then(function () { queue.close() process.exit(1) }) @@ -481,6 +496,7 @@ if (!process.argv.slice(2).length) { program.outputHelp(); } + function processJob(job, configuration, exitProcess = true) { var generatorOptions = configuration.generator var storagePlugins = configuration.storage @@ -504,7 +520,32 @@ function processJob(job, configuration, exitProcess = true) { }) } -function openConfig(delayQueueCreation = false) { +function processConfiguration(configuration, doctype = '') { + var defaultGeneratorOpts = configuration.generator || {} + var defaultWebhookOpts = configuration.webhook + var storagePlugins = configuration.storage || {} + var documentOptions = configuration.document || {} + + var docgen = (documentOptions[doctype] && documentOptions[doctype].generator) + ? documentOptions[doctype].generator : defaultGeneratorOpts + + var generator = Object.assign(defaultGeneratorOpts, docgen) + + var docSpecificConfig = { + //webhooks use the doc specific config or the default config + webhook: (documentOptions[doctype] && documentOptions[doctype].webhook) ? documentOptions[doctype].webhook : defaultWebhookOpts, + storage: (documentOptions[doctype] && documentOptions[doctype].storagekey && storagePlugins[documentOptions[doctype].storagekey]) + ? storagePlugins[documentOptions[doctype].storagekey] + : storagePlugins, + //generators use the default config with changes from the document specific config, if present + generator: generator + } + + return Object.assign(configuration, docSpecificConfig) +} + + +function openConfig(delayQueueCreation = false, doctype = '') { configuration = defaultConfig if (!program.config) { @@ -532,6 +573,8 @@ function openConfig(delayQueueCreation = false) { throw new Error('There is no pdf folder in the storage folder. Create it: storage/pdf') } + configuration = processConfiguration(configuration, doctype) + function initiateQueue() { var db = configuration.db(configuration) var queueOptions = configuration.queue @@ -553,16 +596,17 @@ function listJobs(queue, failed = false, limit) { limit ).then(function (response) { var table = new Table({ - head: ['ID', 'URL', 'Meta', 'PDF Gen. tries', 'Created at', 'Completed at'], - colWidths: [40, 40, 50, 20, 20, 20] + head: ['ID', 'URL', 'Doc Type', 'Meta', 'PDF Gen. tries', 'Created at', 'Completed at'], + colWidths: [40, 40, 20, 45, 20, 20, 20] }); - for(var i in response) { + for (var i in response) { var job = response[i] table.push([ job.id, job.url, + job.doctype, JSON.stringify(job.meta), job.generations.length, formatDate(job.created_at), diff --git a/src/api.js b/src/api.js index e180b7d..ff44222 100644 --- a/src/api.js +++ b/src/api.js @@ -27,7 +27,8 @@ function createApi(createQueue, options = {}) { queue .addToQueue({ url: req.body.url, - meta: req.body.meta || {} + meta: req.body.meta || {}, + doctype: req.body.doctype || '' }).then(function (response) { queue.close() diff --git a/src/error.js b/src/error.js index b42836b..40760c2 100644 --- a/src/error.js +++ b/src/error.js @@ -16,23 +16,26 @@ function getErrorCode(type) { var ERROR_INVALID_TOKEN = 'ERROR_INVALID_TOKEN' var ERROR_INVALID_URL = 'ERROR_INVALID_URL' -var ERROR_HTML_PDF_CHROME_ERROR = 'ERROR_HTML_PDF_CHROME_ERROR' +var ERROR_PUPPETEER = 'ERROR_PUPPETEER' var ERROR_META_IS_NOT_OBJECT = 'ERROR_META_IS_NOT_OBJECT' +var ERROR_DOCTYPE_IS_NOT_STRING = 'ERROR_DOCTYPE_IS_NOT_STRING' var ERROR_INVALID_JSON_RESPONSE = 'ERROR_INVALID_JSON_RESPONSE' var errorCodes = { [ERROR_INVALID_TOKEN]: '001', [ERROR_INVALID_URL]: '002', - [ERROR_HTML_PDF_CHROME_ERROR]: '003', + [ERROR_PUPPETEER]: '003', [ERROR_META_IS_NOT_OBJECT]: '004', - [ERROR_INVALID_JSON_RESPONSE]: '005' + [ERROR_INVALID_JSON_RESPONSE]: '005', + [ERROR_DOCTYPE_IS_NOT_STRING]: '006' } var errorMessages = { [ERROR_INVALID_TOKEN]: 'Invalid token.', [ERROR_INVALID_URL]: 'Invalid url.', - [ERROR_HTML_PDF_CHROME_ERROR]: 'html-pdf-chrome error:', + [ERROR_PUPPETEER]: 'puppeteer error:', [ERROR_META_IS_NOT_OBJECT]: 'Meta data is not a valid object', + [ERROR_DOCTYPE_IS_NOT_STRING]: 'Doctype in request is not a valid string', [ERROR_INVALID_JSON_RESPONSE]: 'Invalid JSON response' } @@ -42,7 +45,8 @@ module.exports = { getErrorCode: getErrorCode, ERROR_INVALID_TOKEN: ERROR_INVALID_TOKEN, ERROR_INVALID_URL: ERROR_INVALID_URL, - ERROR_HTML_PDF_CHROME_ERROR: ERROR_HTML_PDF_CHROME_ERROR, + ERROR_PUPPETEER: ERROR_PUPPETEER, ERROR_META_IS_NOT_OBJECT: ERROR_META_IS_NOT_OBJECT, - ERROR_INVALID_JSON_RESPONSE: ERROR_INVALID_JSON_RESPONSE + ERROR_INVALID_JSON_RESPONSE: ERROR_INVALID_JSON_RESPONSE, + ERROR_DOCTYPE_IS_NOT_STRING: ERROR_DOCTYPE_IS_NOT_STRING } diff --git a/src/queue.js b/src/queue.js index d88347c..c8bb15a 100644 --- a/src/queue.js +++ b/src/queue.js @@ -44,8 +44,13 @@ function addToQueue (db, data) { return error.createErrorResponse(error.ERROR_META_IS_NOT_OBJECT) } + if (data.doctype && typeof data.doctype !== 'string') { + return error.createErrorResponse(error.ERROR_DOCTYPE_IS_NOT_STRING) + } + data = Object.assign(defaults, data, { id: id, + doctype: data.doctype || '', created_at: createdAt, completed_at: null, generations: [], diff --git a/src/webhook.js b/src/webhook.js index 9f3ef25..0f42b13 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -24,6 +24,7 @@ function ping (job, options) { var bodyRaw = { id: job.id, url: job.url, + doctype: job.doctype, meta: job.meta, storage: job.storage } diff --git a/test/api.test.js b/test/api.test.js index 9c96137..4819af9 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -73,7 +73,7 @@ describe('api: POST /', function () { .end(function (err, res) { if (err) return done(err) - if (!addToQueue.calledWith({ url: 'https://google.com', meta: meta })) { + if (!addToQueue.calledWith({ url: 'https://google.com', meta: meta, doctype: ''})) { throw new Error('Queue was not called with correct url') } diff --git a/test/pdfGenerator.test.js b/test/pdfGenerator.test.js index 92ccf67..59acf66 100644 --- a/test/pdfGenerator.test.js +++ b/test/pdfGenerator.test.js @@ -66,6 +66,7 @@ describe('PDF Generator', function() { createStub.onCall(0).returns(new Promise((resolve, reject) => reject('error'))) createGenerator('storage', {}, {})('url', {id: 1}).then(response => { + console.log(JSON.stringify(response)) if (!error.isError(response)) { throw new Exception('Generator rejection did not resolve in error promise') } diff --git a/test/queue.test.js b/test/queue.test.js index aaf7b03..0be750e 100644 --- a/test/queue.test.js +++ b/test/queue.test.js @@ -80,7 +80,8 @@ describe('queue : retrieval', function() { it('should create error when passing invalid meta', function() { var response = queue.addToQueue({ meta: 'not-object', - url: 'http://localhost' + url: 'http://localhost', + doctype: 'foo' }) assert(response.error) @@ -92,6 +93,7 @@ describe('queue : retrieval', function() { meta: { hello: true }, + doctype: '', url: 'http://localhost' }) diff --git a/test/webhook.test.js b/test/webhook.test.js index 9e581d3..8fc7844 100644 --- a/test/webhook.test.js +++ b/test/webhook.test.js @@ -5,6 +5,7 @@ var fetch = require('node-fetch') var job = { id: 1, url: 'http://localhost', + doctype: '', meta: { id: 1 }, @@ -83,7 +84,7 @@ describe('webhook', function() { } if (fetchOptions.body !== JSON.stringify(job)) { - throw new Error('Body was not correct.') + throw new Error('Body was not correct. ' + JSON.stringify(fetchOptions) + '\n\n' + JSON.stringify(job)) } })