From 0d1b4611c8a32214f6c9bfd5103ae885e39d08e7 Mon Sep 17 00:00:00 2001 From: Tanay Pant <7481165+tanay1337@users.noreply.github.com> Date: Thu, 18 Jun 2020 10:08:31 +0200 Subject: [PATCH 01/13] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20Make=20a=20few=20lan?= =?UTF-8?q?guage=20changes=20(#681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 10 +++++----- LICENSE.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 568f8fc9a7..fcc046beef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ The most important directories: execution, active webhooks and workflows - [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - - [/packages/node-dev](/packages/node-dev) - Simple CLI to create new n8n-nodes + - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which get used by front- & backend @@ -159,7 +159,7 @@ tests of all packages. ## Create Custom Nodes -It is very easy to create own nodes for n8n. More information about that can +It is very straightforward to create your own nodes for n8n. More information about that can be found in the documentation of "n8n-node-dev" which is a small CLI which helps with n8n-node-development. @@ -177,9 +177,9 @@ If you want to create a node which should be added to n8n follow these steps: 1. Create a new folder for the new node. For a service named "Example" the folder would be called: `/packages/nodes-base/nodes/Example` - 1. If there is already a similar node simply copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. + 1. If there is already a similar node, copy the existing one in the new folder and rename it. If none exists yet, create a boilerplate node with [n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) and copy that one in the folder. - 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to simply copy existing similar ones. + 1. If the node needs credentials because it has to authenticate with an API or similar create new ones. Existing ones can be found in folder `/packages/nodes-base/credentials`. Also there it is the easiest to copy existing similar ones. 1. Add the path to the new node (and optionally credentials) to package.json of `nodes-base`. It already contains a property `n8n` with its own keys `credentials` and `nodes`. @@ -236,6 +236,6 @@ docsify serve ./docs That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button. -We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally just a few lines long. +We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally only a few lines long. A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in. diff --git a/LICENSE.md b/LICENSE.md index aac54547eb..c2aec2148e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -215,7 +215,7 @@ Licensor: n8n GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright [2020] [n8n GmbH] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 55ed53579f9b7c22e818c49eead34c5cffceeb8f Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 24 Jun 2020 15:46:56 +0200 Subject: [PATCH 02/13] Postmark trigger --- .../credentials/PostmarkApi.credentials.ts | 18 ++ .../nodes/Postmark/GenericFunctions.ts | 47 +++ .../nodes/Postmark/PostmarkTrigger.node.ts | 275 ++++++++++++++++++ .../nodes-base/nodes/Postmark/postmark.png | Bin 0 -> 3301 bytes packages/nodes-base/package.json | 2 + 5 files changed, 342 insertions(+) create mode 100644 packages/nodes-base/credentials/PostmarkApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Postmark/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Postmark/postmark.png diff --git a/packages/nodes-base/credentials/PostmarkApi.credentials.ts b/packages/nodes-base/credentials/PostmarkApi.credentials.ts new file mode 100644 index 0000000000..88df53aa30 --- /dev/null +++ b/packages/nodes-base/credentials/PostmarkApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class PostmarkApi implements ICredentialType { + name = 'postmarkApi'; + displayName = 'Postmark API'; + properties = [ + { + displayName: 'Server Token', + name: 'serverToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts new file mode 100644 index 0000000000..d65777d881 --- /dev/null +++ b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts @@ -0,0 +1,47 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions +} from 'n8n-workflow'; + + +export async function postmarkApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method : string, endpoint : string, body: any = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('postmarkApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Postmark-Server-Token' : credentials.serverToken + }, + method, + body, + uri: 'https://api.postmarkapp.com' + endpoint, + json: true + }; + if (body === {}) { + delete options.body; + } + options = Object.assign({}, options, option); + + try { + return await this.helpers.request!(options); + } catch (error) { + throw new Error(`Postmark: ${error.statusCode} Message: ${error.message}`); + } +} + + diff --git a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts new file mode 100644 index 0000000000..15af530cad --- /dev/null +++ b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts @@ -0,0 +1,275 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + postmarkApiRequest, +} from './GenericFunctions'; + +export class PostmarkTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Postmark Trigger', + name: 'postmarkTrigger', + icon: 'file:postmark.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Postmark events occur.', + defaults: { + name: 'Postmark Trigger', + color: '#fedd00', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'postmarkApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Open', + name: 'open', + type: 'boolean', + default: false, + description: 'Listing for if the Open webhook is enabled/disabled.', + }, + { + displayName: 'First Open Only', + name: 'postFirstOpenOnly', + type: 'boolean', + default: false, + displayOptions: { + show: { + open: [ + true + ], + }, + }, + description: 'Webhook will only post on first open if enabled.', + }, + { + displayName: 'Click', + name: 'click', + type: 'boolean', + default: false, + description: 'Listing for if the Click webhook is enabled/disabled.', + }, + { + displayName: 'Delivery', + name: 'delivery', + type: 'boolean', + default: false, + description: 'Listing for if the Delivery webhook is enabled/disabled.', + }, + { + displayName: 'Bounce', + name: 'bounce', + type: 'boolean', + default: false, + description: 'Listing for if the Bounce webhook is enabled/disabled.', + }, + { + displayName: 'Bounce Include Content', + name: 'bounceIncludeContent', + type: 'boolean', + default: false, + displayOptions: { + show: { + bounce: [ + true + ], + }, + }, + description: 'Webhook will send full bounce content if IncludeContent is enabled.', + }, + { + displayName: 'Spam Complaint', + name: 'spamComplaint', + type: 'boolean', + default: false, + description: 'Listing for if the Spam webhook is enabled/disabled.', + }, + { + displayName: 'Spam Complaint Include Content', + name: 'spamComplaintIncludeContent', + type: 'boolean', + default: false, + displayOptions: { + show: { + spamComplaint: [ + true + ], + }, + }, + description: 'Webhook will send full spam content if IncludeContent is enabled.', + }, + { + displayName: 'Subscription Change', + name: 'subscriptionChange', + type: 'boolean', + default: false, + description: 'Listing for if the Subscription Change webhook is enabled/disabled.', + }, + ], + + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + + // Webhook got created before so check if it still exists + const endpoint = `/webhooks/${webhookData.webhookId}`; + + const responseData = await postmarkApiRequest.call(this, 'GET', endpoint, {}); + + if (responseData.ID === undefined) { + return false; + } + else if (responseData.ID === webhookData.id) { + return true; + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + + const endpoint = `/webhooks`; + + // tslint:disable-next-line: no-any + const body : any = { + Url: webhookUrl, + Triggers: { + Open:{ + Enabled: false, + PostFirstOpenOnly: false + }, + Click:{ + Enabled: false + }, + Delivery:{ + Enabled: false + }, + Bounce:{ + Enabled: false, + IncludeContent: false + }, + SpamComplaint:{ + Enabled: false, + IncludeContent: false + }, + SubscriptionChange: { + Enabled: false + } + } + }; + + const open = this.getNodeParameter('open', 0); + const postFirstOpenOnly = this.getNodeParameter('postFirstOpenOnly', 0); + const click = this.getNodeParameter('click', 0); + const delivery = this.getNodeParameter('delivery', 0); + const bounce = this.getNodeParameter('bounce', 0); + const bounceIncludeContent = this.getNodeParameter('bounceIncludeContent', 0); + const spamComplaint = this.getNodeParameter('spamComplaint', 0); + const spamComplaintIncludeContent = this.getNodeParameter('spamComplaintIncludeContent', 0); + const subscriptionChange = this.getNodeParameter('subscriptionChange', 0); + + if (open) { + body.Triggers.Open.Enabled = true; + + if (postFirstOpenOnly) { + body.Triggers.Open.PostFirstOpenOnly = true; + } + } + if (click) { + body.Triggers.Click.Enabled = true; + } + if (delivery) { + body.Triggers.Delivery.Enabled = true; + } + if (bounce) { + body.Triggers.Bounce.Enabled = true; + + if (bounceIncludeContent) { + body.Triggers.Bounce.IncludeContent = true; + } + } + if (spamComplaint) { + body.Triggers.SpamComplaint.Enabled = true; + + if (spamComplaintIncludeContent) { + body.Triggers.SpamComplaint.IncludeContent = true; + } + } + if (subscriptionChange) { + body.Triggers.SubscriptionChange.Enabled = true; + } + + const responseData = await postmarkApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.ID === undefined) { + // Required data is missing so was not successful + return false; + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = responseData.ID as string; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + const endpoint = `/webhooks/${webhookData.webhookId}`; + const body = {}; + + try { + await postmarkApiRequest.call(this, 'DELETE', endpoint, body); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + delete webhookData.webhookEvents; + } + + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Postmark/postmark.png b/packages/nodes-base/nodes/Postmark/postmark.png new file mode 100644 index 0000000000000000000000000000000000000000..44f90b2a6c25f66c011e849512dbbbea5f128dfa GIT binary patch literal 3301 zcmcInc~lek7LKec0xB+`h%v4RlVq|%0s#vcpcQE>sZ`X+3z=bg6Cf8LxkGryVNcfb4HW$rn% z*Wb_EeCDE=7!1bT*T*XWy}Rnq=_crv@$Qx!z0HvMY*t_}W>)%h8s^w{)) zIUJba7sO47=F)jMcQ>r7nt>8XU?qfAOT zfG%geT(1e+-n(X81}KN=V9ueZ81CR1{t$;xVXP zkVcP$A^~SEOaWjrg$K|fQX~M8xFisUBFT{uZ%h|z?}cz<_36{wkGCxk;i4Q<8Dh{$ zFi0lDAOM0+bb!XC5CMo!rU4WZnH0;AVU;XpA#Phcp#rd1wjx3og;Br9+$yK>G&E(@1nFeWv=UY`UB?>)?j-neA_ zyvvvO?fW_>7q_1iCbpu8idHR>_&zZ`_UV4MX5EciWb3oEoE+;)k$9fxo`T}rrq{A^ z76n8H3g=igdj;ZZ=bG<5dHk^VfT6u?Fx;-f^=i!fq0qSY0DR&bg9?``5P|G+zE>r9 z-9RTRY`9Nc7e>E%dbf3WxFq)B)%nqvzgn{3MIn#R=QIn0d_qFfKQp1ryZ40uoRw9z zroOSUnC0!gn?j{t=)-MFfAG1$=C((MaB09cBMh}`IxCYaZ(hH? ze5Pmif!RMbHu^RDNF-;5JI^`x4Gu1}WSu&F`llB!Gy=_rc7q>t)Ej;Fl-J*Nc<*m6F|9x}f zZgPED$H*&lmP;pJS$O!fjv2=6ndQ`^2x5x4#-QZXsjC4C9P!%qALJJrgbynkzS>Yx z7r0qtk?!zl&v#)Ds2raNqr4@+#+9#%^E@lh{tA{e4a$D;l;)qg zbcvN1TW#sUO!#!|=&z=$s?OhXbzfU)8|M4|=@+AEmJfDnT6rbel@5m_I$m9lVQU@{ zRYK(0+uPr0X~DH7-#6$vdJHeWu;Pg8%iP>|icNWT{k<*PZg)D#D2Sj<4bF(Q3(NcR z^=70h!eFuWeo^kplXY$J&PVNNUif#ta)dk^(%OCrO-u} z(3zai+U^~3_EuGVe0*?hY*9t1Q$ba4$u4W=$jQxvU6=2ryt;2^T^$uBVhVIF#N^7Q z$+f57++2Mxg?b1CsxQ2Das7@e%XkuQ^kTz;j8{py-6K}dcc(_}ZHsL0yO#H1sI_Lh z$CT6`G+wZuIhC2p&-g4_L^ry^eE0E|G>b?(bGFp11m|K zQr^Cy=bE+185F}1MuDLK-3tFG?;khFUf>o>mr_vV_ui*VuZRF-|n^I}eN zk7R$Yut9soBqN%0%&hD1VjJM%&ia8v@|qtX|NhvrGoYel!58XB2AO%wmgG6!KR2|^ zI1yhw?A)`Ana=9xe3M|Njg7Xh7+I?&@3&}g9j@`L5$YZmzN}@i+=CTOk2kQ2=4+C= z+J3mmfB80s#bh6HD|z<)rlxoI5{@11X?d=W>8EUPS-y6=bJN3yC+h?QYR46Aq&aD7 ze4BikvE^P)5oJfJv6bDb+3oY~MvF07A%wCO#2;-}KWk-2*}N$)J#n|=?V>h=?Z^gK ziTvWcO9CH*r4CLhLxma6r=HSR8-DOIC_Ad&8eZG~VCa#9h6cwtee+fqO=fqM#Yg;b z{@doB{Vy^*zk0!_*=wBn+CPm@U$|xDrbDEPHrqxwuXhx*TwGt6M5rw&b&u;pdNA!p zvj_WwHx%4)yP`YTfBpIl+XLl*=KaZozd^W^@G#(_G^gm)aGckd%C>pd`DXt@jt`f9 z+tVAD>ulL#qb+Qzi4%3vY7JH0%Ven>1C@A2YSHVbcQam^UawkXkz@Sy5rnl&H|ZOe wby@5&eYzTx^7f#2SJEY`lBH3`11HoU3?)ewA9`$Vj{S)EuJ`lG_lVf>Z!h~rQvd(} literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index a09d97d032..99d390ee09 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -107,6 +107,7 @@ "dist/credentials/PayPalApi.credentials.js", "dist/credentials/PipedriveApi.credentials.js", "dist/credentials/Postgres.credentials.js", + "dist/credentials/PostmarkApi.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", "dist/credentials/RundeckApi.credentials.js", @@ -248,6 +249,7 @@ "dist/nodes/Pipedrive/Pipedrive.node.js", "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/Postgres/Postgres.node.js", + "dist/nodes/Postmark/PostmarkTrigger.node.js", "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js", "dist/nodes/ReadPdf.node.js", From d199e1e638a62c909ec9a3034635c4f5409097db Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Fri, 26 Jun 2020 13:32:48 +0200 Subject: [PATCH 03/13] checkExists modified --- .../nodes/Postmark/GenericFunctions.ts | 48 +++++ .../nodes/Postmark/PostmarkTrigger.node.ts | 197 +++++++----------- 2 files changed, 128 insertions(+), 117 deletions(-) diff --git a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts index d65777d881..5844d28d7c 100644 --- a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts @@ -37,6 +37,8 @@ export async function postmarkApiRequest(this: IExecuteFunctions | IWebhookFunct } options = Object.assign({}, options, option); + console.log(options); + try { return await this.helpers.request!(options); } catch (error) { @@ -44,4 +46,50 @@ export async function postmarkApiRequest(this: IExecuteFunctions | IWebhookFunct } } +// tslint:disable-next-line: no-any +export function convertTriggerObjectToStringArray (webhookObject : any) : string[] { + const triggers = webhookObject.Triggers; + const webhookEvents : string[] = []; + + // Translate Webhook trigger settings to string array + if (triggers.Open.Enabled) { + webhookEvents.push('open'); + } + if (triggers.Open.PostFirstOpenOnly) { + webhookEvents.push('firstOpen'); + } + if (triggers.Click.Enabled) { + webhookEvents.push('click'); + } + if (triggers.Delivery.Enabled) { + webhookEvents.push('delivery'); + } + if (triggers.Bounce.Enabled) { + webhookEvents.push('bounce'); + } + if (triggers.Bounce.IncludeContent) { + webhookEvents.push('bounceContent'); + } + if (triggers.SpamComplaint.Enabled) { + webhookEvents.push('spamComplaint'); + } + if (triggers.SpamComplaint.IncludeContent) { + webhookEvents.push('spamComplaintContent'); + } + if (triggers.SubscriptionChange.Enabled) { + webhookEvents.push('subscriptionChange'); + } + + return webhookEvents; +} + +export function eventExists (currentEvents : string[], webhookEvents: string[]) { + for (const currentEvent of currentEvents) { + if (!webhookEvents.includes(currentEvent)) { + return false; + } + } + return true; +} + diff --git a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts index 15af530cad..f6f09afdd4 100644 --- a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts +++ b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts @@ -10,7 +10,9 @@ import { } from 'n8n-workflow'; import { - postmarkApiRequest, + convertTriggerObjectToStringArray, + eventExists, + postmarkApiRequest } from './GenericFunctions'; export class PostmarkTrigger implements INodeType { @@ -43,88 +45,54 @@ export class PostmarkTrigger implements INodeType { ], properties: [ { - displayName: 'Open', - name: 'open', - type: 'boolean', - default: false, - description: 'Listing for if the Open webhook is enabled/disabled.', - }, - { - displayName: 'First Open Only', - name: 'postFirstOpenOnly', - type: 'boolean', - default: false, - displayOptions: { - show: { - open: [ - true - ], + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'Open', + value: 'open', + description: 'Trigger webhook on open.' }, - }, - description: 'Webhook will only post on first open if enabled.', - }, - { - displayName: 'Click', - name: 'click', - type: 'boolean', - default: false, - description: 'Listing for if the Click webhook is enabled/disabled.', - }, - { - displayName: 'Delivery', - name: 'delivery', - type: 'boolean', - default: false, - description: 'Listing for if the Delivery webhook is enabled/disabled.', - }, - { - displayName: 'Bounce', - name: 'bounce', - type: 'boolean', - default: false, - description: 'Listing for if the Bounce webhook is enabled/disabled.', - }, - { - displayName: 'Bounce Include Content', - name: 'bounceIncludeContent', - type: 'boolean', - default: false, - displayOptions: { - show: { - bounce: [ - true - ], + { + name: 'First Open', + value: 'firstOpen', + description: 'Trigger on first open only.' }, - }, - description: 'Webhook will send full bounce content if IncludeContent is enabled.', - }, - { - displayName: 'Spam Complaint', - name: 'spamComplaint', - type: 'boolean', - default: false, - description: 'Listing for if the Spam webhook is enabled/disabled.', - }, - { - displayName: 'Spam Complaint Include Content', - name: 'spamComplaintIncludeContent', - type: 'boolean', - default: false, - displayOptions: { - show: { - spamComplaint: [ - true - ], + { + name: 'Click', + value: 'click', }, - }, - description: 'Webhook will send full spam content if IncludeContent is enabled.', - }, - { - displayName: 'Subscription Change', - name: 'subscriptionChange', - type: 'boolean', - default: false, - description: 'Listing for if the Subscription Change webhook is enabled/disabled.', + { + name: 'Delivery', + value: 'delivery', + }, + { + name: 'Bounce', + value: 'bounce', + }, + { + name: 'Bounce Content', + value: 'bounceContent', + description: 'Webhook will send full bounce content.' + }, + { + name: 'Spam Complaint', + value: 'spamComplaint', + }, + { + name: 'Spam Complaint Content', + value: 'spamComplaintContent', + description: 'Webhook will send full bounce content.' + }, + { + name: 'Subscription Change', + value: 'subscriptionChange', + }, + ], + default: [], + required: true, + description: 'Webhook events that will be enabled for that endpoint.', }, ], @@ -135,23 +103,28 @@ export class PostmarkTrigger implements INodeType { default: { async checkExists(this: IHookFunctions): Promise { const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events') as string[]; - if (webhookData.webhookId === undefined) { - // No webhook id is set so no webhook can exist - return false; - } - - // Webhook got created before so check if it still exists - const endpoint = `/webhooks/${webhookData.webhookId}`; + // Get all webhooks + const endpoint = `/webhooks`; const responseData = await postmarkApiRequest.call(this, 'GET', endpoint, {}); - if (responseData.ID === undefined) { + // No webhooks exist + if (responseData.Webhooks.length === 0) { return false; } - else if (responseData.ID === webhookData.id) { - return true; + + // If webhooks exist, check if any match current settings + for (const webhook of responseData.Webhooks) { + if (webhook.Url === webhookUrl && eventExists(events, convertTriggerObjectToStringArray(webhook))) { + webhookData.webhookId = webhook.ID; + // webhook identical to current settings. re-assign webhook id to found webhook. + return true; + } } + return false; }, async create(this: IHookFunctions): Promise { @@ -187,44 +160,34 @@ export class PostmarkTrigger implements INodeType { } }; - const open = this.getNodeParameter('open', 0); - const postFirstOpenOnly = this.getNodeParameter('postFirstOpenOnly', 0); - const click = this.getNodeParameter('click', 0); - const delivery = this.getNodeParameter('delivery', 0); - const bounce = this.getNodeParameter('bounce', 0); - const bounceIncludeContent = this.getNodeParameter('bounceIncludeContent', 0); - const spamComplaint = this.getNodeParameter('spamComplaint', 0); - const spamComplaintIncludeContent = this.getNodeParameter('spamComplaintIncludeContent', 0); - const subscriptionChange = this.getNodeParameter('subscriptionChange', 0); + const events = this.getNodeParameter('events') as string[]; - if (open) { + if (events.includes('open')) { body.Triggers.Open.Enabled = true; - - if (postFirstOpenOnly) { - body.Triggers.Open.PostFirstOpenOnly = true; - } } - if (click) { + if (events.includes('firstOpen')) { + body.Triggers.Open.Enabled = true; + body.Triggers.Open.PostFirstOpenOnly = true; + } + if (events.includes('click')) { body.Triggers.Click.Enabled = true; } - if (delivery) { + if (events.includes('delivery')) { body.Triggers.Delivery.Enabled = true; } - if (bounce) { + if (events.includes('bounce')) { body.Triggers.Bounce.Enabled = true; - - if (bounceIncludeContent) { - body.Triggers.Bounce.IncludeContent = true; - } } - if (spamComplaint) { + if (events.includes('bounceContent')) { + body.Triggers.Bounce.IncludeContent = true; + } + if (events.includes('spamComplaint')) { body.Triggers.SpamComplaint.Enabled = true; - - if (spamComplaintIncludeContent) { - body.Triggers.SpamComplaint.IncludeContent = true; - } } - if (subscriptionChange) { + if (events.includes('spamComplaintContent')) { + body.Triggers.SpamComplaint.IncludeContent = true; + } + if (events.includes('subscriptionChange')) { body.Triggers.SubscriptionChange.Enabled = true; } From 7c9ecdb1722f5c0f80327cab6b43b91459c235d4 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Fri, 26 Jun 2020 14:56:24 +0200 Subject: [PATCH 04/13] Removed console log --- packages/nodes-base/nodes/Postmark/GenericFunctions.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts index 5844d28d7c..115fac51f0 100644 --- a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts @@ -37,8 +37,6 @@ export async function postmarkApiRequest(this: IExecuteFunctions | IWebhookFunct } options = Object.assign({}, options, option); - console.log(options); - try { return await this.helpers.request!(options); } catch (error) { From 87b67d66701e1e820e24e08513cd917a44c7f867 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Tue, 7 Jul 2020 09:35:15 +0200 Subject: [PATCH 05/13] :white_check_mark: Replaced png and added descriptions to all events --- .../nodes/Postmark/PostmarkTrigger.node.ts | 5 +++++ .../nodes-base/nodes/Postmark/postmark.png | Bin 3301 -> 1297 bytes 2 files changed, 5 insertions(+) diff --git a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts index f6f09afdd4..9cd8f7b01c 100644 --- a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts +++ b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts @@ -62,14 +62,17 @@ export class PostmarkTrigger implements INodeType { { name: 'Click', value: 'click', + description: 'Trigger on click.' }, { name: 'Delivery', value: 'delivery', + description: 'Trigger on delivery.' }, { name: 'Bounce', value: 'bounce', + description: 'Trigger on bounce.' }, { name: 'Bounce Content', @@ -79,6 +82,7 @@ export class PostmarkTrigger implements INodeType { { name: 'Spam Complaint', value: 'spamComplaint', + description: 'Trigger on spam complaint.' }, { name: 'Spam Complaint Content', @@ -88,6 +92,7 @@ export class PostmarkTrigger implements INodeType { { name: 'Subscription Change', value: 'subscriptionChange', + description: 'Trigger on subscription change.' }, ], default: [], diff --git a/packages/nodes-base/nodes/Postmark/postmark.png b/packages/nodes-base/nodes/Postmark/postmark.png index 44f90b2a6c25f66c011e849512dbbbea5f128dfa..6298b4ae94bdbd1b930a5bdd870200fa9f8f157a 100644 GIT binary patch delta 1269 zcmV2c7k>o9NklH0HK@eA>;KxcAA})#x!JQ}wZbd=6&_clvw1|S}LX?VP zYCo(=o3#0Al4&v@@7;S`%rqTaXx==gGnUSOF&~dR^L}&Qx##@veM3Esn@(P{0e_OH z1>GpMpc|zYbfeUQZj@TkYlwi><%<$P6JYrvE(Ena8Xy!MunS^D_jD*dWl}+oHR-F7Mm3 zbno_DTVSD@AZL>0w2}~`rk0DWFn`4o#xHo|Q*Gb@dl z$;F)id52*yrlUwmVDVaX@q9Jg;>dgt|tOlR>5gVHH;%igiEjt>s6`|Wt9QcILU z5FUK#+@rhaVQWb4Bfrge5B3t#GOoI6+mu6GC)nqlZSrk)hJTclqmB#)y)d zFL+;mpWSoMQddQE^;t=U za1B}ntRcPHOsgtX0WvUA@A`0b*YbgVb9as80b8q?#*KhZ1%Ga^uefTOLHM3)4_r1A z4fI7@Hy5_uk>5N}^t=cT$|OtlIzu~$Ed%*CUpbpeSNt>y5qbu`ufAs?4hpzR`(y24 zM%|$mG#jVLbb^AhjxpXwWdO!ht1L|*F-9jC)$!bA2N-gL{sRi9A fwV)fNn-cn8xOMsu?|pBW00000NkvXXu0mjfo}+Cw literal 3301 zcmcInc~lek7LKec0xB+`h%v4RlVq|%0s#vcpcQE>sZ`X+3z=bg6Cf8LxkGryVNcfb4HW$rn% z*Wb_EeCDE=7!1bT*T*XWy}Rnq=_crv@$Qx!z0HvMY*t_}W>)%h8s^w{)) zIUJba7sO47=F)jMcQ>r7nt>8XU?qfAOT zfG%geT(1e+-n(X81}KN=V9ueZ81CR1{t$;xVXP zkVcP$A^~SEOaWjrg$K|fQX~M8xFisUBFT{uZ%h|z?}cz<_36{wkGCxk;i4Q<8Dh{$ zFi0lDAOM0+bb!XC5CMo!rU4WZnH0;AVU;XpA#Phcp#rd1wjx3og;Br9+$yK>G&E(@1nFeWv=UY`UB?>)?j-neA_ zyvvvO?fW_>7q_1iCbpu8idHR>_&zZ`_UV4MX5EciWb3oEoE+;)k$9fxo`T}rrq{A^ z76n8H3g=igdj;ZZ=bG<5dHk^VfT6u?Fx;-f^=i!fq0qSY0DR&bg9?``5P|G+zE>r9 z-9RTRY`9Nc7e>E%dbf3WxFq)B)%nqvzgn{3MIn#R=QIn0d_qFfKQp1ryZ40uoRw9z zroOSUnC0!gn?j{t=)-MFfAG1$=C((MaB09cBMh}`IxCYaZ(hH? ze5Pmif!RMbHu^RDNF-;5JI^`x4Gu1}WSu&F`llB!Gy=_rc7q>t)Ej;Fl-J*Nc<*m6F|9x}f zZgPED$H*&lmP;pJS$O!fjv2=6ndQ`^2x5x4#-QZXsjC4C9P!%qALJJrgbynkzS>Yx z7r0qtk?!zl&v#)Ds2raNqr4@+#+9#%^E@lh{tA{e4a$D;l;)qg zbcvN1TW#sUO!#!|=&z=$s?OhXbzfU)8|M4|=@+AEmJfDnT6rbel@5m_I$m9lVQU@{ zRYK(0+uPr0X~DH7-#6$vdJHeWu;Pg8%iP>|icNWT{k<*PZg)D#D2Sj<4bF(Q3(NcR z^=70h!eFuWeo^kplXY$J&PVNNUif#ta)dk^(%OCrO-u} z(3zai+U^~3_EuGVe0*?hY*9t1Q$ba4$u4W=$jQxvU6=2ryt;2^T^$uBVhVIF#N^7Q z$+f57++2Mxg?b1CsxQ2Das7@e%XkuQ^kTz;j8{py-6K}dcc(_}ZHsL0yO#H1sI_Lh z$CT6`G+wZuIhC2p&-g4_L^ry^eE0E|G>b?(bGFp11m|K zQr^Cy=bE+185F}1MuDLK-3tFG?;khFUf>o>mr_vV_ui*VuZRF-|n^I}eN zk7R$Yut9soBqN%0%&hD1VjJM%&ia8v@|qtX|NhvrGoYel!58XB2AO%wmgG6!KR2|^ zI1yhw?A)`Ana=9xe3M|Njg7Xh7+I?&@3&}g9j@`L5$YZmzN}@i+=CTOk2kQ2=4+C= z+J3mmfB80s#bh6HD|z<)rlxoI5{@11X?d=W>8EUPS-y6=bJN3yC+h?QYR46Aq&aD7 ze4BikvE^P)5oJfJv6bDb+3oY~MvF07A%wCO#2;-}KWk-2*}N$)J#n|=?V>h=?Z^gK ziTvWcO9CH*r4CLhLxma6r=HSR8-DOIC_Ad&8eZG~VCa#9h6cwtee+fqO=fqM#Yg;b z{@doB{Vy^*zk0!_*=wBn+CPm@U$|xDrbDEPHrqxwuXhx*TwGt6M5rw&b&u;pdNA!p zvj_WwHx%4)yP`YTfBpIl+XLl*=KaZozd^W^@G#(_G^gm)aGckd%C>pd`DXt@jt`f9 z+tVAD>ulL#qb+Qzi4%3vY7JH0%Ven>1C@A2YSHVbcQam^UawkXkz@Sy5rnl&H|ZOe wby@5&eYzTx^7f#2SJEY`lBH3`11HoU3?)ewA9`$Vj{S)EuJ`lG_lVf>Z!h~rQvd(} From d74e59801a6e5de7ecd5ec0d2d5c7d14e2bbeed3 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Tue, 7 Jul 2020 10:48:56 +0200 Subject: [PATCH 06/13] Credentials name to Server API Token --- packages/nodes-base/credentials/PostmarkApi.credentials.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/credentials/PostmarkApi.credentials.ts b/packages/nodes-base/credentials/PostmarkApi.credentials.ts index 88df53aa30..b5f73621c4 100644 --- a/packages/nodes-base/credentials/PostmarkApi.credentials.ts +++ b/packages/nodes-base/credentials/PostmarkApi.credentials.ts @@ -9,7 +9,7 @@ export class PostmarkApi implements ICredentialType { displayName = 'Postmark API'; properties = [ { - displayName: 'Server Token', + displayName: 'Server API Token', name: 'serverToken', type: 'string' as NodePropertyTypes, default: '', From 657d5498d6c2e18cd3de942decdf6504e1a326f1 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Tue, 7 Jul 2020 11:44:55 +0200 Subject: [PATCH 07/13] :construction: Incorrect event description fixed --- packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts index 9cd8f7b01c..ed902a1347 100644 --- a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts +++ b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts @@ -87,7 +87,7 @@ export class PostmarkTrigger implements INodeType { { name: 'Spam Complaint Content', value: 'spamComplaintContent', - description: 'Webhook will send full bounce content.' + description: 'Webhook will send full spam complaint content.' }, { name: 'Subscription Change', From 8aff042e04b6e5ee16e26eb5a77349edd629d2d4 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 7 Jul 2020 13:03:53 -0400 Subject: [PATCH 08/13] * :sparkles: Endpoint to preset credentials * :sparkles: Endpoint to preset credentials * :zap: Improvements * :bug: Small fix * :bug: Small fix --- packages/cli/config/index.ts | 26 +++++++++----- packages/cli/src/CredentialsOverwrites.ts | 3 +- packages/cli/src/Server.ts | 43 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index a3c2cfd03a..847587460f 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -128,15 +128,23 @@ const config = convict({ credentials: { overwrite: { - // Allows to set default values for credentials which - // get automatically prefilled and the user does not get - // displayed and can not change. - // Format: { CREDENTIAL_NAME: { PARAMTER: VALUE }} - doc: 'Overwrites for credentials', - format: '*', - default: '{}', - env: 'CREDENTIALS_OVERWRITE' - } + data: { + // Allows to set default values for credentials which + // get automatically prefilled and the user does not get + // displayed and can not change. + // Format: { CREDENTIAL_NAME: { PARAMTER: VALUE }} + doc: 'Overwrites for credentials', + format: '*', + default: '{}', + env: 'CREDENTIALS_OVERWRITE_DATA' + }, + endpoint: { + doc: 'Fetch credentials from API', + format: String, + default: '', + env: 'CREDENTIALS_OVERWRITE_ENDPOINT', + }, + }, }, executions: { diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/CredentialsOverwrites.ts index a6e115100e..ca09b87626 100644 --- a/packages/cli/src/CredentialsOverwrites.ts +++ b/packages/cli/src/CredentialsOverwrites.ts @@ -20,7 +20,7 @@ class CredentialsOverwritesClass { return; } - const data = await GenericHelpers.getConfigValue('credentials.overwrite') as string; + const data = await GenericHelpers.getConfigValue('credentials.overwrite.data') as string; try { this.overwriteData = JSON.parse(data); @@ -30,6 +30,7 @@ class CredentialsOverwritesClass { } applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { + const overwrites = this.get(type); if (overwrites === undefined) { diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 1b3750a20c..2507b6b136 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -58,6 +58,9 @@ import { WorkflowExecuteAdditionalData, WorkflowRunner, GenericHelpers, + CredentialsOverwrites, + ICredentialsOverwrite, + LoadNodesAndCredentials, } from './'; import { @@ -105,6 +108,7 @@ class App { testWebhooks: TestWebhooks.TestWebhooks; endpointWebhook: string; endpointWebhookTest: string; + endpointPresetCredentials: string; externalHooks: IExternalHooksClass; saveDataErrorExecution: string; saveDataSuccessExecution: string; @@ -119,6 +123,8 @@ class App { sslKey: string; sslCert: string; + presetCredentialsLoaded: boolean; + constructor() { this.app = express(); @@ -141,6 +147,9 @@ class App { this.sslCert = config.get('ssl_cert'); this.externalHooks = ExternalHooks(); + + this.presetCredentialsLoaded = false; + this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; } @@ -1650,6 +1659,40 @@ class App { }); + if (this.endpointPresetCredentials !== '') { + + // POST endpoint to set preset credentials + this.app.post(`/${this.endpointPresetCredentials}`, async (req: express.Request, res: express.Response) => { + + if (this.presetCredentialsLoaded === false) { + + const body = req.body as ICredentialsOverwrite; + + if (req.headers['content-type'] !== 'application/json') { + ResponseHelper.sendErrorResponse(res, new Error('Body must be a valid JSON, make sure the content-type is application/json')); + return; + } + + const loadNodesAndCredentials = LoadNodesAndCredentials(); + + const credentialsOverwrites = CredentialsOverwrites(); + + await credentialsOverwrites.init(body); + + const credentialTypes = CredentialTypes(); + + await credentialTypes.init(loadNodesAndCredentials.credentialTypes); + + this.presetCredentialsLoaded = true; + + ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); + + } else { + ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once')); + } + }); + } + // Serve the website const startTime = (new Date()).toUTCString(); const editorUiPath = require.resolve('n8n-editor-ui'); From 97c8af661c6616a999cbfd58f6df00497c6e0dba Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 7 Jul 2020 20:35:58 +0200 Subject: [PATCH 09/13] :zap: Small improvement on PostmarkTrigger-Node --- .../nodes/Postmark/GenericFunctions.ts | 8 +- .../nodes/Postmark/PostmarkTrigger.node.ts | 83 +++++++++++-------- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts index 115fac51f0..df1e3a1f09 100644 --- a/packages/nodes-base/nodes/Postmark/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Postmark/GenericFunctions.ts @@ -66,13 +66,15 @@ export function convertTriggerObjectToStringArray (webhookObject : any) : string webhookEvents.push('bounce'); } if (triggers.Bounce.IncludeContent) { - webhookEvents.push('bounceContent'); + webhookEvents.push('includeContent'); } if (triggers.SpamComplaint.Enabled) { webhookEvents.push('spamComplaint'); } if (triggers.SpamComplaint.IncludeContent) { - webhookEvents.push('spamComplaintContent'); + if (!webhookEvents.includes('IncludeContent')) { + webhookEvents.push('includeContent'); + } } if (triggers.SubscriptionChange.Enabled) { webhookEvents.push('subscriptionChange'); @@ -89,5 +91,3 @@ export function eventExists (currentEvents : string[], webhookEvents: string[]) } return true; } - - diff --git a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts index ed902a1347..4956d647b7 100644 --- a/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts +++ b/packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts @@ -50,55 +50,69 @@ export class PostmarkTrigger implements INodeType { type: 'multiOptions', options: [ { - name: 'Open', - value: 'open', - description: 'Trigger webhook on open.' - }, - { - name: 'First Open', - value: 'firstOpen', - description: 'Trigger on first open only.' + name: 'Bounce', + value: 'bounce', + description: 'Trigger on bounce.', }, { name: 'Click', value: 'click', - description: 'Trigger on click.' + description: 'Trigger on click.', }, { name: 'Delivery', value: 'delivery', - description: 'Trigger on delivery.' + description: 'Trigger on delivery.', }, { - name: 'Bounce', - value: 'bounce', - description: 'Trigger on bounce.' - }, - { - name: 'Bounce Content', - value: 'bounceContent', - description: 'Webhook will send full bounce content.' + name: 'Open', + value: 'open', + description: 'Trigger webhook on open.', }, { name: 'Spam Complaint', value: 'spamComplaint', - description: 'Trigger on spam complaint.' - }, - { - name: 'Spam Complaint Content', - value: 'spamComplaintContent', - description: 'Webhook will send full spam complaint content.' + description: 'Trigger on spam complaint.', }, { name: 'Subscription Change', value: 'subscriptionChange', - description: 'Trigger on subscription change.' + description: 'Trigger on subscription change.', }, ], default: [], required: true, description: 'Webhook events that will be enabled for that endpoint.', }, + { + displayName: 'First Open', + name: 'firstOpen', + description: 'Only fires on first open for event "Open".', + type: 'boolean', + default: false, + displayOptions: { + show: { + events: [ + 'open', + ], + }, + }, + }, + { + displayName: 'Include Content', + name: 'includeContent', + description: 'Includes message content for events "Bounce" and "Spam Complaint".', + type: 'boolean', + default: false, + displayOptions: { + show: { + events: [ + 'bounce', + 'spamComplaint', + ], + }, + }, + }, ], }; @@ -110,6 +124,12 @@ export class PostmarkTrigger implements INodeType { const webhookData = this.getWorkflowStaticData('node'); const webhookUrl = this.getNodeWebhookUrl('default'); const events = this.getNodeParameter('events') as string[]; + if (this.getNodeParameter('includeContent') as boolean) { + events.push('includeContent'); + } + if (this.getNodeParameter('firstOpen') as boolean) { + events.push('firstOpen'); + } // Get all webhooks const endpoint = `/webhooks`; @@ -169,10 +189,7 @@ export class PostmarkTrigger implements INodeType { if (events.includes('open')) { body.Triggers.Open.Enabled = true; - } - if (events.includes('firstOpen')) { - body.Triggers.Open.Enabled = true; - body.Triggers.Open.PostFirstOpenOnly = true; + body.Triggers.Open.PostFirstOpenOnly = this.getNodeParameter('firstOpen') as boolean; } if (events.includes('click')) { body.Triggers.Click.Enabled = true; @@ -182,15 +199,11 @@ export class PostmarkTrigger implements INodeType { } if (events.includes('bounce')) { body.Triggers.Bounce.Enabled = true; - } - if (events.includes('bounceContent')) { - body.Triggers.Bounce.IncludeContent = true; + body.Triggers.Bounce.IncludeContent = this.getNodeParameter('includeContent') as boolean; } if (events.includes('spamComplaint')) { body.Triggers.SpamComplaint.Enabled = true; - } - if (events.includes('spamComplaintContent')) { - body.Triggers.SpamComplaint.IncludeContent = true; + body.Triggers.SpamComplaint.IncludeContent = this.getNodeParameter('includeContent') as boolean; } if (events.includes('subscriptionChange')) { body.Triggers.SubscriptionChange.Enabled = true; From 224842c790fd0f976d8d50adef871ef2e5c4fe83 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jul 2020 09:40:47 +0200 Subject: [PATCH 10/13] :zap: Get rid of mmmagic for mime-type detection --- packages/core/package.json | 5 +-- packages/core/src/NodeExecuteFunctions.ts | 39 +++++++++++++---------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 7a4b0c7349..ac55db99c9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,7 @@ "@types/express": "^4.16.1", "@types/jest": "^24.0.18", "@types/lodash.get": "^4.4.6", - "@types/mmmagic": "^0.4.29", + "@types/mime-types": "^2.1.0", "@types/node": "^10.10.1", "@types/request-promise-native": "^1.0.15", "jest": "^24.9.0", @@ -43,8 +43,9 @@ "client-oauth2": "^4.2.5", "cron": "^1.7.2", "crypto-js": "3.1.9-1", + "file-type": "^14.6.2", "lodash.get": "^4.4.2", - "mmmagic": "^0.5.2", + "mime-types": "^2.1.27", "n8n-workflow": "~0.33.0", "p-cancelable": "^2.0.0", "request": "^2.88.2", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index d500d56f88..5efbe32914 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -44,14 +44,9 @@ import * as express from 'express'; import * as path from 'path'; import { OptionsWithUrl, OptionsWithUri } from 'request'; import * as requestPromise from 'request-promise-native'; - -import { Magic, MAGIC_MIME_TYPE } from 'mmmagic'; - import { createHmac } from 'crypto'; - - -const magic = new Magic(MAGIC_MIME_TYPE); - +import { fromBuffer } from 'file-type'; +import { lookup } from 'mime-types'; /** @@ -66,18 +61,28 @@ const magic = new Magic(MAGIC_MIME_TYPE); */ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise { if (!mimeType) { - // If not mime type is given figure it out - mimeType = await new Promise( - (resolve, reject) => { - magic.detect(binaryData, (err: Error, mimeType: string) => { - if (err) { - return reject(err); - } + // If no mime type is given figure it out - return resolve(mimeType); - }); + if (filePath) { + // Use file path to guess mime type + const mimeTypeLookup = lookup(filePath); + if (mimeTypeLookup) { + mimeType = mimeTypeLookup; } - ); + } + + if (!mimeType) { + // Use buffer to guess mime type + const fileTypeData = await fromBuffer(binaryData); + if (fileTypeData) { + mimeType = fileTypeData.mime; + } + } + + if (!mimeType) { + // Fall back to text + mimeType = 'text/plain'; + } } const returnData: IBinaryData = { From b1035d539dd41ff748f176b77217fc3e0c955ddb Mon Sep 17 00:00:00 2001 From: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com> Date: Wed, 8 Jul 2020 10:00:13 +0200 Subject: [PATCH 11/13] :sparkles: MSSQL Node Integration (#729) * basic setup mssql node * executeQuery for MSSQL working * work on insert MSSQL, incomplete * :construction: basic setup update functionality * :hammer: refactor insert for handling >1000 values * :sparkles: complete MSSQL node * :sparkles: add delete action to node * :construction: handling multiple tables and column sets * :bug: enabling usage of expression for every field * :hammer: remove lodash dependency * :sparkles: enable continue on fail option * :racehorse: minify icon * :hammer: improve table creation, item copying, :bug: correct output of node when active continue on fail * :bug: move mssql types to dev dependencies * :art: remove auto formatting from redis * :art: apply corrected syntax format * :rewind: reset Redis node to master stage * :bug: fix building issue --- .../MicrosoftSqlServer.credentials.ts | 47 ++ .../Microsoft/SqlServer/GenericFunctions.ts | 144 ++++++ .../SqlServer/MicrosoftSqlServer.node.ts | 410 ++++++++++++++++++ .../Microsoft/SqlServer/TableInterface.ts | 7 + .../nodes/Microsoft/SqlServer/mssql.png | Bin 0 -> 3447 bytes packages/nodes-base/nodes/utils/utilities.ts | 57 +++ packages/nodes-base/package.json | 6 +- 7 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts create mode 100644 packages/nodes-base/nodes/Microsoft/SqlServer/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/SqlServer/TableInterface.ts create mode 100644 packages/nodes-base/nodes/Microsoft/SqlServer/mssql.png create mode 100644 packages/nodes-base/nodes/utils/utilities.ts diff --git a/packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts b/packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts new file mode 100644 index 0000000000..b0a14de771 --- /dev/null +++ b/packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts @@ -0,0 +1,47 @@ +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; + +export class MicrosoftSqlServer implements ICredentialType { + name = 'microsoftSqlServer'; + displayName = 'Microsoft SQL Server'; + properties = [ + { + displayName: 'Server', + name: 'server', + type: 'string' as NodePropertyTypes, + default: 'localhost' + }, + { + displayName: 'Database', + name: 'database', + type: 'string' as NodePropertyTypes, + default: 'master' + }, + { + displayName: 'User', + name: 'user', + type: 'string' as NodePropertyTypes, + default: 'sa' + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true + }, + default: '' + }, + { + displayName: 'Port', + name: 'port', + type: 'number' as NodePropertyTypes, + default: 1433 + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string' as NodePropertyTypes, + default: '' + } + ]; +} diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/SqlServer/GenericFunctions.ts new file mode 100644 index 0000000000..8309b35db0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/SqlServer/GenericFunctions.ts @@ -0,0 +1,144 @@ +import { IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { ITables } from './TableInterface'; + +/** + * Returns a copy of the item which only contains the json data and + * of that only the defined properties + * + * @param {INodeExecutionData} item The item to copy + * @param {string[]} properties The properties it should include + * @returns + */ +export function copyInputItem( + item: INodeExecutionData, + properties: string[], +): IDataObject { + // Prepare the data to insert and copy it to be returned + let newItem: IDataObject = {}; + for (const property of properties) { + if (item.json[property] === undefined) { + newItem[property] = null; + } else { + newItem[property] = JSON.parse(JSON.stringify(item.json[property])); + } + } + return newItem; +} + +/** + * Creates an ITables with the columns for the operations + * + * @param {INodeExecutionData[]} items The items to extract the tables/columns for + * @param {function} getNodeParam getter for the Node's Parameters + * @returns {ITables} {tableName: {colNames: [items]}}; + */ +export function createTableStruct( + getNodeParam: Function, + items: INodeExecutionData[], + additionalProperties: string[] = [], + keyName?: string, +): ITables { + return items.reduce((tables, item, index) => { + const table = getNodeParam('table', index) as string; + const columnString = getNodeParam('columns', index) as string; + const columns = columnString.split(',').map(column => column.trim()); + const itemCopy = copyInputItem(item, columns.concat(additionalProperties)); + const keyParam = keyName + ? (getNodeParam(keyName, index) as string) + : undefined; + if (tables[table] === undefined) { + tables[table] = {}; + } + if (tables[table][columnString] === undefined) { + tables[table][columnString] = []; + } + if (keyName) { + itemCopy[keyName] = keyParam; + } + tables[table][columnString].push(itemCopy); + return tables; + }, {} as ITables); +} + +/** + * Executes a queue of queries on given ITables. + * + * @param {ITables} tables The ITables to be processed. + * @param {function} buildQueryQueue function that builds the queue of promises + * @returns {Promise} + */ +export function executeQueryQueue( + tables: ITables, + buildQueryQueue: Function, +): Promise { + return Promise.all( + Object.keys(tables).map(table => { + const columnsResults = Object.keys(tables[table]).map(columnString => { + return Promise.all( + buildQueryQueue({ + table: table, + columnString: columnString, + items: tables[table][columnString], + }), + ); + }); + return Promise.all(columnsResults); + }), + ); +} + +/** + * Extracts the values from the item for INSERT + * + * @param {IDataObject} item The item to extract + * @returns {string} (Val1, Val2, ...) + */ +export function extractValues(item: IDataObject): string { + return `(${Object.values(item as any) + .map(val => (typeof val === 'string' ? `'${val}'` : val)) // maybe other types such as dates have to be handled as well + .join(',')})`; +} + +/** + * Extracts the SET from the item for UPDATE + * + * @param {IDataObject} item The item to extract from + * @param {string[]} columns The columns to update + * @returns {string} col1 = val1, col2 = val2 + */ +export function extractUpdateSet(item: IDataObject, columns: string[]): string { + return columns + .map( + column => + `${column} = ${ + typeof item[column] === 'string' ? `'${item[column]}'` : item[column] + }`, + ) + .join(','); +} + +/** + * Extracts the WHERE condition from the item for UPDATE + * + * @param {IDataObject} item The item to extract from + * @param {string} key The column name to build the condition with + * @returns {string} id = '123' + */ +export function extractUpdateCondition(item: IDataObject, key: string): string { + return `${key} = ${ + typeof item[key] === 'string' ? `'${item[key]}'` : item[key] + }`; +} + +/** + * Extracts the WHERE condition from the items for DELETE + * + * @param {IDataObject[]} items The items to extract the values from + * @param {string} key The column name to extract the value from for the delete condition + * @returns {string} (Val1, Val2, ...) + */ +export function extractDeleteValues(items: IDataObject[], key: string): string { + return `(${items + .map(item => (typeof item[key] === 'string' ? `'${item[key]}'` : item[key])) + .join(',')})`; +} diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts b/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts new file mode 100644 index 0000000000..b24d93b0f5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts @@ -0,0 +1,410 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { chunk, flatten } from '../../utils/utilities'; + +import * as mssql from 'mssql'; + +import { ITables } from './TableInterface'; + +import { + copyInputItem, + createTableStruct, + executeQueryQueue, + extractDeleteValues, + extractUpdateCondition, + extractUpdateSet, + extractValues, +} from './GenericFunctions'; + +export class MicrosoftSqlServer implements INodeType { + description: INodeTypeDescription = { + displayName: 'Microsoft SQL Server', + name: 'microsoftSqlServer', + icon: 'file:mssql.png', + group: ['input'], + version: 1, + description: 'Gets, add and update data in Microsoft SQL Server.', + defaults: { + name: 'Microsoft SQL Server', + color: '#1d4bab', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftSqlServer', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Execute Query', + value: 'executeQuery', + description: 'Executes a SQL query.', + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert rows in database.', + }, + { + name: 'Update', + value: 'update', + description: 'Updates rows in database.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Deletes rows in database.', + }, + ], + default: 'insert', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // executeQuery + // ---------------------------------- + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + rows: 5, + }, + displayOptions: { + show: { + operation: ['executeQuery'], + }, + }, + default: '', + placeholder: 'SELECT id, name FROM product WHERE id < 40', + required: true, + description: 'The SQL query to execute.', + }, + + // ---------------------------------- + // insert + // ---------------------------------- + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: ['insert'], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to insert data to.', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + operation: ['insert'], + }, + }, + default: '', + placeholder: 'id,name,description', + description: + 'Comma separated list of the properties which should used as columns for the new rows.', + }, + + // ---------------------------------- + // update + // ---------------------------------- + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: ['update'], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to update data in', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + operation: ['update'], + }, + }, + default: 'id', + required: true, + description: + 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + operation: ['update'], + }, + }, + default: '', + placeholder: 'name,description', + description: + 'Comma separated list of the properties which should used as columns for rows to update.', + }, + + // ---------------------------------- + // delete + // ---------------------------------- + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: ['delete'], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to delete data.', + }, + { + displayName: 'Delete Key', + name: 'deleteKey', + type: 'string', + displayOptions: { + show: { + operation: ['delete'], + }, + }, + default: 'id', + required: true, + description: + 'Name of the property which decides which rows in the database should be deleted. Normally that would be "id".', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials('microsoftSqlServer'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const config = { + server: credentials.server as string, + port: credentials.port as number, + database: credentials.database as string, + user: credentials.user as string, + password: credentials.password as string, + domain: credentials.domain ? (credentials.domain as string) : undefined, + }; + + const pool = new mssql.ConnectionPool(config); + await pool.connect(); + + let returnItems: any = []; + + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0) as string; + + try { + if (operation === 'executeQuery') { + // ---------------------------------- + // executeQuery + // ---------------------------------- + + const rawQuery = this.getNodeParameter('query', 0) as string; + + const queryResult = await pool.request().query(rawQuery); + + const result = + queryResult.recordsets.length > 1 + ? flatten(queryResult.recordsets) + : queryResult.recordsets[0]; + + returnItems = this.helpers.returnJsonArray(result as IDataObject[]); + } else if (operation === 'insert') { + // ---------------------------------- + // insert + // ---------------------------------- + + const tables = createTableStruct(this.getNodeParameter, items); + const queriesResults = await executeQueryQueue( + tables, + ({ + table, + columnString, + items, + }: { + table: string; + columnString: string; + items: IDataObject[]; + }): Promise[] => { + return chunk(items, 1000).map(insertValues => { + const values = insertValues + .map((item: IDataObject) => extractValues(item)) + .join(','); + + return pool + .request() + .query( + `INSERT INTO ${table}(${columnString}) VALUES ${values};`, + ); + }); + }, + ); + + const rowsAffected = flatten(queriesResults).reduce( + (acc: number, resp: mssql.IResult): number => + (acc += resp.rowsAffected.reduce((sum, val) => (sum += val))), + 0, + ); + + returnItems = this.helpers.returnJsonArray({ + rowsAffected, + } as IDataObject); + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + const updateKeys = items.map( + (item, index) => this.getNodeParameter('updateKey', index) as string, + ); + const tables = createTableStruct( + this.getNodeParameter, + items, + ['updateKey'], + 'updateKey', + ); + const queriesResults = await executeQueryQueue( + tables, + ({ + table, + columnString, + items, + }: { + table: string; + columnString: string; + items: IDataObject[]; + }): Promise[] => { + return items.map(item => { + const columns = columnString + .split(',') + .map(column => column.trim()); + + const setValues = extractUpdateSet(item, columns); + const condition = extractUpdateCondition( + item, + item.updateKey as string, + ); + + return pool + .request() + .query(`UPDATE ${table} SET ${setValues} WHERE ${condition};`); + }); + }, + ); + + const rowsAffected = flatten(queriesResults).reduce( + (acc: number, resp: mssql.IResult): number => + (acc += resp.rowsAffected.reduce((sum, val) => (sum += val))), + 0, + ); + + returnItems = this.helpers.returnJsonArray({ + rowsAffected, + } as IDataObject); + } else if (operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + const tables = items.reduce((tables, item, index) => { + const table = this.getNodeParameter('table', index) as string; + const deleteKey = this.getNodeParameter('deleteKey', index) as string; + if (tables[table] === undefined) { + tables[table] = {}; + } + if (tables[table][deleteKey] === undefined) { + tables[table][deleteKey] = []; + } + tables[table][deleteKey].push(item); + return tables; + }, {} as ITables); + + const queriesResults = await Promise.all( + Object.keys(tables).map(table => { + const deleteKeyResults = Object.keys(tables[table]).map( + deleteKey => { + const deleteItemsList = chunk( + tables[table][deleteKey].map(item => + copyInputItem(item as INodeExecutionData, [deleteKey]), + ), + 1000, + ); + const queryQueue = deleteItemsList.map(deleteValues => { + return pool + .request() + .query( + `DELETE FROM ${table} WHERE ${deleteKey} IN ${extractDeleteValues( + deleteValues, + deleteKey, + )};`, + ); + }); + return Promise.all(queryQueue); + }, + ); + return Promise.all(deleteKeyResults); + }), + ); + + const rowsAffected = flatten(queriesResults).reduce( + (acc: number, resp: mssql.IResult): number => + (acc += resp.rowsAffected.reduce((sum, val) => (sum += val))), + 0, + ); + + returnItems = this.helpers.returnJsonArray({ + rowsAffected, + } as IDataObject); + } else { + await pool.close(); + throw new Error(`The operation "${operation}" is not supported!`); + } + } catch (err) { + if (this.continueOnFail() === true) { + returnItems = items; + } else { + await pool.close(); + throw err; + } + } + + // Close the connection + await pool.close(); + + return this.prepareOutputData(returnItems); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/TableInterface.ts b/packages/nodes-base/nodes/Microsoft/SqlServer/TableInterface.ts new file mode 100644 index 0000000000..a0343e6e0b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/SqlServer/TableInterface.ts @@ -0,0 +1,7 @@ +import { IDataObject } from 'n8n-workflow'; + +export interface ITables { + [key: string]: { + [key: string]: Array; + }; +} diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/mssql.png b/packages/nodes-base/nodes/Microsoft/SqlServer/mssql.png new file mode 100644 index 0000000000000000000000000000000000000000..18349dc1cc58adfd3e724c264f96d2e3546b4474 GIT binary patch literal 3447 zcmXAr2{=^!7sn@r$oeWG(O_0%r?M0wyp|?gWhq-oB8*UEDMAPl!w3;&UxyiFH)fcz zjD6q6zDtVef8XbSpXc8DyXQH-b3W&M&+mC|6xu|Oi$j*)jNm=-$9I_V)uWKQbY4lDieOXER!>7XfRtn&pTT+Y7Et{5w4h^(|x)6!Nvau9e(FXzm0o({hNO{$UCXGNT|p zE(0fmB@;gt)WhJ5ODl9FtQ`vJLBbZ~5v1;Z0Dz8!0-lSD%f<~!aC(Ez+y@k4oq69m;_KMFg!q8Xf7_m zfRQXLr`%ko?ic|+!&(|6A;FaauX?Sm2q_s2D4F@#cf|%KWtsVmulYrm){A*x3jV#F zLhiP7H6iV zxVt#m+ub)cP*+k!p8VY2+L$Mm7UyN+(vscp-?*-f5Q4R?PX z%Oy0aAFc2AFK49wYuGn%bk;p~Fb4+{zk}cZuT!g}qp65PG0*gaOuwn1t)Lr?K0Sn; zYdk*&%*~sE-6i)IgQ%Tvj*?m2@MG&`z}c`l+lyVm5m6b=30 zTb1~~WAwKckFI=F5vHi76Q3I}mc==d!WSCOjj$j|Yl_Riuzfkl$0jA#4?j3LKmJnT zlWU6Ia~j(bRT@JD^BZIQgZm?6MdqKL7rhgv7g(&q9`|5@TrMficiQsl&bzsna4q#8 zvy~EjAWjK*f_s3+-T5f=Bgs{sE^NS&?XPizBLfg$tk+~dxa`8W&jRxKuaY(?$+u-2 zXB%U!J)=Op^;I{}(N`xzXYC5t5;g)?`ny=`a@Mz0L|7<%zM>(7>&o;$VegNev|Ar_ zFZE1qx0#12v?;}1VYXuV=zmc$g*;G>k1IyhcQJe-IrueNf;R5FYcJ+1&o>)YuWK_S z42eb%()VvF+~55N&NU@9B>)&ihDj{4@E%6x7d<`-XbLk$?&m~?%%L^b zBP3)TycMDu#9H=)?(;XDIF#GdXom4dQ|>*@yNC91%r8R0h0vMkXr3Q56BizhOl@_J z1Xc?fQPl{ehO?uoUl;?1+D6xuiiF>HcwO(Eo)8#XN1=IkmLbfqfD1 zTQ1s-!G29VCG{pa-_6&sM@wGGURx^vr})LgbF=F!tQd~>qvQog9xB@}{=dd&9J*pZ zF=>z1$FRx%l+iQW4X>ncgon#{wVjDbd~S|zwk-I8T4i~wec0;euySlaQ+P+YJtVz_-2jKa=_ae1IdgCjM|ge2pP^Y z=ZCXo9B0BWbkNh^i-%LhYu5^IP5PxafoIZS*Izh@X|LTARArm$4TndrpW+AaJflUt zOWFMT_OW--1a|X+=K7F*Y;o|sbA*GQS z=X@c7YHX)^?UAQUf*@=9NQP%m(!y?Zfl!DK$8O{W!C43A&L3y^kQK@N!fqdo6R|(D zG_GVM>${Y1sI)&{t`Gbhl|A#kr$@$>UH*a|s15Z~ux8}kDrQ+AuS_D(AK_o&=r54E zcbO**Q!{e!$*hl>3Z`mnbuj4M9AD5=(chc%XF?LQQ^xs@)&~W9n3tnUQkIe~M{mdT zTbo3_WDlhj+B)0n2CqN<_miias@SQ8bhAYylTr=iVHx8ci6A0V$d%1>u4e;n6HI5; z=A}>V3Cx6#fAk-ZJ4n82OZ+30C&oh4tvh0!TJapS4$W~{+@3}HYTz<5zq^&Nx9fNC zfa1XqVIa)KDIUE0^su(!%h)#VVZIIpN<2X}tIKbJnE+Cp!q@awTuafveLjNQA#+N5#&6et(1MDu2bp4~e1{Vk7hY4i5==WDyG z6YsvtYw9|=uX#?~FNr?1FmZBqki*JZyCi06CFER>6b8F#E(Ht1~Rx^4WMGb|0#^H38T;h3pTwSE2R@20ci z&PoQNUrFuX9F=4GW1;7bt47N%3)wm?SYI?(X4V%vz$D%H9XRwr^E@{8-t~cM`4b)N zOmhSgEpW|=vj4z8z&K5z=1-0Zo9^u(0SrFS^*2F+DYP(_Bk8^M0Eo8<}SPn=!Pb~<10RfW2VQMKrK z85h6Ms^`pq^RQZVfcFo#kng$Z@^#6oV+0y~`%tyi-yq&K<(W>DA#> zHYT0e9fPxsNt086qIm7_Ger!cyTd_Xmc2R;d)NcJPk3ojHm=VGA4sw<3b2QRrx^?p zsuJ&Lm5RzGrf0;W^Wv2!xjb-JH_M5z)5>}$=XGvEJ@lSA8^>5(V&xDKf>`@kS$i5& zD3oZ$oa3OUOFH!;9074@!m4Jjm8Or#8bt4?gyCG*#FFd8u)ZAQR=|rp|WC0_#nORXgD`&Ie7RrxGNE zJac@lQUhc0{kBWblkpoiR;=4K`d{T`#eHdZROnbLcj9#7P_SG3>K6&Y*rc_EFB;HW zC}&GC(}SS3M2CuQ9hQ$@9Jw#4d!cFkJ_yW9OCf`f#^(``4ZJE&X->Mk{nlMBf?;V| z0D9pc3v=_GF@2?pRmX;*;q|+3zjN3ez)h)KseG0GSAAUlkPNu~;Ez&;fPjZwtP2c9oDnrz% SQGp*wkb$m=PRUKXu>S$oUE5&* literal 0 HcmV?d00001 diff --git a/packages/nodes-base/nodes/utils/utilities.ts b/packages/nodes-base/nodes/utils/utilities.ts new file mode 100644 index 0000000000..73105a901c --- /dev/null +++ b/packages/nodes-base/nodes/utils/utilities.ts @@ -0,0 +1,57 @@ +/** + * Creates an array of elements split into groups the length of `size`. + * If `array` can't be split evenly, the final chunk will be the remaining + * elements. + * + * @param {Array} array The array to process. + * @param {number} [size=1] The length of each chunk + * @returns {Array} Returns the new array of chunks. + * @example + * + * chunk(['a', 'b', 'c', 'd'], 2) + * // => [['a', 'b'], ['c', 'd']] + * + * chunk(['a', 'b', 'c', 'd'], 3) + * // => [['a', 'b', 'c'], ['d']] + */ +export function chunk(array: any[], size: number = 1) { + const length = array == null ? 0 : array.length; + if (!length || size < 1) { + return []; + } + let index = 0; + let resIndex = 0; + const result = new Array(Math.ceil(length / size)); + + while (index < length) { + result[resIndex++] = array.slice(index, (index += size)); + } + return result; +} + +/** + * Takes a multidimensional array and converts it to a one-dimensional array. + * + * @param {Array} nestedArray The array to be flattened. + * @returns {Array} Returns the new flattened array. + * @example + * + * flatten([['a', 'b'], ['c', 'd']]) + * // => ['a', 'b', 'c', 'd'] + * + */ +export function flatten(nestedArray: any[][]) { + const result = []; + + (function loop(array: any[]) { + for (var i = 0; i < array.length; i++) { + if (Array.isArray(array[i])) { + loop(array[i]); + } else { + result.push(array[i]); + } + } + })(nestedArray); + + return result; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 35700feb0a..1d196f3eef 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -96,6 +96,7 @@ "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", + "dist/credentials/MicrosoftSqlServer.credentials.js", "dist/credentials/MoceanApi.credentials.js", "dist/credentials/MondayComApi.credentials.js", "dist/credentials/MongoDb.credentials.js", @@ -207,7 +208,7 @@ "dist/nodes/Google/Calendar/GoogleCalendar.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", - "dist/nodes/Google/Task/GoogleTasks.node.js", + "dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/Harvest/Harvest.node.js", @@ -241,6 +242,7 @@ "dist/nodes/MessageBird/MessageBird.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", + "dist/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.js", "dist/nodes/MoveBinaryData.node.js", "dist/nodes/Mocean/Mocean.node.js", "dist/nodes/MondayCom/MondayCom.node.js", @@ -322,6 +324,7 @@ "@types/lodash.set": "^4.3.6", "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.5.4", + "@types/mssql": "^6.0.2", "@types/node": "^10.10.1", "@types/nodemailer": "^6.4.0", "@types/redis": "^2.8.11", @@ -354,6 +357,7 @@ "moment": "2.24.0", "moment-timezone": "^0.5.28", "mongodb": "^3.5.5", + "mssql": "^6.2.0", "mysql2": "^2.0.1", "n8n-core": "~0.37.0", "nodemailer": "^6.4.6", From 6d0210e8f3db665d6642360c04d384d4635d5fea Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jul 2020 10:08:00 +0200 Subject: [PATCH 12/13] :shirt: Fix some lint issues --- .../nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts b/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts index b24d93b0f5..4f6c2342a4 100644 --- a/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts +++ b/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts @@ -222,7 +222,7 @@ export class MicrosoftSqlServer implements INodeType { const pool = new mssql.ConnectionPool(config); await pool.connect(); - let returnItems: any = []; + let returnItems: INodeExecutionData[] = []; const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0) as string; @@ -259,7 +259,7 @@ export class MicrosoftSqlServer implements INodeType { table: string; columnString: string; items: IDataObject[]; - }): Promise[] => { + }): Array> => { return chunk(items, 1000).map(insertValues => { const values = insertValues .map((item: IDataObject) => extractValues(item)) @@ -307,7 +307,7 @@ export class MicrosoftSqlServer implements INodeType { table: string; columnString: string; items: IDataObject[]; - }): Promise[] => { + }): Array> => { return items.map(item => { const columns = columnString .split(',') From b598036b3d014b7ee0dac1d5e937ccead3d8f481 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jul 2020 10:08:00 +0200 Subject: [PATCH 13/13] :shirt: Fix some lint issues --- ...entials.ts => MicrosoftSql.credentials.ts} | 6 +++--- .../{SqlServer => Sql}/GenericFunctions.ts | 0 .../MicrosoftSql.node.ts} | 20 +++++++++--------- .../{SqlServer => Sql}/TableInterface.ts | 0 .../Microsoft/{SqlServer => Sql}/mssql.png | Bin packages/nodes-base/package.json | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) rename packages/nodes-base/credentials/{MicrosoftSqlServer.credentials.ts => MicrosoftSql.credentials.ts} (86%) rename packages/nodes-base/nodes/Microsoft/{SqlServer => Sql}/GenericFunctions.ts (100%) rename packages/nodes-base/nodes/Microsoft/{SqlServer/MicrosoftSqlServer.node.ts => Sql/MicrosoftSql.node.ts} (95%) rename packages/nodes-base/nodes/Microsoft/{SqlServer => Sql}/TableInterface.ts (100%) rename packages/nodes-base/nodes/Microsoft/{SqlServer => Sql}/mssql.png (100%) diff --git a/packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts similarity index 86% rename from packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts rename to packages/nodes-base/credentials/MicrosoftSql.credentials.ts index b0a14de771..812e9bfdd7 100644 --- a/packages/nodes-base/credentials/MicrosoftSqlServer.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts @@ -1,8 +1,8 @@ import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; -export class MicrosoftSqlServer implements ICredentialType { - name = 'microsoftSqlServer'; - displayName = 'Microsoft SQL Server'; +export class MicrosoftSql implements ICredentialType { + name = 'microsoftSql'; + displayName = 'Microsoft SQL'; properties = [ { displayName: 'Server', diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Sql/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/SqlServer/GenericFunctions.ts rename to packages/nodes-base/nodes/Microsoft/Sql/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts similarity index 95% rename from packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts rename to packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index b24d93b0f5..1d036147f6 100644 --- a/packages/nodes-base/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -22,23 +22,23 @@ import { extractValues, } from './GenericFunctions'; -export class MicrosoftSqlServer implements INodeType { +export class MicrosoftSql implements INodeType { description: INodeTypeDescription = { - displayName: 'Microsoft SQL Server', - name: 'microsoftSqlServer', + displayName: 'Microsoft SQL', + name: 'microsoftSql', icon: 'file:mssql.png', group: ['input'], version: 1, - description: 'Gets, add and update data in Microsoft SQL Server.', + description: 'Gets, add and update data in Microsoft SQL.', defaults: { - name: 'Microsoft SQL Server', + name: 'Microsoft SQL', color: '#1d4bab', }, inputs: ['main'], outputs: ['main'], credentials: [ { - name: 'microsoftSqlServer', + name: 'microsoftSql', required: true, }, ], @@ -204,7 +204,7 @@ export class MicrosoftSqlServer implements INodeType { }; async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials('microsoftSqlServer'); + const credentials = this.getCredentials('microsoftSql'); if (credentials === undefined) { throw new Error('No credentials got returned!'); @@ -222,7 +222,7 @@ export class MicrosoftSqlServer implements INodeType { const pool = new mssql.ConnectionPool(config); await pool.connect(); - let returnItems: any = []; + let returnItems: INodeExecutionData[] = []; const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0) as string; @@ -259,7 +259,7 @@ export class MicrosoftSqlServer implements INodeType { table: string; columnString: string; items: IDataObject[]; - }): Promise[] => { + }): Array> => { return chunk(items, 1000).map(insertValues => { const values = insertValues .map((item: IDataObject) => extractValues(item)) @@ -307,7 +307,7 @@ export class MicrosoftSqlServer implements INodeType { table: string; columnString: string; items: IDataObject[]; - }): Promise[] => { + }): Array> => { return items.map(item => { const columns = columnString .split(',') diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/TableInterface.ts b/packages/nodes-base/nodes/Microsoft/Sql/TableInterface.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/SqlServer/TableInterface.ts rename to packages/nodes-base/nodes/Microsoft/Sql/TableInterface.ts diff --git a/packages/nodes-base/nodes/Microsoft/SqlServer/mssql.png b/packages/nodes-base/nodes/Microsoft/Sql/mssql.png similarity index 100% rename from packages/nodes-base/nodes/Microsoft/SqlServer/mssql.png rename to packages/nodes-base/nodes/Microsoft/Sql/mssql.png diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1d196f3eef..3c51466ea3 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -96,7 +96,7 @@ "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", "dist/credentials/MicrosoftOAuth2Api.credentials.js", "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", - "dist/credentials/MicrosoftSqlServer.credentials.js", + "dist/credentials/MicrosoftSql.credentials.js", "dist/credentials/MoceanApi.credentials.js", "dist/credentials/MondayComApi.credentials.js", "dist/credentials/MongoDb.credentials.js", @@ -242,7 +242,7 @@ "dist/nodes/MessageBird/MessageBird.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", - "dist/nodes/Microsoft/SqlServer/MicrosoftSqlServer.node.js", + "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js", "dist/nodes/MoveBinaryData.node.js", "dist/nodes/Mocean/Mocean.node.js", "dist/nodes/MondayCom/MondayCom.node.js",