From b6d45e30efe05c2c0178bdd330acb74558c35def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 30 Apr 2021 20:37:46 +0200 Subject: [PATCH 01/41] :zap: Add self-hosted support to ERPNext (#1679) --- .../credentials/ERPNextApi.credentials.ts | 40 ++++++++++++++++++- .../nodes/ERPNext/GenericFunctions.ts | 27 ++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/credentials/ERPNextApi.credentials.ts b/packages/nodes-base/credentials/ERPNextApi.credentials.ts index d94210600e..1ad997f74c 100644 --- a/packages/nodes-base/credentials/ERPNextApi.credentials.ts +++ b/packages/nodes-base/credentials/ERPNextApi.credentials.ts @@ -20,13 +20,51 @@ export class ERPNextApi implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', }, + { + displayName: 'Environment', + name: 'environment', + type: 'options' as NodePropertyTypes, + default: 'cloudHosted', + options: [ + { + name: 'Cloud-hosted', + value: 'cloudHosted', + }, + { + name: 'Self-hosted', + value: 'selfHosted', + }, + ], + }, { displayName: 'Subdomain', name: 'subdomain', type: 'string' as NodePropertyTypes, default: '', placeholder: 'n8n', - description: 'ERPNext subdomain. For instance, entering n8n will make the url look like: https://n8n.erpnext.com/.', + description: 'Subdomain of cloud-hosted ERPNext instance. For example, "n8n" is the subdomain in: https://n8n.erpnext.com', + displayOptions: { + show: { + environment: [ + 'cloudHosted', + ], + }, + }, + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://www.mydomain.com', + description: 'Fully qualified domain name of self-hosted ERPNext instance.', + displayOptions: { + show: { + environment: [ + 'selfHosted', + ], + }, + }, }, ]; } diff --git a/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts b/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts index 08de44e8e0..4a85c0064a 100644 --- a/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ERPNext/GenericFunctions.ts @@ -24,8 +24,8 @@ export async function erpNextApiRequest( uri?: string, option: IDataObject = {}, ) { - - const credentials = this.getCredentials('erpNextApi'); + const credentials = this.getCredentials('erpNextApi') as ERPNextApiCredentials; + const baseUrl = getBaseUrl(credentials); if (credentials === undefined) { throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); @@ -40,7 +40,7 @@ export async function erpNextApiRequest( method, body, qs: query, - uri: uri || `https://${credentials.subdomain}.erpnext.com${resource}`, + uri: uri || `${baseUrl}${resource}`, json: true, }; @@ -56,13 +56,12 @@ export async function erpNextApiRequest( try { return await this.helpers.request!(options); } catch (error) { - if (error.statusCode === 403) { - throw new NodeApiError(this.getNode(), { message: `DocType unavailable.` }); + throw new NodeApiError(this.getNode(), { message: 'DocType unavailable.' }); } if (error.statusCode === 307) { - throw new NodeApiError(this.getNode(), { message:`Please ensure the subdomain is correct.` }); + throw new NodeApiError(this.getNode(), { message: 'Please ensure the subdomain is correct.' }); } throw new NodeApiError(this.getNode(), error); @@ -95,3 +94,19 @@ export async function erpNextApiRequestAllItems( return returnData; } + +/** + * Return the base API URL based on the user's environment. + */ +const getBaseUrl = ({ environment, domain, subdomain }: ERPNextApiCredentials) => + environment === 'cloudHosted' + ? `https://${subdomain}.erpnext.com` + : domain; + +type ERPNextApiCredentials = { + apiKey: string; + apiSecret: string; + environment: 'cloudHosted' | 'selfHosted'; + subdomain?: string; + domain?: string; +}; From 901551ae9905d20444c29726249543908761453e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 15:19:59 -0400 Subject: [PATCH 02/41] :zap: Add delete folder operation to FTP Node (#1704) * :zap: Add delete folder operation to FTP Node * :zap: Minor improvement Co-authored-by: Jan Oberhauser --- packages/nodes-base/nodes/Ftp.node.ts | 56 +++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Ftp.node.ts b/packages/nodes-base/nodes/Ftp.node.ts index 12835376e0..ef254b055d 100644 --- a/packages/nodes-base/nodes/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp.node.ts @@ -102,7 +102,7 @@ export class Ftp implements INodeType { { name: 'Delete', value: 'delete', - description: 'Delete a file.', + description: 'Delete a file/folder.', }, { name: 'Download', @@ -148,6 +148,46 @@ export class Ftp implements INodeType { required: true, }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { + show: { + operation: [ + 'delete', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Folder', + name: 'folder', + type: 'boolean', + default: false, + description: 'When set to true, folders can be deleted.', + required: true, + }, + { + displayName: 'Recursive', + displayOptions: { + show: { + folder: [ + true, + ], + }, + }, + name: 'recursive', + type: 'boolean', + default: false, + description: 'If true, remove all files and directories in target directory.', + required: true, + }, + ], + }, + // ---------------------------------- // download // ---------------------------------- @@ -401,8 +441,13 @@ export class Ftp implements INodeType { if (operation === 'delete') { const path = this.getNodeParameter('path', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; - responseData = await sftp!.delete(path); + if (options.folder === true) { + responseData = await sftp!.rmdir(path, !!options.recursive); + } else { + responseData = await sftp!.delete(path); + } returnItems.push({ json: { success: true } }); } @@ -488,8 +533,13 @@ export class Ftp implements INodeType { if (operation === 'delete') { const path = this.getNodeParameter('path', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; - responseData = await ftp!.delete(path); + if (options.folder === true) { + responseData = await ftp!.rmdir(path, !!options.recursive); + } else { + responseData = await ftp!.delete(path); + } returnItems.push({ json: { success: true } }); } From 144bf3ea00bb0d8531ad964fc3ca63d296a88677 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 15:22:46 -0400 Subject: [PATCH 03/41] :zap: Add svg logos to Facebook nodes (#1713) --- .../nodes/Facebook/FacebookGraphApi.node.ts | 2 +- .../nodes/Facebook/FacebookTrigger.node.ts | 2 +- packages/nodes-base/nodes/Facebook/facebook.png | Bin 2413 -> 0 bytes packages/nodes-base/nodes/Facebook/facebook.svg | 16 ++++++++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) delete mode 100644 packages/nodes-base/nodes/Facebook/facebook.png create mode 100644 packages/nodes-base/nodes/Facebook/facebook.svg diff --git a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index 0beabbb9b6..0e73c19863 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -20,7 +20,7 @@ export class FacebookGraphApi implements INodeType { description: INodeTypeDescription = { displayName: 'Facebook Graph API', name: 'facebookGraphApi', - icon: 'file:facebook.png', + icon: 'file:facebook.svg', group: ['transform'], version: 1, description: 'Interacts with Facebook using the Graph API', diff --git a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts index d532bfc1a9..b462d83a10 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts @@ -29,7 +29,7 @@ export class FacebookTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Facebook Trigger', name: 'facebookTrigger', - icon: 'file:facebook.png', + icon: 'file:facebook.svg', group: ['trigger'], version: 1, subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}', diff --git a/packages/nodes-base/nodes/Facebook/facebook.png b/packages/nodes-base/nodes/Facebook/facebook.png deleted file mode 100644 index 85e6670eeba4930e5b4a9ab96fdf727f4ccb038e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2413 zcmb7_c|4SB8^`b2%vdrrl%;GF*@{Hj8M}Csr7UfjlBG^BV^^3g89JQ?LzL_~)Nv#! zidUf{ojS?B^~NMeMqb8PW*9T?GgAHW{`FqZ=kwgx=lNXs^|`;l=en+2FeDfUIA;eZ z2LOWsa1eTcfB~-A$A$y~;Nk+b0RR|)f=K}c8@90x z00bNfLje&qMpR533TVdx7y|l+K#CyIB1nu#1Pl%kc%&2owNH8{PQ=<>e=CWsicV6= zve{PZVemlhw4O|Bc(-l#{$vc}h}Et$vIpkr5oK<6btx?MqlN)Fk!}7xspWEEA2sZa z&OY>vs>p4>ve4@geR;ysE3jTvOn$qjmbS5p=|LxFZ=YkwgMvfO#l*&4Orxe}T+O_Z zmw)qC$(>&-tM1;bee}4Y@yXNYFaGFw+4<_vcYXZ>gG0k3G9jA59%{n2cVvK~l0xeX2@2*kk=LhO&PE~z7eM2(~D=RB? zJ%@kTTRNK%tt&{Ea1_`gjs0wfR{^5LQ4nz=`CuQP~JF&t`o z6kO3xpJ^?)o>zQAVvNpgTiK$c-$RYBTKTK3Xp~l7G^XvszBOz7)Z}oreB;a$!&*mC z1*-3s{HXXQw>i3o7&WfFzjKM?SH@&=qk~^}Ee2M_+_pGL^V+tmWj{iW3}!3(cn23Y z$hzwE|Lo{no)^Sz`Q39>Rl@@jLeG)E51kAH{eKt=9*~-0v~?FhE4MQINZQ{fYj>?h z8yk-@qX#rGE3v0uy7U(gnmwM_8_-VBoqsRie=@u53V{^dbf78BMK0-HaL+$6M!L@j zdRWH)elTk`$7&f+-Qm)qeTNn@M){?2LcbF@AFO&(ua`9&Q5I97J+0}{fBr(JYuui) zEdI5SQMZWTr{xkaAi1R76Njk1&Zf?Db-s~EuN%^z zrm+&u%dT&0dAim!!$eb0R(1JtpP;FO9ZeF5*_IL1`HgH)TSM7MmiOzVsS9TjidK(t z_?|e|@~3_UN~aYvYaH^=%#>#KDveBv%C^~uP4HBm?1px%Jzt+FNn-6mH9Tnj0iLW&UKQR+$S+r!sghizxBDbuG- zSCuB6-=S+V78H~3$ZK+yHP$K;B@acG)t9#>4y`B9aonO;=8uj{4yAMRX~RL ziwIXF(cT)25xoExvQk3ToeJfG%wOy;X)xRdS%bCQ>#x_$DkpxEqUm5sY{k6gXpGWN zu;-?-*-22AwcKAd#uw>JmUpW7m3C^90sytY0Zuqw;X;GP z003}tXao}WS-X?SNm->2TIaiQ43>(Xm79Oq<;;@W=kLb9RSJ<%W2f+&Pio>x&)ZnH z`nTx%lh~KVTt3-1-J>Cw%JZYlyE8xE=Q~_3KIGuz!JyF~kqT6aTmfiHt_X~GIJ9Uudt|@)XdLmIl*ZK zX`g?}PN)zHh8NCB1c1R&(B%N#g`zNkC+x#Pim8z2vyw_5bhk2C|09^Nm~lBK{-zg} z3GsO+m`T-LMa>EyT$XG0T0UdWnz1ukk4cNHr6>!wn5m6AeUL%5B(%LBT^ghvB6Amc zU2{{Rm8ez*XTwX&Pmy576`q=6tQQsUH3M zKefD?p@}{pE7n*#nUOI0K4EWz`4>uFQg6HhXH+i84He&+%aT|)AC!CxXBBTeRr#`V zfznWD`Fpe&KmH;`V=bM|&04Qny6p=S% z<#{VSE^D`j?#ur5W@6-*fim&A!!4Y(M!9$er)oumaRE3Z0Lo4>q9DB}h#yXxD4)L9 zcdh~lrAU@~kv{J3;fQ6Z@V$8gpv8cS>rE`B-{xlV9=mn>aA(Wc#g4)fn=~g*acNkt zP8^;OD83=dz{4M-=`SUsamKeJz-94bEU&q`&yL+^8_IQD@wVKlMjnpMwT_yczjSUz zVj4TXGB#cpU7VF@L76;l^s|MOXYz8hEc4Tzi-U_f3mVt3W5gz-4w)Tanj7wkXRP3! H35Nd*gH!z` diff --git a/packages/nodes-base/nodes/Facebook/facebook.svg b/packages/nodes-base/nodes/Facebook/facebook.svg new file mode 100644 index 0000000000..14969a7085 --- /dev/null +++ b/packages/nodes-base/nodes/Facebook/facebook.svg @@ -0,0 +1,16 @@ + + + + + + + + + From 029b1390ee737cd5d987aecfe7395542675a3434 Mon Sep 17 00:00:00 2001 From: MedAliMarz Date: Fri, 30 Apr 2021 21:25:39 +0200 Subject: [PATCH 04/41] :bug: Fix a small bug on Xero Node (#1681) --- packages/nodes-base/nodes/Xero/Xero.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Xero/Xero.node.ts b/packages/nodes-base/nodes/Xero/Xero.node.ts index 27369c647e..1f23e3620e 100644 --- a/packages/nodes-base/nodes/Xero/Xero.node.ts +++ b/packages/nodes-base/nodes/Xero/Xero.node.ts @@ -434,7 +434,6 @@ export class Xero implements INodeType { } } if (resource === 'contact') { - } if (operation === 'create') { const organizationId = this.getNodeParameter('organizationId', i) as string; const name = this.getNodeParameter('name', i) as string; @@ -670,6 +669,7 @@ export class Xero implements INodeType { responseData = await xeroApiRequest.call(this, 'POST', `/Contacts/${contactId}`, { organizationId, Contacts: [body] }); responseData = responseData.Contacts; } + } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { From 444fe64bc1d30d4ab32b19dde544ec8842096a52 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 15:44:46 -0400 Subject: [PATCH 05/41] :zap: Handle comma separated binary properties (Gmail) (#1711) --- .../nodes/Google/Gmail/DraftDescription.ts | 3 +- .../nodes/Google/Gmail/Gmail.node.ts | 72 ++++++++++--------- .../nodes/Google/Gmail/MessageDescription.ts | 51 ++++++------- 3 files changed, 65 insertions(+), 61 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts b/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts index b2fd38cd3e..ded9971f6a 100644 --- a/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/DraftDescription.ts @@ -209,7 +209,8 @@ export const draftFields = [ name: 'property', type: 'string', default: '', - description: 'Name of the binary property containing the data to be added to the email as an attachment', + description: `Name of the binary property containing the data to be added to the email as an attachment.
+ Multiples can be set separated by comma.`, }, ], }, diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts index 158c0de9b0..2fd4457849 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts @@ -325,28 +325,29 @@ export class Gmail implements INodeType { if (additionalFields.attachmentsUi) { const attachmentsUi = additionalFields.attachmentsUi as IDataObject; - let attachmentsBinary = []; + const attachmentsBinary = []; if (!isEmpty(attachmentsUi)) { if (attachmentsUi.hasOwnProperty('attachmentsBinary') && !isEmpty(attachmentsUi.attachmentsBinary) && items[i].binary) { - // @ts-ignore - attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => { - if (items[i].binary!.hasOwnProperty(value.property)) { - const aux: IAttachments = { name: '', content: '', type: '' }; - aux.name = items[i].binary![value.property].fileName || 'unknown'; - aux.content = items[i].binary![value.property].data; - aux.type = items[i].binary![value.property].mimeType; - return aux; + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + for (const binaryProperty of (property as string).split(',')) { + if (items[i].binary![binaryProperty] !== undefined) { + const binaryData = items[i].binary![binaryProperty]; + attachmentsBinary.push({ + name: binaryData.fileName || 'unknown', + content: binaryData.data, + type: binaryData.mimeType, + }); + } } - }); + } } qs = { userId: 'me', uploadType: 'media', }; - attachmentsList = attachmentsBinary; } } @@ -408,32 +409,32 @@ export class Gmail implements INodeType { if (additionalFields.attachmentsUi) { const attachmentsUi = additionalFields.attachmentsUi as IDataObject; - let attachmentsBinary = []; + const attachmentsBinary = []; if (!isEmpty(attachmentsUi)) { if (attachmentsUi.hasOwnProperty('attachmentsBinary') && !isEmpty(attachmentsUi.attachmentsBinary) && items[i].binary) { - // @ts-ignore - attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => { - if (items[i].binary!.hasOwnProperty(value.property)) { - const aux: IAttachments = { name: '', content: '', type: '' }; - aux.name = items[i].binary![value.property].fileName || 'unknown'; - aux.content = items[i].binary![value.property].data; - aux.type = items[i].binary![value.property].mimeType; - return aux; + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + for (const binaryProperty of (property as string).split(',')) { + if (items[i].binary![binaryProperty] !== undefined) { + const binaryData = items[i].binary![binaryProperty]; + attachmentsBinary.push({ + name: binaryData.fileName || 'unknown', + content: binaryData.data, + type: binaryData.mimeType, + }); + } } - }); + } } qs = { userId: 'me', uploadType: 'media', }; - attachmentsList = attachmentsBinary; } } - // if no recipient is defined then grab the one who sent the email if (toStr === '') { endpoint = `/gmail/v1/users/me/messages/${id}`; @@ -500,7 +501,7 @@ export class Gmail implements INodeType { const dataPropertyNameDownload = additionalFields.dataPropertyAttachmentsPrefixName as string || 'attachment_'; nodeExecutionData = await parseRawEmail.call(this, responseData, dataPropertyNameDownload); - } else { + } else { nodeExecutionData = { json: responseData, }; @@ -628,28 +629,29 @@ export class Gmail implements INodeType { if (additionalFields.attachmentsUi) { const attachmentsUi = additionalFields.attachmentsUi as IDataObject; - let attachmentsBinary = []; + const attachmentsBinary = []; if (!isEmpty(attachmentsUi)) { if (attachmentsUi.hasOwnProperty('attachmentsBinary') && !isEmpty(attachmentsUi.attachmentsBinary) && items[i].binary) { - // @ts-ignore - attachmentsBinary = attachmentsUi.attachmentsBinary.map((value) => { - if (items[i].binary!.hasOwnProperty(value.property)) { - const aux: IAttachments = { name: '', content: '', type: '' }; - aux.name = items[i].binary![value.property].fileName || 'unknown'; - aux.content = items[i].binary![value.property].data; - aux.type = items[i].binary![value.property].mimeType; - return aux; + for (const { property } of attachmentsUi.attachmentsBinary as IDataObject[]) { + for (const binaryProperty of (property as string).split(',')) { + if (items[i].binary![binaryProperty] !== undefined) { + const binaryData = items[i].binary![binaryProperty]; + attachmentsBinary.push({ + name: binaryData.fileName || 'unknown', + content: binaryData.data, + type: binaryData.mimeType, + }); + } } - }); + } } qs = { userId: 'me', uploadType: 'media', }; - attachmentsList = attachmentsBinary; } } diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts index 581a13cb3d..a860580951 100644 --- a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts @@ -226,30 +226,6 @@ export const messageFields = [ }, default: {}, options: [ - { - displayName: 'CC Email', - name: 'ccList', - type: 'string', - description: 'The email addresses of the copy recipients.', - typeOptions: { - multipleValues: true, - multipleValueButtonText: 'Add CC Email', - }, - placeholder: 'info@example.com', - default: [], - }, - { - displayName: 'BCC Email', - name: 'bccList', - type: 'string', - description: 'The email addresses of the blind copy recipients.', - typeOptions: { - multipleValues: true, - multipleValueButtonText: 'Add BCC Email', - }, - placeholder: 'info@example.com', - default: [], - }, { displayName: 'Attachments', name: 'attachmentsUi', @@ -268,7 +244,8 @@ export const messageFields = [ name: 'property', type: 'string', default: '', - description: 'Name of the binary properties which contain data which should be added to email as attachment', + description: `Name of the binary property containing the data to be added to the email as an attachment.
+ Multiples can be set separated by comma.`, }, ], }, @@ -276,6 +253,30 @@ export const messageFields = [ default: '', description: 'Array of supported attachments to add to the message.', }, + { + displayName: 'BCC Email', + name: 'bccList', + type: 'string', + description: 'The email addresses of the blind copy recipients.', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add BCC Email', + }, + placeholder: 'info@example.com', + default: [], + }, + { + displayName: 'CC Email', + name: 'ccList', + type: 'string', + description: 'The email addresses of the copy recipients.', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add CC Email', + }, + placeholder: 'info@example.com', + default: [], + }, ], }, { From c9d0b17291c75730e2b8e5b8e813ec364af53725 Mon Sep 17 00:00:00 2001 From: MedAliMarz Date: Fri, 30 Apr 2021 21:58:23 +0200 Subject: [PATCH 06/41] :bug: Fix Post operations on Orbit Node (#1699) --- packages/nodes-base/nodes/Orbit/Orbit.node.ts | 12 +++++++----- packages/nodes-base/nodes/Orbit/PostDescription.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Orbit/Orbit.node.ts b/packages/nodes-base/nodes/Orbit/Orbit.node.ts index 42ec460492..ad0402c293 100644 --- a/packages/nodes-base/nodes/Orbit/Orbit.node.ts +++ b/packages/nodes-base/nodes/Orbit/Orbit.node.ts @@ -413,22 +413,24 @@ export class Orbit implements INodeType { const url = this.getNodeParameter('url', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const body: IDataObject = { + type: "post", url, }; if (additionalFields.publishedAt) { - body.published_at = additionalFields.publishedAt as string; + body.occurred_at = additionalFields.publishedAt as string; } - responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/posts`, body); + responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/activities/`, body); responseData = responseData.data; } if (operation === 'getAll') { const workspaceId = this.getNodeParameter('workspaceId', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const filters = this.getNodeParameter('filters', i) as IDataObject; - let endpoint = `/${workspaceId}/posts`; + let endpoint = `/${workspaceId}/activities`; + qs.type = 'content'; if (filters.memberId) { - endpoint = `/${workspaceId}/members/${filters.memberId}/posts`; + endpoint = `/${workspaceId}/members/${filters.memberId}/activities`; } if (returnAll === true) { responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs); @@ -443,7 +445,7 @@ export class Orbit implements INodeType { const memberId = this.getNodeParameter('memberId', i) as string; const postId = this.getNodeParameter('postId', i) as string; - responseData = await orbitApiRequest.call(this, 'DELETE', `/${workspaceId}/members/${memberId}/posts/${postId}`); + responseData = await orbitApiRequest.call(this, 'DELETE', `/${workspaceId}/members/${memberId}/activities/${postId}`); responseData = { success: true }; } } diff --git a/packages/nodes-base/nodes/Orbit/PostDescription.ts b/packages/nodes-base/nodes/Orbit/PostDescription.ts index 7cb798163a..f54a0be6c9 100644 --- a/packages/nodes-base/nodes/Orbit/PostDescription.ts +++ b/packages/nodes-base/nodes/Orbit/PostDescription.ts @@ -114,7 +114,7 @@ export const postFields = [ default: {}, options: [ { - displayName: 'Published At', + displayName: 'Occurred At', name: 'publishedAt', type: 'dateTime', default: '', From bf93a122ed8b12274020d9f7967369ff37d4019d Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 16:29:10 -0400 Subject: [PATCH 07/41] :zap: Add file name when downloading files (Google Drive) (#1710) --- .../nodes/Google/Drive/GoogleDrive.node.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts index 220aee7598..7deb795b68 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.ts @@ -274,6 +274,32 @@ export class GoogleDrive implements INodeType { }, description: 'Name of the binary property to which to
write the data of the read file.', }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'download', + ], + resource: [ + 'file', + ], + }, + }, + options: [ + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'File name. Ex: data.pdf', + }, + ], + }, // ---------------------------------- @@ -2011,6 +2037,7 @@ export class GoogleDrive implements INodeType { // ---------------------------------- const fileId = this.getNodeParameter('fileId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; const requestOptions = { resolveWithFullResponse: true, @@ -2021,10 +2048,15 @@ export class GoogleDrive implements INodeType { const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${fileId}`, {}, { alt: 'media' }, undefined, requestOptions); let mimeType: string | undefined; + let fileName: string | undefined = undefined; if (response.headers['content-type']) { mimeType = response.headers['content-type']; } + if (options.fileName) { + fileName = options.fileName as string; + } + const newItem: INodeExecutionData = { json: items[i].json, binary: {}, @@ -2043,7 +2075,7 @@ export class GoogleDrive implements INodeType { const data = Buffer.from(response.body as string); - items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType); + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType); } else if (operation === 'list') { // ---------------------------------- From 5d9280a7ad020b7da92b32fb6dd97527689da170 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 30 Apr 2021 15:34:53 -0500 Subject: [PATCH 08/41] :shirt: Fix lint issue --- packages/nodes-base/nodes/Orbit/Orbit.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Orbit/Orbit.node.ts b/packages/nodes-base/nodes/Orbit/Orbit.node.ts index ad0402c293..6059c0cfc4 100644 --- a/packages/nodes-base/nodes/Orbit/Orbit.node.ts +++ b/packages/nodes-base/nodes/Orbit/Orbit.node.ts @@ -413,7 +413,7 @@ export class Orbit implements INodeType { const url = this.getNodeParameter('url', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const body: IDataObject = { - type: "post", + type: 'post', url, }; if (additionalFields.publishedAt) { From 7c418aafe7d5ee4a3e4721b201888f2be782b82b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 16:35:42 -0400 Subject: [PATCH 09/41] :bug: Fix issue when looking up values on Google Sheets (#1708) --- packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts index 80e7c6831d..2aa2541f23 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheet.ts @@ -415,6 +415,12 @@ export class GoogleSheet { for (let i = 0; i < keys.length; i++) { inputData[rowIndex][i] = ''; } + } else if (inputData[rowIndex].length < keys.length) { + for (let i = 0; i < keys.length; i++) { + if (inputData[rowIndex][i] === undefined) { + inputData[rowIndex].push(''); + } + } } } // Loop over all the lookup values and try to find a row to return From efd40ea7a67d4a4bb38194a828024b2546d357e1 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 16:38:51 -0400 Subject: [PATCH 10/41] :bug: Fix issue of Redis never returning (#1716) Fixes #1709. When the node returned an error the reject method was not called. Hence, the process kept running forever. --- packages/nodes-base/nodes/Redis/Redis.node.ts | 121 +++++++++--------- 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index ab01f3a0b3..ecf623ed51 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -451,77 +451,80 @@ export class Redis implements INodeType { }); client.on('ready', async (err: Error | null) => { + try { + if (operation === 'info') { + const clientInfo = util.promisify(client.info).bind(client); + const result = await clientInfo(); - if (operation === 'info') { - const clientInfo = util.promisify(client.info).bind(client); - const result = await clientInfo(); + resolve(this.prepareOutputData([{ json: convertInfoToObject(result as unknown as string) }])); + client.quit(); - resolve(this.prepareOutputData([{ json: convertInfoToObject(result as unknown as string) }])); - client.quit(); + } else if (['delete', 'get', 'keys', 'set'].includes(operation)) { + const items = this.getInputData(); + const returnItems: INodeExecutionData[] = []; - } else if (['delete', 'get', 'keys', 'set'].includes(operation)) { - const items = this.getInputData(); - const returnItems: INodeExecutionData[] = []; + let item: INodeExecutionData; + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + item = { json: {} }; - let item: INodeExecutionData; - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - item = { json: {} }; + if (operation === 'delete') { + const keyDelete = this.getNodeParameter('key', itemIndex) as string; - if (operation === 'delete') { - const keyDelete = this.getNodeParameter('key', itemIndex) as string; + const clientDel = util.promisify(client.del).bind(client); + // @ts-ignore + await clientDel(keyDelete); + returnItems.push(items[itemIndex]); + } else if (operation === 'get') { + const propertyName = this.getNodeParameter('propertyName', itemIndex) as string; + const keyGet = this.getNodeParameter('key', itemIndex) as string; + const keyType = this.getNodeParameter('keyType', itemIndex) as string; - const clientDel = util.promisify(client.del).bind(client); - // @ts-ignore - await clientDel(keyDelete); - returnItems.push(items[itemIndex]); - } else if (operation === 'get') { - const propertyName = this.getNodeParameter('propertyName', itemIndex) as string; - const keyGet = this.getNodeParameter('key', itemIndex) as string; - const keyType = this.getNodeParameter('keyType', itemIndex) as string; + const value = await getValue(client, keyGet, keyType) || null; - const value = await getValue(client, keyGet, keyType) || null; + const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; - const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + if (options.dotNotation === false) { + item.json[propertyName] = value; + } else { + set(item.json, propertyName, value); + } - if (options.dotNotation === false) { - item.json[propertyName] = value; - } else { - set(item.json, propertyName, value); + returnItems.push(item); + } else if (operation === 'keys') { + const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string; + + const clientKeys = util.promisify(client.keys).bind(client); + const keys = await clientKeys(keyPattern); + + const promises: { + [key: string]: GenericValue; + } = {}; + + for (const keyName of keys) { + promises[keyName] = await getValue(client, keyName); + } + + for (const keyName of keys) { + item.json[keyName] = await promises[keyName]; + } + returnItems.push(item); + } else if (operation === 'set') { + const keySet = this.getNodeParameter('key', itemIndex) as string; + const value = this.getNodeParameter('value', itemIndex) as string; + const keyType = this.getNodeParameter('keyType', itemIndex) as string; + const expire = this.getNodeParameter('expire', itemIndex, false) as boolean; + const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number; + + await setValue(client, keySet, value, expire, ttl, keyType); + returnItems.push(items[itemIndex]); } - - returnItems.push(item); - } else if (operation === 'keys') { - const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string; - - const clientKeys = util.promisify(client.keys).bind(client); - const keys = await clientKeys(keyPattern); - - const promises: { - [key: string]: GenericValue; - } = {}; - - for (const keyName of keys) { - promises[keyName] = await getValue(client, keyName); - } - - for (const keyName of keys) { - item.json[keyName] = await promises[keyName]; - } - returnItems.push(item); - } else if (operation === 'set') { - const keySet = this.getNodeParameter('key', itemIndex) as string; - const value = this.getNodeParameter('value', itemIndex) as string; - const keyType = this.getNodeParameter('keyType', itemIndex) as string; - const expire = this.getNodeParameter('expire', itemIndex, false) as boolean; - const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number; - - await setValue(client, keySet, value, expire, ttl, keyType); - returnItems.push(items[itemIndex]); } - } - client.quit(); - resolve(this.prepareOutputData(returnItems)); + client.quit(); + resolve(this.prepareOutputData(returnItems)); + } + } catch (error) { + reject(error); } }); }); From 4cf80552247043b58292baf66da3a4128362c746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 30 Apr 2021 23:00:28 +0200 Subject: [PATCH 11/41] :sparkles: Add Kitemaker node (#1676) * :zap: Add Kitemaker node * :zap: Require status ID for workItem:create * :pencil2: Reword button text * :zap: Add credentials file * :zap: Implement pagination * :zap: Improvements * :zap: Remove not needed parameter Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/KitemakerApi.credentials.ts | 18 + .../nodes/Kitemaker/GenericFunctions.ts | 85 ++++ .../nodes/Kitemaker/Kitemaker.node.ts | 321 +++++++++++++++ .../descriptions/OrganizationDescription.ts | 27 ++ .../descriptions/SpaceDescription.ts | 71 ++++ .../Kitemaker/descriptions/UserDescription.ts | 71 ++++ .../descriptions/WorkItemDescription.ts | 372 ++++++++++++++++++ .../nodes/Kitemaker/descriptions/index.ts | 4 + .../nodes-base/nodes/Kitemaker/kitemaker.svg | 18 + .../nodes-base/nodes/Kitemaker/mutations.ts | 69 ++++ .../nodes-base/nodes/Kitemaker/queries.ts | 199 ++++++++++ packages/nodes-base/package.json | 2 + 12 files changed, 1257 insertions(+) create mode 100644 packages/nodes-base/credentials/KitemakerApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/Kitemaker.node.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/descriptions/OrganizationDescription.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/descriptions/SpaceDescription.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/descriptions/UserDescription.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/descriptions/WorkItemDescription.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/kitemaker.svg create mode 100644 packages/nodes-base/nodes/Kitemaker/mutations.ts create mode 100644 packages/nodes-base/nodes/Kitemaker/queries.ts diff --git a/packages/nodes-base/credentials/KitemakerApi.credentials.ts b/packages/nodes-base/credentials/KitemakerApi.credentials.ts new file mode 100644 index 0000000000..2f372e0d31 --- /dev/null +++ b/packages/nodes-base/credentials/KitemakerApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class KitemakerApi implements ICredentialType { + name = 'kitemakerApi'; + displayName = 'Kitemaker API'; + documentationUrl = 'kitemaker'; + properties = [ + { + displayName: 'Personal Access Token', + name: 'personalAccessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Kitemaker/GenericFunctions.ts b/packages/nodes-base/nodes/Kitemaker/GenericFunctions.ts new file mode 100644 index 0000000000..6b623a6a5f --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/GenericFunctions.ts @@ -0,0 +1,85 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + NodeApiError, +} from 'n8n-workflow'; + +export async function kitemakerRequest( + this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, + body: IDataObject = {}, +) { + const { personalAccessToken } = this.getCredentials('kitemakerApi') as { personalAccessToken: string }; + + const options = { + headers: { + Authorization: `Bearer ${personalAccessToken}`, + }, + method: 'POST', + body, + uri: 'https://toil.kitemaker.co/developers/graphql', + json: true, + }; + + const responseData = await this.helpers.request!.call(this, options); + + if (responseData.errors) { + throw new NodeApiError(this.getNode(), responseData); + } + + return responseData; +} + +export async function kitemakerRequestAllItems( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + body: { query: string; variables: { [key: string]: string } }, +) { + const resource = this.getNodeParameter('resource', 0) as 'space' | 'user' | 'workItem'; + const [group, items] = getGroupAndItems(resource); + + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + const limit = this.getNodeParameter('limit', 0, 0) as number; + + const returnData: IDataObject[] = []; + let responseData; + + do { + responseData = await kitemakerRequest.call(this, body); + body.variables.cursor = responseData.data[group].cursor; + returnData.push(...responseData.data[group][items]); + + if (!returnAll && returnData.length > limit) { + return returnData.slice(0, limit); + } + + } while (responseData.data[group].hasMore); + + return returnData; +} + +function getGroupAndItems(resource: 'space' | 'user' | 'workItem') { + const map: { [key: string]: { [key: string]: string } } = { + space: { group: 'organization', items: 'spaces' }, + user: { group: 'organization', items: 'users' }, + workItem: { group: 'workItems', items: 'workItems' }, + }; + + return [ + map[resource]['group'], + map[resource]['items'], + ]; +} + +export function createLoadOptions( + resources: Array<{ name?: string; username?: string; title?: string; id: string }>, +): Array<{ name: string; value: string }> { + return resources.map(option => { + if (option.username) return ({ name: option.username, value: option.id }); + if (option.title) return ({ name: option.title, value: option.id }); + return ({ name: option.name ?? 'Unnamed', value: option.id }); + }); +} diff --git a/packages/nodes-base/nodes/Kitemaker/Kitemaker.node.ts b/packages/nodes-base/nodes/Kitemaker/Kitemaker.node.ts new file mode 100644 index 0000000000..650049be61 --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/Kitemaker.node.ts @@ -0,0 +1,321 @@ +import { + IExecuteFunctions +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; + +import { + organizationOperations, + spaceFields, + spaceOperations, + userFields, + userOperations, + workItemFields, + workItemOperations, +} from './descriptions'; + +import { + createLoadOptions, + kitemakerRequest, + kitemakerRequestAllItems, +} from './GenericFunctions'; + +import { + getAllSpaces, + getAllUsers, + getAllWorkItems, + getLabels, + getOrganization, + getSpaces, + getStatuses, + getUsers, + getWorkItem, + getWorkItems, +} from './queries'; + +import { + createWorkItem, + editWorkItem, +} from './mutations'; + +export class Kitemaker implements INodeType { + description: INodeTypeDescription = { + displayName: 'Kitemaker', + name: 'kitemaker', + icon: 'file:kitemaker.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the Kitemaker GraphQL API', + defaults: { + name: 'Kitemaker', + color: '#662482', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'kitemakerApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Organization', + value: 'organization', + }, + { + name: 'Space', + value: 'space', + }, + { + name: 'User', + value: 'user', + }, + { + name: 'Work Item', + value: 'workItem', + }, + ], + default: 'workItem', + required: true, + description: 'Resource to operate on.', + }, + ...organizationOperations, + ...spaceOperations, + ...spaceFields, + ...userOperations, + ...userFields, + ...workItemOperations, + ...workItemFields, + ], + }; + + methods = { + loadOptions: { + async getLabels(this: ILoadOptionsFunctions) { + const responseData = await kitemakerRequest.call(this, { query: getLabels }); + const { data: { organization: { spaces } } } = responseData; + + return createLoadOptions(spaces[0].labels); + }, + + async getSpaces(this: ILoadOptionsFunctions) { + const responseData = await kitemakerRequest.call(this, { query: getSpaces }); + const { data: { organization: { spaces } } } = responseData; + + return createLoadOptions(spaces); + }, + + async getStatuses(this: ILoadOptionsFunctions) { + const responseData = await kitemakerRequest.call(this, { query: getStatuses }); + const { data: { organization: { spaces } } } = responseData; + + return createLoadOptions(spaces[0].statuses); + }, + + async getUsers(this: ILoadOptionsFunctions) { + const responseData = await kitemakerRequest.call(this, { query: getUsers }); + const { data: { organization: { users } } } = responseData; + + return createLoadOptions(users); + }, + + async getWorkItems(this: ILoadOptionsFunctions) { + const spaceId = this.getNodeParameter('spaceId', 0) as string; + + const responseData = await kitemakerRequest.call(this, { + query: getWorkItems, + variables: { spaceId }, + }); + + const { data: { workItems: { workItems } } } = responseData; + + return createLoadOptions(workItems); + }, + + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + let responseData; + const returnData: IDataObject[] = []; + + // https://github.com/kitemakerhq/docs/blob/main/kitemaker.graphql + + for (let i = 0; i < items.length; i++) { + + if (resource === 'organization') { + + // ********************************************************************* + // organization + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // organization: get + // ---------------------------------- + + responseData = await kitemakerRequest.call(this, { + query: getOrganization, + }); + + returnData.push(responseData.data.organization); + + } + + } else if (resource === 'space') { + + // ********************************************************************* + // space + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // space: getAll + // ---------------------------------- + + const allItems = await kitemakerRequestAllItems.call(this, { + query: getAllSpaces, + variables: {}, + }); + + returnData.push(...allItems); + + } + + } else if (resource === 'user') { + + // ********************************************************************* + // user + // ********************************************************************* + + if (operation === 'getAll') { + + // ---------------------------------- + // user: getAll + // ---------------------------------- + + const allItems = await kitemakerRequestAllItems.call(this, { + query: getAllUsers, + variables: {}, + }); + + returnData.push(...allItems); + + } + + } else if (resource === 'workItem') { + + // ********************************************************************* + // workItem + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // workItem: create + // ---------------------------------- + + const input = { + title: this.getNodeParameter('title', i) as string, + statusId: this.getNodeParameter('statusId', i) as string[], + }; + + if (!input.statusId.length) { + throw new Error('Please enter a status to set for the work item to create.'); + } + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(input, additionalFields); + } + + responseData = await kitemakerRequest.call(this, { + query: createWorkItem, + variables: { input }, + }); + + returnData.push(responseData.data.createWorkItem.workItem); + + } else if (operation === 'get') { + + // ---------------------------------- + // workItem: get + // ---------------------------------- + + const workItemId = this.getNodeParameter('workItemId', i) as string; + + responseData = await kitemakerRequest.call(this, { + query: getWorkItem, + variables: { workItemId }, + }); + + returnData.push(responseData.data.workItem); + + } else if (operation === 'getAll') { + + // ---------------------------------- + // workItem: getAll + // ---------------------------------- + + const allItems = await kitemakerRequestAllItems.call(this, { + query: getAllWorkItems, + variables: { + spaceId: this.getNodeParameter('spaceId', i) as string, + }, + }); + + returnData.push(...allItems); + + } else if (operation === 'update') { + + // ---------------------------------- + // workItem: update + // ---------------------------------- + + const input = { + id: this.getNodeParameter('workItemId', i), + }; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (!Object.keys(updateFields).length) { + throw new Error('Please enter at least one field to update for the work item.'); + } + + Object.assign(input, updateFields); + + responseData = await kitemakerRequest.call(this, { + query: editWorkItem, + variables: { input }, + }); + + returnData.push(responseData.data.editWorkItem.workItem); + + } + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Kitemaker/descriptions/OrganizationDescription.ts b/packages/nodes-base/nodes/Kitemaker/descriptions/OrganizationDescription.ts new file mode 100644 index 0000000000..ee478b044f --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/descriptions/OrganizationDescription.ts @@ -0,0 +1,27 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const organizationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform.', + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve data on the logged-in user\'s organization.', + }, + ], + displayOptions: { + show: { + resource: [ + 'organization', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Kitemaker/descriptions/SpaceDescription.ts b/packages/nodes-base/nodes/Kitemaker/descriptions/SpaceDescription.ts new file mode 100644 index 0000000000..0c48ab3c67 --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/descriptions/SpaceDescription.ts @@ -0,0 +1,71 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const spaceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform.', + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve data on all the spaces in the
logged-in user\'s organization.', + }, + ], + displayOptions: { + show: { + resource: [ + 'space', + ], + }, + }, + }, +] as INodeProperties[]; + +export const spaceFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'space', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'space', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Kitemaker/descriptions/UserDescription.ts b/packages/nodes-base/nodes/Kitemaker/descriptions/UserDescription.ts new file mode 100644 index 0000000000..aca2711242 --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/descriptions/UserDescription.ts @@ -0,0 +1,71 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'getAll', + description: 'Operation to perform.', + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve data on all the users in the
logged-in user\'s organization.', + }, + ], + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + }, +] as INodeProperties[]; + +export const userFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Kitemaker/descriptions/WorkItemDescription.ts b/packages/nodes-base/nodes/Kitemaker/descriptions/WorkItemDescription.ts new file mode 100644 index 0000000000..637b279fde --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/descriptions/WorkItemDescription.ts @@ -0,0 +1,372 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const workItemOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform.', + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Get', + value: 'get', + }, + { + name: 'Get All', + value: 'getAll', + }, + { + name: 'Update', + value: 'update', + }, + ], + displayOptions: { + show: { + resource: [ + 'workItem', + ], + }, + }, + }, +] as INodeProperties[]; + +export const workItemFields = [ + // ---------------------------------- + // workItem: create + // ---------------------------------- + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + description: 'Title of the work item to create.', + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Status ID', + name: 'statusId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStatuses', + }, + default: [], + required: true, + description: 'ID of the status to set on the item to create.', + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Description of the item to create. Markdown supported.', + }, + { + displayName: 'Effort', + name: 'effort', + type: 'options', + default: 'SMALL', + description: 'Effort to set for the item to create.', + options: [ + { + name: 'Small', + value: 'SMALL', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'Large', + value: 'LARGE', + }, + ], + }, + { + displayName: 'Impact', + name: 'impact', + type: 'options', + default: 'SMALL', + description: 'Impact to set for the item to create.', + options: [ + { + name: 'Small', + value: 'SMALL', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'Large', + value: 'LARGE', + }, + ], + }, + { + displayName: 'Label IDs', + name: 'labelIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getLabels', + }, + default: [], + description: 'ID of the label to set on the item to create.', + }, + { + displayName: 'Member IDs', + name: 'memberIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: 'ID of the user to assign to the item to create.', + }, + ], + }, + + // ---------------------------------- + // workItem: get + // ---------------------------------- + { + displayName: 'Work Item ID', + name: 'workItemId', + type: 'string', + default: '', + required: true, + description: 'ID of the work item to retrieve.', + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // workItem: getAll + // ---------------------------------- + { + displayName: 'Space ID', + name: 'spaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSpaces', + }, + default: [], + required: true, + description: 'ID of the space to retrieve the work items from.', + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 5, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // workItem: update + // ---------------------------------- + { + displayName: 'Work Item ID', + name: 'workItemId', + type: 'string', + default: '', + required: true, + description: 'ID of the work item to update.', + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'workItem', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Description of the item to update. Markdown supported.', + }, + { + displayName: 'Effort', + name: 'effort', + type: 'options', + default: 'SMALL', + description: 'Effort to set for the item to update.', + options: [ + { + name: 'Small', + value: 'SMALL', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'Large', + value: 'LARGE', + }, + ], + }, + { + displayName: 'Impact', + name: 'impact', + type: 'options', + default: 'SMALL', + description: 'Impact to set for the item to update.', + options: [ + { + name: 'Small', + value: 'SMALL', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'Large', + value: 'LARGE', + }, + ], + }, + { + displayName: 'Status ID', + name: 'statusId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStatuses', + }, + default: [], + description: 'ID of the status to set on the item to update.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title to set for the work item to update.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Kitemaker/descriptions/index.ts b/packages/nodes-base/nodes/Kitemaker/descriptions/index.ts new file mode 100644 index 0000000000..1e2d9a6b8e --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/descriptions/index.ts @@ -0,0 +1,4 @@ +export * from './OrganizationDescription'; +export * from './SpaceDescription'; +export * from './UserDescription'; +export * from './WorkItemDescription'; diff --git a/packages/nodes-base/nodes/Kitemaker/kitemaker.svg b/packages/nodes-base/nodes/Kitemaker/kitemaker.svg new file mode 100644 index 0000000000..b6053c44a2 --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/kitemaker.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/packages/nodes-base/nodes/Kitemaker/mutations.ts b/packages/nodes-base/nodes/Kitemaker/mutations.ts new file mode 100644 index 0000000000..be23cb45ab --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/mutations.ts @@ -0,0 +1,69 @@ +// ---------------------------------- +// mutations +// ---------------------------------- + +export const createWorkItem = ` + mutation($input: CreateWorkItemInput!) { + createWorkItem(input: $input) { + workItem { + id + number + title + description + status { + id + name + } + members { + id + username + } + watchers { + id + username + } + labels { + id + name + } + effort + impact + updatedAt + createdAt + } + } + } +`; + +export const editWorkItem = ` + mutation ($input: EditWorkItemInput!) { + editWorkItem(input: $input) { + workItem { + id + number + title + description + status { + id + name + } + members { + id + username + } + watchers { + id + username + } + labels { + id + name + } + effort + impact + updatedAt + createdAt + } + } + } +`; diff --git a/packages/nodes-base/nodes/Kitemaker/queries.ts b/packages/nodes-base/nodes/Kitemaker/queries.ts new file mode 100644 index 0000000000..74f584a7c7 --- /dev/null +++ b/packages/nodes-base/nodes/Kitemaker/queries.ts @@ -0,0 +1,199 @@ +// ---------------------------------- +// queries +// ---------------------------------- + +export const getAllSpaces = ` + query { + organization { + spaces { + id + name + labels { + id + name + color + } + statuses { + id + name + type + default + } + } + } + } +`; + +export const getAllUsers = ` + query { + organization { + users { + id + username + } + } + } +`; + +export const getLabels = ` + query { + organization { + spaces { + labels { + id + name + color + } + } + } + } +`; + +export const getOrganization = ` + query { + organization { + id + name + } + } +`; + +export const getSpaces = ` + query { + organization { + spaces { + id + name + labels { + id + name + color + } + statuses { + id + name + type + default + } + } + } + } +`; + +export const getStatuses = ` + query { + organization { + spaces { + statuses { + id + name + type + default + } + } + } + } +`; + +export const getUsers = ` + query { + organization { + users { + id + username + } + } + } +`; + +export const getWorkItems = ` + query($spaceId: ID!) { + workItems(spaceId: $spaceId) { + workItems { + id + title + } + } + } +`; + +export const getWorkItem = ` + query($workItemId: ID!) { + workItem(id: $workItemId) { + id + number + title + description + status { + id + name + } + sort + members { + id + username + } + watchers { + id + username + } + labels { + id + name + } + comments { + id + actor { + __typename + } + body + threadId + updatedAt + createdAt + } + effort + impact + updatedAt + createdAt + } + } +`; + +export const getAllWorkItems = ` + query($spaceId: ID!, $cursor: String) { + workItems(spaceId: $spaceId, cursor: $cursor) { + hasMore, + cursor, + workItems { + id + title + description + labels { + id + } + comments { + id + body + actor { + ... on User { + id + username + } + ... on IntegrationUser { + id + externalName + } + ... on Integration { + id + type + } + ... on Application { + id + name + } + } + } + } + } + } +`; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6056f40f71..963d6c5912 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -133,6 +133,7 @@ "dist/credentials/JotFormApi.credentials.js", "dist/credentials/Kafka.credentials.js", "dist/credentials/KeapOAuth2Api.credentials.js", + "dist/credentials/KitemakerApi.credentials.js", "dist/credentials/LemlistApi.credentials.js", "dist/credentials/LineNotifyOAuth2Api.credentials.js", "dist/credentials/LingvaNexApi.credentials.js", @@ -406,6 +407,7 @@ "dist/nodes/Kafka/KafkaTrigger.node.js", "dist/nodes/Keap/Keap.node.js", "dist/nodes/Keap/KeapTrigger.node.js", + "dist/nodes/Kitemaker/Kitemaker.node.js", "dist/nodes/Lemlist/Lemlist.node.js", "dist/nodes/Lemlist/LemlistTrigger.node.js", "dist/nodes/Line/Line.node.js", From fc54f7c82b671c540480ab4baf0c0280e4e565f1 Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Sat, 1 May 2021 00:35:34 +0200 Subject: [PATCH 12/41] :sparkles: Add query parameters for CrateDB, PostgresDB, TimescaleDB and QuestDB (Parametrized Queries) (#1577) * Adding support to ParameterizedQuery on Postgres Node * Created another parameter to toggle on replacement so it's clear to users what is happening. * Fixed lint issues * :zap: Formatting * Improvements to questDB node so it is more consistent * Fixed lint issues * Fixed typing issue * :zap: Apply suggestions BHesseldieck Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com> * Standardized output for postgres and postgres-like nodes This changes the behavior of CrateDB, Postgres, QuestDB and TimescaleDB nodes. The Execute Query operation used to execute multiple queries but return the result from only one of the queries. This change causes the node output to containt results from all queries that ran, making the behavior more consistent across all n8n. * Fixing lint issues * :zap: Minor improvements * :zap: Fix breaking changes files Co-authored-by: Gustavo Arjones Co-authored-by: Jan Oberhauser Co-authored-by: Jan Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com> --- packages/cli/BREAKING-CHANGES.md | 10 +++++ .../nodes-base/nodes/CrateDb/CrateDb.node.ts | 19 +++++++++- .../nodes/Postgres/Postgres.node.functions.ts | 37 +++++++++++++------ .../nodes/Postgres/Postgres.node.ts | 19 +++++++++- .../nodes-base/nodes/QuestDb/QuestDb.node.ts | 21 +++++++++-- .../nodes/TimescaleDb/TimescaleDb.node.ts | 24 +++++++++--- 6 files changed, 105 insertions(+), 25 deletions(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 55be53d7c7..428e8317c1 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,15 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.118.0 + +### What changed? +In the Postgres, CrateDB, QuestDB and TimescaleDB nodes the `Execute Query` operation returns the result from all queries executed instead of just one of the results. + +### When is action necessary? + +If you use any of the above mentioned nodes with the `Execute Query` operation and the result is relevant to you, you are encouraged to revisit your logic. The node output may now contain more information than before. This change was made so that the behavior is more consistent across n8n where input with multiple rows should yield results acccording all input data instead of only one. Please note: n8n was already running multiple queries based on input. Only the output was changed. + ## 0.117.0 ### What changed? @@ -50,6 +59,7 @@ If you are using a Dropbox APP with permission type, "App Folder". ### How to upgrade: Open your Dropbox node's credentials and set the "APP Access Type" parameter to "App Folder". +>>>>>>> master ## 0.111.0 diff --git a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts index da74abdd6c..4dc1130b52 100644 --- a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts +++ b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts @@ -80,9 +80,9 @@ export class CrateDb implements INodeType { }, }, default: '', - placeholder: 'SELECT id, name FROM product WHERE id < 40', + placeholder: 'SELECT id, name FROM product WHERE quantity > $1 AND price <= $2', required: true, - description: 'The SQL query to execute.', + description: 'The SQL query to execute. You can use n8n expressions or $1 and $2 in conjunction with query parameters.', }, // ---------------------------------- @@ -235,6 +235,21 @@ export class CrateDb implements INodeType { 'See the docs for more examples', ].join('
'), }, + { + displayName: 'Query Parameters', + name: 'queryParams', + type: 'string', + displayOptions: { + show: { + '/operation': [ + 'executeQuery', + ], + }, + }, + default: '', + placeholder: 'quantity,price', + description: 'Comma separated list of properties which should be used as query parameters.', + }, ], }, ], diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts index 30cf358579..1ba9440de3 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts @@ -60,27 +60,40 @@ export async function pgQuery( getNodeParam: Function, pgp: pgPromise.IMain<{}, pg.IClient>, db: pgPromise.IDatabase<{}, pg.IClient>, - input: INodeExecutionData[], + items: INodeExecutionData[], continueOnFail: boolean, overrideMode?: string, ): Promise { const additionalFields = getNodeParam('additionalFields', 0) as IDataObject; + + let valuesArray = [] as string[][]; + if (additionalFields.queryParams) { + const propertiesString = additionalFields.queryParams as string; + const properties = propertiesString.split(',').map(column => column.trim()); + const paramsItems = getItemsCopy(items, properties); + valuesArray = paramsItems.map((row) => properties.map(col => row[col])) as string[][]; + } + + const allQueries = [] as Array<{query: string, values?: string[]}>; + for (let i = 0; i < items.length; i++) { + const query = getNodeParam('query', i) as string; + const values = valuesArray[i]; + const queryFormat = { query, values }; + allQueries.push(queryFormat); + } + const mode = overrideMode ? overrideMode : (additionalFields.mode ?? 'multiple') as string; if (mode === 'multiple') { - const queries: string[] = []; - for (let i = 0; i < input.length; i++) { - queries.push(getNodeParam('query', i) as string); - } - return (await db.multi(pgp.helpers.concat(queries))).flat(1); + return (await db.multi(pgp.helpers.concat(allQueries))).flat(1); } else if (mode === 'transaction') { return db.tx(async t => { const result: IDataObject[] = []; - for (let i = 0; i < input.length; i++) { + for (let i = 0; i < allQueries.length; i++) { try { - Array.prototype.push.apply(result, await t.any(getNodeParam('query', i) as string)); + Array.prototype.push.apply(result, await t.any(allQueries[i].query, allQueries[i].values)); } catch (err) { if (continueOnFail === false) throw err; - result.push({ ...input[i].json, code: err.code, message: err.message }); + result.push({ ...items[i].json, code: err.code, message: err.message }); return result; } } @@ -89,12 +102,12 @@ export async function pgQuery( } else if (mode === 'independently') { return db.task(async t => { const result: IDataObject[] = []; - for (let i = 0; i < input.length; i++) { + for (let i = 0; i < allQueries.length; i++) { try { - Array.prototype.push.apply(result, await t.any(getNodeParam('query', i) as string)); + Array.prototype.push.apply(result, await t.any(allQueries[i].query, allQueries[i].values)); } catch (err) { if (continueOnFail === false) throw err; - result.push({ ...input[i].json, code: err.code, message: err.message }); + result.push({ ...items[i].json, code: err.code, message: err.message }); } } return result; diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index ff30f77ecf..c549bfab14 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -73,9 +73,9 @@ export class Postgres implements INodeType { }, }, default: '', - placeholder: 'SELECT id, name FROM product WHERE id < 40', + placeholder: 'SELECT id, name FROM product WHERE quantity > $1 AND price <= $2', required: true, - description: 'The SQL query to execute.', + description: 'The SQL query to execute. You can use n8n expressions or $1 and $2 in conjunction with query parameters.', }, // ---------------------------------- @@ -232,6 +232,21 @@ export class Postgres implements INodeType { 'See the docs for more examples', ].join('
'), }, + { + displayName: 'Query Parameters', + name: 'queryParams', + type: 'string', + displayOptions: { + show: { + '/operation': [ + 'executeQuery', + ], + }, + }, + default: '', + placeholder: 'quantity,price', + description: 'Comma separated list of properties which should be used as query parameters.', + }, ], }, ], diff --git a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts index 67a1ceca41..03e26d41ed 100644 --- a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts +++ b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts @@ -73,9 +73,9 @@ export class QuestDb implements INodeType { }, }, default: '', - placeholder: 'SELECT id, name FROM product WHERE id < 40', + placeholder: 'SELECT id, name FROM product WHERE quantity > $1 AND price <= $2', required: true, - description: 'The SQL query to execute.', + description: 'The SQL query to execute. You can use n8n expressions or $1 and $2 in conjunction with query parameters.', }, // ---------------------------------- @@ -176,6 +176,21 @@ export class QuestDb implements INodeType { 'See the docs for more examples', ].join('
'), }, + { + displayName: 'Query Parameters', + name: 'queryParams', + type: 'string', + displayOptions: { + show: { + '/operation': [ + 'executeQuery', + ], + }, + }, + default: '', + placeholder: 'quantity,price', + description: 'Comma separated list of properties which should be used as query parameters.', + }, ], }, { @@ -215,7 +230,7 @@ export class QuestDb implements INodeType { const db = pgp(config); - let returnItems = []; + let returnItems: INodeExecutionData[] = []; const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0) as string; diff --git a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts index 77ad53fb6a..32e7590d47 100644 --- a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts +++ b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts @@ -11,7 +11,6 @@ import { } from 'n8n-workflow'; import { - getItemCopy, pgInsert, pgQuery, pgUpdate, @@ -77,15 +76,13 @@ export class TimescaleDb implements INodeType { }, displayOptions: { show: { - operation: [ - 'executeQuery', - ], + operation: ['executeQuery'], }, }, default: '', - placeholder: 'SELECT id, name FROM product WHERE id < 40', + placeholder: 'SELECT id, name FROM product WHERE quantity > $1 AND price <= $2', required: true, - description: 'The SQL query to execute.', + description: 'The SQL query to execute. You can use n8n expressions or $1 and $2 in conjunction with query parameters.', }, // ---------------------------------- @@ -256,6 +253,21 @@ export class TimescaleDb implements INodeType { 'See the docs for more examples', ].join('
'), }, + { + displayName: 'Query Parameters', + name: 'queryParams', + type: 'string', + displayOptions: { + show: { + '/operation': [ + 'executeQuery', + ], + }, + }, + default: '', + placeholder: 'quantity,price', + description: 'Comma separated list of properties which should be used as query parameters.', + }, ], }, ], From f79bc633c032f2fe40846316b737beb64d48c71d Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 30 Apr 2021 19:44:12 -0500 Subject: [PATCH 13/41] :sparkles: Extend Twist Node (#1721) * Add get/getAll:messageConversation to Twist node * Add delete:messageConversation to Twist node * Add update:messageConversation to Twist node * Add archive/unarchive/delete:channel to Twist node * Add add/update/get/getAll/remove:Thread to Twist node * Add add/update/get/getAll/remove:Comment to Twist node * Lint fixes * Fix operations's descriptions * Enhance Twist node code * Reorder attributes alphabetically * Fix typos * Fix the ouput of get:Comment operation * Fix getAll:Comment & getAll:Thread operations outputs * :bug: Add missing scopes and remove not needed parameters Co-authored-by: dali --- .../credentials/TwistOAuth2Api.credentials.ts | 2 + .../nodes/Twist/ChannelDescription.ts | 44 +- .../nodes/Twist/CommentDescription.ts | 561 +++++++++++++++++ .../Twist/MessageConversationDescription.ts | 315 +++++++++- .../nodes/Twist/ThreadDescription.ts | 567 ++++++++++++++++++ packages/nodes-base/nodes/Twist/Twist.node.ts | 482 ++++++++++++++- 6 files changed, 1946 insertions(+), 25 deletions(-) create mode 100644 packages/nodes-base/nodes/Twist/CommentDescription.ts create mode 100644 packages/nodes-base/nodes/Twist/ThreadDescription.ts diff --git a/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts index 850a53dabc..3c6b820791 100644 --- a/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts @@ -6,7 +6,9 @@ import { const scopes = [ 'attachments:write', 'channels:remove', + 'comments:remove', 'messages:remove', + 'threads:remove', 'workspaces:read', ]; diff --git a/packages/nodes-base/nodes/Twist/ChannelDescription.ts b/packages/nodes-base/nodes/Twist/ChannelDescription.ts index 6412125330..fd0e33a3eb 100644 --- a/packages/nodes-base/nodes/Twist/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Twist/ChannelDescription.ts @@ -15,11 +15,21 @@ export const channelOperations = [ }, }, options: [ + { + name: 'Archive', + value: 'archive', + description: 'Archive a channel', + }, { name: 'Create', value: 'create', description: 'Initiates a public or private channel-based conversation', }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a channel', + }, { name: 'Get', value: 'get', @@ -30,6 +40,11 @@ export const channelOperations = [ value: 'getAll', description: 'Get all channels', }, + { + name: 'Unarchive', + value: 'unarchive', + description: 'Unarchive a channel', + }, { name: 'Update', value: 'update', @@ -64,7 +79,7 @@ export const channelFields = [ }, }, required: true, - description: 'The id of the workspace.', + description: 'The ID of the workspace.', }, { displayName: 'Name', @@ -156,28 +171,28 @@ export const channelFields = [ }, ], default: 0, - description: 'The color of the channel', + description: 'The color of the channel.', }, { displayName: 'Description', name: 'description', type: 'string', default: '', - description: 'The description of the channel', + description: 'The description of the channel.', }, { displayName: 'Public', name: 'public', type: 'boolean', default: false, - description: 'If enabled, the channel will be marked as public', + description: 'If enabled, the channel will be marked as public.', }, { displayName: 'Temp ID', name: 'temp_id', type: 'number', default: -1, - description: 'The temporary id of the channel. It needs to be a negative number.', + description: 'The temporary ID of the channel. It needs to be a negative number.', }, { displayName: 'User IDs', @@ -194,8 +209,9 @@ export const channelFields = [ }, ], }, + /* -------------------------------------------------------------------------- */ - /* channel:get */ + /* channel:get/archive/unarchive/delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Channel ID', @@ -205,7 +221,10 @@ export const channelFields = [ displayOptions: { show: { operation: [ + 'archive', + 'delete', 'get', + 'unarchive', ], resource: [ 'channel', @@ -213,8 +232,9 @@ export const channelFields = [ }, }, required: true, - description: 'The ID of the channel', + description: 'The ID of the channel.', }, + /* -------------------------------------------------------------------------- */ /* channel:getAll */ /* -------------------------------------------------------------------------- */ @@ -302,7 +322,7 @@ export const channelFields = [ name: 'archived', type: 'boolean', default: false, - description: 'If enabled, only archived conversations are returned', + description: 'If enabled, only archived conversations are returned.', }, ], }, @@ -400,28 +420,28 @@ export const channelFields = [ }, ], default: 0, - description: 'The color of the channel', + description: 'The color of the channel.', }, { displayName: 'Description', name: 'description', type: 'string', default: '', - description: 'The description of the channel', + description: 'The description of the channel.', }, { displayName: 'Name', name: 'name', type: 'string', default: '', - description: 'The name of the channel', + description: 'The name of the channel.', }, { displayName: 'Public', name: 'public', type: 'boolean', default: false, - description: 'If enabled, the channel will be marked as public', + description: 'If enabled, the channel will be marked as public.', }, ], }, diff --git a/packages/nodes-base/nodes/Twist/CommentDescription.ts b/packages/nodes-base/nodes/Twist/CommentDescription.ts new file mode 100644 index 0000000000..0aa0500d1a --- /dev/null +++ b/packages/nodes-base/nodes/Twist/CommentDescription.ts @@ -0,0 +1,561 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const commentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'comment', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new comment to a thread', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a comment', + }, + { + name: 'Get', + value: 'get', + description: 'Get information about a comment', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all comments', + }, + { + name: 'Update', + value: 'update', + description: 'Update a comment', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const commentFields = [ + /*-------------------------------------------------------------------------- */ + /* comment:create */ + /* ------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the thread.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The content of the comment.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + { + displayName: 'Mark thread position', + name: 'mark_thread_position', + type: 'boolean', + default: true, + description: 'By default, the position of the thread is marked.', + }, + { + displayName: 'Recipients', + name: 'recipients', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that will attached to the comment.', + }, + { + displayName: 'Temporary ID', + name: 'temp_id', + type: 'number', + default: 0, + description: 'The temporary ID of the comment.', + }, + { + displayName: 'Send as integration', + name: 'send_as_integration', + type: 'boolean', + default: false, + description: 'Displays the integration as the comment creator.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* comment:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get', + 'delete', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the comment.', + }, + + /* -------------------------------------------------------------------------- */ + /* comment:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'As IDs', + name: 'as_ids', + type: 'boolean', + default: false, + description: 'If enabled, only the ids of the comments are returned.', + }, + { + displayName: 'Ending Object Index', + name: 'to_obj_index', + type: 'number', + default: 50, + description: 'Limit comments ending at the specified object index.', + }, + { + displayName: 'Newer Than', + name: 'newer_than_ts', + type: 'dateTime', + default: '', + description: 'Limits comments to those newer when the specified Unix time.', + }, + { + displayName: 'Older Than', + name: 'older_than_ts', + type: 'dateTime', + default: '', + description: 'Limits comments to those older than the specified Unix time.', + }, + { + displayName: 'Order By', + name: 'order_by', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: 'ASC', + description: 'The order of the comments returned - one of DESC or ASC.', + }, + { + displayName: 'Starting Object Index', + name: 'from_obj_index', + type: 'number', + default: 0, + description: 'Limit comments starting at the specified object index.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* comment:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the comment.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + description: 'The content of the comment.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts b/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts index d81f1254dc..c08201339e 100644 --- a/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts +++ b/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts @@ -20,6 +20,26 @@ export const messageConversationOperations = [ value: 'create', description: 'Create a message in a conversation', }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a message in a conversation', + }, + { + name: 'Get', + value: 'get', + description: 'Get a message in a conversation', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all messages in a conversation', + }, + { + name: 'Update', + value: 'update', + description: 'Update a message in a conversation', + }, ], default: 'create', description: 'The operation to perform.', @@ -91,7 +111,7 @@ export const messageConversationFields = [ ], }, }, - description: `The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.`, + description: 'The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.', }, { displayName: 'Additional Fields', @@ -108,7 +128,7 @@ export const messageConversationFields = [ }, }, default: {}, - description: 'Other options to set', + description: 'Other options to set.', placeholder: 'Add options', options: [ { @@ -128,7 +148,7 @@ export const messageConversationFields = [ displayName: 'Action', name: 'action', type: 'options', - description: 'The action of the button', + description: 'The action of the button.', options: [ { name: 'Open URL', @@ -171,7 +191,7 @@ export const messageConversationFields = [ displayName: 'Type', name: 'type', type: 'options', - description: 'The type of the button, for now just action is available.', + description: 'The type of the button. (Currently only action is available).', options: [ { name: 'Action', @@ -191,7 +211,7 @@ export const messageConversationFields = [ ], }, }, - description: 'URL to redirect', + description: 'URL to redirect.', default: '', }, ], @@ -213,7 +233,7 @@ export const messageConversationFields = [ loadOptionsMethod: 'getUsers', }, default: [], - description: `The users that are directly mentioned`, + description: 'The users that are directly mentioned.', }, // { // displayName: 'Direct Group Mentions ', @@ -223,8 +243,289 @@ export const messageConversationFields = [ // loadOptionsMethod: 'getGroups', // }, // default: [], - // description: `The groups that are directly mentioned`, + // description: 'The groups that are directly mentioned.', // }, ], }, + + /* -------------------------------------------------------------------------- */ + /* messageConversation:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the workspace.', + }, + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getConversations', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'messageConversation', + ], + }, + }, + default: {}, + description: 'Other options to set.', + options: [ + { + displayName: 'Ending Object Index', + name: 'to_obj_index', + type: 'number', + default: 50, + description: 'Limit messages ending at the specified object index.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Limits the number of messages returned.', + }, + { + displayName: 'Order By', + name: 'order_by', + type: 'options', + default: 'ASC', + description: 'The order of the conversations returned - one of DESC or ASC.', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + }, + { + displayName: 'Starting Object Index', + name: 'from_obj_index', + type: 'number', + default: 0, + description: 'Limit messages starting at the specified object index.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* messageConversation:get/delete/update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'delete', + 'get', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation message.', + }, + + /* -------------------------------------------------------------------------- */ + /* messageConversation:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation Message ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation message.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'messageConversation', + ], + }, + }, + default: {}, + description: 'Other options to set.', + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + description: 'The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: 'The users that are directly mentioned.', + }, + ], + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/ThreadDescription.ts b/packages/nodes-base/nodes/Twist/ThreadDescription.ts new file mode 100644 index 0000000000..4ce8f48a47 --- /dev/null +++ b/packages/nodes-base/nodes/Twist/ThreadDescription.ts @@ -0,0 +1,567 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const threadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'thread', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new thread in a channel', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a thread', + }, + { + name: 'Get', + value: 'get', + description: 'Get information about a thread', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all threads', + }, + { + name: 'Update', + value: 'update', + description: 'Update a thread', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const threadFields = [ + /*-------------------------------------------------------------------------- */ + /* thread:create */ + /* ------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The title of the new thread (1 < length < 300).', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The content of the thread.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + { + displayName: 'Recipients', + name: 'recipients', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that will attached to the thread.', + }, + { + displayName: 'Send as integration', + name: 'send_as_integration', + type: 'boolean', + default: false, + description: 'Displays the integration as the thread creator.', + }, + { + displayName: 'Temporary ID', + name: 'temp_id', + type: 'number', + default: 0, + description: 'The temporary ID of the thread.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* thread:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get', + 'delete', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the thread.', + }, + /* -------------------------------------------------------------------------- */ + /* thread:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'As IDs', + name: 'as_ids', + type: 'boolean', + default: false, + description: 'If enabled, only the IDs of the threads are returned.', + }, + { + displayName: 'Filter By', + name: 'filter_by', + type: 'options', + options: [ + { + name: 'Attached to me', + value: 'attached_to_me', + }, + { + name: 'Everyone', + value: 'everyone', + }, + { + name: 'Starred', + value: 'is_starred', + }, + ], + default: '', + description: 'A filter can be one of attached_to_me, everyone and is_starred.', + }, + { + displayName: 'Newer Than', + name: 'newer_than_ts', + type: 'dateTime', + default: '', + description: 'Limits threads to those newer when the specified Unix time.', + }, + { + displayName: 'Older Than', + name: 'older_than_ts', + type: 'dateTime', + default: '', + description: 'Limits threads to those older than the specified Unix time.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* thread:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the thread.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + description: 'The content of the thread.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'The title of the thread (1 < length < 300).', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/Twist.node.ts b/packages/nodes-base/nodes/Twist/Twist.node.ts index d2fe9f18dd..3d9c70092c 100644 --- a/packages/nodes-base/nodes/Twist/Twist.node.ts +++ b/packages/nodes-base/nodes/Twist/Twist.node.ts @@ -29,7 +29,16 @@ import { messageConversationOperations, } from './MessageConversationDescription'; +import { + threadFields, + threadOperations +} from './ThreadDescription'; +import { + commentFields, + commentOperations +} from './CommentDescription'; import uuid = require('uuid'); +import * as moment from 'moment'; export class Twist implements INodeType { description: INodeTypeDescription = { @@ -62,18 +71,30 @@ export class Twist implements INodeType { name: 'Channel', value: 'channel', }, + { + name: 'Comment', + value: 'comment', + }, { name: 'Message Conversation', value: 'messageConversation', }, + { + name: 'Thread', + value: 'thread', + }, ], default: 'messageConversation', description: 'The resource to operate on.', }, ...channelOperations, ...channelFields, + ...commentOperations, + ...commentFields, ...messageConversationOperations, ...messageConversationFields, + ...threadOperations, + ...threadFields, ], }; @@ -169,10 +190,15 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'POST', '/channels/add', body); } + //https://developer.twist.com/v3/#remove-channel + if (operation === 'delete') { + qs.id = this.getNodeParameter('channelId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/channels/remove', {}, qs); + } //https://developer.twist.com/v3/#get-channel if (operation === 'get') { - const channelId = this.getNodeParameter('channelId', i) as string; - qs.id = channelId; + qs.id = this.getNodeParameter('channelId', i) as string; responseData = await twistApiRequest.call(this, 'GET', '/channels/getone', {}, qs); } @@ -202,6 +228,190 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'POST', '/channels/update', body); } + //https://developer.twist.com/v3/#archive-channel + if (operation === 'archive') { + qs.id = this.getNodeParameter('channelId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/channels/archive', {}, qs); + } + //https://developer.twist.com/v3/#unarchive-channel + if (operation === 'unarchive') { + qs.id = this.getNodeParameter('channelId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/channels/unarchive', {}, qs); + } + } + if (resource === 'comment') { + //https://developer.twist.com/v3/#add-comment + if (operation === 'create') { + const threadId = this.getNodeParameter('threadId', i) as string; + const content = this.getNodeParameter('content', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + thread_id: threadId, + content, + }; + Object.assign(body, additionalFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/comments/add', body); + } + //https://developer.twist.com/v3/#remove-comment + if (operation === 'delete') { + qs.id = this.getNodeParameter('commentId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/comments/remove', {}, qs); + } + //https://developer.twist.com/v3/#get-comment + if (operation === 'get') { + qs.id = this.getNodeParameter('commentId', i) as string; + + responseData = await twistApiRequest.call(this, 'GET', '/comments/getone', {}, qs); + responseData = responseData?.comment; + } + //https://developer.twist.com/v3/#get-all-comments + if (operation === 'getAll') { + const threadId = this.getNodeParameter('threadId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.thread_id = threadId; + + Object.assign(qs, filters); + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + if (qs.older_than_ts) { + qs.older_than_ts = moment(qs.older_than_ts as string).unix(); + } + if (qs.newer_than_ts) { + qs.newer_than_ts = moment(qs.newer_than_ts as string).unix(); + } + + responseData = await twistApiRequest.call(this, 'GET', '/comments/get', {}, qs); + if (qs.as_ids) { + responseData = (responseData as Array).map(id => ({ ID: id })); + } + } + //https://developer.twist.com/v3/#update-comment + if (operation === 'update') { + const commentId = this.getNodeParameter('commentId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id: commentId, + }; + Object.assign(body, updateFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/comments/update', body); + } } if (resource === 'messageConversation') { //https://developer.twist.com/v3/#add-message-to-conversation @@ -244,7 +454,7 @@ export class Twist implements INodeType { attachments.push(await twistApiRequest.call( this, 'POST', - `/attachments/upload`, + '/attachments/upload', {}, {}, { @@ -265,11 +475,11 @@ export class Twist implements INodeType { } if (body.direct_mentions) { - const direcMentions: string[] = []; + const directMentions: string[] = []; for (const directMention of body.direct_mentions as number[]) { - direcMentions.push(`[name](twist-mention://${directMention})`); + directMentions.push(`[name](twist-mention://${directMention})`); } - body.content = `${direcMentions.join(' ')} ${body.content}`; + body.content = `${directMentions.join(' ')} ${body.content}`; } // if (body.direct_group_mentions) { @@ -282,6 +492,266 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/add', body); } + //https://developer.twist.com/v3/#get-message + if (operation === 'get') { + qs.id = this.getNodeParameter('id', i) as string; + + responseData = await twistApiRequest.call(this, 'GET', '/conversation_messages/getone', {}, qs); + } + //https://developer.twist.com/v3/#get-all-messages + if (operation === 'getAll') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + qs.conversation_id = conversationId; + Object.assign(qs, additionalFields); + + responseData = await twistApiRequest.call(this, 'GET', '/conversation_messages/get', {}, qs); + } + //https://developer.twist.com/v3/#remove-message-from-conversation + if (operation === 'delete') { + qs.id = this.getNodeParameter('id', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/remove', {}, qs); + } + //https://developer.twist.com/v3/#update-message-in-conversation + if (operation === 'update') { + const id = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id, + }; + Object.assign(body, updateFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/update', body); + } + } + if (resource === 'thread') { + //https://developer.twist.com/v3/#add-thread + if (operation === 'create') { + const channelId = this.getNodeParameter('channelId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const content = this.getNodeParameter('content', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + channel_id: channelId, + content, + title, + }; + Object.assign(body, additionalFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/threads/add', body); + } + //https://developer.twist.com/v3/#remove-thread + if (operation === 'delete') { + qs.id = this.getNodeParameter('threadId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/threads/remove', {}, qs); + } + //https://developer.twist.com/v3/#get-thread + if (operation === 'get') { + qs.id = this.getNodeParameter('threadId', i) as string; + + responseData = await twistApiRequest.call(this, 'GET', '/threads/getone', {}, qs); + } + //https://developer.twist.com/v3/#get-all-threads + if (operation === 'getAll') { + const channelId = this.getNodeParameter('channelId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel_id = channelId; + + Object.assign(qs, filters); + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + if (qs.older_than_ts) { + qs.older_than_ts = moment(qs.older_than_ts as string).unix(); + } + if (qs.newer_than_ts) { + qs.newer_than_ts = moment(qs.newer_than_ts as string).unix(); + } + + responseData = await twistApiRequest.call(this, 'GET', '/threads/get', {}, qs); + if (qs.as_ids) { + responseData = (responseData as Array).map(id => ({ ID: id })); + } + } + //https://developer.twist.com/v3/#update-thread + if (operation === 'update') { + const threadId = this.getNodeParameter('threadId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id: threadId, + }; + Object.assign(body, updateFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/threads/update', body); + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); From 0dd760f67dde3f98d07466e3bf8e0cf72ba40b2d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 30 Apr 2021 20:07:08 -0500 Subject: [PATCH 14/41] :shirt: Fix lint issue --- packages/nodes-base/nodes/Twist/Twist.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Twist/Twist.node.ts b/packages/nodes-base/nodes/Twist/Twist.node.ts index 3d9c70092c..93ad3ae0b7 100644 --- a/packages/nodes-base/nodes/Twist/Twist.node.ts +++ b/packages/nodes-base/nodes/Twist/Twist.node.ts @@ -343,7 +343,7 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'GET', '/comments/get', {}, qs); if (qs.as_ids) { - responseData = (responseData as Array).map(id => ({ ID: id })); + responseData = (responseData as number[]).map(id => ({ ID: id })); } } //https://developer.twist.com/v3/#update-comment @@ -683,7 +683,7 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'GET', '/threads/get', {}, qs); if (qs.as_ids) { - responseData = (responseData as Array).map(id => ({ ID: id })); + responseData = (responseData as number[]).map(id => ({ ID: id })); } } //https://developer.twist.com/v3/#update-thread From 6c773d7a862b910e6bd503b74fc45491c03ca73e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Apr 2021 21:23:25 -0400 Subject: [PATCH 15/41] :sparkles: Add MQTT & Trigger Node (#1705) * :sparkles: MQTT-Node * :zap: Small fix * :zap: Error when the publish method faile * :zap: Improvements * :zap: Improvements * :zap: Add Send Input Data parameter * :zap: Minor improvements Co-authored-by: Jan Oberhauser --- .../credentials/Mqtt.credentials.ts | 18 +- packages/nodes-base/nodes/MQTT/Mqtt.node.json | 21 +++ packages/nodes-base/nodes/MQTT/Mqtt.node.ts | 171 ++++++++++++++++++ .../nodes-base/nodes/MQTT/MqttTrigger.node.ts | 49 +++-- packages/nodes-base/nodes/MQTT/mqtt.png | Bin 2310 -> 0 bytes packages/nodes-base/nodes/MQTT/mqtt.svg | 21 +++ packages/nodes-base/package.json | 1 + 7 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 packages/nodes-base/nodes/MQTT/Mqtt.node.json create mode 100644 packages/nodes-base/nodes/MQTT/Mqtt.node.ts delete mode 100644 packages/nodes-base/nodes/MQTT/mqtt.png create mode 100644 packages/nodes-base/nodes/MQTT/mqtt.svg diff --git a/packages/nodes-base/credentials/Mqtt.credentials.ts b/packages/nodes-base/credentials/Mqtt.credentials.ts index 05d4b0ec5c..a474e0501b 100644 --- a/packages/nodes-base/credentials/Mqtt.credentials.ts +++ b/packages/nodes-base/credentials/Mqtt.credentials.ts @@ -3,15 +3,11 @@ import { NodePropertyTypes, } from 'n8n-workflow'; - export class Mqtt implements ICredentialType { name = 'mqtt'; displayName = 'MQTT'; documentationUrl = 'mqtt'; properties = [ - // The credentials to get from user and save encrypted. - // Properties can be defined exactly in the same way - // as node properties. { displayName: 'Protocol', name: 'protocol', @@ -55,5 +51,19 @@ export class Mqtt implements ICredentialType { }, default: '', }, + { + displayName: 'Clean Session', + name: 'clean', + type: 'boolean' as NodePropertyTypes, + default: true, + description: `Set to false to receive QoS 1 and 2 messages while offline.`, + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Client ID. If left empty, one is autogenrated for you', + }, ]; } diff --git a/packages/nodes-base/nodes/MQTT/Mqtt.node.json b/packages/nodes-base/nodes/MQTT/Mqtt.node.json new file mode 100644 index 0000000000..595ac2b74b --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/Mqtt.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.mqtt", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication", + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/mqtt" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.mqtt/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts new file mode 100644 index 0000000000..fc71fe6221 --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts @@ -0,0 +1,171 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import * as mqtt from 'mqtt'; + +import { + IClientOptions, +} from 'mqtt'; + +export class Mqtt implements INodeType { + description: INodeTypeDescription = { + displayName: 'MQTT', + name: 'mqtt', + icon: 'file:mqtt.svg', + group: ['input'], + version: 1, + description: 'Push messages to MQTT', + defaults: { + name: 'MQTT', + color: '#9b27af', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'mqtt', + required: true, + }, + ], + properties: [ + { + displayName: 'Topic', + name: 'topic', + type: 'string', + required: true, + default: '', + description: `The topic to publish to`, + }, + { + displayName: 'Send Input Data', + name: 'sendInputData', + type: 'boolean', + default: true, + description: 'Send the the data the node receives as JSON.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + required: true, + displayOptions: { + show: { + sendInputData: [ + false, + ], + }, + }, + default: '', + description: 'The message to publish', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'QoS', + name: 'qos', + type: 'options', + options: [ + { + name: 'Received at Most Once', + value: 0, + }, + { + name: 'Received at Least Once', + value: 1, + }, + { + name: 'Exactly Once', + value: 2, + }, + ], + default: 0, + description: 'QoS subscription level', + }, + { + displayName: 'Retain', + name: 'retain', + type: 'boolean', + default: false, + description: `Normally if a publisher publishes a message to a topic, and no one is subscribed to
+ that topic the message is simply discarded by the broker. However the publisher can tell the broker
+ to keep the last message on that topic by setting the retain flag to true.`, + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = (items.length as unknown) as number; + const credentials = this.getCredentials('mqtt') as IDataObject; + + const protocol = credentials.protocol as string || 'mqtt'; + const host = credentials.host as string; + const brokerUrl = `${protocol}://${host}`; + const port = credentials.port as number || 1883; + const clientId = credentials.clientId as string || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; + const clean = credentials.clean as boolean; + + const clientOptions: IClientOptions = { + port, + clean, + clientId, + }; + + if (credentials.username && credentials.password) { + clientOptions.username = credentials.username as string; + clientOptions.password = credentials.password as string; + } + + const client = mqtt.connect(brokerUrl, clientOptions); + const sendInputData = this.getNodeParameter('sendInputData', 0) as boolean; + + // tslint:disable-next-line: no-any + const data = await new Promise((resolve, reject): any => { + client.on('connect', () => { + for (let i = 0; i < length; i++) { + + let message; + const topic = (this.getNodeParameter('topic', i) as string); + const options = (this.getNodeParameter('options', i) as IDataObject); + + try { + if (sendInputData === true) { + message = JSON.stringify(items[i].json); + } else { + message = this.getNodeParameter('message', i) as string; + } + client.publish(topic, message, options); + } catch (e) { + reject(e); + } + } + //wait for the in-flight messages to be acked. + //needed for messages with QoS 1 & 2 + client.end(false, {}, () => { + resolve([items]); + }); + + client.on('error', (e: string | undefined) => { + reject(e); + }); + }); + }); + + return data as INodeExecutionData[][]; + } +} diff --git a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts index c99078e919..94bbda8c18 100644 --- a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts +++ b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts @@ -13,14 +13,14 @@ import { import * as mqtt from 'mqtt'; import { - IClientOptions, + IClientOptions, ISubscriptionMap, } from 'mqtt'; export class MqttTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'MQTT Trigger', name: 'mqttTrigger', - icon: 'file:mqtt.png', + icon: 'file:mqtt.svg', group: ['trigger'], version: 1, description: 'Listens to MQTT events', @@ -43,7 +43,9 @@ export class MqttTrigger implements INodeType { type: 'string', default: '', description: `Topics to subscribe to, multiple can be defined with comma.
- wildcard characters are supported (+ - for single level and # - for multi level)`, + wildcard characters are supported (+ - for single level and # - for multi level)
+ By default all subscription used QoS=0. To set a different QoS, write the QoS desired
+ after the topic preceded by a colom. For Example: topicA:1,topicB:2`, }, { displayName: 'Options', @@ -52,6 +54,13 @@ export class MqttTrigger implements INodeType { placeholder: 'Add Option', default: {}, options: [ + { + displayName: 'JSON Parse Body', + name: 'jsonParseBody', + type: 'boolean', + default: false, + description: 'Try to parse the message to an object.', + }, { displayName: 'Only Message', name: 'onlyMessage', @@ -59,13 +68,6 @@ export class MqttTrigger implements INodeType { default: false, description: 'Returns only the message property.', }, - { - displayName: 'JSON Parse Message', - name: 'jsonParseMessage', - type: 'boolean', - default: false, - description: 'Try to parse the message to an object.', - }, ], }, ], @@ -81,6 +83,13 @@ export class MqttTrigger implements INodeType { const topics = (this.getNodeParameter('topics') as string).split(','); + const topicsQoS: IDataObject = {}; + + for (const data of topics) { + const [topic, qos] = data.split(':'); + topicsQoS[topic] = (qos) ? { qos: parseInt(qos, 10) } : { qos: 0 }; + } + const options = this.getNodeParameter('options') as IDataObject; if (!topics) { @@ -91,9 +100,13 @@ export class MqttTrigger implements INodeType { const host = credentials.host as string; const brokerUrl = `${protocol}://${host}`; const port = credentials.port as number || 1883; + const clientId = credentials.clientId as string || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; + const clean = credentials.clean as boolean; const clientOptions: IClientOptions = { port, + clean, + clientId, }; if (credentials.username && credentials.password) { @@ -108,20 +121,19 @@ export class MqttTrigger implements INodeType { async function manualTriggerFunction() { await new Promise((resolve, reject) => { client.on('connect', () => { - client.subscribe(topics, (err, granted) => { + client.subscribe(topicsQoS as ISubscriptionMap, (err, granted) => { if (err) { reject(err); } client.on('message', (topic: string, message: Buffer | string) => { // tslint:disable-line:no-any - let result: IDataObject = {}; message = message.toString() as string; - if (options.jsonParseMessage) { + if (options.jsonParseBody) { try { message = JSON.parse(message.toString()); - } catch (error) { } + } catch (err) { } } result.message = message; @@ -129,10 +141,9 @@ export class MqttTrigger implements INodeType { if (options.onlyMessage) { //@ts-ignore - result = message; + result = [message as string]; } - - self.emit([self.helpers.returnJsonArray([result])]); + self.emit([self.helpers.returnJsonArray(result)]); resolve(true); }); }); @@ -144,7 +155,9 @@ export class MqttTrigger implements INodeType { }); } - manualTriggerFunction(); + if (this.getMode() === 'trigger') { + manualTriggerFunction(); + } async function closeFunction() { client.end(); diff --git a/packages/nodes-base/nodes/MQTT/mqtt.png b/packages/nodes-base/nodes/MQTT/mqtt.png deleted file mode 100644 index 12c5f24952b3cc910c95bbd7809f443be26385cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2310 zcmV+h3HkPkP)q_E1s6v?#7r7 z%bcyxM!zw0Kvh39%mcJA^YbP{8>J$jqFA1Ii=c~CW)-B$ut%UV5y!+MOV72}c9F`b zk4CJh3L~=h%fN|ak4B3aDp*$BcHQ{nKqGZHm32CWz$nM^p9}iv(&F~o5%dUp1U-Tt zL64wE&<<#x6$D0r8Gh^_-YG9#X(WazOI#XsBv!Xg{=2uT^Vt5@$Nu$~)TD-16RXRYD77@KUb3U&+?LDFEf3H5Z~>3_&whKU-(kWrkO4~p`Qesfk^ zz482}a#BMfR%oth#R-byDeY7`>y%6A2lu*lV?9d$b+!1N%W8#YYQ~hsJEJ-pP2NGC zfIj!UfaxbS{$JE^9vMkadW%ojG#ffe(! z&Wsx6#WQHke6JOGNu7;d`{D{oKufo)(jEm1nw%K4EH}RSYU@YIdI@d6vx0Efg{#&S zrIQ#rWOV(ho2_3PV>Zq@m{Qn&qiaL`FK+$!?%nd1xVPq9e6OPH-_;wwTCLP5a$^dN zcTE&cTD!R!uY@iB%ltd#EqQ5rg(pq1bl`lyw7hsnH0%zd4q7@iF=*)^G}K}&rU_T9 zEy{2O-FT__(~TEjE6X(GT)SP?vV33S(Y0sC&-0u6>j*%XzF)QB>(wfqa$o$h&Rboi zf@B`Bv)4^a*cSEe#ioy!f2Q=Jp~ION6Zm4@qRvL+zPJ;B#^eu%4Hx=eP5YkM{MQYc zv zb!XlzS9vLTM(~dwv+{6qXQOFf+;Kp|)E6F85(5XI<&yv%yvJ0<8of*Cwua7}*@xpc z&5nQjd04Fe^v%3<{mP@s;mblwc2u5w`|^S_5wQ)yXB4g33!f z-CWs9t7$+7MEkBd3^8rmA6H29503$8Y22}$1RGkpMnA-u4o>u6vS)tBt*#H|ee%o% z&tH|TfW`8&&(sxsvm!q!G-*QFKdXx0F{pLQmB*9)rjKDQtg*4%XNngLXkX@BKflGG z^pyVe0rdEov6*>`O)cGfZM{do@JI?u+4I8vn)X8pfF|L9WR76PY%M($G>pl9&yS9O zZ`S>q4mkBZkJ5K7&;@uL$vVTBm_^&>hNXrWc2!sGt)mnqsidMeP7h6+pwQ7OodO!G z9 zk8J%i(mgk}k{j*^c^t!??(gx%m_4T5hqhgZJhF2A-EI_1BLs_U!YOy69o?91XmG1o zM#C^pyBRIJIVnyAw~Mjfm>veSEZ`6?NY|w_xl4@MyS!|Ds{7G)H-~Ux@OA03xUb9m zLFj_i;Y4P~ z!Zay0eB^UPDYHZ+JF2SQy`g-DcHuE|pIiRfcWGi$p7LG2Tkqn%4QE>sQZ%D=^6!0@ zZb|^-qo3G9K8~TLJ5BmmFR|5_LZyJj(j~VbU_Ab?_2467{hz`2mOF55U6HL2))Wv6 zV{QbSws%2bU|?UW?F5Ywg1QIh2e-eyU45wDRKbuOg$@0wKCHpRjS3QN7<_Q8(_eVs z7qlHEdPXXzXN3k&2=dVA287nYw-BVpMySiwW$Nj&Sj{kxx%4i=i{eRd~8@3dsPcox#uO g9i|`o^piyYA3KLr@}Cs({r~^~07*qoM6N<$f^m3?c>n+a diff --git a/packages/nodes-base/nodes/MQTT/mqtt.svg b/packages/nodes-base/nodes/MQTT/mqtt.svg new file mode 100644 index 0000000000..3e202aa2ce --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/mqtt.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 963d6c5912..516fa72e4e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -439,6 +439,7 @@ "dist/nodes/Mocean/Mocean.node.js", "dist/nodes/MondayCom/MondayCom.node.js", "dist/nodes/MongoDb/MongoDb.node.js", + "dist/nodes/MQTT/Mqtt.node.js", "dist/nodes/MQTT/MqttTrigger.node.js", "dist/nodes/MoveBinaryData.node.js", "dist/nodes/Msg91/Msg91.node.js", From f2ec7ec6aca69dc1ce4ea41029198bdb51fe553a Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Sat, 1 May 2021 03:28:07 +0200 Subject: [PATCH 16/41] :zap: Add and update codex files (#1719) --- .../nodes/Dropbox/Dropbox.node.json | 7 +++++++ .../Eventbrite/EventbriteTrigger.node.json | 7 +++++++ .../Google/BigQuery/GoogleBigQuery.node.json | 21 +++++++++++++++++++ .../Google/Calendar/GoogleCalendar.node.json | 5 +++++ .../nodes/Google/Drive/GoogleDrive.node.json | 5 +++++ .../nodes/Google/Gmail/Gmail.node.json | 5 +++++ .../nodes/Google/Sheet/GoogleSheets.node.json | 5 +++++ .../nodes/Hubspot/Hubspot.node.json | 5 +++++ .../nodes/Mailcheck/Mailcheck.node.json | 8 ++++++- .../OneDrive/MicrosoftOneDrive.node.json | 7 +++++++ .../nodes-base/nodes/N8nTrigger.node.json | 15 +++++++++++++ .../nodes-base/nodes/Trello/Trello.node.json | 5 +++++ .../nodes/Trello/TrelloTrigger.node.json | 7 +++++++ .../nodes/WorkflowTrigger.node.json | 15 +++++++++++++ .../nodes/Zendesk/Zendesk.node.json | 7 +++++++ 15 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.json create mode 100644 packages/nodes-base/nodes/N8nTrigger.node.json create mode 100644 packages/nodes-base/nodes/WorkflowTrigger.node.json diff --git a/packages/nodes-base/nodes/Dropbox/Dropbox.node.json b/packages/nodes-base/nodes/Dropbox/Dropbox.node.json index c4251d30f0..5ae88a50c4 100644 --- a/packages/nodes-base/nodes/Dropbox/Dropbox.node.json +++ b/packages/nodes-base/nodes/Dropbox/Dropbox.node.json @@ -15,6 +15,13 @@ { "url": "https://docs.n8n.io/nodes/n8n-nodes-base.dropbox/" } + ], + "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + } ] } } \ No newline at end of file diff --git a/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.json b/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.json index 1442bbd2f3..626eec3c78 100644 --- a/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.json +++ b/packages/nodes-base/nodes/Eventbrite/EventbriteTrigger.node.json @@ -15,6 +15,13 @@ { "url": "https://docs.n8n.io/nodes/n8n-nodes-base.eventbriteTrigger/" } + ], + "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + } ] } } \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.json b/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.json new file mode 100644 index 0000000000..920fe82e79 --- /dev/null +++ b/packages/nodes-base/nodes/Google/BigQuery/GoogleBigQuery.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.googleBigQuery", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Data & Storage", + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/google" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleBigQuery/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.json b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.json index 0113380aea..ed4457d823 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.json +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.json @@ -27,6 +27,11 @@ "icon": "🎫", "url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/" }, + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, { "label": "5 workflow automations for Mattermost that we love at n8n", "icon": "🤖", diff --git a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.json b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.json index 99b6331324..be55a73881 100644 --- a/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.json +++ b/packages/nodes-base/nodes/Google/Drive/GoogleDrive.node.json @@ -17,6 +17,11 @@ } ], "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, { "label": "Why this Product Manager loves workflow automation with n8n", "icon": "🧠", diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json index 9d3f7e8ce7..57b61b714a 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.json @@ -27,6 +27,11 @@ "icon": "🎫", "url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/" }, + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, { "label": "Using Automation to Boost Productivity in the Workplace", "icon": "💪", diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.json b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.json index 355c2a2f15..649f5f4955 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.json +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.json @@ -48,6 +48,11 @@ "icon": "📈", "url": "https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/" }, + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, { "label": "How Honest Burgers Use Automation to Save $100k per year", "icon": "🍔", diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.json b/packages/nodes-base/nodes/Hubspot/Hubspot.node.json index b0ebe88f6f..e2ac50d601 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.json +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.json @@ -17,6 +17,11 @@ } ], "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, { "label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin", "icon": "🎖", diff --git a/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.json b/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.json index b205ef1b28..663ce32994 100644 --- a/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.json +++ b/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.json @@ -3,13 +3,19 @@ "nodeVersion": "1.0", "codexVersion": "1.0", "categories": [ - "Utility" + "Utility", + "Marketing & Content" ], "resources": { "credentialDocumentation": [ { "url": "https://docs.n8n.io/credentials/mailcheck" } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.mailcheck/" + } ] } } \ No newline at end of file diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.json b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.json index ce00669547..3e90220118 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.json +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.json @@ -15,6 +15,13 @@ { "url": "https://docs.n8n.io/nodes/n8n-nodes-base.microsoftOneDrive/" } + ], + "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + } ] } } \ No newline at end of file diff --git a/packages/nodes-base/nodes/N8nTrigger.node.json b/packages/nodes-base/nodes/N8nTrigger.node.json new file mode 100644 index 0000000000..396dd11d3f --- /dev/null +++ b/packages/nodes-base/nodes/N8nTrigger.node.json @@ -0,0 +1,15 @@ +{ + "node": "n8n-nodes-base.n8nTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Core Nodes" + ], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.n8nTrigger/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Trello/Trello.node.json b/packages/nodes-base/nodes/Trello/Trello.node.json index 522237dd3a..82b6ca627e 100644 --- a/packages/nodes-base/nodes/Trello/Trello.node.json +++ b/packages/nodes-base/nodes/Trello/Trello.node.json @@ -17,6 +17,11 @@ } ], "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + }, { "label": "How a digital strategist uses n8n for online marketing", "icon": "💻", diff --git a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.json b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.json index 614372268b..8c9c974224 100644 --- a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.json +++ b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.json @@ -15,6 +15,13 @@ { "url": "https://docs.n8n.io/nodes/n8n-nodes-base.trelloTrigger/" } + ], + "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + } ] } } \ No newline at end of file diff --git a/packages/nodes-base/nodes/WorkflowTrigger.node.json b/packages/nodes-base/nodes/WorkflowTrigger.node.json new file mode 100644 index 0000000000..3b937f2106 --- /dev/null +++ b/packages/nodes-base/nodes/WorkflowTrigger.node.json @@ -0,0 +1,15 @@ +{ + "node": "n8n-nodes-base.WorkflowTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Core Nodes" + ], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.WorkflowTrigger/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.json b/packages/nodes-base/nodes/Zendesk/Zendesk.node.json index 194fc5d275..87307add31 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.json +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.json @@ -15,6 +15,13 @@ { "url": "https://docs.n8n.io/nodes/n8n-nodes-base.zendesk/" } + ], + "generic": [ + { + "label": "Hey founders! Your business doesn't need you to operate", + "icon": " 🖥️", + "url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/" + } ] } } \ No newline at end of file From 0cc3aea629b9dc225d76763d86d0ad49d11d3932 Mon Sep 17 00:00:00 2001 From: lublak <44057030+lublak@users.noreply.github.com> Date: Sat, 1 May 2021 03:29:44 +0200 Subject: [PATCH 17/41] :heavy_plus_sign: Add xml2js dependency to n8n-workflow (#1717) --- packages/workflow/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 7f53397619..53885db2d0 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -37,7 +37,8 @@ }, "dependencies": { "lodash.get": "^4.4.2", - "riot-tmpl": "^3.0.8" + "riot-tmpl": "^3.0.8", + "xml2js": "^0.4.23" }, "jest": { "transform": { From ab1c8037de65bce85703f292fdd5a10dbcb88f3d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 30 Apr 2021 20:42:32 -0500 Subject: [PATCH 18/41] :zap: Unify some dependency versions --- packages/cli/package.json | 2 +- packages/node-dev/package.json | 2 +- packages/nodes-base/package.json | 2 +- packages/workflow/package.json | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index daa98944e7..8dd4e347ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -64,7 +64,7 @@ "@types/jest": "^26.0.13", "@types/localtunnel": "^1.9.0", "@types/lodash.get": "^4.4.6", - "@types/node": "14.0.27", + "@types/node": "^14.14.40", "@types/open": "^6.1.0", "@types/parseurl": "^1.3.1", "@types/request-promise-native": "~1.0.15", diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 55fa3b59b1..64c637beb4 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -47,7 +47,7 @@ "@oclif/dev-cli": "^1.22.2", "@types/copyfiles": "^2.1.1", "@types/inquirer": "^6.5.0", - "@types/tmp": "^0.1.0", + "@types/tmp": "^0.2.0", "@types/vorpal": "^1.11.0", "tslint": "^6.1.2" }, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 516fa72e4e..4986fdb5dd 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -639,7 +639,7 @@ "uuid": "^3.4.0", "vm2": "^3.6.10", "xlsx": "^0.16.7", - "xml2js": "^0.4.22" + "xml2js": "^0.4.23" }, "jest": { "transform": { diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 53885db2d0..4a6e11004f 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -30,6 +30,7 @@ "@types/jest": "^26.0.13", "@types/lodash.get": "^4.4.6", "@types/node": "^14.14.40", + "@types/xml2js": "^0.4.3", "jest": "^26.4.2", "ts-jest": "^26.3.0", "tslint": "^6.1.2", From 9a7de7d07709ee196f92e092fac0e381225800af Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 30 Apr 2021 21:12:11 -0500 Subject: [PATCH 19/41] :bug: Fix bug with activating some trigger nodes #1715 --- .../nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts | 2 +- packages/nodes-base/nodes/Github/GithubTrigger.node.ts | 4 ++-- packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts index 8fe8eb9718..fffbe576c8 100644 --- a/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts +++ b/packages/nodes-base/nodes/GetResponse/GetResponseTrigger.node.ts @@ -180,7 +180,7 @@ export class GetResponseTrigger implements INodeType { } } } catch (error) { - if (error.message.includes('[404]')) { + if (error.httpCode === '404') { return false; } } diff --git a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts index 98388c45d1..64a5b6faf0 100644 --- a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts +++ b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts @@ -353,7 +353,7 @@ export class GithubTrigger implements INodeType { try { await githubApiRequest.call(this, 'GET', endpoint, {}); } catch (error) { - if (error.message.includes('[404]:')) { + if (error.httpCode === '404') { // Webhook does not exist delete webhookData.webhookId; delete webhookData.webhookEvents; @@ -399,7 +399,7 @@ export class GithubTrigger implements INodeType { try { responseData = await githubApiRequest.call(this, 'POST', endpoint, body); } catch (error) { - if (error.message.includes('[422]:')) { + if (error.httpCode === '422') { // Webhook exists already // Get the data of the already registered webhook diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts index 7a8828b93e..9098d987b7 100644 --- a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -179,7 +179,7 @@ export class GitlabTrigger implements INodeType { try { await gitlabApiRequest.call(this, 'GET', endpoint, {}); } catch (error) { - if (error.message.includes('[404]:')) { + if (error.httpCode === '404') { // Webhook does not exist delete webhookData.webhookId; delete webhookData.webhookEvents; From b7a074abd7e57a936962f39658471ad23a4f7cf0 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 30 Apr 2021 21:14:07 -0500 Subject: [PATCH 20/41] :zap: Fix breaking changes files --- packages/cli/BREAKING-CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 428e8317c1..1132496ee4 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -59,7 +59,6 @@ If you are using a Dropbox APP with permission type, "App Folder". ### How to upgrade: Open your Dropbox node's credentials and set the "APP Access Type" parameter to "App Folder". ->>>>>>> master ## 0.111.0 From c83c05456db008a47d3b3d1b7af89eabeb0c380b Mon Sep 17 00:00:00 2001 From: lublak <44057030+lublak@users.noreply.github.com> Date: Sat, 1 May 2021 04:22:15 +0200 Subject: [PATCH 21/41] :zap: Use native fs promise where possible (#1684) --- packages/cli/src/GenericHelpers.ts | 9 +++------ packages/cli/src/LoadNodesAndCredentials.ts | 16 +++++----------- packages/node-dev/src/Build.ts | 6 +++++- .../nodes-base/nodes/ExecuteWorkflow.node.ts | 7 ++----- packages/nodes-base/nodes/ReadBinaryFile.node.ts | 7 ++----- .../nodes-base/nodes/ReadBinaryFiles.node.ts | 7 ++----- .../nodes-base/nodes/WriteBinaryFile.node.ts | 7 ++----- 7 files changed, 21 insertions(+), 38 deletions(-) diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index 74e9578fec..8dba633baa 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -3,14 +3,11 @@ import * as express from 'express'; import { join as pathJoin } from 'path'; import { readFile as fsReadFile, -} from 'fs'; -import { promisify } from 'util'; +} from 'fs/promises'; import { IDataObject } from 'n8n-workflow'; import { IPackageVersions } from './'; -const fsReadFileAsync = promisify(fsReadFile); - let versionCache: IPackageVersions | undefined; @@ -72,7 +69,7 @@ export async function getVersions(): Promise { return versionCache; } - const packageFile = await fsReadFileAsync(pathJoin(__dirname, '../../package.json'), 'utf8') as string; + const packageFile = await fsReadFile(pathJoin(__dirname, '../../package.json'), 'utf8') as string; const packageData = JSON.parse(packageFile); versionCache = { @@ -122,7 +119,7 @@ export async function getConfigValue(configKey: string): Promise => { const results: string[] = []; const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`; - for (const file of await fsReaddirAsync(nodeModulesPath)) { + for (const file of await fsReaddir(nodeModulesPath)) { const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0; const isNpmScopedPackage = file.indexOf('@') === 0; if (!isN8nNodesPackage && !isNpmScopedPackage) { continue; } - if (!(await fsStatAsync(nodeModulesPath)).isDirectory()) { + if (!(await fsStat(nodeModulesPath)).isDirectory()) { continue; } if (isN8nNodesPackage) { results.push(`${relativePath}${file}`); } @@ -234,7 +228,7 @@ class LoadNodesAndCredentialsClass { const packagePath = path.join(this.nodeModulesPath, packageName); // Read the data from the package.json file to see if any n8n data is defiend - const packageFileString = await fsReadFileAsync(path.join(packagePath, 'package.json'), 'utf8'); + const packageFileString = await fsReadFile(path.join(packagePath, 'package.json'), 'utf8'); const packageFile = JSON.parse(packageFileString); if (!packageFile.hasOwnProperty('n8n')) { return; diff --git a/packages/node-dev/src/Build.ts b/packages/node-dev/src/Build.ts index ce2f16a263..4b470a6ff1 100644 --- a/packages/node-dev/src/Build.ts +++ b/packages/node-dev/src/Build.ts @@ -1,9 +1,13 @@ import { ChildProcess, spawn } from 'child_process'; const copyfiles = require('copyfiles'); + import { readFile as fsReadFile, +} from 'fs/promises'; +import { write as fsWrite, } from 'fs'; + import { join } from 'path'; import { file } from 'tmp-promise'; import { promisify } from 'util'; @@ -32,7 +36,7 @@ export async function createCustomTsconfig () { const tsconfigPath = join(__dirname, '../../src/tsconfig-build.json'); // Read the tsconfi file - const tsConfigString = await fsReadFileAsync(tsconfigPath, { encoding: 'utf8'}) as string; + const tsConfigString = await fsReadFile(tsconfigPath, { encoding: 'utf8'}) as string; const tsConfig = JSON.parse(tsConfigString); // Set absolute include paths diff --git a/packages/nodes-base/nodes/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow.node.ts index e4883cd4e8..7ca01e8327 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow.node.ts @@ -1,9 +1,6 @@ import { readFile as fsReadFile, -} from 'fs'; -import { promisify } from 'util'; - -const fsReadFileAsync = promisify(fsReadFile); +} from 'fs/promises'; import { IExecuteFunctions } from 'n8n-core'; import { @@ -162,7 +159,7 @@ export class ExecuteWorkflow implements INodeType { let workflowJson; try { - workflowJson = await fsReadFileAsync(workflowPath, { encoding: 'utf8' }) as string; + workflowJson = await fsReadFile(workflowPath, { encoding: 'utf8' }) as string; } catch (error) { if (error.code === 'ENOENT') { throw new NodeOperationError(this.getNode(), `The file "${workflowPath}" could not be found.`); diff --git a/packages/nodes-base/nodes/ReadBinaryFile.node.ts b/packages/nodes-base/nodes/ReadBinaryFile.node.ts index 6b595d3b10..fe5a3d8bce 100644 --- a/packages/nodes-base/nodes/ReadBinaryFile.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFile.node.ts @@ -8,10 +8,7 @@ import { import { readFile as fsReadFile, -} from 'fs'; -import { promisify } from 'util'; - -const fsReadFileAsync = promisify(fsReadFile); +} from 'fs/promises'; export class ReadBinaryFile implements INodeType { @@ -64,7 +61,7 @@ export class ReadBinaryFile implements INodeType { let data; try { - data = await fsReadFileAsync(filePath) as Buffer; + data = await fsReadFile(filePath) as Buffer; } catch (error) { if (error.code === 'ENOENT') { throw new NodeOperationError(this.getNode(), `The file "${filePath}" could not be found.`); diff --git a/packages/nodes-base/nodes/ReadBinaryFiles.node.ts b/packages/nodes-base/nodes/ReadBinaryFiles.node.ts index 7a42764f35..509402bb21 100644 --- a/packages/nodes-base/nodes/ReadBinaryFiles.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFiles.node.ts @@ -9,10 +9,7 @@ import * as path from 'path'; import { readFile as fsReadFile, -} from 'fs'; -import { promisify } from 'util'; - -const fsReadFileAsync = promisify(fsReadFile); +} from 'fs/promises'; export class ReadBinaryFiles implements INodeType { @@ -61,7 +58,7 @@ export class ReadBinaryFiles implements INodeType { let item: INodeExecutionData; let data: Buffer; for (const filePath of files) { - data = await fsReadFileAsync(filePath) as Buffer; + data = await fsReadFile(filePath) as Buffer; item = { binary: { diff --git a/packages/nodes-base/nodes/WriteBinaryFile.node.ts b/packages/nodes-base/nodes/WriteBinaryFile.node.ts index 6c6f9a83ad..5796110119 100644 --- a/packages/nodes-base/nodes/WriteBinaryFile.node.ts +++ b/packages/nodes-base/nodes/WriteBinaryFile.node.ts @@ -13,10 +13,7 @@ import { import { writeFile as fsWriteFile, -} from 'fs'; -import { promisify } from 'util'; - -const fsWriteFileAsync = promisify(fsWriteFile); +} from 'fs/promises'; export class WriteBinaryFile implements INodeType { @@ -80,7 +77,7 @@ export class WriteBinaryFile implements INodeType { } // Write the file to disk - await fsWriteFileAsync(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary'); + await fsWriteFile(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary'); const newItem: INodeExecutionData = { json: {}, From d239ba5cff855218751f68cf5a607b5ba560219a Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sat, 1 May 2021 05:23:51 +0300 Subject: [PATCH 22/41] :arrow_up: Set mqtt@4.2.6 on n8n-nodes-base Snyk has created this PR to upgrade mqtt from 4.2.1 to 4.2.6. See this package in npm: https://www.npmjs.com/package/mqtt See this project in Snyk: https://app.snyk.io/org/janober/project/a08454f4-33a1-49bc-bb2a-f31792e94f42?utm_source=github&utm_medium=upgrade-pr --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 4986fdb5dd..539010cbb8 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -620,7 +620,7 @@ "moment": "2.29.1", "moment-timezone": "^0.5.28", "mongodb": "3.6.3", - "mqtt": "4.2.1", + "mqtt": "4.2.6", "mssql": "^6.2.0", "mysql2": "~2.2.0", "n8n-core": "~0.68.0", From eab7b0a10a47b231caaabd9640d1ea57f610c943 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sat, 1 May 2021 05:25:18 +0300 Subject: [PATCH 23/41] :arrow_up: Set tslib@0.114.1 on n8n Snyk has created this PR to upgrade tslib from 1.13.0 to 1.14.1. See this package in npm: https://www.npmjs.com/package/tslib See this project in Snyk: https://app.snyk.io/org/janober/project/fc678bbc-0ac7-4659-9458-8f7f360e2566?utm_source=github&utm_medium=upgrade-pr --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8dd4e347ab..7eba64834e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -114,7 +114,7 @@ "request-promise-native": "^1.0.7", "sqlite3": "^5.0.1", "sse-channel": "^3.1.1", - "tslib": "1.13.0", + "tslib": "1.14.1", "typeorm": "^0.2.30" }, "jest": { From 03639b0e3ac46b1e787d4f5b784222e235a1fc29 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sat, 1 May 2021 05:26:52 +0300 Subject: [PATCH 24/41] :arrow_up: Set mongodb@3.6.5 on n8n-nodes-base Snyk has created this PR to upgrade mongodb from 3.6.3 to 3.6.5. See this package in npm: https://www.npmjs.com/package/mongodb See this project in Snyk: https://app.snyk.io/org/janober/project/a08454f4-33a1-49bc-bb2a-f31792e94f42?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Jan --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 539010cbb8..38d615717d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -619,7 +619,7 @@ "mailparser": "^3.2.0", "moment": "2.29.1", "moment-timezone": "^0.5.28", - "mongodb": "3.6.3", + "mongodb": "3.6.5", "mqtt": "4.2.6", "mssql": "^6.2.0", "mysql2": "~2.2.0", From 35cae02a36eb4e31765c1f76c6827de3fa955b64 Mon Sep 17 00:00:00 2001 From: Colton Anglin Date: Fri, 30 Apr 2021 21:48:40 -0500 Subject: [PATCH 25/41] :zap: Add option to use Field IDs on Quickbase Node (#1651) * QuickBase: Use FieldIDs instead of names * Fix name change * Delete tmp-209473KO4eyCT5LSi * Fix name change * Change default to false --- .../nodes/QuickBase/GenericFunctions.ts | 4 +- .../nodes/QuickBase/QuickBase.node.ts | 70 +++++++++++------ .../nodes/QuickBase/RecordDescription.ts | 77 ++++++------------- 3 files changed, 74 insertions(+), 77 deletions(-) diff --git a/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts b/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts index 255d3d6d85..89ad286456 100644 --- a/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts +++ b/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts @@ -41,8 +41,10 @@ export async function quickbaseApiRequest(this: IExecuteFunctions | ILoadOptions body, qs, uri: `https://api.quickbase.com/v1${resource}`, - json: true, + json: true }; + + if (Object.keys(body).length === 0) { delete options.body; } diff --git a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts index a3de7a4683..02a17e8a04 100644 --- a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts +++ b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts @@ -230,8 +230,8 @@ export class QuickBase implements INodeType { if (operation === 'create') { const tableId = this.getNodeParameter('tableId', 0) as string; - const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); - + const useFieldIDs = this.getNodeParameter('useFieldIDs', 0) as boolean; + const simple = this.getNodeParameter('simple', 0) as boolean; const data: IDataObject[] = []; @@ -244,10 +244,16 @@ export class QuickBase implements INodeType { const columns = this.getNodeParameter('columns', i) as string; const columnList = columns.split(',').map(column => column.trim()); - - for (const key of Object.keys(items[i].json)) { - if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { - record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + if (useFieldIDs) { + for (const key of Object.keys(items[i].json)) { + record[key] = { value: items[i].json[key] }; + } + } else { + const { fieldsLabelKey } = await getFieldsObject.call(this, tableId); + for (const key of Object.keys(items[i].json)) { + if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { + record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + } } } @@ -259,8 +265,9 @@ export class QuickBase implements INodeType { to: tableId, }; - // If not fields are set return at least the record id - body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; + // If no fields are set return at least the record id + // 3 == Default Quickbase RecordID # + body.fieldsToReturn = [3]; if (options.fields) { body.fieldsToReturn = options.fields as string[]; @@ -275,7 +282,7 @@ export class QuickBase implements INodeType { for (const record of records) { const data: IDataObject = {}; for (const [key, value] of Object.entries(record)) { - data[fieldsIdKey[key]] = (value as IDataObject).value; + data[key] = (value as IDataObject).value; } responseData.push(data); } @@ -365,6 +372,8 @@ export class QuickBase implements INodeType { const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); + const useFieldIDs = this.getNodeParameter('useFieldIDs', 0) as boolean; + const simple = this.getNodeParameter('simple', 0) as boolean; const updateKey = this.getNodeParameter('updateKey', 0) as string; @@ -380,9 +389,16 @@ export class QuickBase implements INodeType { const columnList = columns.split(',').map(column => column.trim()); - for (const key of Object.keys(items[i].json)) { - if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { - record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + if (useFieldIDs) { + for (const key of Object.keys(items[i].json)) { + record[key] = { value: items[i].json[key] }; + } + } else { + const { fieldsLabelKey } = await getFieldsObject.call(this, tableId); + for (const key of Object.keys(items[i].json)) { + if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { + record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + } } } @@ -390,8 +406,6 @@ export class QuickBase implements INodeType { throw new NodeOperationError(this.getNode(), `The update key ${updateKey} could not be found in the input`); } - record[fieldsLabelKey['Record ID#']] = { value: items[i].json[updateKey] }; - data.push(record); } @@ -400,8 +414,9 @@ export class QuickBase implements INodeType { to: tableId, }; - // If not fields are set return at least the record id - body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; + // If no fields are set return at least the record id + // 3 == Default Quickbase RecordID # + //body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; if (options.fields) { body.fieldsToReturn = options.fields as string[]; @@ -432,7 +447,8 @@ export class QuickBase implements INodeType { if (operation === 'upsert') { const tableId = this.getNodeParameter('tableId', 0) as string; - const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); + const useFieldIDs = this.getNodeParameter('useFieldIDs', 0) as boolean; + const simple = this.getNodeParameter('simple', 0) as boolean; @@ -451,9 +467,16 @@ export class QuickBase implements INodeType { const columnList = columns.split(',').map(column => column.trim()); - for (const key of Object.keys(items[i].json)) { - if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { - record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + if (useFieldIDs) { + for (const key of Object.keys(items[i].json)) { + record[key] = { value: items[i].json[key] }; + } + } else { + const { fieldsLabelKey } = await getFieldsObject.call(this, tableId); + for (const key of Object.keys(items[i].json)) { + if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { + record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + } } } @@ -472,8 +495,9 @@ export class QuickBase implements INodeType { mergeFieldId, }; - // If not fields are set return at least the record id - body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; + // If no fields are set return at least the record id + // 3 == Default Quickbase RecordID # + body.fieldsToReturn = [3]; if (options.fields) { body.fieldsToReturn = options.fields as string[]; @@ -488,7 +512,7 @@ export class QuickBase implements INodeType { for (const record of records) { const data: IDataObject = {}; for (const [key, value] of Object.entries(record)) { - data[fieldsIdKey[key]] = (value as IDataObject).value; + data[key] = (value as IDataObject).value; } responseData.push(data); } diff --git a/packages/nodes-base/nodes/QuickBase/RecordDescription.ts b/packages/nodes-base/nodes/QuickBase/RecordDescription.ts index 73614d9297..276b8c73fb 100644 --- a/packages/nodes-base/nodes/QuickBase/RecordDescription.ts +++ b/packages/nodes-base/nodes/QuickBase/RecordDescription.ts @@ -69,7 +69,7 @@ export const recordFields = [ description: 'The table identifier', }, { - displayName: 'Columns', + displayName: 'Insert Fields', name: 'columns', type: 'string', displayOptions: { @@ -84,11 +84,30 @@ export const recordFields = [ }, default: '', required: true, - placeholder: 'id,name,description', + placeholder: 'Select Fields...', description: 'Comma separated list of the properties which should used as columns for the new rows.', }, { - displayName: 'Simple', + displayName: 'Use Field IDs', + name: 'useFieldIDs', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'create', + 'upsert', + 'update' + ], + }, + }, + default: false, + description: 'Use Field IDs instead of Field Names in Insert Fields.', + }, + { + displayName: 'Simplified Response', name: 'simple', type: 'boolean', displayOptions: { @@ -122,7 +141,7 @@ export const recordFields = [ }, options: [ { - displayName: 'Fields', + displayName: 'Return Fields', name: 'fields', type: 'multiOptions', typeOptions: { @@ -133,7 +152,7 @@ export const recordFields = [ }, default: [], description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, - }, + } ], }, /* -------------------------------------------------------------------------- */ @@ -255,54 +274,6 @@ export const recordFields = [ }, }, options: [ - // { - // displayName: 'Group By', - // name: 'groupByUi', - // placeholder: 'Add Group By', - // type: 'fixedCollection', - // typeOptions: { - // multipleValues: true, - // }, - // default: {}, - // options: [ - // { - // name: 'groupByValues', - // displayName: 'Group By', - // values: [ - // { - // displayName: 'Field ID', - // name: 'fieldId', - // type: 'options', - // typeOptions: { - // loadOptionsMethod: 'getTableFields', - // }, - // default: '', - // description: 'The unique identifier of a field in a table.', - // }, - // { - // displayName: 'Grouping', - // name: 'grouping', - // type: 'options', - // options: [ - // { - // name: 'ASC', - // value: 'ASC', - // }, - // { - // name: 'DESC', - // value: 'DESC', - // }, - // { - // name: 'Equal Values', - // value: 'equal-values', - // }, - // ], - // default: 'ASC', - // }, - // ], - // }, - // ], - // }, { displayName: 'Select', name: 'select', From 0b69310bedc5979550696aae6ac5881aea7b1206 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 30 Apr 2021 21:49:15 -0500 Subject: [PATCH 26/41] :zap: Minor improvements to Quickbase Node --- .../nodes/QuickBase/GenericFunctions.ts | 2 +- .../nodes/QuickBase/QuickBase.node.ts | 13 ++---- .../nodes/QuickBase/RecordDescription.ts | 44 ++++++++++--------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts b/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts index 89ad286456..d1393ddbfc 100644 --- a/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts +++ b/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts @@ -41,7 +41,7 @@ export async function quickbaseApiRequest(this: IExecuteFunctions | ILoadOptions body, qs, uri: `https://api.quickbase.com/v1${resource}`, - json: true + json: true, }; diff --git a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts index 02a17e8a04..b26128c22d 100644 --- a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts +++ b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts @@ -230,8 +230,6 @@ export class QuickBase implements INodeType { if (operation === 'create') { const tableId = this.getNodeParameter('tableId', 0) as string; - const useFieldIDs = this.getNodeParameter('useFieldIDs', 0) as boolean; - const simple = this.getNodeParameter('simple', 0) as boolean; const data: IDataObject[] = []; @@ -244,7 +242,7 @@ export class QuickBase implements INodeType { const columns = this.getNodeParameter('columns', i) as string; const columnList = columns.split(',').map(column => column.trim()); - if (useFieldIDs) { + if (options.useFieldIDs === true) { for (const key of Object.keys(items[i].json)) { record[key] = { value: items[i].json[key] }; } @@ -372,8 +370,6 @@ export class QuickBase implements INodeType { const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); - const useFieldIDs = this.getNodeParameter('useFieldIDs', 0) as boolean; - const simple = this.getNodeParameter('simple', 0) as boolean; const updateKey = this.getNodeParameter('updateKey', 0) as string; @@ -389,7 +385,7 @@ export class QuickBase implements INodeType { const columnList = columns.split(',').map(column => column.trim()); - if (useFieldIDs) { + if (options.useFieldIDs === true) { for (const key of Object.keys(items[i].json)) { record[key] = { value: items[i].json[key] }; } @@ -447,9 +443,6 @@ export class QuickBase implements INodeType { if (operation === 'upsert') { const tableId = this.getNodeParameter('tableId', 0) as string; - const useFieldIDs = this.getNodeParameter('useFieldIDs', 0) as boolean; - - const simple = this.getNodeParameter('simple', 0) as boolean; const updateKey = this.getNodeParameter('updateKey', 0) as string; @@ -467,7 +460,7 @@ export class QuickBase implements INodeType { const columnList = columns.split(',').map(column => column.trim()); - if (useFieldIDs) { + if (options.useFieldIDs === true) { for (const key of Object.keys(items[i].json)) { record[key] = { value: items[i].json[key] }; } diff --git a/packages/nodes-base/nodes/QuickBase/RecordDescription.ts b/packages/nodes-base/nodes/QuickBase/RecordDescription.ts index 276b8c73fb..7b572fe837 100644 --- a/packages/nodes-base/nodes/QuickBase/RecordDescription.ts +++ b/packages/nodes-base/nodes/QuickBase/RecordDescription.ts @@ -69,7 +69,7 @@ export const recordFields = [ description: 'The table identifier', }, { - displayName: 'Insert Fields', + displayName: 'Columns', name: 'columns', type: 'string', displayOptions: { @@ -87,25 +87,6 @@ export const recordFields = [ placeholder: 'Select Fields...', description: 'Comma separated list of the properties which should used as columns for the new rows.', }, - { - displayName: 'Use Field IDs', - name: 'useFieldIDs', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'record', - ], - operation: [ - 'create', - 'upsert', - 'update' - ], - }, - }, - default: false, - description: 'Use Field IDs instead of Field Names in Insert Fields.', - }, { displayName: 'Simplified Response', name: 'simple', @@ -152,7 +133,14 @@ export const recordFields = [ }, default: [], description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, - } + }, + { + displayName: 'Use Field IDs', + name: 'useFieldIDs', + type: 'boolean', + default: false, + description: 'Use Field IDs instead of Field Names in Columns.', + }, ], }, /* -------------------------------------------------------------------------- */ @@ -443,6 +431,13 @@ export const recordFields = [ default: [], description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, }, + { + displayName: 'Use Field IDs', + name: 'useFieldIDs', + type: 'boolean', + default: false, + description: 'Use Field IDs instead of Field Names in Columns.', + }, // { // displayName: 'Merge Field ID', // name: 'mergeFieldId', @@ -583,6 +578,13 @@ export const recordFields = [ default: [], description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, }, + { + displayName: 'Use Field IDs', + name: 'useFieldIDs', + type: 'boolean', + default: false, + description: 'Use Field IDs instead of Field Names in Columns.', + }, ], }, ] as INodeProperties[]; From c972f3dd50a184f47c6a8030f6b2e641487c795b Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Sun, 2 May 2021 05:43:01 +0200 Subject: [PATCH 27/41] :sparkles: Added logging to n8n (#1381) * Added logging to n8n This commit adds logging to n8n using the Winston library. For now, this commit only allows logging to console (default behavior) or file (need to pass in config via environment variables). Other logging methods can be further implemented using hooks. These were skipped for now as it would require adding more dependencies. Logging level is notice by default, meaning no additional messages would be displayed at the moment. Logging level can be set to info or debug as well to enrich the generated logs. The ILogger interface was added to the workflow project as it would make it available for all other projects but the implementation was done on the cli project. * Lint fixes and logging level naming. Also fixed the way we use the logger as it was not working previously * Improvements to logging framework Using appropriate single quotes Improving the way the logger is declared * Improved naming for Log Types * Removed logger global variable, replacing it by a proxy * Add logging to CLI commands * Remove unused GenericHelpers * Changed back some messages to console instead of logger and added npm shortcuts for worker and webhook * Fix typos * Adding basic file rotation to logs as suggested by @mutdmour * Fixed linting issues * Correcting comment to correctly reflect space usage * Added settings for log files rotation * Correcting config type from String to Number * Changed default file settings to number To reflect previous changes to the type * Changed the way log messages are added to be called statically. Also minor naming improvements * Applying latest corrections sent by @ivov * :zap: Some logging improvements * Saving logs to a folder inside n8n home instead of root * Fixed broken tests and linting * Changed some log messages to improve formatting * Adding quotes to names on log messages * Added execution and session IDs to logs. Also removed unnecessary line breaks * :zap: Added file caller to log messages (#1657) This is done using callsites library which already existed in the project as another library's dependency. So in fact it does not add any new dependency. * Adding logs to help debug Salesforce node * :zap: Add function name to logs and add more logs * :zap: Improve some error messages * :zap: Improve some more log messages * :zap: Rename logging env variables to match others Co-authored-by: dali Co-authored-by: Jan Oberhauser --- package.json | 4 +- packages/cli/commands/execute.ts | 47 +++++--- packages/cli/commands/export/credentials.ts | 44 ++++--- packages/cli/commands/export/workflow.ts | 42 ++++--- packages/cli/commands/import/credentials.ts | 22 +++- packages/cli/commands/import/workflow.ts | 22 +++- packages/cli/commands/start.ts | 23 +++- packages/cli/commands/update/workflow.ts | 33 +++-- packages/cli/commands/webhook.ts | 31 +++-- packages/cli/commands/worker.ts | 43 ++++--- packages/cli/config/index.ts | 37 ++++++ packages/cli/package.json | 1 + packages/cli/src/ActiveWorkflowRunner.ts | 20 ++- packages/cli/src/Logger.ts | 114 ++++++++++++++++++ packages/cli/src/Push.ts | 12 +- packages/cli/src/WebhookHelpers.ts | 5 +- .../cli/src/WorkflowExecuteAdditionalData.ts | 29 +++-- packages/cli/src/WorkflowHelpers.ts | 13 +- packages/cli/src/WorkflowRunner.ts | 8 ++ packages/cli/src/WorkflowRunnerProcess.ts | 23 ++-- packages/core/src/ActiveWorkflows.ts | 3 + packages/core/src/NodeExecuteFunctions.ts | 9 ++ packages/core/src/WorkflowExecute.ts | 11 +- packages/core/test/WorkflowExecute.test.ts | 12 ++ .../nodes/Salesforce/GenericFunctions.ts | 6 + .../nodes/Salesforce/Salesforce.node.ts | 6 + packages/workflow/src/Interfaces.ts | 10 ++ packages/workflow/src/LoggerProxy.ts | 45 +++++++ packages/workflow/src/index.ts | 2 + 29 files changed, 548 insertions(+), 129 deletions(-) create mode 100644 packages/cli/src/Logger.ts create mode 100644 packages/workflow/src/LoggerProxy.ts diff --git a/package.json b/package.json index 866d256baf..44145dbb5e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "start:windows": "cd packages/cli/bin && n8n", "test": "lerna run test", "tslint": "lerna exec npm run tslint", - "watch": "lerna run --parallel watch" + "watch": "lerna run --parallel watch", + "webhook": "./packages/cli/bin/n8n webhook", + "worker": "./packages/cli/bin/n8n worker" }, "devDependencies": { "lerna": "^3.13.1", diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 864748f8cb..9eafd28c9e 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -13,7 +13,6 @@ import { CredentialTypes, Db, ExternalHooks, - GenericHelpers, IWorkflowBase, IWorkflowExecutionDataProcess, LoadNodesAndCredentials, @@ -23,6 +22,13 @@ import { WorkflowRunner, } from '../src'; +import { + getLogger, +} from '../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; export class Execute extends Command { static description = '\nExecutes a given workflow'; @@ -44,6 +50,9 @@ export class Execute extends Command { async run() { + const logger = getLogger(); + LoggerProxy.init(logger); + const { flags } = this.parse(Execute); // Start directly with the init of the database to improve startup time @@ -54,12 +63,12 @@ export class Execute extends Command { const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init(); if (!flags.id && !flags.file) { - GenericHelpers.logOutput(`Either option "--id" or "--file" have to be set!`); + console.info(`Either option "--id" or "--file" have to be set!`); return; } if (flags.id && flags.file) { - GenericHelpers.logOutput(`Either "id" or "file" can be set never both!`); + console.info(`Either "id" or "file" can be set never both!`); return; } @@ -71,7 +80,7 @@ export class Execute extends Command { workflowData = JSON.parse(await fs.readFile(flags.file, 'utf8')); } catch (error) { if (error.code === 'ENOENT') { - GenericHelpers.logOutput(`The file "${flags.file}" could not be found.`); + console.info(`The file "${flags.file}" could not be found.`); return; } @@ -81,7 +90,7 @@ export class Execute extends Command { // Do a basic check if the data in the file looks right // TODO: Later check with the help of TypeScript data if it is valid or not if (workflowData === undefined || workflowData.nodes === undefined || workflowData.connections === undefined) { - GenericHelpers.logOutput(`The file "${flags.file}" does not contain valid workflow data.`); + console.info(`The file "${flags.file}" does not contain valid workflow data.`); return; } workflowId = workflowData.id!.toString(); @@ -95,8 +104,8 @@ export class Execute extends Command { workflowId = flags.id; workflowData = await Db.collections!.Workflow!.findOne(workflowId); if (workflowData === undefined) { - GenericHelpers.logOutput(`The workflow with the id "${workflowId}" does not exist.`); - return; + console.info(`The workflow with the id "${workflowId}" does not exist.`); + process.exit(1); } } @@ -138,7 +147,7 @@ export class Execute extends Command { if (startNode === undefined) { // If the workflow does not contain a start-node we can not know what // should be executed and with which data to start. - GenericHelpers.logOutput(`The workflow does not contain a "Start" node. So it can not be executed.`); + console.info(`The workflow does not contain a "Start" node. So it can not be executed.`); return Promise.resolve(); } @@ -163,9 +172,10 @@ export class Execute extends Command { } if (data.data.resultData.error) { - this.log('Execution was NOT successfull:'); - this.log('===================================='); - this.log(JSON.stringify(data, null, 2)); + console.info('Execution was NOT successful. See log message for details.'); + logger.info('Execution error:'); + logger.info('===================================='); + logger.info(JSON.stringify(data, null, 2)); const { error } = data.data.resultData; throw { @@ -174,14 +184,15 @@ export class Execute extends Command { }; } - this.log('Execution was successfull:'); - this.log('===================================='); - this.log(JSON.stringify(data, null, 2)); + console.info('Execution was successful:'); + console.info('===================================='); + console.info(JSON.stringify(data, null, 2)); } catch (e) { - console.error('\nGOT ERROR'); - console.log('===================================='); - console.error(e.message); - console.error(e.stack); + console.error('Error executing workflow. See log messages for details.'); + logger.error('\nExecution error:'); + logger.info('===================================='); + logger.error(e.message); + logger.error(e.stack); this.exit(1); } diff --git a/packages/cli/commands/export/credentials.ts b/packages/cli/commands/export/credentials.ts index ec12033446..de1aafe6dd 100644 --- a/packages/cli/commands/export/credentials.ts +++ b/packages/cli/commands/export/credentials.ts @@ -14,10 +14,17 @@ import { import { Db, - GenericHelpers, ICredentialsDecryptedDb, } from '../../src'; +import { + getLogger, +} from '../../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; + import * as fs from 'fs'; import * as path from 'path'; @@ -59,8 +66,11 @@ export class ExportCredentialsCommand extends Command { }; async run() { - const { flags } = this.parse(ExportCredentialsCommand); + const logger = getLogger(); + LoggerProxy.init(logger); + const { flags } = this.parse(ExportCredentialsCommand); + if (flags.backup) { flags.all = true; flags.pretty = true; @@ -68,41 +78,42 @@ export class ExportCredentialsCommand extends Command { } if (!flags.all && !flags.id) { - GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`); + console.info(`Either option "--all" or "--id" have to be set!`); return; } if (flags.all && flags.id) { - GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`); + console.info(`You should either use "--all" or "--id" but never both!`); return; } if (flags.separate) { try { if (!flags.output) { - GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`); + console.info(`You must inform an output directory via --output when using --separate`); return; } if (fs.existsSync(flags.output)) { if (!fs.lstatSync(flags.output).isDirectory()) { - GenericHelpers.logOutput(`The paramenter --output must be a directory`); + console.info(`The paramenter --output must be a directory`); return; } } else { fs.mkdirSync(flags.output, { recursive: true }); } } catch (e) { - console.error('\nFILESYSTEM ERROR'); - console.log('===================================='); - console.error(e.message); - console.error(e.stack); + console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.'); + logger.error('\nFILESYSTEM ERROR'); + logger.info('===================================='); + logger.error(e.message); + logger.error(e.stack); this.exit(1); } } else if (flags.output) { if (fs.existsSync(flags.output)) { if (fs.lstatSync(flags.output).isDirectory()) { - GenericHelpers.logOutput(`The paramenter --output must be a writeble file`); + console.info(`The paramenter --output must be a writeble file`); return; } } @@ -143,18 +154,21 @@ export class ExportCredentialsCommand extends Command { const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + credentials[i].id + '.json'; fs.writeFileSync(filename, fileContents); } - console.log('Successfully exported', i, 'credentials.'); + console.info(`Successfully exported ${i} credentials.`); } else { const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined); if (flags.output) { fs.writeFileSync(flags.output!, fileContents); - console.log('Successfully exported', credentials.length, 'credentials.'); + console.info(`Successfully exported ${credentials.length} credentials.`); } else { - console.log(fileContents); + console.info(fileContents); } } + // Force exit as process won't exit using MySQL or Postgres. + process.exit(0); } catch (error) { - this.error(error.message); + console.error('Error exporting credentials. See log messages for details.'); + logger.error(error.message); this.exit(1); } } diff --git a/packages/cli/commands/export/workflow.ts b/packages/cli/commands/export/workflow.ts index 0cf1d712ad..9d478dbaef 100644 --- a/packages/cli/commands/export/workflow.ts +++ b/packages/cli/commands/export/workflow.ts @@ -9,9 +9,16 @@ import { import { Db, - GenericHelpers, } from '../../src'; +import { + getLogger, +} from '../../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; + import * as fs from 'fs'; import * as path from 'path'; @@ -49,6 +56,9 @@ export class ExportWorkflowsCommand extends Command { }; async run() { + const logger = getLogger(); + LoggerProxy.init(logger); + const { flags } = this.parse(ExportWorkflowsCommand); if (flags.backup) { @@ -58,41 +68,42 @@ export class ExportWorkflowsCommand extends Command { } if (!flags.all && !flags.id) { - GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`); + console.info(`Either option "--all" or "--id" have to be set!`); return; } if (flags.all && flags.id) { - GenericHelpers.logOutput(`You should either use "--all" or "--id" but never both!`); + console.info(`You should either use "--all" or "--id" but never both!`); return; } if (flags.separate) { try { if (!flags.output) { - GenericHelpers.logOutput(`You must inform an output directory via --output when using --separate`); + console.info(`You must inform an output directory via --output when using --separate`); return; } if (fs.existsSync(flags.output)) { if (!fs.lstatSync(flags.output).isDirectory()) { - GenericHelpers.logOutput(`The paramenter --output must be a directory`); + console.info(`The paramenter --output must be a directory`); return; } } else { fs.mkdirSync(flags.output, { recursive: true }); } } catch (e) { - console.error('\nFILESYSTEM ERROR'); - console.log('===================================='); - console.error(e.message); - console.error(e.stack); + console.error('Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.'); + logger.error('\nFILESYSTEM ERROR'); + logger.info('===================================='); + logger.error(e.message); + logger.error(e.stack); this.exit(1); } } else if (flags.output) { if (fs.existsSync(flags.output)) { if (fs.lstatSync(flags.output).isDirectory()) { - GenericHelpers.logOutput(`The paramenter --output must be a writeble file`); + console.info(`The paramenter --output must be a writeble file`); return; } } @@ -119,18 +130,21 @@ export class ExportWorkflowsCommand extends Command { const filename = (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + workflows[i].id + '.json'; fs.writeFileSync(filename, fileContents); } - console.log('Successfully exported', i, 'workflows.'); + console.info(`Successfully exported ${i} workflows.`); } else { const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined); if (flags.output) { fs.writeFileSync(flags.output!, fileContents); - console.log('Successfully exported', workflows.length, workflows.length === 1 ? 'workflow.' : 'workflows.'); + console.info(`Successfully exported ${workflows.length} ${workflows.length === 1 ? 'workflow.' : 'workflows.'}`); } else { - console.log(fileContents); + console.info(fileContents); } } + // Force exit as process won't exit using MySQL or Postgres. + process.exit(0); } catch (error) { - this.error(error.message); + console.error('Error exporting workflows. See log messages for details.'); + logger.error(error.message); this.exit(1); } } diff --git a/packages/cli/commands/import/credentials.ts b/packages/cli/commands/import/credentials.ts index ed0da4e94b..b038f33693 100644 --- a/packages/cli/commands/import/credentials.ts +++ b/packages/cli/commands/import/credentials.ts @@ -10,9 +10,16 @@ import { import { Db, - GenericHelpers, } from '../../src'; +import { + getLogger, +} from '../../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; + import * as fs from 'fs'; import * as glob from 'glob-promise'; import * as path from 'path'; @@ -37,17 +44,20 @@ export class ImportCredentialsCommand extends Command { }; async run() { + const logger = getLogger(); + LoggerProxy.init(logger); + const { flags } = this.parse(ImportCredentialsCommand); if (!flags.input) { - GenericHelpers.logOutput(`An input file or directory with --input must be provided`); + console.info(`An input file or directory with --input must be provided`); return; } if (flags.separate) { if (fs.existsSync(flags.input)) { if (!fs.lstatSync(flags.input).isDirectory()) { - GenericHelpers.logOutput(`The paramenter --input must be a directory`); + console.info(`The paramenter --input must be a directory`); return; } } @@ -89,9 +99,11 @@ export class ImportCredentialsCommand extends Command { await Db.collections.Credentials!.save(fileContents[i]); } } - console.log('Successfully imported', i, 'credentials.'); + console.info(`Successfully imported ${i} ${i === 1 ? 'credential.' : 'credentials.'}`); + process.exit(0); } catch (error) { - this.error(error.message); + console.error('An error occurred while exporting credentials. See log messages for details.'); + logger.error(error.message); this.exit(1); } } diff --git a/packages/cli/commands/import/workflow.ts b/packages/cli/commands/import/workflow.ts index 7a0dc49e10..5b31041a44 100644 --- a/packages/cli/commands/import/workflow.ts +++ b/packages/cli/commands/import/workflow.ts @@ -5,9 +5,16 @@ import { import { Db, - GenericHelpers, } from '../../src'; +import { + getLogger, +} from '../../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; + import * as fs from 'fs'; import * as glob from 'glob-promise'; import * as path from 'path'; @@ -32,17 +39,20 @@ export class ImportWorkflowsCommand extends Command { }; async run() { + const logger = getLogger(); + LoggerProxy.init(logger); + const { flags } = this.parse(ImportWorkflowsCommand); if (!flags.input) { - GenericHelpers.logOutput(`An input file or directory with --input must be provided`); + console.info(`An input file or directory with --input must be provided`); return; } if (flags.separate) { if (fs.existsSync(flags.input)) { if (!fs.lstatSync(flags.input).isDirectory()) { - GenericHelpers.logOutput(`The paramenter --input must be a directory`); + console.info(`The paramenter --input must be a directory`); return; } } @@ -69,9 +79,11 @@ export class ImportWorkflowsCommand extends Command { } } - console.log('Successfully imported', i, i === 1 ? 'workflow.' : 'workflows.'); + console.info(`Successfully imported ${i} ${i === 1 ? 'workflow.' : 'workflows.'}`); + process.exit(0); } catch (error) { - this.error(error.message); + console.error('An error occurred while exporting workflows. See log messages for details.'); + logger.error(error.message); this.exit(1); } } diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index c01ad0ed4a..023e3e827f 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -25,11 +25,17 @@ import { } from '../src'; import { IDataObject } from 'n8n-workflow'; +import { + getLogger, +} from '../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; let processExistCode = 0; - export class Start extends Command { static description = 'Starts n8n. Makes Web-UI available and starts active workflows'; @@ -71,7 +77,7 @@ export class Start extends Command { * get removed. */ static async stopProcess() { - console.log(`\nStopping n8n...`); + getLogger().info('\nStopping n8n...'); try { const externalHooks = ExternalHooks(); @@ -132,13 +138,18 @@ export class Start extends Command { // Wrap that the process does not close but we can still use async await (async () => { try { + const logger = getLogger(); + LoggerProxy.init(logger); + logger.info('Initializing n8n process'); + // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init().catch((error: Error) => { - console.error(`There was an error initializing DB: ${error.message}`); + logger.error(`There was an error initializing DB: "${error.message}"`); processExistCode = 1; // @ts-ignore process.emit('SIGINT'); + process.exit(1); }); // Make sure the settings exist @@ -184,7 +195,7 @@ export class Start extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.'); + logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); process.exit(1); } } @@ -213,9 +224,9 @@ export class Start extends Command { redis.on('error', (error) => { if (error.toString().includes('ECONNREFUSED') === true) { - console.warn('Redis unavailable - trying to reconnect...'); + logger.warn('Redis unavailable - trying to reconnect...'); } else { - console.warn('Error with Redis: ', error); + logger.warn('Error with Redis: ', error); } }); } diff --git a/packages/cli/commands/update/workflow.ts b/packages/cli/commands/update/workflow.ts index 06be05759d..c7d81901aa 100644 --- a/packages/cli/commands/update/workflow.ts +++ b/packages/cli/commands/update/workflow.ts @@ -11,6 +11,13 @@ import { GenericHelpers, } from '../../src'; +import { + getLogger, +} from '../../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; export class UpdateWorkflowCommand extends Command { static description = '\Update workflows'; @@ -34,25 +41,28 @@ export class UpdateWorkflowCommand extends Command { }; async run() { + const logger = getLogger(); + LoggerProxy.init(logger); + const { flags } = this.parse(UpdateWorkflowCommand); if (!flags.all && !flags.id) { - GenericHelpers.logOutput(`Either option "--all" or "--id" have to be set!`); + console.info(`Either option "--all" or "--id" have to be set!`); return; } if (flags.all && flags.id) { - GenericHelpers.logOutput(`Either something else on top should be "--all" or "--id" can be set never both!`); + console.info(`Either something else on top should be "--all" or "--id" can be set never both!`); return; } const updateQuery: IDataObject = {}; if (flags.active === undefined) { - GenericHelpers.logOutput(`No update flag like "--active=true" has been set!`); + console.info(`No update flag like "--active=true" has been set!`); return; } else { if (!['false', 'true'].includes(flags.active)) { - GenericHelpers.logOutput(`Valid values for flag "--active" are only "false" or "true"!`); + console.info(`Valid values for flag "--active" are only "false" or "true"!`); return; } updateQuery.active = flags.active === 'true'; @@ -63,20 +73,21 @@ export class UpdateWorkflowCommand extends Command { const findQuery: IDataObject = {}; if (flags.id) { - console.log(`Deactivating workflow with ID: ${flags.id}`); + console.info(`Deactivating workflow with ID: ${flags.id}`); findQuery.id = flags.id; } else { - console.log('Deactivating all workflows'); + console.info('Deactivating all workflows'); findQuery.active = true; } await Db.collections.Workflow!.update(findQuery, updateQuery); - console.log('Done'); + console.info('Done'); } catch (e) { - console.error('\nGOT ERROR'); - console.log('===================================='); - console.error(e.message); - console.error(e.stack); + console.error('Error updating database. See log messages for details.'); + logger.error('\nGOT ERROR'); + logger.info('===================================='); + logger.error(e.message); + logger.error(e.stack); this.exit(1); } diff --git a/packages/cli/commands/webhook.ts b/packages/cli/commands/webhook.ts index 68c5876543..da09857380 100644 --- a/packages/cli/commands/webhook.ts +++ b/packages/cli/commands/webhook.ts @@ -20,6 +20,13 @@ import { } from '../src'; import { IDataObject } from 'n8n-workflow'; +import { + getLogger, +} from '../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; let processExistCode = 0; @@ -42,7 +49,7 @@ export class Webhook extends Command { * get removed. */ static async stopProcess() { - console.log(`\nStopping n8n...`); + LoggerProxy.info(`\nStopping n8n...`); try { const externalHooks = ExternalHooks(); @@ -72,7 +79,7 @@ export class Webhook extends Command { let count = 0; while (executingWorkflows.length !== 0) { if (count++ % 4 === 0) { - console.log(`Waiting for ${executingWorkflows.length} active executions to finish...`); + LoggerProxy.info(`Waiting for ${executingWorkflows.length} active executions to finish...`); } await new Promise((resolve) => { setTimeout(resolve, 500); @@ -81,7 +88,7 @@ export class Webhook extends Command { } } catch (error) { - console.error('There was an error shutting down n8n.', error); + LoggerProxy.error('There was an error shutting down n8n.', error); } process.exit(processExistCode); @@ -89,6 +96,9 @@ export class Webhook extends Command { async run() { + const logger = getLogger(); + LoggerProxy.init(logger); + // Make sure that n8n shuts down gracefully if possible process.on('SIGTERM', Webhook.stopProcess); process.on('SIGINT', Webhook.stopProcess); @@ -116,11 +126,12 @@ export class Webhook extends Command { try { // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init().catch(error => { - console.error(`There was an error initializing DB: ${error.message}`); + logger.error(`There was an error initializing DB: "${error.message}"`); processExistCode = 1; // @ts-ignore process.emit('SIGINT'); + process.exit(1); }); // Make sure the settings exist @@ -166,7 +177,7 @@ export class Webhook extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.'); + logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); process.exit(1); } } @@ -195,9 +206,9 @@ export class Webhook extends Command { redis.on('error', (error) => { if (error.toString().includes('ECONNREFUSED') === true) { - console.warn('Redis unavailable - trying to reconnect...'); + logger.warn('Redis unavailable - trying to reconnect...'); } else { - console.warn('Error with Redis: ', error); + logger.warn('Error with Redis: ', error); } }); } @@ -209,14 +220,16 @@ export class Webhook extends Command { await activeWorkflowRunner.initWebhooks(); const editorUrl = GenericHelpers.getBaseUrl(); - this.log('Webhook listener waiting for requests.'); + console.info('Webhook listener waiting for requests.'); } catch (error) { - this.error(`There was an error: ${error.message}`); + console.error('Exiting due to error. See log message for details.'); + logger.error(`Webhook process cannot continue. "${error.message}"`); processExistCode = 1; // @ts-ignore process.emit('SIGINT'); + process.exit(1); } })(); } diff --git a/packages/cli/commands/worker.ts b/packages/cli/commands/worker.ts index d7af9ac794..56da5d9c98 100644 --- a/packages/cli/commands/worker.ts +++ b/packages/cli/commands/worker.ts @@ -37,6 +37,14 @@ import { WorkflowExecuteAdditionalData, } from '../src'; +import { + getLogger, +} from '../src/Logger'; + +import { + LoggerProxy, +} from 'n8n-workflow'; + import * as config from '../config'; import * as Bull from 'bull'; import * as Queue from '../src/Queue'; @@ -71,7 +79,7 @@ export class Worker extends Command { * get removed. */ static async stopProcess() { - console.log(`\nStopping n8n...`); + LoggerProxy.info(`Stopping n8n...`); // Stop accepting new jobs Worker.jobQueue.pause(true); @@ -95,7 +103,7 @@ export class Worker extends Command { while (Object.keys(Worker.runningJobs).length !== 0) { if (count++ % 4 === 0) { const waitLeft = Math.ceil((stopTime - new Date().getTime()) / 1000); - console.log(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`); + LoggerProxy.info(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (wait ${waitLeft} more seconds)`); } await new Promise((resolve) => { setTimeout(resolve, 500); @@ -103,7 +111,7 @@ export class Worker extends Command { } } catch (error) { - console.error('There was an error shutting down n8n.', error); + LoggerProxy.error('There was an error shutting down n8n.', error); } process.exit(Worker.processExistCode); @@ -113,7 +121,7 @@ export class Worker extends Command { const jobData = job.data as IBullJobData; const executionDb = await Db.collections.Execution!.findOne(jobData.executionId) as IExecutionFlattedDb; const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb) as IExecutionResponse; - console.log(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`); + LoggerProxy.info(`Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${jobData.executionId})`); let staticData = currentExecutionDb.workflowData!.staticData; if (jobData.loadStaticData === true) { @@ -170,7 +178,10 @@ export class Worker extends Command { } async run() { - console.log('Starting n8n worker...'); + const logger = getLogger(); + LoggerProxy.init(logger); + + console.info('Starting n8n worker...'); // Make sure that n8n shuts down gracefully if possible process.on('SIGTERM', Worker.stopProcess); @@ -183,11 +194,12 @@ export class Worker extends Command { // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init().catch(error => { - console.error(`There was an error initializing DB: ${error.message}`); + logger.error(`There was an error initializing DB: "${error.message}"`); Worker.processExistCode = 1; // @ts-ignore process.emit('SIGINT'); + process.exit(1); }); // Make sure the settings exist @@ -221,10 +233,10 @@ export class Worker extends Command { const versions = await GenericHelpers.getVersions(); - console.log('\nn8n worker is now ready'); - console.log(` * Version: ${versions.cli}`); - console.log(` * Concurrency: ${flags.concurrency}`); - console.log(''); + console.info('\nn8n worker is now ready'); + console.info(` * Version: ${versions.cli}`); + console.info(` * Concurrency: ${flags.concurrency}`); + console.info(''); Worker.jobQueue.on('global:progress', (jobId, progress) => { // Progress of a job got updated which does get used @@ -252,27 +264,28 @@ export class Worker extends Command { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { - console.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + '. Exiting process.'); + logger.error('Unable to connect to Redis after ' + redisConnectionTimeoutLimit + ". Exiting process."); process.exit(1); } } - console.warn('Redis unavailable - trying to reconnect...'); + logger.warn('Redis unavailable - trying to reconnect...'); } else if (error.toString().includes('Error initializing Lua scripts') === true) { // This is a non-recoverable error // Happens when worker starts and Redis is unavailable // Even if Redis comes back online, worker will be zombie - console.error('Error initializing worker.'); + logger.error('Error initializing worker.'); process.exit(2); } else { - console.error('Error from queue: ', error); + logger.error('Error from queue: ', error); } }); } catch (error) { - this.error(`There was an error: ${error.message}`); + logger.error(`Worker process cannot continue. "${error.message}"`); Worker.processExistCode = 1; // @ts-ignore process.emit('SIGINT'); + process.exit(1); } })(); diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 9b2c6ea614..c8d97684d9 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -1,5 +1,7 @@ import * as convict from 'convict'; import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as core from 'n8n-core'; dotenv.config(); @@ -572,6 +574,41 @@ const config = convict({ }, }, + logs: { + level: { + doc: 'Log output level. Options are error, warn, info, verbose and debug.', + format: String, + default: 'info', + env: 'N8N_LOG_LEVEL', + }, + output: { + doc: 'Where to output logs. Options are: console, file. Multiple can be separated by comma (",")', + format: String, + default: 'console', + env: 'N8N_LOG_OUTPUT', + }, + file: { + fileCountMax: { + doc: 'Maximum number of files to keep.', + format: Number, + default: 100, + env: 'N8N_LOG_FILE_COUNT_MAX', + }, + fileSizeMax: { + doc: 'Maximum size for each log file in MB.', + format: Number, + default: 16, + env: 'N8N_LOG_FILE_SIZE_MAX', + }, + location: { + doc: 'Log file location; only used if log output is set to file.', + format: String, + default: path.join(core.UserSettings.getUserN8nFolderPath(), 'logs/n8n.log'), + env: 'N8N_LOG_FILE_LOCATION', + }, + }, + }, + }); // Overwrite default configuration with settings which got defined in diff --git a/packages/cli/package.json b/packages/cli/package.json index 7eba64834e..0c269ea504 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -87,6 +87,7 @@ "body-parser": "^1.18.3", "body-parser-xml": "^1.1.0", "bull": "^3.19.0", + "callsites": "^3.1.0", "client-oauth2": "^4.2.5", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 07cd0a8b82..9f9b9f3580 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -35,6 +35,9 @@ import { } from 'n8n-workflow'; import * as express from 'express'; +import { + LoggerProxy as Logger, +} from 'n8n-workflow'; export class ActiveWorkflowRunner { private activeWorkflows: ActiveWorkflows | null = null; @@ -43,7 +46,6 @@ export class ActiveWorkflowRunner { [key: string]: IActivationError; } = {}; - async init() { // Get the active workflows from database @@ -59,20 +61,24 @@ export class ActiveWorkflowRunner { this.activeWorkflows = new ActiveWorkflows(); if (workflowsData.length !== 0) { - console.log('\n ================================'); - console.log(' Start Active Workflows:'); - console.log(' ================================'); + console.info(' ================================'); + console.info(' Start Active Workflows:'); + console.info(' ================================'); for (const workflowData of workflowsData) { console.log(` - ${workflowData.name}`); + Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, {workflowName: workflowData.name, workflowId: workflowData.id}); try { await this.add(workflowData.id.toString(), 'init', workflowData); + Logger.verbose(`Successfully started workflow "${workflowData.name}"`, {workflowName: workflowData.name, workflowId: workflowData.id}); console.log(` => Started`); } catch (error) { console.log(` => ERROR: Workflow could not be activated:`); console.log(` ${error.message}`); + Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, {workflowName: workflowData.name, workflowId: workflowData.id}); } } + Logger.verbose('Finished initializing active workflows (startup)'); } } @@ -88,6 +94,7 @@ export class ActiveWorkflowRunner { */ async removeAll(): Promise { const activeWorkflowId: string[] = []; + Logger.verbose('Call to remove all active workflows received (removeAll)'); if (this.activeWorkflows !== null) { // TODO: This should be renamed! @@ -117,6 +124,7 @@ export class ActiveWorkflowRunner { * @memberof ActiveWorkflowRunner */ async executeWebhook(httpMethod: WebhookHttpMethod, path: string, req: express.Request, res: express.Response): Promise { + Logger.debug(`Received webhoook "${httpMethod}" for path "${path}"`); if (this.activeWorkflows === null) { throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404); } @@ -437,6 +445,7 @@ export class ActiveWorkflowRunner { return ((workflow: Workflow, node: INode) => { const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode, activation); returnFunctions.__emit = (data: INodeExecutionData[][]): void => { + Logger.debug(`Received event to trigger execution for workflow "${workflow.name}"`); this.runWorkflow(workflowData, node, data, additionalData, mode); }; return returnFunctions; @@ -458,6 +467,7 @@ export class ActiveWorkflowRunner { return ((workflow: Workflow, node: INode) => { const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation); returnFunctions.emit = (data: INodeExecutionData[][]): void => { + Logger.debug(`Received trigger for workflow "${workflow.name}"`); WorkflowHelpers.saveStaticData(workflow); this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) => console.error(err)); }; @@ -492,6 +502,7 @@ export class ActiveWorkflowRunner { const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(['n8n-nodes-base.start']); if (canBeActivated === false) { + Logger.error(`Unable to activate workflow "${workflowData.name}"`); throw new Error(`The workflow can not be activated because it does not contain any nodes which could start the workflow. Only workflows which have trigger or webhook nodes can be activated.`); } @@ -507,6 +518,7 @@ export class ActiveWorkflowRunner { if (workflowInstance.getTriggerNodes().length !== 0 || workflowInstance.getPollNodes().length !== 0) { await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions); + Logger.info(`Successfully activated workflow "${workflowData.name}"`); } if (this.activationErrors[workflowId] !== undefined) { diff --git a/packages/cli/src/Logger.ts b/packages/cli/src/Logger.ts new file mode 100644 index 0000000000..3bbc61e6a9 --- /dev/null +++ b/packages/cli/src/Logger.ts @@ -0,0 +1,114 @@ +import config = require('../config'); +import * as winston from 'winston'; + +import { + IDataObject, + ILogger, + LogTypes, +} from 'n8n-workflow'; + +import * as callsites from 'callsites'; +import { basename } from 'path'; + +class Logger implements ILogger { + private logger: winston.Logger; + + constructor() { + const level = config.get('logs.level'); + const output = (config.get('logs.output') as string).split(',').map(output => output.trim()); + + this.logger = winston.createLogger({ + level, + }); + + if (output.includes('console')) { + let format: winston.Logform.Format; + if (['debug', 'verbose'].includes(level)) { + format = winston.format.combine( + winston.format.metadata(), + winston.format.timestamp(), + winston.format.colorize({ all: true }), + winston.format.printf(({ level, message, timestamp, metadata }) => { + return `${timestamp} | ${level.padEnd(18)} | ${message}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : ''); + }) as winston.Logform.Format + ); + } else { + format = winston.format.printf(({ message }) => message) as winston.Logform.Format; + } + + this.logger.add( + new winston.transports.Console({ + format, + }) + ); + } + + if (output.includes('file')) { + const fileLogFormat = winston.format.combine( + winston.format.timestamp(), + winston.format.metadata(), + winston.format.json() + ); + this.logger.add( + new winston.transports.File({ + filename: config.get('logs.file.location'), + format: fileLogFormat, + maxsize: config.get('logs.file.fileSizeMax') as number * 1048576, // config * 1mb + maxFiles: config.get('logs.file.fileCountMax'), + }) + ); + } + } + + log(type: LogTypes, message: string, meta: object = {}) { + const callsite = callsites(); + // We are using the third array element as the structure is as follows: + // [0]: this file + // [1]: Should be LoggerProxy + // [2]: Should point to the caller. + // Note: getting line number is useless because at this point + // We are in runtime, so it means we are looking at compiled js files + const logDetails = {} as IDataObject; + if (callsite[2] !== undefined) { + logDetails.file = basename(callsite[2].getFileName() || ''); + const functionName = callsite[2].getFunctionName(); + if (functionName) { + logDetails.function = functionName; + } + } + this.logger.log(type, message, {...meta, ...logDetails}); + } + + // Convenience methods below + + debug(message: string, meta: object = {}) { + this.log('debug', message, meta); + } + + info(message: string, meta: object = {}) { + this.log('info', message, meta); + } + + error(message: string, meta: object = {}) { + this.log('error', message, meta); + } + + verbose(message: string, meta: object = {}) { + this.log('verbose', message, meta); + } + + warn(message: string, meta: object = {}) { + this.log('warn', message, meta); + } + +} + +let activeLoggerInstance: Logger | undefined; + +export function getLogger() { + if (activeLoggerInstance === undefined) { + activeLoggerInstance = new Logger(); + } + + return activeLoggerInstance; +} diff --git a/packages/cli/src/Push.ts b/packages/cli/src/Push.ts index 94172ac35b..78a20fde57 100644 --- a/packages/cli/src/Push.ts +++ b/packages/cli/src/Push.ts @@ -7,6 +7,10 @@ import { IPushDataType, } from '.'; +import { + LoggerProxy as Logger, +} from 'n8n-workflow'; + export class Push { private channel: sseChannel; private connections: { @@ -24,6 +28,7 @@ export class Push { this.channel.on('disconnect', (channel: string, res: express.Response) => { if (res.req !== undefined) { + Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId }); delete this.connections[res.req.query.sessionId as string]; } }); @@ -39,6 +44,8 @@ export class Push { * @memberof Push */ add(sessionId: string, req: express.Request, res: express.Response) { + Logger.debug(`Add editor-UI session`, { sessionId }); + if (this.connections[sessionId] !== undefined) { // Make sure to remove existing connection with the same session // id if one exists already @@ -64,11 +71,12 @@ export class Push { send(type: IPushDataType, data: any, sessionId?: string) { // tslint:disable-line:no-any if (sessionId !== undefined && this.connections[sessionId] === undefined) { - // TODO: Log that properly! - console.error(`The session "${sessionId}" is not registred.`); + Logger.error(`The session "${sessionId}" is not registred.`, { sessionId }); return; } + Logger.debug(`Send data of type "${type}" to editor-UI`, { dataType: type, sessionId }); + const sendData: IPushData = { type, data, diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 8913777329..f0d5f63818 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -30,13 +30,14 @@ import { IWebhookData, IWebhookResponseData, IWorkflowExecuteAdditionalData, + LoggerProxy as Logger, NodeHelpers, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; -const activeExecutions = ActiveExecutions.getInstance(); +const activeExecutions = ActiveExecutions.getInstance(); /** * Returns all the webhooks which should be created for the give workflow @@ -286,6 +287,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { const workflowRunner = new WorkflowRunner(); const executionId = await workflowRunner.run(runData, true, !didSendResponse); + Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId }); + // Get a promise which resolves when the workflow did execute and send then response const executePromise = activeExecutions.getPostExecutePromise(executionId) as Promise; executePromise.then((data) => { diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 8e0793385a..dd7e8a6533 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -37,6 +37,7 @@ import { IWorkflowExecuteAdditionalData, IWorkflowExecuteHooks, IWorkflowHooksOptionalParameters, + LoggerProxy as Logger, Workflow, WorkflowExecuteMode, WorkflowHooks, @@ -44,11 +45,10 @@ import { import * as config from '../config'; -import { LessThanOrEqual } from "typeorm"; +import { LessThanOrEqual } from 'typeorm'; const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; - /** * Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects * all the data and executes it @@ -85,9 +85,11 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo // Run the error workflow // To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow. if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) { + Logger.verbose(`Start external error workflow`, { executionId: this.executionId, errorWorkflowId: workflowData.settings.errorWorkflow.toString(), workflowId: this.workflowData.id }); // If a specific error workflow is set run only that one WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); } else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) { + Logger.verbose(`Start internal error workflow`, { executionId: this.executionId, workflowId: this.workflowData.id }); // If the workflow contains WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData); } @@ -102,6 +104,8 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo let throttling = false; function pruneExecutionData(): void { if (!throttling) { + Logger.verbose('Pruning execution data from database'); + throttling = true; const timeout = config.get('executions.pruneDataTimeout') as number; // in seconds const maxAge = config.get('executions.pruneDataMaxAge') as number; // in h @@ -133,6 +137,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { if (this.sessionId === undefined) { return; } + Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); const pushInstance = Push.getInstance(); pushInstance.send('nodeExecuteBefore', { @@ -147,6 +152,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { if (this.sessionId === undefined) { return; } + Logger.debug(`Executing hook on node "${nodeName}" (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); const pushInstance = Push.getInstance(); pushInstance.send('nodeExecuteAfter', { @@ -158,6 +164,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { ], workflowExecuteBefore: [ async function (this: WorkflowHooks): Promise { + Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); // Push data to session which started the workflow if (this.sessionId === undefined) { return; @@ -168,13 +175,14 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { mode: this.mode, startedAt: new Date(), retryOf: this.retryOf, - workflowId: this.workflowData.id as string, + workflowId: this.workflowData.id, sessionId: this.sessionId as string, workflowName: this.workflowData.name, }, this.sessionId); }, ], workflowExecuteAfter: [ async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { + Logger.debug(`Executing hook (hookFunctionsPush)`, { executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); // Push data to session which started the workflow if (this.sessionId === undefined) { return; @@ -195,6 +203,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { }; // Push data to editor-ui once workflow finished + Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, workflowId: this.workflowData.id }); // TODO: Look at this again const sendData: IPushDataExecutionFinished = { executionId: this.executionId, @@ -232,6 +241,8 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx } try { + Logger.debug(`Save execution progress to database for execution ID ${this.executionId} `, { executionId: this.executionId, nodeName }); + const execution = await Db.collections.Execution!.findOne(this.executionId); if (execution === undefined) { @@ -286,7 +297,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx // For busy machines, we may get "Database is locked" errors. // We do this to prevent crashes and executions ending in `unknown` state. - console.log(`Failed saving execution progress to database for execution ID ${this.executionId}`, err); + Logger.error(`Failed saving execution progress to database for execution ID ${this.executionId} (hookFunctionsPreExecute, nodeExecuteAfter)`, { ...err, executionId: this.executionId, sessionId: this.sessionId, workflowId: this.workflowData.id }); } }, @@ -307,6 +318,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { workflowExecuteBefore: [], workflowExecuteAfter: [ async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { + Logger.debug(`Executing hook (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id }); // Prune old execution data if (config.get('executions.pruneData')) { @@ -321,8 +333,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { try { await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); } catch (e) { - // TODO: Add proper logging! - console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`); + Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`, { executionId: this.executionId, workflowId: this.workflowData.id }); } } @@ -375,6 +386,9 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { fullExecutionData.workflowId = this.workflowData.id.toString(); } + // Leave log message before flatten as that operation increased memory usage a lot and the chance of a crash is highest here + Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, { executionId: this.executionId, workflowId: this.workflowData.id }); + const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); // Save the Execution in DB @@ -420,8 +434,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { try { await WorkflowHelpers.saveStaticDataById(this.workflowData.id as string, newStaticData); } catch (e) { - // TODO: Add proper logging! - console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`); + Logger.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`, { sessionId: this.sessionId, workflowId: this.workflowData.id }); } } diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index 7071c1b715..76c264d643 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -18,8 +18,8 @@ import { IRunExecutionData, ITaskData, IWorkflowCredentials, - Workflow, -} from 'n8n-workflow'; + LoggerProxy as Logger, + Workflow,} from 'n8n-workflow'; import * as config from '../config'; @@ -86,7 +86,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData if (workflowData === undefined) { // The error workflow could not be found - console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`); + Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find error workflow "${workflowId}"`, { workflowId }); return; } @@ -105,7 +105,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData } if (workflowStartNode === undefined) { - console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`); + Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${ERROR_TRIGGER_TYPE}" in workflow "${workflowId}"`); return; } @@ -153,7 +153,7 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData const workflowRunner = new WorkflowRunner(); await workflowRunner.run(runData); } catch (error) { - console.error(`ERROR: Calling Error Workflow for "${workflowErrorData.workflow.id}": ${error.message}`); + Logger.error(`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, { workflowId: workflowErrorData.workflow.id }); } } @@ -315,8 +315,7 @@ export async function saveStaticData(workflow: Workflow): Promise { await saveStaticDataById(workflow.id!, workflow.staticData); workflow.staticData.__dataChanged = false; } catch (e) { - // TODO: Add proper logging! - console.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: ${e.message}`); + Logger.error(`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, { workflowId: workflow.id }); } } } diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 7ded8d81b3..1cf4e43b35 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -29,6 +29,7 @@ import { import { ExecutionError, IRun, + LoggerProxy as Logger, Workflow, WorkflowExecuteMode, WorkflowHooks, @@ -177,20 +178,24 @@ export class WorkflowRunner { // Register the active execution const executionId = await this.activeExecutions.add(data, undefined); + Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, {executionId}); additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true); let workflowExecution: PCancelable; if (data.executionData !== undefined) { + Logger.debug(`Execution ID ${executionId} had Execution data. Running with payload.`, {executionId}); const workflowExecute = new WorkflowExecute(additionalData, data.executionMode, data.executionData); workflowExecution = workflowExecute.processRunExecutionData(workflow); } else if (data.runData === undefined || data.startNodes === undefined || data.startNodes.length === 0 || data.destinationNode === undefined) { + Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {executionId}); // Execute all nodes // Can execute without webhook so go on const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); workflowExecution = workflowExecute.run(workflow, undefined, data.destinationNode); } else { + Logger.debug(`Execution ID ${executionId} is a partial execution.`, {executionId}); // Execute only the nodes between start and destination nodes const workflowExecute = new WorkflowExecute(additionalData, data.executionMode); workflowExecution = workflowExecute.runPartialWorkflow(workflow, data.runData, data.startNodes, data.destinationNode); @@ -450,6 +455,7 @@ export class WorkflowRunner { // Listen to data from the subprocess subprocess.on('message', async (message: IProcessMessage) => { + Logger.debug(`Received child process message of type ${message.type} for execution ID ${executionId}.`, {executionId}); if (message.type === 'start') { // Now that the execution actually started set the timeout again so that does not time out to early. startedAt = new Date(); @@ -491,11 +497,13 @@ export class WorkflowRunner { // Also get informed when the processes does exit especially when it did crash or timed out subprocess.on('exit', async (code, signal) => { if (signal === 'SIGTERM'){ + Logger.debug(`Subprocess for execution ID ${executionId} timed out.`, {executionId}); // Execution timed out and its process has been terminated const timeoutError = new WorkflowOperationError('Workflow execution timed out!'); this.processError(timeoutError, startedAt, data.executionMode, executionId); } else if (code !== 0) { + Logger.debug(`Subprocess for execution ID ${executionId} finished with error code ${code}.`, {executionId}); // Process did exit with error code, so something went wrong. const executionError = new WorkflowOperationError('Workflow execution process did crash for an unknown reason!'); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index f3c381fb7d..14ae4a1abf 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -20,23 +20,29 @@ import { ExecutionError, IDataObject, IExecuteWorkflowInfo, + ILogger, INodeExecutionData, INodeType, INodeTypeData, IRun, - IRunExecutionData, ITaskData, IWorkflowExecuteAdditionalData, IWorkflowExecuteHooks, + LoggerProxy, Workflow, WorkflowHooks, WorkflowOperationError, } from 'n8n-workflow'; +import { + getLogger, +} from '../src/Logger'; + import * as config from '../config'; export class WorkflowRunnerProcess { data: IWorkflowExecutionDataProcessWithExecution | undefined; + logger: ILogger; startedAt = new Date(); workflow: Workflow | undefined; workflowExecute: WorkflowExecute | undefined; @@ -57,7 +63,13 @@ export class WorkflowRunnerProcess { process.on('SIGTERM', WorkflowRunnerProcess.stopProcess); process.on('SIGINT', WorkflowRunnerProcess.stopProcess); + const logger = this.logger = getLogger(); + LoggerProxy.init(logger); + this.data = inputData; + + logger.verbose('Initializing n8n sub-process', { pid: process.pid, workflowId: this.data.workflowData.id }); + let className: string; let tempNode: INodeType; let filePath: string; @@ -152,6 +164,8 @@ export class WorkflowRunnerProcess { throw e; } + await sendToParentProcess('finishExecution', { executionId, result }); + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result); return returnData!.data!.main; }; @@ -187,12 +201,7 @@ export class WorkflowRunnerProcess { parameters, }); } catch (error) { - // TODO: Add proper logging - console.error(`There was a problem sending hook: "${hook}"`); - console.error('Parameters:'); - console.error(parameters); - console.error('Error:'); - console.error(error); + this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error}); } } diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 320e9fa73f..7b2342b1c6 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -7,6 +7,7 @@ import { IPollResponse, ITriggerResponse, IWorkflowExecuteAdditionalData, + LoggerProxy as Logger, Workflow, WorkflowActivateMode, WorkflowExecuteMode, @@ -17,6 +18,7 @@ import { IWorkflowData, } from './'; + export class ActiveWorkflows { private workflowData: { [key: string]: IWorkflowData; @@ -163,6 +165,7 @@ export class ActiveWorkflows { // The trigger function to execute when the cron-time got reached const executeTrigger = async () => { + Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {workflowName: workflow.name, workflowId: workflow.id}); const pollResponse = await workflow.runPoll(node, pollFunctions); if (pollResponse !== null) { diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index e77e1a3d94..aeeb39c810 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -51,6 +51,9 @@ import * as requestPromise from 'request-promise-native'; import { createHmac } from 'crypto'; import { fromBuffer } from 'file-type'; import { lookup } from 'mime-types'; +import { + LoggerProxy as Logger, +} from 'n8n-workflow'; const requestPromiseWithDefaults = requestPromise.defaults({ timeout: 300000, // 5 minutes @@ -188,8 +191,12 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin }; } + Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" expired. Should revalidate.`); + const newToken = await token.refresh(tokenRefreshOptions); + Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`); + credentials.oauthTokenData = newToken.data; // Find the name of the credentials @@ -201,6 +208,8 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin // Save the refreshed token await additionalData.credentialsHelper.updateCredentials(name, credentialsType, credentials); + Logger.debug(`OAuth2 token for "${credentialsType}" used by node "${node.name}" has been saved to database successfully.`); + // Make the request again with the new token const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 40fe227d33..8d474a458f 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -15,6 +15,7 @@ import { ITaskDataConnections, IWaitingForExecution, IWorkflowExecuteAdditionalData, + LoggerProxy as Logger, Workflow, WorkflowExecuteMode, WorkflowOperationError, @@ -482,6 +483,8 @@ export class WorkflowExecute { * @memberof WorkflowExecute */ processRunExecutionData(workflow: Workflow): PCancelable { + Logger.verbose('Workflow execution started', { workflowId: workflow.id }); + const startedAt = new Date(); const workflowIssues = workflow.checkReadyForExecution(); @@ -502,7 +505,6 @@ export class WorkflowExecute { this.runExecutionData.startData = {}; } - let currentExecutionTry = ''; let lastExecutionTry = ''; @@ -564,6 +566,7 @@ export class WorkflowExecute { executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; executionNode = executionData.node; + Logger.debug(`Start processing node "${executionNode.name}"`, { node: executionNode.name, workflowId: workflow.id }); await this.executeHook('nodeExecuteBefore', [executionNode.name]); // Get the index of the current run @@ -661,7 +664,9 @@ export class WorkflowExecute { } } + Logger.debug(`Running node "${executionNode.name}" started`, { node: executionNode.name, workflowId: workflow.id }); nodeSuccessData = await workflow.runNode(executionData.node, executionData.data, this.runExecutionData, runIndex, this.additionalData, NodeExecuteFunctions, this.mode); + Logger.debug(`Running node "${executionNode.name}" finished successfully`, { node: executionNode.name, workflowId: workflow.id }); if (nodeSuccessData === undefined) { // Node did not get executed @@ -698,6 +703,8 @@ export class WorkflowExecute { message: error.message, stack: error.stack, }; + + Logger.debug(`Running node "${executionNode.name}" finished with error`, { node: executionNode.name, workflowId: workflow.id }); } } @@ -829,8 +836,10 @@ export class WorkflowExecute { const fullRunData = this.getFullRunData(startedAt); if (executionError !== undefined) { + Logger.verbose(`Workflow execution finished with error`, { error: executionError, workflowId: workflow.id }); fullRunData.data.resultData.error = executionError; } else { + Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id }); fullRunData.finished = true; } diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts index c89cc07fd8..8c7a51c0ba 100644 --- a/packages/core/test/WorkflowExecute.test.ts +++ b/packages/core/test/WorkflowExecute.test.ts @@ -1,8 +1,10 @@ import { IConnections, + ILogger, INode, IRun, + LoggerProxy, Workflow, } from 'n8n-workflow'; @@ -1154,9 +1156,19 @@ describe('WorkflowExecute', () => { }, ]; + const fakeLogger = { + log: () => {}, + debug: () => {}, + verbose: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + } as ILogger; + const executionMode = 'manual'; const nodeTypes = Helpers.NodeTypes(); + LoggerProxy.init(fakeLogger); for (const testData of tests) { test(testData.description, async () => { diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index 54be4408c8..49a61cc52f 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -18,6 +18,10 @@ import * as moment from 'moment-timezone'; import * as jwt from 'jsonwebtoken'; +import { + LoggerProxy as Logger +} from 'n8n-workflow'; + export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0, 'oAuth2') as string; @@ -29,6 +33,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin const response = await getAccessToken.call(this, credentials as IDataObject); const { instance_url, access_token } = response; const options = getOptions.call(this, method, (uri || endpoint), body, qs, instance_url as string); + Logger.debug(`Authentication for "Salesforce" node is using "jwt". Invoking URI ${options.uri}`); options.headers!.Authorization = `Bearer ${access_token}`; //@ts-ignore return await this.helpers.request(options); @@ -38,6 +43,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin const credentials = this.getCredentials(credentialsType); const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1]; const options = getOptions.call(this, method, (uri || endpoint), body, qs, `https://${subdomain}.salesforce.com`); + Logger.debug(`Authentication for "Salesforce" node is using "OAuth2". Invoking URI ${options.uri}`); //@ts-ignore return await this.helpers.requestOAuth2.call(this, credentialsType, options); } diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index 977c25cdb1..dee9fffe5b 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -112,6 +112,10 @@ import { userOperations, } from './UserDescription'; +import { + LoggerProxy as Logger, +} from 'n8n-workflow'; + export class Salesforce implements INodeType { description: INodeTypeDescription = { displayName: 'Salesforce', @@ -923,6 +927,8 @@ export class Salesforce implements INodeType { const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; + Logger.debug(`Running "Salesforce" node named "${this.getNode.name}" resource "${resource}" operation "${operation}"`); + for (let i = 0; i < items.length; i++) { if (resource === 'lead') { //https://developer.salesforce.com/docs/api-explorer/sobject/Lead/post-lead diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ba78499b5a..cecdaf925a 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -764,6 +764,16 @@ export interface IWorkflowSettings { [key: string]: IDataObject | string | number | boolean | undefined; } +export type LogTypes = 'debug' | 'verbose' | 'info' | 'warn' | 'error'; + +export interface ILogger { + log: (type: LogTypes, message: string, meta?: object) => void; + debug: (message: string, meta?: object) => void; + verbose: (message: string, meta?: object) => void; + info: (message: string, meta?: object) => void; + warn: (message: string, meta?: object) => void; + error: (message: string, meta?: object) => void; +} export interface IRawErrorObject { [key: string]: string | object | number | boolean | undefined | null | string[] | object[] | number[] | boolean[]; } diff --git a/packages/workflow/src/LoggerProxy.ts b/packages/workflow/src/LoggerProxy.ts new file mode 100644 index 0000000000..52ddfea9f6 --- /dev/null +++ b/packages/workflow/src/LoggerProxy.ts @@ -0,0 +1,45 @@ +import { + ILogger, + LogTypes, +} from './Interfaces'; + + +let logger: ILogger | undefined; + +export function init(loggerInstance: ILogger) { + logger = loggerInstance; +} + +export function getInstance(): ILogger { + if (logger === undefined) { + throw new Error('LoggerProxy not initialized'); + } + + return logger; +} + +export function log(type: LogTypes, message: string, meta: object = {}) { + getInstance().log(type, message, meta); +} + +// Convenience methods below + +export function debug(message: string, meta: object = {}) { + getInstance().log('debug', message, meta); +} + +export function info(message: string, meta: object = {}) { + getInstance().log('info', message, meta); +} + +export function error(message: string, meta: object = {}) { + getInstance().log('error', message, meta); +} + +export function verbose(message: string, meta: object = {}) { + getInstance().log('verbose', message, meta); +} + +export function warn(message: string, meta: object = {}) { + getInstance().log('warn', message, meta); +} diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 1248d7da9d..74fb590ea3 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -6,9 +6,11 @@ export * from './WorkflowDataProxy'; export * from './WorkflowErrors'; export * from './WorkflowHooks'; +import * as LoggerProxy from './LoggerProxy'; import * as NodeHelpers from './NodeHelpers'; import * as ObservableObject from './ObservableObject'; export { + LoggerProxy, NodeHelpers, ObservableObject, }; From 469b92e32a7ddd721e6c2e16aa754ea612361be2 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 1 May 2021 23:50:15 -0400 Subject: [PATCH 28/41] :zap: Add continueOnFail to Google Calendar (#1722) * :zap: Add continueOnFail to Google Calendar * :zap: Minor improvements Co-authored-by: Jan Oberhauser --- .../Google/Calendar/GoogleCalendar.node.ts | 804 +++++++++--------- 1 file changed, 409 insertions(+), 395 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts index 755094916b..c813f44fd9 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts @@ -180,429 +180,443 @@ export class GoogleCalendar implements INodeType { const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { - if (resource === 'calendar') { - //https://developers.google.com/calendar/v3/reference/freebusy/query - if (operation === 'availability') { - const timezone = this.getTimezone(); - const calendarId = this.getNodeParameter('calendar', i) as string; - const timeMin = this.getNodeParameter('timeMin', i) as string; - const timeMax = this.getNodeParameter('timeMax', i) as string; - const options = this.getNodeParameter('options', i) as IDataObject; - const outputFormat = options.outputFormat || 'availability'; + try { + if (resource === 'calendar') { + //https://developers.google.com/calendar/v3/reference/freebusy/query + if (operation === 'availability') { + const timezone = this.getTimezone(); + const calendarId = this.getNodeParameter('calendar', i) as string; + const timeMin = this.getNodeParameter('timeMin', i) as string; + const timeMax = this.getNodeParameter('timeMax', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const outputFormat = options.outputFormat || 'availability'; - const body: IDataObject = { - timeMin: moment.tz(timeMin, timezone).utc().format(), - timeMax: moment.tz(timeMax, timezone).utc().format(), - items: [ - { - id: calendarId, - }, - ], - timeZone: options.timezone || timezone, - }; - - responseData = await googleApiRequest.call( - this, - 'POST', - `/calendar/v3/freeBusy`, - body, - {}, - ); - - if (responseData.calendars[calendarId].errors) { - throw new NodeApiError(this.getNode(), responseData.calendars[calendarId]); - } - - if (outputFormat === 'availability') { - responseData = { - available: !responseData.calendars[calendarId].busy.length, + const body: IDataObject = { + timeMin: moment.tz(timeMin, timezone).utc().format(), + timeMax: moment.tz(timeMax, timezone).utc().format(), + items: [ + { + id: calendarId, + }, + ], + timeZone: options.timezone || timezone, }; - } else if (outputFormat === 'bookedSlots') { - responseData = responseData.calendars[calendarId].busy; + responseData = await googleApiRequest.call( + this, + 'POST', + `/calendar/v3/freeBusy`, + body, + {}, + ); + + if (responseData.calendars[calendarId].errors) { + throw new NodeApiError(this.getNode(), responseData.calendars[calendarId]); + } + + if (outputFormat === 'availability') { + responseData = { + available: !responseData.calendars[calendarId].busy.length, + }; + + } else if (outputFormat === 'bookedSlots') { + responseData = responseData.calendars[calendarId].busy; + } } } - } - if (resource === 'event') { - //https://developers.google.com/calendar/v3/reference/events/insert - if (operation === 'create') { - const calendarId = this.getNodeParameter('calendar', i) as string; - const start = this.getNodeParameter('start', i) as string; - const end = this.getNodeParameter('end', i) as string; - const useDefaultReminders = this.getNodeParameter( - 'useDefaultReminders', - i, - ) as boolean; - const additionalFields = this.getNodeParameter( - 'additionalFields', - i, - ) as IDataObject; - if (additionalFields.maxAttendees) { - qs.maxAttendees = additionalFields.maxAttendees as number; - } - if (additionalFields.sendNotifications) { - qs.sendNotifications = additionalFields.sendNotifications as boolean; - } - if (additionalFields.sendUpdates) { - qs.sendUpdates = additionalFields.sendUpdates as string; - } - const body: IEvent = { - start: { - dateTime: start, - timeZone: additionalFields.timeZone || this.getTimezone(), - }, - end: { - dateTime: end, - timeZone: additionalFields.timeZone || this.getTimezone(), - }, - }; - if (additionalFields.attendees) { - body.attendees = []; - (additionalFields.attendees as string[]).forEach(attendee => { - body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email }))); - }); - } - if (additionalFields.color) { - body.colorId = additionalFields.color as string; - } - if (additionalFields.description) { - body.description = additionalFields.description as string; - } - if (additionalFields.guestsCanInviteOthers) { - body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean; - } - if (additionalFields.guestsCanModify) { - body.guestsCanModify = additionalFields.guestsCanModify as boolean; - } - if (additionalFields.guestsCanSeeOtherGuests) { - body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean; - } - if (additionalFields.id) { - body.id = additionalFields.id as string; - } - if (additionalFields.location) { - body.location = additionalFields.location as string; - } - if (additionalFields.summary) { - body.summary = additionalFields.summary as string; - } - if (additionalFields.showMeAs) { - body.transparency = additionalFields.showMeAs as string; - } - if (additionalFields.visibility) { - body.visibility = additionalFields.visibility as string; - } - if (!useDefaultReminders) { - const reminders = (this.getNodeParameter( - 'remindersUi', + if (resource === 'event') { + //https://developers.google.com/calendar/v3/reference/events/insert + if (operation === 'create') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const start = this.getNodeParameter('start', i) as string; + const end = this.getNodeParameter('end', i) as string; + const useDefaultReminders = this.getNodeParameter( + 'useDefaultReminders', i, - ) as IDataObject).remindersValues as IDataObject[]; - body.reminders = { - useDefault: false, + ) as boolean; + const additionalFields = this.getNodeParameter( + 'additionalFields', + i, + ) as IDataObject; + if (additionalFields.maxAttendees) { + qs.maxAttendees = additionalFields.maxAttendees as number; + } + if (additionalFields.sendNotifications) { + qs.sendNotifications = additionalFields.sendNotifications as boolean; + } + if (additionalFields.sendUpdates) { + qs.sendUpdates = additionalFields.sendUpdates as string; + } + const body: IEvent = { + start: { + dateTime: start, + timeZone: additionalFields.timeZone || this.getTimezone(), + }, + end: { + dateTime: end, + timeZone: additionalFields.timeZone || this.getTimezone(), + }, }; - if (reminders) { - body.reminders.overrides = reminders; + if (additionalFields.attendees) { + body.attendees = []; + (additionalFields.attendees as string[]).forEach(attendee => { + body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email }))); + }); } - } - if (additionalFields.allday) { - body.start = { - date: moment(start) - .utc() - .format('YYYY-MM-DD'), - }; - body.end = { - date: moment(end) - .utc() - .format('YYYY-MM-DD'), - }; - } - //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z - //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html - body.recurrence = []; - if (additionalFields.rrule) { - body.recurrence = [`RRULE:${additionalFields.rrule}`]; - } else { - if ( - additionalFields.repeatHowManyTimes && - additionalFields.repeatUntil - ) { - throw new NodeOperationError(this.getNode(), - `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, - ); + if (additionalFields.color) { + body.colorId = additionalFields.color as string; } - if (additionalFields.repeatFrecuency) { - body.recurrence?.push( - `FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`, - ); + if (additionalFields.description) { + body.description = additionalFields.description as string; } - if (additionalFields.repeatHowManyTimes) { - body.recurrence?.push( - `COUNT=${additionalFields.repeatHowManyTimes};`, - ); + if (additionalFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean; } - if (additionalFields.repeatUntil) { - body.recurrence?.push( - `UNTIL=${moment(additionalFields.repeatUntil as string) + if (additionalFields.guestsCanModify) { + body.guestsCanModify = additionalFields.guestsCanModify as boolean; + } + if (additionalFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean; + } + if (additionalFields.id) { + body.id = additionalFields.id as string; + } + if (additionalFields.location) { + body.location = additionalFields.location as string; + } + if (additionalFields.summary) { + body.summary = additionalFields.summary as string; + } + if (additionalFields.showMeAs) { + body.transparency = additionalFields.showMeAs as string; + } + if (additionalFields.visibility) { + body.visibility = additionalFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter( + 'remindersUi', + i, + ) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (additionalFields.allday) { + body.start = { + date: moment(start) .utc() - .format('YYYYMMDDTHHmmss')}Z`, - ); - } - if (body.recurrence.length !== 0) { - body.recurrence = [`RRULE:${body.recurrence.join('')}`]; - } - } - - if (additionalFields.conferenceDataUi) { - const conferenceData = (additionalFields.conferenceDataUi as IDataObject).conferenceDataValues as IDataObject; - if (conferenceData) { - - qs.conferenceDataVersion = 1; - body.conferenceData = { - createRequest: { - requestId: uuid(), - conferenceSolution: { - type: conferenceData.conferenceSolution as string, - }, - }, + .format('YYYY-MM-DD'), + }; + body.end = { + date: moment(end) + .utc() + .format('YYYY-MM-DD'), }; } - } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (additionalFields.rrule) { + body.recurrence = [`RRULE:${additionalFields.rrule}`]; + } else { + if ( + additionalFields.repeatHowManyTimes && + additionalFields.repeatUntil + ) { + throw new NodeOperationError(this.getNode(), + `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, + ); + } + if (additionalFields.repeatFrecuency) { + body.recurrence?.push( + `FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`, + ); + } + if (additionalFields.repeatHowManyTimes) { + body.recurrence?.push( + `COUNT=${additionalFields.repeatHowManyTimes};`, + ); + } + if (additionalFields.repeatUntil) { + body.recurrence?.push( + `UNTIL=${moment(additionalFields.repeatUntil as string) + .utc() + .format('YYYYMMDDTHHmmss')}Z`, + ); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } + } - responseData = await googleApiRequest.call( - this, - 'POST', - `/calendar/v3/calendars/${calendarId}/events`, - body, - qs, - ); - } - //https://developers.google.com/calendar/v3/reference/events/delete - if (operation === 'delete') { - const calendarId = this.getNodeParameter('calendar', i) as string; - const eventId = this.getNodeParameter('eventId', i) as string; - const options = this.getNodeParameter('options', i) as IDataObject; - if (options.sendUpdates) { - qs.sendUpdates = options.sendUpdates as number; - } - responseData = await googleApiRequest.call( - this, - 'DELETE', - `/calendar/v3/calendars/${calendarId}/events/${eventId}`, - {}, - ); - responseData = { success: true }; - } - //https://developers.google.com/calendar/v3/reference/events/get - if (operation === 'get') { - const calendarId = this.getNodeParameter('calendar', i) as string; - const eventId = this.getNodeParameter('eventId', i) as string; - const options = this.getNodeParameter('options', i) as IDataObject; - if (options.maxAttendees) { - qs.maxAttendees = options.maxAttendees as number; - } - if (options.timeZone) { - qs.timeZone = options.timeZone as string; - } - responseData = await googleApiRequest.call( - this, - 'GET', - `/calendar/v3/calendars/${calendarId}/events/${eventId}`, - {}, - qs, - ); - } - //https://developers.google.com/calendar/v3/reference/events/list - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - const calendarId = this.getNodeParameter('calendar', i) as string; - const options = this.getNodeParameter('options', i) as IDataObject; - if (options.iCalUID) { - qs.iCalUID = options.iCalUID as string; - } - if (options.maxAttendees) { - qs.maxAttendees = options.maxAttendees as number; - } - if (options.orderBy) { - qs.orderBy = options.orderBy as number; - } - if (options.query) { - qs.q = options.query as number; - } - if (options.showDeleted) { - qs.showDeleted = options.showDeleted as boolean; - } - if (options.showHiddenInvitations) { - qs.showHiddenInvitations = options.showHiddenInvitations as boolean; - } - if (options.singleEvents) { - qs.singleEvents = options.singleEvents as boolean; - } - if (options.timeMax) { - qs.timeMax = options.timeMax as string; - } - if (options.timeMin) { - qs.timeMin = options.timeMin as string; - } - if (options.timeZone) { - qs.timeZone = options.timeZone as string; - } - if (options.updatedMin) { - qs.updatedMin = options.updatedMin as string; - } - if (returnAll) { - responseData = await googleApiRequestAllItems.call( + if (additionalFields.conferenceDataUi) { + const conferenceData = (additionalFields.conferenceDataUi as IDataObject).conferenceDataValues as IDataObject; + if (conferenceData) { + + qs.conferenceDataVersion = 1; + body.conferenceData = { + createRequest: { + requestId: uuid(), + conferenceSolution: { + type: conferenceData.conferenceSolution as string, + }, + }, + }; + } + } + + responseData = await googleApiRequest.call( this, - 'items', - 'GET', + 'POST', `/calendar/v3/calendars/${calendarId}/events`, - {}, + body, qs, ); - } else { - qs.maxResults = this.getNodeParameter('limit', i) as number; + } + //https://developers.google.com/calendar/v3/reference/events/delete + if (operation === 'delete') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.sendUpdates) { + qs.sendUpdates = options.sendUpdates as number; + } + responseData = await googleApiRequest.call( + this, + 'DELETE', + `/calendar/v3/calendars/${calendarId}/events/${eventId}`, + {}, + ); + responseData = { success: true }; + } + //https://developers.google.com/calendar/v3/reference/events/get + if (operation === 'get') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } responseData = await googleApiRequest.call( this, 'GET', - `/calendar/v3/calendars/${calendarId}/events`, + `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs, ); - responseData = responseData.items; + } + //https://developers.google.com/calendar/v3/reference/events/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const calendarId = this.getNodeParameter('calendar', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.iCalUID) { + qs.iCalUID = options.iCalUID as string; + } + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.orderBy) { + qs.orderBy = options.orderBy as number; + } + if (options.query) { + qs.q = options.query as number; + } + if (options.showDeleted) { + qs.showDeleted = options.showDeleted as boolean; + } + if (options.showHiddenInvitations) { + qs.showHiddenInvitations = options.showHiddenInvitations as boolean; + } + if (options.singleEvents) { + qs.singleEvents = options.singleEvents as boolean; + } + if (options.timeMax) { + qs.timeMax = options.timeMax as string; + } + if (options.timeMin) { + qs.timeMin = options.timeMin as string; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + if (options.updatedMin) { + qs.updatedMin = options.updatedMin as string; + } + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + `/calendar/v3/calendars/${calendarId}/events`, + {}, + qs, + ); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/calendar/v3/calendars/${calendarId}/events`, + {}, + qs, + ); + responseData = responseData.items; + } + } + //https://developers.google.com/calendar/v3/reference/events/patch + if (operation === 'update') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const useDefaultReminders = this.getNodeParameter( + 'useDefaultReminders', + i, + ) as boolean; + const updateFields = this.getNodeParameter( + 'updateFields', + i, + ) as IDataObject; + if (updateFields.maxAttendees) { + qs.maxAttendees = updateFields.maxAttendees as number; + } + if (updateFields.sendNotifications) { + qs.sendNotifications = updateFields.sendNotifications as boolean; + } + if (updateFields.sendUpdates) { + qs.sendUpdates = updateFields.sendUpdates as string; + } + const body: IEvent = {}; + if (updateFields.start) { + body.start = { + dateTime: updateFields.start, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.end) { + body.end = { + dateTime: updateFields.end, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.attendees) { + body.attendees = []; + (updateFields.attendees as string[]).forEach(attendee => { + body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email }))); + }); + } + if (updateFields.color) { + body.colorId = updateFields.color as string; + } + if (updateFields.description) { + body.description = updateFields.description as string; + } + if (updateFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean; + } + if (updateFields.guestsCanModify) { + body.guestsCanModify = updateFields.guestsCanModify as boolean; + } + if (updateFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean; + } + if (updateFields.id) { + body.id = updateFields.id as string; + } + if (updateFields.location) { + body.location = updateFields.location as string; + } + if (updateFields.summary) { + body.summary = updateFields.summary as string; + } + if (updateFields.showMeAs) { + body.transparency = updateFields.showMeAs as string; + } + if (updateFields.visibility) { + body.visibility = updateFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter( + 'remindersUi', + i, + ) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (updateFields.allday && updateFields.start && updateFields.end) { + body.start = { + date: moment(updateFields.start as string) + .utc() + .format('YYYY-MM-DD'), + }; + body.end = { + date: moment(updateFields.end as string) + .utc() + .format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (updateFields.rrule) { + body.recurrence = [`RRULE:${updateFields.rrule}`]; + } else { + if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) { + throw new NodeOperationError(this.getNode(), + `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, + ); + } + if (updateFields.repeatFrecuency) { + body.recurrence?.push( + `FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`, + ); + } + if (updateFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); + } + if (updateFields.repeatUntil) { + body.recurrence?.push( + `UNTIL=${moment(updateFields.repeatUntil as string) + .utc() + .format('YYYYMMDDTHHmmss')}Z`, + ); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } else { + delete body.recurrence; + } + } + responseData = await googleApiRequest.call( + this, + 'PATCH', + `/calendar/v3/calendars/${calendarId}/events/${eventId}`, + body, + qs, + ); } } - //https://developers.google.com/calendar/v3/reference/events/patch - if (operation === 'update') { - const calendarId = this.getNodeParameter('calendar', i) as string; - const eventId = this.getNodeParameter('eventId', i) as string; - const useDefaultReminders = this.getNodeParameter( - 'useDefaultReminders', - i, - ) as boolean; - const updateFields = this.getNodeParameter( - 'updateFields', - i, - ) as IDataObject; - if (updateFields.maxAttendees) { - qs.maxAttendees = updateFields.maxAttendees as number; - } - if (updateFields.sendNotifications) { - qs.sendNotifications = updateFields.sendNotifications as boolean; - } - if (updateFields.sendUpdates) { - qs.sendUpdates = updateFields.sendUpdates as string; - } - const body: IEvent = {}; - if (updateFields.start) { - body.start = { - dateTime: updateFields.start, - timeZone: updateFields.timeZone || this.getTimezone(), - }; - } - if (updateFields.end) { - body.end = { - dateTime: updateFields.end, - timeZone: updateFields.timeZone || this.getTimezone(), - }; - } - if (updateFields.attendees) { - body.attendees = []; - (updateFields.attendees as string[]).forEach(attendee => { - body.attendees!.push.apply(body.attendees, attendee.split(',').map(a => a.trim()).map(email => ({ email }))); - }); - } - if (updateFields.color) { - body.colorId = updateFields.color as string; - } - if (updateFields.description) { - body.description = updateFields.description as string; - } - if (updateFields.guestsCanInviteOthers) { - body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean; - } - if (updateFields.guestsCanModify) { - body.guestsCanModify = updateFields.guestsCanModify as boolean; - } - if (updateFields.guestsCanSeeOtherGuests) { - body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean; - } - if (updateFields.id) { - body.id = updateFields.id as string; - } - if (updateFields.location) { - body.location = updateFields.location as string; - } - if (updateFields.summary) { - body.summary = updateFields.summary as string; - } - if (updateFields.showMeAs) { - body.transparency = updateFields.showMeAs as string; - } - if (updateFields.visibility) { - body.visibility = updateFields.visibility as string; - } - if (!useDefaultReminders) { - const reminders = (this.getNodeParameter( - 'remindersUi', - i, - ) as IDataObject).remindersValues as IDataObject[]; - body.reminders = { - useDefault: false, - }; - if (reminders) { - body.reminders.overrides = reminders; - } - } - if (updateFields.allday && updateFields.start && updateFields.end) { - body.start = { - date: moment(updateFields.start as string) - .utc() - .format('YYYY-MM-DD'), - }; - body.end = { - date: moment(updateFields.end as string) - .utc() - .format('YYYY-MM-DD'), - }; - } - //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z - //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html - body.recurrence = []; - if (updateFields.rrule) { - body.recurrence = [`RRULE:${updateFields.rrule}`]; - } else { - if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) { - throw new NodeOperationError(this.getNode(), - `You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`, - ); - } - if (updateFields.repeatFrecuency) { - body.recurrence?.push( - `FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`, - ); - } - if (updateFields.repeatHowManyTimes) { - body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); - } - if (updateFields.repeatUntil) { - body.recurrence?.push( - `UNTIL=${moment(updateFields.repeatUntil as string) - .utc() - .format('YYYYMMDDTHHmmss')}Z`, - ); - } - if (body.recurrence.length !== 0) { - body.recurrence = [`RRULE:${body.recurrence.join('')}`]; - } else { - delete body.recurrence; - } - } - responseData = await googleApiRequest.call( - this, - 'PATCH', - `/calendar/v3/calendars/${calendarId}/events/${eventId}`, - body, - qs, + } catch (error) { + if (this.continueOnFail() !== true) { + throw error; + } else { + // Return the actual reason as error + returnData.push( + { + error: error.message, + }, ); + continue; } } From ea48882291278fd22e1d13fb6e20c4e56f535105 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sun, 2 May 2021 06:53:20 +0300 Subject: [PATCH 29/41] :arrow_up: Set mongodb@3.6.6 on n8n-nodes-base Snyk has created this PR to upgrade mongodb from 3.6.5 to 3.6.6. See this package in npm: https://www.npmjs.com/package/mongodb See this project in Snyk: https://app.snyk.io/org/janober/project/a08454f4-33a1-49bc-bb2a-f31792e94f42?utm_source=github&utm_medium=upgrade-pr --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 38d615717d..3168299b8c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -619,7 +619,7 @@ "mailparser": "^3.2.0", "moment": "2.29.1", "moment-timezone": "^0.5.28", - "mongodb": "3.6.5", + "mongodb": "3.6.6", "mqtt": "4.2.6", "mssql": "^6.2.0", "mysql2": "~2.2.0", From 74d8f3d1501b9edca4104ece121518c8e5a25ca3 Mon Sep 17 00:00:00 2001 From: Mika Luhta <12100880+mluhta@users.noreply.github.com> Date: Sun, 2 May 2021 06:10:44 +0200 Subject: [PATCH 30/41] :sparkles: Add isRead property & message:move to Outlook (#1724) --- .../Microsoft/Outlook/DraftMessageSharedDescription.ts | 7 +++++++ .../nodes/Microsoft/Outlook/MessageDescription.ts | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts index faa72220c8..9478096b56 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/DraftMessageSharedDescription.ts @@ -175,6 +175,13 @@ export const draftMessageSharedFields = [ ], default: 'Low', }, + { + displayName: 'Is Read', + name: 'isRead', + description: 'Indicates whether the message has been read.', + type: 'boolean', + default: false, + }, { displayName: 'Read Receipt Requested', name: 'isReadReceiptRequested', diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts index d45f1bae17..e67b6ce3ea 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/MessageDescription.ts @@ -35,6 +35,11 @@ export const messageOperations = [ value: 'getMime', description: 'Get MIME content of a message', }, + { + name: 'Move', + value: 'move', + description: 'Move a message', + }, { name: 'Reply', value: 'reply', @@ -75,6 +80,7 @@ export const messageFields = [ 'get', 'getAttachment', 'getMime', + 'move', 'update', 'reply', ], @@ -615,7 +621,7 @@ export const messageFields = [ { displayName: 'Folder ID', name: 'folderId', - description: 'Folder ID', + description: 'Target Folder ID.', type: 'string', default: '', required: true, From 9bc62e41c1f1f5f4645e673e63a3140962e82bcb Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:56:20 +0000 Subject: [PATCH 31/41] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-workflow@0.?= =?UTF-8?q?57.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/workflow/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 4a6e11004f..e5247ba349 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.56.0", + "version": "0.57.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 6980ecf2a8e8bd5f597531cf89850ab3e1ce7f51 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:56:37 +0000 Subject: [PATCH 32/41] :arrow_up: Set n8n-workflow@0.57.0 on n8n-core --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 70e6874e66..00437d3e17 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,7 +47,7 @@ "file-type": "^14.6.2", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.56.0", + "n8n-workflow": "~0.57.0", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "request": "^2.88.2", From 56b93c3e7e65074c743b7b7422c2ff4ef6773fac Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:56:37 +0000 Subject: [PATCH 33/41] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-core@0.69.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 00437d3e17..69a76944f1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.68.0", + "version": "0.69.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From e88161525c84b78de14a0ac73697be4fb643f621 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:56:52 +0000 Subject: [PATCH 34/41] :arrow_up: Set n8n-core@0.69.0 and n8n-workflow@0.57.0 on n8n-nodes-base --- packages/nodes-base/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 3168299b8c..1b337bce87 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -586,7 +586,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.56.0", + "n8n-workflow": "~0.57.0", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~3.9.7" @@ -623,7 +623,7 @@ "mqtt": "4.2.6", "mssql": "^6.2.0", "mysql2": "~2.2.0", - "n8n-core": "~0.68.0", + "n8n-core": "~0.69.0", "nodemailer": "^6.5.0", "pdf-parse": "^1.1.1", "pg": "^8.3.0", From d992534fc4c1ad4af1cfb86f467a94ee5e541fe9 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:56:52 +0000 Subject: [PATCH 35/41] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-nodes-base@?= =?UTF-8?q?0.115.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1b337bce87..dd9011245e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.114.0", + "version": "0.115.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From e5043676be87784a8671984a3524f771dd81dec2 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:57:49 +0000 Subject: [PATCH 36/41] :arrow_up: Set n8n-workflow@0.57.0 on n8n-editor-ui --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 1295acde1b..a3799ef638 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -65,7 +65,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.56.0", + "n8n-workflow": "~0.57.0", "node-sass": "^4.12.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", From 8c2ffc5d0e5b9e06982ffaffc6fcb23f5f37dad2 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:57:49 +0000 Subject: [PATCH 37/41] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n-editor-ui@0?= =?UTF-8?q?.88.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index a3799ef638..ccf591fe6b 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.87.0", + "version": "0.88.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 318142a9e5ee86fe13d77bd576c942e9e51ce25f Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:58:37 +0000 Subject: [PATCH 38/41] :arrow_up: Set n8n-core@0.69.0, n8n-editor-ui@0.88.0, n8n-nodes-base@0.115.0 and n8n-workflow@0.57.0 on n8n --- packages/cli/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0c269ea504..26c9893846 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -104,10 +104,10 @@ "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", "mysql2": "~2.2.0", - "n8n-core": "~0.68.0", - "n8n-editor-ui": "~0.87.0", - "n8n-nodes-base": "~0.114.0", - "n8n-workflow": "~0.56.0", + "n8n-core": "~0.69.0", + "n8n-editor-ui": "~0.88.0", + "n8n-nodes-base": "~0.115.0", + "n8n-workflow": "~0.57.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", From 043cf6730c54f65aafd8744d5a0e604e4922d1d3 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 2 May 2021 13:58:37 +0000 Subject: [PATCH 39/41] =?UTF-8?q?:bookmark:=20Release=C2=A0n8n@0.118.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 26c9893846..9a1673bedc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.117.0", + "version": "0.118.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 3d2eaa3aaa20e9d4d7504fbe1a7d54bde68ee5a9 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 3 May 2021 16:15:02 -0500 Subject: [PATCH 40/41] :zap: Check on startup for Node.js version 14 or later --- packages/cli/bin/n8n | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/bin/n8n b/packages/cli/bin/n8n index 583dfa5bf4..b72c643fb3 100755 --- a/packages/cli/bin/n8n +++ b/packages/cli/bin/n8n @@ -24,8 +24,8 @@ if (process.argv.length === 2) { var nodeVersion = process.versions.node.split('.'); -if (parseInt(nodeVersion[0], 10) < 12 || parseInt(nodeVersion[0], 10) === 12 && parseInt(nodeVersion[1], 10) < 9) { - console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 12.9 or later!\n`); +if (parseInt(nodeVersion[0], 10) < 14) { + console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 14 or later!\n`); process.exit(0); } From 715e41b5907de39e4ccf7084e3b6b1bdcb5928dc Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 3 May 2021 16:21:22 -0500 Subject: [PATCH 41/41] :zap: Update required Node.js version also to 14 in package.json of cli --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9a1673bedc..aa48f92da2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -44,7 +44,7 @@ "workflow" ], "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "files": [ "bin",