diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4257b33 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +## v1.1.0 [2023-08-25] + +* Add the ability to set [render options](https://carbone.io/api-reference.html#options). Thanks, [@mrtnblv](https://github.com/mrtnblv)! + +## v1.0.1 [2023-05-30] + +* Add docs suggesting alternative for PDF rendering when running N8N on Docker (i.e. Gotenberg) + +## v1.0.0 [2023-50-27] + +* Initial release diff --git a/README.md b/README.md index d870bd7..15c8e07 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes Must receive an input item with both `$json` and `$binary` keys. The `$json` key may be used to compose the "context", which will be provided to the templating engine. The `$binary` key should contain a DOCX document that contains a valid Carbone template. +This operation can take "advanced options", which are passed directly to Carbone's rendering engine. See [Carbone's docs](https://carbone.io/api-reference.html#options) for information about each option. They appear in the Options dropdown, at the bottom of the Render operation: + +![a screenshot of the advanced options](images/image.png) + ### Convert to PDF > **NOTE:** This operation requires LibreOffice to be installed. If using the native NPM install, you should install LibreOffice system-wide. If using the Docker images, this operation doesn't seem to work :( @@ -170,3 +174,27 @@ By default, this configuration will override the incoming file with the response 1. In the new Put Output in Field textfield that appears, set it to a name that is different to the name of the input file (e.g., if the input file is in `data`, set it to `data_pdf` or something) ![](./images/gotenberg_no_owrite.png) + +## Development + +More information [here](https://docs.n8n.io/integrations/creating-nodes/test/run-node-locally/). + +You must have a local (non-Docker) installation of N8N. + +1. Clone this repo +1. `npm i` +1. Make changes as required +1. `npm run build` +1. `npm link` +1. Go to N8N's install dir (`~/.n8n/nodes/` on Linux), then run `npm link n8n-nodes-carbonejs` +1. `n8n start`. If you need to start the N8N instance on another port, `N8N_PORT=5679 n8n start` +1. There's no need to visit the web UI to install the node: it's already installed since it lives in the correct directory +1. After making changes in the code and rebuilding, you'll need to stop N8N (Ctrl+C) and restart it (`n8n start`) +1. For faster changes, instead of rebuilding the code each time, run `npm run dev`. This will start the TypeScript compiler in watch mode, which will recompile the code on every change. You'll still need to restart N8N manually, though. + +### Releasing changes + +- [ ] Bump the version in `package.json`. We use [SemVer](https://semver.org/). +- [ ] Add an entry to the top of `CHANGELOG.md` describing the changes. +- [ ] Push changes, open a PR and merge it to master branch (if developing on another branch) +- [ ] Create a release. This will kick off the CI which will build and publish the package on NPM diff --git a/images/image.png b/images/image.png new file mode 100644 index 0000000..204e9e6 Binary files /dev/null and b/images/image.png differ diff --git a/nodes/CarboneNode/CarboneNode.node.ts b/nodes/CarboneNode/CarboneNode.node.ts index 16b97c0..92b261a 100644 --- a/nodes/CarboneNode/CarboneNode.node.ts +++ b/nodes/CarboneNode/CarboneNode.node.ts @@ -9,7 +9,7 @@ import { } from 'n8n-workflow'; import type { Readable } from 'stream'; import { BINARY_ENCODING } from 'n8n-workflow'; -import { convertDocumentToPdf, isWordDocument, renderDocument } from './CarboneUtils'; +import { convertDocumentToPdf, isWordDocument, renderDocument, buildOptions } from './CarboneUtils'; const nodeOperations: INodePropertyOptions[] = [ { @@ -60,6 +60,69 @@ const nodeOperationOptions: INodeProperties[] = [ }, ]; +const nodeOptions: INodeProperties[] = [ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Timezone', + name: 'timezone', + type: 'string', + default: 'Europe/Paris', + description: + 'Convert document dates to a timezone. The date must be chained with the `:formatD` formatter. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List, in the column "TZ identifier"', + }, + { + displayName: 'Locale', + name: 'lang', + type: 'string', + default: 'en', + description: + 'Locale of the generated document, it will used for translation `{t()}`, formatting numbers with `:formatN`, and currencies `:formatC`. See https://github.com/carboneio/carbone/blob/master/formatters/_locale.js.', + }, + { + displayName: 'Complement', + name: 'complement', + type: 'json', + default: '{}', + description: 'Extra data accessible in the template with {c.} instead of {d.}', + }, + { + displayName: 'Alias', + name: 'variableStr', + type: 'string', + default: '', + placeholder: 'e.g. {#def = d.id}', // eslint-disable-line n8n-nodes-base/node-param-placeholder-miscased-id + description: 'Predefined alias. See https://carbone.io/documentation.html#alias.', + }, + { + displayName: 'Enums', + name: 'enum', + type: 'json', + default: '', + placeholder: 'e.g. {"ORDER_STATUS": ["open", "close"]}', + description: 'Object with enumerations, use it in reports with `convEnum` formatters', + }, + { + displayName: 'Translations', + name: 'translations', + type: 'json', + default: '', + placeholder: 'e.g. {"es-es": {"one": "uno"}}', + description: + 'When the report is generated, all text between `{t( )}` is replaced with the corresponding translation. The `lang` option is required to select the correct translation. See https://carbone.io/documentation.html#translations', + }, + ], + displayOptions: { + show: { operation: ['render'] }, + }, + }, +]; + export class CarboneNode implements INodeType { description: INodeTypeDescription = { displayName: 'Carbone', @@ -84,7 +147,8 @@ export class CarboneNode implements INodeType { default: 'render', }, { - displayName: 'This operation requires LibreOffice to be installed! If using Docker, see this link for a suggested alternative.', + displayName: + 'This operation requires LibreOffice to be installed! If using Docker, see this link for a suggested alternative.', name: 'notice', type: 'notice', default: '', @@ -93,6 +157,7 @@ export class CarboneNode implements INodeType { }, }, ...nodeOperationOptions, + ...nodeOptions, ], }; @@ -130,7 +195,8 @@ export class CarboneNode implements INodeType { fileContent = Buffer.from(binaryData.data, BINARY_ENCODING); } - const rendered = await renderDocument(fileContent, context); + const options = buildOptions(this, itemIndex); + const rendered = await renderDocument(fileContent, context, options); item.json = context; // Overwrite the item's JSON data with the used context diff --git a/nodes/CarboneNode/CarboneUtils.ts b/nodes/CarboneNode/CarboneUtils.ts index a4ebd5b..148bd92 100644 --- a/nodes/CarboneNode/CarboneUtils.ts +++ b/nodes/CarboneNode/CarboneUtils.ts @@ -5,7 +5,7 @@ import path from 'path'; import carbone from 'carbone'; import type { Readable } from 'stream'; -import { IBinaryData } from 'n8n-workflow'; +import { IBinaryData, IExecuteFunctions } from 'n8n-workflow'; // These two functions come straight from https://advancedweb.hu/secure-tempfiles-in-nodejs-without-dependencies/#solution, // plus typing. These should be safe (from pesky hackers and race conditions), and require no third-party dependencies @@ -24,15 +24,32 @@ const withTempDir = async (fn: (dirPath: string) => T): Promise => { const isWordDocument = (data: IBinaryData) => data.mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; +const buildOptions = (node: IExecuteFunctions, index: number): object => { + const additionalFields = node.getNodeParameter('options', index); + // console.debug(additionalFields); + + let options: any = {}; + if(additionalFields.timezone) options.timezone = additionalFields.timezone; + if(additionalFields.lang) options.lang = additionalFields.lang; + if(additionalFields.variableStr) options.variableStr = additionalFields.variableStr; + if(additionalFields.complement) options.complement = JSON.parse(additionalFields.complement as string); + if(additionalFields.enum) options.enum = JSON.parse(additionalFields.enum as string); + if(additionalFields.translations) options.translations = JSON.parse(additionalFields.translations as string); + + // console.debug(options) + return options; +}; + const renderDocument = async ( document: Buffer | Readable, context: any, + options: object, ): Promise => { return withTempFile(async (file) => { await fs.writeFile(file, document); // Save the template to temp dir, since Carbone needs to read from disk return await new Promise((resolve, reject) => { - carbone.render(file, context, {}, function (err, result) { + carbone.render(file, context, options, function (err, result) { if (err) { reject(err); } @@ -65,4 +82,11 @@ const convertDocumentToPdf = async (document: Buffer): Promise => { }); }; -export { withTempFile, withTempDir, isWordDocument, renderDocument, convertDocumentToPdf }; +export { + withTempFile, + withTempDir, + isWordDocument, + buildOptions, + renderDocument, + convertDocumentToPdf, +}; diff --git a/package-lock.json b/package-lock.json index 326a6a4..ec6e89b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n-nodes-carbonejs", - "version": "0.1.0", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "n8n-nodes-carbonejs", - "version": "0.1.0", + "version": "1.1.0", "license": "SEE LICENSE IN LICENSE_CARBONE.md AND SEE LICENSE IN LICENSE_N8N.md", "dependencies": { "carbone": "^3.5.5", diff --git a/package.json b/package.json index e443b74..afee226 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-carbonejs", - "version": "1.0.1", + "version": "1.1.0", "description": "A Carbone JS node that renders Word templates on n8n.io", "keywords": [ "n8n-community-node-package"