From 46d1a5fe58b6d6b4ee0a11be3ee930905ae56e2e Mon Sep 17 00:00:00 2001 From: dali Date: Tue, 16 Feb 2021 07:23:37 +0100 Subject: [PATCH 01/10] Remove executeSingle from EmailSend node --- packages/nodes-base/nodes/EmailSend.node.ts | 151 +++++++++++--------- 1 file changed, 81 insertions(+), 70 deletions(-) diff --git a/packages/nodes-base/nodes/EmailSend.node.ts b/packages/nodes-base/nodes/EmailSend.node.ts index f2ca08ad46..7e203d8890 100644 --- a/packages/nodes-base/nodes/EmailSend.node.ts +++ b/packages/nodes-base/nodes/EmailSend.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions } from 'n8n-core'; import { IDataObject, @@ -124,84 +124,95 @@ export class EmailSend implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - const fromEmail = this.getNodeParameter('fromEmail') as string; - const toEmail = this.getNodeParameter('toEmail') as string; - const ccEmail = this.getNodeParameter('ccEmail') as string; - const bccEmail = this.getNodeParameter('bccEmail') as string; - const subject = this.getNodeParameter('subject') as string; - const text = this.getNodeParameter('text') as string; - const html = this.getNodeParameter('html') as string; - const attachmentPropertyString = this.getNodeParameter('attachments') as string; - const options = this.getNodeParameter('options', {}) as IDataObject; + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const credentials = this.getCredentials('smtp'); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + + item = items[itemIndex]; + console.log(item,itemIndex) + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; + const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; + const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; + const bccEmail = this.getNodeParameter('bccEmail', itemIndex) as string; + const subject = this.getNodeParameter('subject', itemIndex) as string; + const text = this.getNodeParameter('text', itemIndex) as string; + const html = this.getNodeParameter('html', itemIndex) as string; + const attachmentPropertyString = this.getNodeParameter('attachments', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + const credentials = this.getCredentials('smtp'); - const connectionOptions: SMTPTransport.Options = { - host: credentials.host as string, - port: credentials.port as number, - secure: credentials.secure as boolean, - }; - - if(credentials.user || credentials.password) { - // @ts-ignore - connectionOptions.auth = { - user: credentials.user as string, - pass: credentials.password as string, - }; - } - - if (options.allowUnauthorizedCerts === true) { - connectionOptions.tls = { - rejectUnauthorized: false, - }; - } - - const transporter = createTransport(connectionOptions); - - // setup email data with unicode symbols - const mailOptions = { - from: fromEmail, - to: toEmail, - cc: ccEmail, - bcc: bccEmail, - subject, - text, - html, - }; - - if (attachmentPropertyString && item.binary) { - const attachments = []; - const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { - return propertyName.trim(); - }); - - for (const propertyName of attachmentProperties) { - if (!item.binary.hasOwnProperty(propertyName)) { - continue; - } - attachments.push({ - filename: item.binary[propertyName].fileName || 'unknown', - content: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), - }); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); } - if (attachments.length) { + const connectionOptions: SMTPTransport.Options = { + host: credentials.host as string, + port: credentials.port as number, + secure: credentials.secure as boolean, + }; + + if(credentials.user || credentials.password) { // @ts-ignore - mailOptions.attachments = attachments; + connectionOptions.auth = { + user: credentials.user as string, + pass: credentials.password as string, + }; } + + if (options.allowUnauthorizedCerts === true) { + connectionOptions.tls = { + rejectUnauthorized: false, + }; + } + + const transporter = createTransport(connectionOptions); + + // setup email data with unicode symbols + const mailOptions = { + from: fromEmail, + to: toEmail, + cc: ccEmail, + bcc: bccEmail, + subject, + text, + html, + }; + + if (attachmentPropertyString && item.binary) { + const attachments = []; + const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { + return propertyName.trim(); + }); + + for (const propertyName of attachmentProperties) { + if (!item.binary.hasOwnProperty(propertyName)) { + continue; + } + attachments.push({ + filename: item.binary[propertyName].fileName || 'unknown', + content: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), + }); + } + + if (attachments.length) { + // @ts-ignore + mailOptions.attachments = attachments; + } + } + + // Send the email + const info = await transporter.sendMail(mailOptions); + + returnData.push({ json: info }); } - - // Send the email - const info = await transporter.sendMail(mailOptions); - - return { json: info }; + + return this.prepareOutputData(returnData); } } From 8d119852f64be150525b8326d63e019d8542224f Mon Sep 17 00:00:00 2001 From: dali Date: Tue, 16 Feb 2021 08:09:10 +0100 Subject: [PATCH 02/10] Remove executeSingle from ReadBinaryFile node --- packages/nodes-base/nodes/EmailSend.node.ts | 2 +- .../nodes-base/nodes/ReadBinaryFile.node.ts | 62 +++++++++++-------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/packages/nodes-base/nodes/EmailSend.node.ts b/packages/nodes-base/nodes/EmailSend.node.ts index 7e203d8890..2bdc29076f 100644 --- a/packages/nodes-base/nodes/EmailSend.node.ts +++ b/packages/nodes-base/nodes/EmailSend.node.ts @@ -134,7 +134,7 @@ export class EmailSend implements INodeType { for (let itemIndex = 0; itemIndex < length; itemIndex++) { item = items[itemIndex]; - console.log(item,itemIndex) + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; diff --git a/packages/nodes-base/nodes/ReadBinaryFile.node.ts b/packages/nodes-base/nodes/ReadBinaryFile.node.ts index ee38fc0cdc..e8be54cd4c 100644 --- a/packages/nodes-base/nodes/ReadBinaryFile.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFile.node.ts @@ -1,4 +1,4 @@ -import { IExecuteSingleFunctions } from 'n8n-core'; +import { IExecuteFunctions } from 'n8n-core'; import { INodeExecutionData, INodeType, @@ -49,38 +49,46 @@ export class ReadBinaryFile implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; - const filePath = this.getNodeParameter('filePath') as string; + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - let data; - try { - data = await fsReadFileAsync(filePath) as Buffer; - } catch (error) { - if (error.code === 'ENOENT') { - throw new Error(`The file "${filePath}" could not be found.`); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + item = items[itemIndex]; + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; + const filePath = this.getNodeParameter('filePath', itemIndex) as string; + + let data; + try { + data = await fsReadFileAsync(filePath) as Buffer; + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`The file "${filePath}" could not be found.`); + } + + throw error; } - throw error; - } + const newItem: INodeExecutionData = { + json: item.json, + binary: {}, + }; - const newItem: INodeExecutionData = { - json: item.json, - binary: {}, - }; + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, item.binary); + } - if (item.binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - Object.assign(newItem.binary, item.binary); - } - - newItem.binary![dataPropertyName] = await this.helpers.prepareBinaryData(data, filePath); - - return newItem; + newItem.binary![dataPropertyName] = await this.helpers.prepareBinaryData(data, filePath); + returnData.push(newItem); + } + + return this.prepareOutputData(returnData); } } From b0664d2aa04ec7785bb1e08fb8a1222f7ca3732c Mon Sep 17 00:00:00 2001 From: dali Date: Tue, 16 Feb 2021 09:51:48 +0100 Subject: [PATCH 03/10] Remove executeSingle from ReadPdf node --- packages/nodes-base/nodes/ReadPdf.node.ts | 34 ++++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/nodes-base/nodes/ReadPdf.node.ts b/packages/nodes-base/nodes/ReadPdf.node.ts index 785683b8ae..0213969adb 100644 --- a/packages/nodes-base/nodes/ReadPdf.node.ts +++ b/packages/nodes-base/nodes/ReadPdf.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; import { @@ -37,22 +37,30 @@ export class ReadPdf implements INodeType { ], }; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - async executeSingle(this: IExecuteSingleFunctions): Promise { + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const binaryPropertyName = this.getNodeParameter('binaryPropertyName') as string; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + + item = items[itemIndex]; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex) as string; - const item = this.getInputData(); + if (item.binary === undefined) { + item.binary = {}; + } - if (item.binary === undefined) { - item.binary = {}; + const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); + returnData.push({ + binary: item.binary, + json: await pdf(binaryData), + }); + } - - const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); - - return { - binary: item.binary, - json: await pdf(binaryData), - }; + return this.prepareOutputData(returnData); } + } From 735a7e681247db358da5a06a3aca246a15ceedb0 Mon Sep 17 00:00:00 2001 From: dali Date: Tue, 16 Feb 2021 10:42:46 +0100 Subject: [PATCH 04/10] Remove executeSingle from WriteBinaryFile node --- .../nodes-base/nodes/WriteBinaryFile.node.ts | 79 +++++++++++-------- 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/nodes-base/nodes/WriteBinaryFile.node.ts b/packages/nodes-base/nodes/WriteBinaryFile.node.ts index eb56a006fd..f329f47093 100644 --- a/packages/nodes-base/nodes/WriteBinaryFile.node.ts +++ b/packages/nodes-base/nodes/WriteBinaryFile.node.ts @@ -1,6 +1,7 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, + IExecuteSingleFunctions } from 'n8n-core'; import { IDataObject, @@ -53,39 +54,53 @@ export class WriteBinaryFile implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); - const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; - const fileName = this.getNodeParameter('fileName') as string; + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - if (item.binary === undefined) { - throw new Error('No binary data set. So file can not be written!'); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; + + const fileName = this.getNodeParameter('fileName', itemIndex) as string; + + item = items[itemIndex]; + + if (item.binary === undefined) { + throw new Error('No binary data set. So file can not be written!'); + } + + if (item.binary[dataPropertyName] === undefined) { + throw new Error(`The binary property "${dataPropertyName}" does not exist. So no file can be written!`); + } + + // Write the file to disk + await fsWriteFileAsync(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary'); + + const newItem: INodeExecutionData = { + json: {}, + }; + Object.assign(newItem.json, item.json); + + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + newItem.binary = {}; + Object.assign(newItem.binary, item.binary); + } + + // Add the file name to data + + (newItem.json as IDataObject).fileName = fileName; + + returnData.push(newItem); } - - if (item.binary[dataPropertyName] === undefined) { - throw new Error(`The binary property "${dataPropertyName}" does not exist. So no file can be written!`); - } - - // Write the file to disk - await fsWriteFileAsync(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary'); - - const newItem: INodeExecutionData = { - json: {}, - }; - Object.assign(newItem.json, item.json); - - if (item.binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - newItem.binary = {}; - Object.assign(newItem.binary, item.binary); - } - - // Add the file name to data - (newItem.json as IDataObject).fileName = fileName; - - return newItem; + return this.prepareOutputData(returnData); } + } From 81f33b4c043870de7ba71fda77818fa358f04485 Mon Sep 17 00:00:00 2001 From: dali Date: Wed, 17 Feb 2021 08:51:02 +0100 Subject: [PATCH 05/10] Remove executeSingle from FunctionItem node --- .../nodes-base/nodes/FunctionItem.node.ts | 124 ++++++++++-------- 1 file changed, 67 insertions(+), 57 deletions(-) diff --git a/packages/nodes-base/nodes/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem.node.ts index cd0eae6df9..d62e2e4a58 100644 --- a/packages/nodes-base/nodes/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem.node.ts @@ -1,4 +1,4 @@ -import { IExecuteSingleFunctions } from 'n8n-core'; +import { IExecuteFunctions } from 'n8n-core'; import { IBinaryKeyData, IDataObject, @@ -40,74 +40,84 @@ export class FunctionItem implements INodeType { ], }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - let item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); - // Copy the items as they may get changed in the functions - item = JSON.parse(JSON.stringify(item)); + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - // Define the global objects for the custom function - const sandbox = { - getBinaryData: (): IBinaryKeyData | undefined => { - return item.binary; - }, - getNodeParameter: this.getNodeParameter, - getWorkflowStaticData: this.getWorkflowStaticData, - helpers: this.helpers, - item: item.json, - setBinaryData: (data: IBinaryKeyData) => { - item.binary = data; - }, - }; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { - // Make it possible to access data via $node, $parameter, ... - const dataProxy = this.getWorkflowDataProxy(); - Object.assign(sandbox, dataProxy); + item = items[itemIndex]; - const options = { - console: 'inherit', - sandbox, - require: { - external: false as boolean | { modules: string[] }, - builtin: [] as string[], - }, - }; + // Copy the items as they may get changed in the functions + item = JSON.parse(JSON.stringify(item)); - if (process.env.NODE_FUNCTION_ALLOW_BUILTIN) { - options.require.builtin = process.env.NODE_FUNCTION_ALLOW_BUILTIN.split(','); - } + // Define the global objects for the custom function + const sandbox = { + getBinaryData: (): IBinaryKeyData | undefined => { + return item.binary; + }, + getNodeParameter: this.getNodeParameter, + getWorkflowStaticData: this.getWorkflowStaticData, + helpers: this.helpers, + item: item.json, + setBinaryData: (data: IBinaryKeyData) => { + item.binary = data; + }, + }; - if (process.env.NODE_FUNCTION_ALLOW_EXTERNAL) { - options.require.external = { modules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL.split(',') }; - } + // Make it possible to access data via $node, $parameter, ... + const dataProxy = this.getWorkflowDataProxy(itemIndex); + Object.assign(sandbox, dataProxy); - const vm = new NodeVM(options); + const options = { + console: 'inherit', + sandbox, + require: { + external: false as boolean | { modules: string[] }, + builtin: [] as string[], + }, + }; - // Get the code to execute - const functionCode = this.getNodeParameter('functionCode') as string; + if (process.env.NODE_FUNCTION_ALLOW_BUILTIN) { + options.require.builtin = process.env.NODE_FUNCTION_ALLOW_BUILTIN.split(','); + } + + if (process.env.NODE_FUNCTION_ALLOW_EXTERNAL) { + options.require.external = { modules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL.split(',') }; + } + + const vm = new NodeVM(options); + + // Get the code to execute + const functionCode = this.getNodeParameter('functionCode', itemIndex) as string; - let jsonData: IDataObject; - try { - // Execute the function code - jsonData = await vm.run(`module.exports = async function() {${functionCode}}()`, __dirname); - } catch (e) { - return Promise.reject(e); - } + let jsonData: IDataObject; + try { + // Execute the function code + jsonData = await vm.run(`module.exports = async function() {${functionCode}}()`, __dirname); + } catch (e) { + return Promise.reject(e); + } - // Do very basic validation of the data - if (jsonData === undefined) { - throw new Error('No data got returned. Always an object has to be returned!'); - } + // Do very basic validation of the data + if (jsonData === undefined) { + throw new Error('No data got returned. Always an object has to be returned!'); + } - const returnItem: INodeExecutionData = { - json: jsonData, - }; + const returnItem: INodeExecutionData = { + json: jsonData, + }; - if (item.binary) { - returnItem.binary = item.binary; - } - - return returnItem; + if (item.binary) { + returnItem.binary = item.binary; + } + + returnData.push(returnItem); + } + return this.prepareOutputData(returnData); } } From 5cce827d6f8b186d48ae548b70c895f4400638ad Mon Sep 17 00:00:00 2001 From: dali Date: Mon, 22 Feb 2021 15:07:53 +0100 Subject: [PATCH 06/10] Remove executeSingle from Mailgun node --- .../nodes-base/nodes/Mailgun/Mailgun.node.ts | 135 ++++++++++-------- 1 file changed, 72 insertions(+), 63 deletions(-) diff --git a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts index 8e5c1de699..fc2625d631 100644 --- a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts +++ b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; import { IDataObject, @@ -105,80 +105,89 @@ export class Mailgun implements INodeType { }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - const fromEmail = this.getNodeParameter('fromEmail') as string; - const toEmail = this.getNodeParameter('toEmail') as string; - const ccEmail = this.getNodeParameter('ccEmail') as string; - const bccEmail = this.getNodeParameter('bccEmail') as string; - const subject = this.getNodeParameter('subject') as string; - const text = this.getNodeParameter('text') as string; - const html = this.getNodeParameter('html') as string; - const attachmentPropertyString = this.getNodeParameter('attachments') as string; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + item = items[itemIndex]; - const credentials = this.getCredentials('mailgunApi'); + const fromEmail = this.getNodeParameter('fromEmail', itemIndex) as string; + const toEmail = this.getNodeParameter('toEmail', itemIndex) as string; + const ccEmail = this.getNodeParameter('ccEmail', itemIndex) as string; + const bccEmail = this.getNodeParameter('bccEmail', itemIndex) as string; + const subject = this.getNodeParameter('subject', itemIndex) as string; + const text = this.getNodeParameter('text', itemIndex) as string; + const html = this.getNodeParameter('html', itemIndex) as string; + const attachmentPropertyString = this.getNodeParameter('attachments', itemIndex) as string; - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + const credentials = this.getCredentials('mailgunApi'); - const formData: IDataObject = { - from: fromEmail, - to: toEmail, - subject, - text, - html, - }; + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } - if (ccEmail.length !== 0) { - formData.cc = ccEmail; - } - if (bccEmail.length !== 0) { - formData.bcc = bccEmail; - } + const formData: IDataObject = { + from: fromEmail, + to: toEmail, + subject, + text, + html, + }; - if (attachmentPropertyString && item.binary) { + if (ccEmail.length !== 0) { + formData.cc = ccEmail; + } + if (bccEmail.length !== 0) { + formData.bcc = bccEmail; + } - const attachments = []; - const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { - return propertyName.trim(); - }); + if (attachmentPropertyString && item.binary) { - for (const propertyName of attachmentProperties) { - if (!item.binary.hasOwnProperty(propertyName)) { - continue; - } - attachments.push({ - value: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), - options: { - filename: item.binary[propertyName].fileName || 'unknown', - - }, + const attachments = []; + const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => { + return propertyName.trim(); }); + + for (const propertyName of attachmentProperties) { + if (!item.binary.hasOwnProperty(propertyName)) { + continue; + } + attachments.push({ + value: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING), + options: { + filename: item.binary[propertyName].fileName || 'unknown', + + }, + }); + } + + if (attachments.length) { + // @ts-ignore + formData.attachment = attachments; + } } - if (attachments.length) { - // @ts-ignore - formData.attachment = attachments; - } - } + const options = { + method: 'POST', + formData, + uri: `https://${credentials.apiDomain}/v3/${credentials.emailDomain}/messages`, + auth: { + user: 'api', + pass: credentials.apiKey as string, + }, + json: true, + }; - const options = { - method: 'POST', - formData, - uri: `https://${credentials.apiDomain}/v3/${credentials.emailDomain}/messages`, - auth: { - user: 'api', - pass: credentials.apiKey as string, - }, - json: true, - }; + const responseData = await this.helpers.request(options); - const responseData = await this.helpers.request(options); - - return { - json: responseData, - }; + returnData.push({ + json: responseData, + }); + } + return this.prepareOutputData(returnData) } } From 4bf7ea3e26bc16b3a2a7e17d4a1a05224731cd55 Mon Sep 17 00:00:00 2001 From: dali Date: Mon, 22 Feb 2021 16:10:01 +0100 Subject: [PATCH 07/10] :bug: Fix Empty buffer error in ImageEdit node --- packages/nodes-base/nodes/EditImage.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 67ba06679a..af81cbbe67 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -11,7 +11,7 @@ import { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import * as gm from 'gm'; +const gm = require('gm').subClass({imageMagick: true}); import { file } from 'tmp-promise'; import { parse as pathParse, From 256711737c480b2cb699f43d4a0ffc18e555502b Mon Sep 17 00:00:00 2001 From: dali Date: Mon, 22 Feb 2021 16:36:54 +0100 Subject: [PATCH 08/10] Remove executeSingle from Edit Image node --- packages/nodes-base/nodes/EditImage.node.ts | 528 ++++++++++---------- 1 file changed, 269 insertions(+), 259 deletions(-) diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index af81cbbe67..54589c0f56 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -1,6 +1,6 @@ import { BINARY_ENCODING, - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; import { IDataObject, @@ -948,291 +948,301 @@ export class EditImage implements INodeType { }, }; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const returnData: INodeExecutionData[] = []; + const length = items.length as unknown as number; + let item: INodeExecutionData; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + item = items[itemIndex]; - const operation = this.getNodeParameter('operation', 0) as string; - const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; - const options = this.getNodeParameter('options', {}) as IDataObject; + const operation = this.getNodeParameter('operation', itemIndex) as string; + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; - const cleanupFunctions: Array<() => void> = []; + const options = this.getNodeParameter('options', itemIndex,{}) as IDataObject; - let gmInstance: gm.State; + const cleanupFunctions: Array<() => void> = []; - const requiredOperationParameters: { - [key: string]: string[], - } = { - blur: [ - 'blur', - 'sigma', - ], - border: [ - 'borderColor', - 'borderWidth', - 'borderHeight', - ], - create: [ - 'backgroundColor', - 'height', - 'width', - ], - crop: [ - 'height', - 'positionX', - 'positionY', - 'width', - ], - composite: [ - 'dataPropertyNameComposite', - 'positionX', - 'positionY', - ], - draw: [ - 'color', - 'cornerRadius', - 'endPositionX', - 'endPositionY', - 'primitive', - 'startPositionX', - 'startPositionY', - ], - information: [], - resize: [ - 'height', - 'resizeOption', - 'width', - ], - rotate: [ - 'backgroundColor', - 'rotate', - ], - shear: [ - 'degreesX', - 'degreesY', - ], - text: [ - 'font', - 'fontColor', - 'fontSize', - 'lineLength', - 'positionX', - 'positionY', - 'text', - ], - }; + let gmInstance: gm.State; - let operations: IDataObject[] = []; - if (operation === 'multiStep') { - // Operation parameters are already in the correct format - const operationsData = this.getNodeParameter('operations', { operations: [] }) as IDataObject; - operations = operationsData.operations as IDataObject[]; - } else { - // Operation parameters have to first get collected - const operationParameters: IDataObject = {}; - requiredOperationParameters[operation].forEach(parameterName => { - try { - operationParameters[parameterName] = this.getNodeParameter(parameterName); - } catch (e) {} - }); + const requiredOperationParameters: { + [key: string]: string[], + } = { + blur: [ + 'blur', + 'sigma', + ], + border: [ + 'borderColor', + 'borderWidth', + 'borderHeight', + ], + create: [ + 'backgroundColor', + 'height', + 'width', + ], + crop: [ + 'height', + 'positionX', + 'positionY', + 'width', + ], + composite: [ + 'dataPropertyNameComposite', + 'positionX', + 'positionY', + ], + draw: [ + 'color', + 'cornerRadius', + 'endPositionX', + 'endPositionY', + 'primitive', + 'startPositionX', + 'startPositionY', + ], + information: [], + resize: [ + 'height', + 'resizeOption', + 'width', + ], + rotate: [ + 'backgroundColor', + 'rotate', + ], + shear: [ + 'degreesX', + 'degreesY', + ], + text: [ + 'font', + 'fontColor', + 'fontSize', + 'lineLength', + 'positionX', + 'positionY', + 'text', + ], + }; - operations = [ - { - operation, - ...operationParameters, - }, - ]; - } - - if (operations[0].operation !== 'create') { - // "create" generates a new image so does not require any incoming data. - if (item.binary === undefined) { - throw new Error('Item does not contain any binary data.'); - } - - if (item.binary[dataPropertyName as string] === undefined) { - throw new Error(`Item does not contain any binary data with the name "${dataPropertyName}".`); - } - - gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); - gmInstance = gmInstance.background('transparent'); - } - - if (operation === 'information') { - // Just return the information - const imageData = await new Promise((resolve, reject) => { - gmInstance = gmInstance.identify((error, imageData) => { - if (error) { - reject(error); - return; - } - resolve(imageData as unknown as IDataObject); + let operations: IDataObject[] = []; + if (operation === 'multiStep') { + // Operation parameters are already in the correct format + const operationsData = this.getNodeParameter('operations', itemIndex ,{ operations: [] }) as IDataObject; + operations = operationsData.operations as IDataObject[]; + } else { + // Operation parameters have to first get collected + const operationParameters: IDataObject = {}; + requiredOperationParameters[operation].forEach(parameterName => { + try { + operationParameters[parameterName] = this.getNodeParameter(parameterName, itemIndex); + } catch (e) {} }); - }); - item.json = imageData; - return item; - } + operations = [ + { + operation, + ...operationParameters, + }, + ]; + } - for (let i = 0; i < operations.length; i++) { - const operationData = operations[i]; - if (operationData.operation === 'blur') { - gmInstance = gmInstance!.blur(operationData.blur as number, operationData.sigma as number); - } else if (operationData.operation === 'border') { - gmInstance = gmInstance!.borderColor(operationData.borderColor as string).border(operationData.borderWidth as number, operationData.borderHeight as number); - } else if (operationData.operation === 'composite') { - const positionX = operationData.positionX as number; - const positionY = operationData.positionY as number; - - const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; - - if (item.binary![operationData.dataPropertyNameComposite as string] === undefined) { - throw new Error(`Item does not contain any binary data with the name "${operationData.dataPropertyNameComposite}".`); + if (operations[0].operation !== 'create') { + // "create" generates a new image so does not require any incoming data. + if (item.binary === undefined) { + throw new Error('Item does not contain any binary data.'); } - const { fd, path, cleanup } = await file(); - cleanupFunctions.push(cleanup); - await fsWriteFileAsync(fd, Buffer.from(item.binary![operationData.dataPropertyNameComposite as string].data, BINARY_ENCODING)); - - if (operations[0].operation === 'create') { - // It seems like if the image gets created newly we have to create a new gm instance - // else it fails for some reason - gmInstance = gm(gmInstance!.stream('png')).geometry(geometryString).composite(path); - } else { - gmInstance = gmInstance!.geometry(geometryString).composite(path); + if (item.binary[dataPropertyName as string] === undefined) { + throw new Error(`Item does not contain any binary data with the name "${dataPropertyName}".`); } - if (operations.length !== i + 1) { - // If there are other operations after the current one create a new gm instance - // because else things do get messed up - gmInstance = gm(gmInstance.stream()); - } - } else if (operationData.operation === 'create') { - gmInstance = gm(operationData.width as number, operationData.height as number, operationData.backgroundColor as string); - if (!options.format) { - options.format = 'png'; - } - } else if (operationData.operation === 'crop') { - gmInstance = gmInstance!.crop(operationData.width as number, operationData.height as number, operationData.positionX as number, operationData.positionY as number); - } else if (operationData.operation === 'draw') { - gmInstance = gmInstance!.fill(operationData.color as string); + gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); + gmInstance = gmInstance.background('transparent'); + } - if (operationData.primitive === 'line') { - gmInstance = gmInstance.drawLine(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number); - } else if (operationData.primitive === 'rectangle') { - gmInstance = gmInstance.drawRectangle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number, operationData.cornerRadius as number || undefined); - } - } else if (operationData.operation === 'resize') { - const resizeOption = operationData.resizeOption as string; - - // By default use "maximumArea" - let option: gm.ResizeOption = '@'; - if (resizeOption === 'ignoreAspectRatio') { - option = '!'; - } else if (resizeOption === 'minimumArea') { - option = '^'; - } else if (resizeOption === 'onlyIfSmaller') { - option = '<'; - } else if (resizeOption === 'onlyIfLarger') { - option = '>'; - } else if (resizeOption === 'percent') { - option = '%'; - } - - gmInstance = gmInstance!.resize(operationData.width as number, operationData.height as number, option); - } else if (operationData.operation === 'rotate') { - gmInstance = gmInstance!.rotate(operationData.backgroundColor as string, operationData.rotate as number); - } else if (operationData.operation === 'shear') { - gmInstance = gmInstance!.shear(operationData.degreesX as number, operationData.degreesY as number); - } else if (operationData.operation === 'text') { - // Split the text in multiple lines - const lines: string[] = []; - let currentLine = ''; - (operationData.text as string).split('\n').forEach((textLine: string) => { - textLine.split(' ').forEach((textPart: string) => { - if ((currentLine.length + textPart.length + 1) > (operationData.lineLength as number)) { - lines.push(currentLine.trim()); - currentLine = `${textPart} `; + if (operation === 'information') { + // Just return the information + const imageData = await new Promise((resolve, reject) => { + gmInstance = gmInstance.identify((error:any, imageData:any) => { + if (error) { + reject(error); return; } - currentLine += `${textPart} `; + resolve(imageData as unknown as IDataObject); }); - - lines.push(currentLine.trim()); - currentLine = ''; }); - // Combine the lines to a single string - const renderText = lines.join('\n'); - - const font = options.font || operationData.font; - - if (font && font !== 'default') { - gmInstance = gmInstance!.font(font as string); - } - - gmInstance = gmInstance! - .fill(operationData.fontColor as string) - .fontSize(operationData.fontSize as number) - .drawText(operationData.positionX as number, operationData.positionY as number, renderText); + item.json = imageData; + returnData.push(item); } - } - const newItem: INodeExecutionData = { - json: item.json, - binary: {}, - }; + for (let i = 0; i < operations.length; i++) { + const operationData = operations[i]; + if (operationData.operation === 'blur') { + gmInstance = gmInstance!.blur(operationData.blur as number, operationData.sigma as number); + } else if (operationData.operation === 'border') { + gmInstance = gmInstance!.borderColor(operationData.borderColor as string).border(operationData.borderWidth as number, operationData.borderHeight as number); + } else if (operationData.operation === 'composite') { + const positionX = operationData.positionX as number; + const positionY = operationData.positionY as number; - if (item.binary !== undefined) { - // Create a shallow copy of the binary data so that the old - // data references which do not get changed still stay behind - // but the incoming data does not get changed. - Object.assign(newItem.binary, item.binary); - // Make a deep copy of the binary data we change - if (newItem.binary![dataPropertyName as string]) { - newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); - } - } + const geometryString = (positionX >= 0 ? '+' : '') + positionX + (positionY >= 0 ? '+' : '') + positionY; - if (newItem.binary![dataPropertyName as string] === undefined) { - newItem.binary![dataPropertyName as string] = { - data: '', - mimeType: '', - }; - } - - if (options.quality !== undefined) { - gmInstance = gmInstance!.quality(options.quality as number); - } - - if (options.format !== undefined) { - gmInstance = gmInstance!.setFormat(options.format as string); - newItem.binary![dataPropertyName as string].fileExtension = options.format as string; - newItem.binary![dataPropertyName as string].mimeType = `image/${options.format}`; - const fileName = newItem.binary![dataPropertyName as string].fileName; - if (fileName && fileName.includes('.')) { - newItem.binary![dataPropertyName as string].fileName = fileName.split('.').slice(0, -1).join('.') + '.' + options.format; - } - } - - if (options.fileName !== undefined) { - newItem.binary![dataPropertyName as string].fileName = options.fileName as string; - } - - return new Promise((resolve, reject) => { - gmInstance - .toBuffer((error: Error | null, buffer: Buffer) => { - cleanupFunctions.forEach(async cleanup => await cleanup()); - - if (error) { - return reject(error); + if (item.binary![operationData.dataPropertyNameComposite as string] === undefined) { + throw new Error(`Item does not contain any binary data with the name "${operationData.dataPropertyNameComposite}".`); } - newItem.binary![dataPropertyName as string].data = buffer.toString(BINARY_ENCODING); + const { fd, path, cleanup } = await file(); + cleanupFunctions.push(cleanup); + await fsWriteFileAsync(fd, Buffer.from(item.binary![operationData.dataPropertyNameComposite as string].data, BINARY_ENCODING)); - return resolve(newItem); - }); - }); + if (operations[0].operation === 'create') { + // It seems like if the image gets created newly we have to create a new gm instance + // else it fails for some reason + gmInstance = gm(gmInstance!.stream('png')).geometry(geometryString).composite(path); + } else { + gmInstance = gmInstance!.geometry(geometryString).composite(path); + } + + if (operations.length !== i + 1) { + // If there are other operations after the current one create a new gm instance + // because else things do get messed up + gmInstance = gm(gmInstance.stream()); + } + } else if (operationData.operation === 'create') { + gmInstance = gm(operationData.width as number, operationData.height as number, operationData.backgroundColor as string); + if (!options.format) { + options.format = 'png'; + } + } else if (operationData.operation === 'crop') { + gmInstance = gmInstance!.crop(operationData.width as number, operationData.height as number, operationData.positionX as number, operationData.positionY as number); + } else if (operationData.operation === 'draw') { + gmInstance = gmInstance!.fill(operationData.color as string); + + if (operationData.primitive === 'line') { + gmInstance = gmInstance.drawLine(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number); + } else if (operationData.primitive === 'rectangle') { + gmInstance = gmInstance.drawRectangle(operationData.startPositionX as number, operationData.startPositionY as number, operationData.endPositionX as number, operationData.endPositionY as number, operationData.cornerRadius as number || undefined); + } + } else if (operationData.operation === 'resize') { + const resizeOption = operationData.resizeOption as string; + + // By default use "maximumArea" + let option: gm.ResizeOption = '@'; + if (resizeOption === 'ignoreAspectRatio') { + option = '!'; + } else if (resizeOption === 'minimumArea') { + option = '^'; + } else if (resizeOption === 'onlyIfSmaller') { + option = '<'; + } else if (resizeOption === 'onlyIfLarger') { + option = '>'; + } else if (resizeOption === 'percent') { + option = '%'; + } + + gmInstance = gmInstance!.resize(operationData.width as number, operationData.height as number, option); + } else if (operationData.operation === 'rotate') { + gmInstance = gmInstance!.rotate(operationData.backgroundColor as string, operationData.rotate as number); + } else if (operationData.operation === 'shear') { + gmInstance = gmInstance!.shear(operationData.degreesX as number, operationData.degreesY as number); + } else if (operationData.operation === 'text') { + // Split the text in multiple lines + const lines: string[] = []; + let currentLine = ''; + (operationData.text as string).split('\n').forEach((textLine: string) => { + textLine.split(' ').forEach((textPart: string) => { + if ((currentLine.length + textPart.length + 1) > (operationData.lineLength as number)) { + lines.push(currentLine.trim()); + currentLine = `${textPart} `; + return; + } + currentLine += `${textPart} `; + }); + + lines.push(currentLine.trim()); + currentLine = ''; + }); + + // Combine the lines to a single string + const renderText = lines.join('\n'); + + const font = options.font || operationData.font; + + if (font && font !== 'default') { + gmInstance = gmInstance!.font(font as string); + } + + gmInstance = gmInstance! + .fill(operationData.fontColor as string) + .fontSize(operationData.fontSize as number) + .drawText(operationData.positionX as number, operationData.positionY as number, renderText); + } + } + + const newItem: INodeExecutionData = { + json: item.json, + binary: {}, + }; + + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, item.binary); + // Make a deep copy of the binary data we change + if (newItem.binary![dataPropertyName as string]) { + newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); + } + } + + if (newItem.binary![dataPropertyName as string] === undefined) { + newItem.binary![dataPropertyName as string] = { + data: '', + mimeType: '', + }; + } + + if (options.quality !== undefined) { + gmInstance = gmInstance!.quality(options.quality as number); + } + + if (options.format !== undefined) { + gmInstance = gmInstance!.setFormat(options.format as string); + newItem.binary![dataPropertyName as string].fileExtension = options.format as string; + newItem.binary![dataPropertyName as string].mimeType = `image/${options.format}`; + const fileName = newItem.binary![dataPropertyName as string].fileName; + if (fileName && fileName.includes('.')) { + newItem.binary![dataPropertyName as string].fileName = fileName.split('.').slice(0, -1).join('.') + '.' + options.format; + } + } + + if (options.fileName !== undefined) { + newItem.binary![dataPropertyName as string].fileName = options.fileName as string; + } + + returnData.push(await (new Promise((resolve, reject) => { + gmInstance + .toBuffer((error: Error | null, buffer: Buffer) => { + cleanupFunctions.forEach(async cleanup => await cleanup()); + + if (error) { + return reject(error); + } + + newItem.binary![dataPropertyName as string].data = buffer.toString(BINARY_ENCODING); + + return resolve(newItem); + }); + }))); + + } + return this.prepareOutputData(returnData); } } From ac9b7432330adceb864e6ba92d810f7518f58a37 Mon Sep 17 00:00:00 2001 From: dali Date: Tue, 23 Feb 2021 15:33:43 +0100 Subject: [PATCH 09/10] :bug: Fix typescript issues --- packages/nodes-base/nodes/EditImage.node.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 54589c0f56..6250bf707d 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -966,6 +966,7 @@ export class EditImage implements INodeType { const cleanupFunctions: Array<() => void> = []; + // @ts-ignore let gmInstance: gm.State; const requiredOperationParameters: { @@ -1134,6 +1135,7 @@ export class EditImage implements INodeType { const resizeOption = operationData.resizeOption as string; // By default use "maximumArea" + // @ts-ignore let option: gm.ResizeOption = '@'; if (resizeOption === 'ignoreAspectRatio') { option = '!'; From 3ec3f908e80a09d403a84c774c42cc25cbdaace9 Mon Sep 17 00:00:00 2001 From: dali Date: Thu, 11 Mar 2021 12:27:24 +0100 Subject: [PATCH 10/10] :rewind: Revert 'gm' import statement --- packages/nodes-base/nodes/EditImage.node.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 6250bf707d..5472fdac2e 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -11,7 +11,7 @@ import { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -const gm = require('gm').subClass({imageMagick: true}); +import * as gm from 'gm'; import { file } from 'tmp-promise'; import { parse as pathParse, @@ -966,7 +966,6 @@ export class EditImage implements INodeType { const cleanupFunctions: Array<() => void> = []; - // @ts-ignore let gmInstance: gm.State; const requiredOperationParameters: { @@ -1135,7 +1134,6 @@ export class EditImage implements INodeType { const resizeOption = operationData.resizeOption as string; // By default use "maximumArea" - // @ts-ignore let option: gm.ResizeOption = '@'; if (resizeOption === 'ignoreAspectRatio') { option = '!';