From 845f120c9caf1aa93f1354f5793303125f49346d Mon Sep 17 00:00:00 2001 From: jreyesr Date: Wed, 5 Jul 2023 20:33:10 -0500 Subject: [PATCH] Add support for single-file uploads --- nodes/FormTrigger/FormTrigger.node.ts | 138 ++++++++++++++++++-------- package-lock.json | 36 +++++++ package.json | 4 + 3 files changed, 136 insertions(+), 42 deletions(-) diff --git a/nodes/FormTrigger/FormTrigger.node.ts b/nodes/FormTrigger/FormTrigger.node.ts index 9dfee2d..6e19483 100644 --- a/nodes/FormTrigger/FormTrigger.node.ts +++ b/nodes/FormTrigger/FormTrigger.node.ts @@ -4,11 +4,39 @@ import { import { IDataObject, + INodeExecutionData, INodeType, INodeTypeDescription, IWebhookResponseData, } from 'n8n-workflow'; +import fs from 'fs'; + +import formidable from 'formidable'; + +const defaultJS = `$(document).on('submit','#n8n-form',function(e){ + var formData = new FormData($("#n8n-form").get(0)); + $.post({ + url: '#', + data: formData, + contentType: false, + processData: false, + success: function(result) { + var resp = jQuery.parseJSON(result); + if (resp.status === 'ok') { + $("#status").attr('class', 'alert alert-success'); + $("#status").show(); + $('#status-text').text('Form has been submitted.'); + } else { + $("#status").attr('class', 'alert alert-danger'); + $("#status").show(); + $('#status-text').text('Something went wrong.'); + } + }, + }); + return false; +});`; + export class FormTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Form Trigger', @@ -146,6 +174,10 @@ export class FormTrigger implements INodeType { name: 'Email', value: 'email', }, + { + name: 'File', + value: 'file', + }, { name: 'Hidden', value: 'hidden', @@ -230,6 +262,13 @@ export class FormTrigger implements INodeType { type: 'string', description: 'URL for custom CSS, For an example see "https://joffcom.github.io/style.css"', }, + { + displayName: 'Detailed Body', + name: 'detailedBody', + type: 'boolean', + default: false, + description: 'Whether to just output the form data (if False) or more information such as headers and query params (if True) in JSON data', + }, { displayName: 'Form ID', name: 'formId', @@ -247,21 +286,7 @@ export class FormTrigger implements INodeType { { displayName: 'Javascript', name: 'javascript', - default: `$(document).on('submit','#n8n-form',function(e){ - $.post('#', $('#n8n-form').serialize(), function(result) { - var resp = jQuery.parseJSON(result); - if (resp.status === 'ok') { - $("#status").attr('class', 'alert alert-success'); - $("#status").show(); - $('#status-text').text('Form has been submitted.'); - } else { - $("#status").attr('class', 'alert alert-danger'); - $("#status").show(); - $('#status-text').text('Something went wrong.'); - } - }); -return false; -});`, + default: defaultJS, // eslint-disable-line n8n-nodes-base/node-param-default-missing type: 'string', typeOptions: { alwaysOpenEditWindow: true, @@ -289,9 +314,11 @@ return false; async webhook(this: IWebhookFunctions): Promise { const webhookName = this.getWebhookName(); + const req = this.getRequestObject(); + const resp = this.getResponseObject(); + const options = this.getNodeParameter('options', 0) as IDataObject; if (webhookName === 'displayForm') { - const options = this.getNodeParameter('options', 0) as IDataObject; const submitLabel = options.submitLabel ? options.submitLabel : 'Submit'; const cssFile = options.cssFile ? options.cssFile : 'https://joffcom.github.io/style.css'; const pageTitle = this.getNodeParameter('pageTitle', 0) as string; @@ -327,30 +354,12 @@ return false; } } - const defaultJS = `$(document).on('submit','#n8n-form',function(e){ - $.post('#', $('#n8n-form').serialize(), function(result) { - var resp = jQuery.parseJSON(result); - if (resp.status === 'ok') { - $("#status").attr('class', 'alert alert-success'); - $("#status").show(); - $('#status-text').text('Form has been submitted.'); - } else { - $("#status").attr('class', 'alert alert-danger'); - $("#status").show(); - $('#status-text').text('Something went wrong.'); - } - }); - return false; -});`; - const javascript = options.javascript ? options.javascript : defaultJS; const formName = options.formName ? options.formName : 'n8n-form'; const formId = options.formId ? options.formId : 'n8n-form'; const jQuery = options.jQuery ? options.jQuery : 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'; const bootstrapCss = options.bootstrap ? options.bootstrap : 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css'; - - const res = this.getResponseObject(); const testForm = ` ${pageTitle} @@ -384,19 +393,64 @@ return false; `; - res.status(200).send(testForm).end(); + resp.status(200).send(testForm).end(); return { noWebhookResponse: true, }; } - const bodyData = this.getBodyData(); + const form = new formidable.IncomingForm({ multiples: true }); + return new Promise((resolve, reject) => { + form.parse(req, async (err, data, files) => { + const returnItem: INodeExecutionData = { + binary: {}, + json: options.detailedBody ? { + headers: this.getHeaderData(), + params: this.getParamsData(), + query: this.getQueryData(), + body: data, + } : data, + }; + + let count = 0; + // now process the files + for (const xfile of Object.keys(files)) { + const processFiles: formidable.File[] = []; + let multiFile = false; + if (Array.isArray(files[xfile])) { + processFiles.push(...files[xfile] as formidable.File[]); + multiFile = true; + } else { + processFiles.push(files[xfile] as formidable.File); + } + + let fileCount = 0; + for (const file of processFiles) { + let binaryPropertyName = xfile; + if (binaryPropertyName.endsWith('[]')) { + binaryPropertyName = binaryPropertyName.slice(0, -2); + } + if (multiFile === true) { + binaryPropertyName += fileCount++; + } + if (options.binaryPropertyName) { + binaryPropertyName = `${options.binaryPropertyName}${count}`; + } + + const fileJson = file.toJSON() as unknown as IDataObject; + const fileContent = await fs.promises.readFile(file.path); + + returnItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(Buffer.from(fileContent), fileJson.name as string, fileJson.type as string); + + count += 1; + } + } - return { - webhookResponse: '{"status": "ok"}', - workflowData: [ - this.helpers.returnJsonArray(bodyData), - ], - }; + resolve({ + webhookResponse: '{"status": "ok"}', + workflowData: [[returnItem,],], + }); + }); + }); } } diff --git a/package-lock.json b/package-lock.json index 60b1185..b8f03cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,12 @@ "name": "n8n-nodes-form-trigger", "version": "0.1.0", "license": "MIT", + "dependencies": { + "formidable": "^1.2.1" + }, "devDependencies": { "@types/express": "^4.17.6", + "@types/formidable": "^1.0.31", "@types/request-promise-native": "~1.0.15", "@typescript-eslint/parser": "^5.29.0", "eslint-plugin-n8n-nodes-base": "^1.5.4", @@ -271,6 +275,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/formidable": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.2.5.tgz", + "integrity": "sha512-zu3mQJa4hDNubEMViSj937602XdDGzK7Q5pJ5QmLUbNxclbo9tZGt5jtwM352ssZ+pqo5V4H14TBvT/ALqQQcA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2713,6 +2726,15 @@ "node": ">= 0.12" } }, + "node_modules/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -7708,6 +7730,15 @@ "@types/range-parser": "*" } }, + "@types/formidable": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.2.5.tgz", + "integrity": "sha512-zu3mQJa4hDNubEMViSj937602XdDGzK7Q5pJ5QmLUbNxclbo9tZGt5jtwM352ssZ+pqo5V4H14TBvT/ALqQQcA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -9644,6 +9675,11 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==" + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", diff --git a/package.json b/package.json index 5533d21..6b51363 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "devDependencies": { "@types/express": "^4.17.6", + "@types/formidable": "^1.0.31", "@types/request-promise-native": "~1.0.15", "@typescript-eslint/parser": "^5.29.0", "eslint-plugin-n8n-nodes-base": "^1.5.4", @@ -45,5 +46,8 @@ "prettier": "^2.7.1", "tslint": "^6.1.2", "typescript": "~4.6.0" + }, + "dependencies": { + "formidable": "^1.2.1" } }