From 83eaaf1e33663f9b110bed03e2008449e7230e21 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 17 Dec 2019 15:33:28 -0500 Subject: [PATCH 01/13] :sparkles: Vero node done --- .../credentials/VeroApi.credentials.ts | 18 + .../nodes-base/nodes/Vero/EventDescripion.ts | 245 ++++++++++++ .../nodes-base/nodes/Vero/GenericFunctions.ts | 50 +++ .../nodes-base/nodes/Vero/UserDescription.ts | 376 ++++++++++++++++++ packages/nodes-base/nodes/Vero/Vero.node.ts | 232 +++++++++++ packages/nodes-base/nodes/Vero/vero.png | Bin 0 -> 4069 bytes packages/nodes-base/package.json | 4 +- 7 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/VeroApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Vero/EventDescripion.ts create mode 100644 packages/nodes-base/nodes/Vero/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Vero/UserDescription.ts create mode 100644 packages/nodes-base/nodes/Vero/Vero.node.ts create mode 100644 packages/nodes-base/nodes/Vero/vero.png diff --git a/packages/nodes-base/credentials/VeroApi.credentials.ts b/packages/nodes-base/credentials/VeroApi.credentials.ts new file mode 100644 index 0000000000..340224fc44 --- /dev/null +++ b/packages/nodes-base/credentials/VeroApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class VeroApi implements ICredentialType { + name = 'veroApi'; + displayName = 'Vero API'; + properties = [ + { + displayName: 'Auth Token', + name: 'authToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Vero/EventDescripion.ts b/packages/nodes-base/nodes/Vero/EventDescripion.ts new file mode 100644 index 0000000000..8df87fd072 --- /dev/null +++ b/packages/nodes-base/nodes/Vero/EventDescripion.ts @@ -0,0 +1,245 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Track', + value: 'track', + description: `This endpoint tracks an event for a specific customer. + If the customer profile doesn’t exist, Vero will create it.`, + }, + ], + default: 'track', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + +/* -------------------------------------------------------------------------- */ +/* event:track */ +/* -------------------------------------------------------------------------- */ + + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ] + }, + }, + description: 'The unique identifier of the customer', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ] + }, + }, + description: 'Email', + }, + { + displayName: 'Event Name', + name: 'eventName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ] + }, + }, + description: 'The name of the event tracked.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ] + }, + } + }, + { + displayName: 'Data', + name: 'dataAttributesUi', + placeholder: 'Add Data', + description: 'key value pairs that represent any properties you want to track with this event', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ], + jsonParameters: [ + false + ], + }, + }, + options: [ + { + name: 'dataAttributesValues', + displayName: 'Data', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Name of the property to set.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property to set.', + }, + ] + }, + ], + }, + { + displayName: 'Extra', + name: 'extraAttributesUi', + placeholder: 'Add Extra', + description: 'Key value pairs that represent reserved, Vero-specific operators. Refer to the note on “deduplication” below.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ], + jsonParameters: [ + false + ], + }, + }, + options: [ + { + name: 'extraAttributesValues', + displayName: 'Extra', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Name of the property to set.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property to set.', + }, + ] + }, + ], + }, + { + displayName: 'Data', + name: 'dataAttributesJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'key value pairs that represent the custom user properties you want to update', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ], + jsonParameters: [ + true, + ], + }, + }, + }, + { + displayName: 'Extra', + name: 'extraAttributesJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Key value pairs that represent reserved, Vero-specific operators. Refer to the note on “deduplication” below.', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'track', + ], + jsonParameters: [ + true, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Vero/GenericFunctions.ts b/packages/nodes-base/nodes/Vero/GenericFunctions.ts new file mode 100644 index 0000000000..2e5053fb7e --- /dev/null +++ b/packages/nodes-base/nodes/Vero/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function veroApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('veroApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + method, + qs, + body, + form: { + auth_token: credentials.authToken, + ...body, + }, + uri: uri ||`https://api.getvero.com/api/v2${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error.message; + if (error.response.body) { + errorMessage = error.response.body.message || error.response.body.Message || error.message; + } + + throw new Error(errorMessage); + } +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Vero/UserDescription.ts b/packages/nodes-base/nodes/Vero/UserDescription.ts new file mode 100644 index 0000000000..f90cb49a8c --- /dev/null +++ b/packages/nodes-base/nodes/Vero/UserDescription.ts @@ -0,0 +1,376 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create/Update', + value: 'create', + description: `Creates a new user profile if the user doesn’t exist yet. + Otherwise, the user profile is updated based on the properties provided.`, + }, + { + name: 'Alias', + value: 'alias', + description: 'Changes a user’s identifier.', + }, + { + name: 'Unsubscribe', + value: 'unsubscribe', + description: 'Unsubscribes a single user.', + }, + { + name: 'Re-subscribe', + value: 'resubscribe', + description: 'Resubscribe a single user.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a single user.', + }, + { + name: 'Add Tags', + value: 'addTags', + description: 'Adds a tag to a user’s profile.', + }, + { + name: 'Remove Tags', + value: 'removeTags', + description: 'Removes a tag from a user’s profile.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ + +/* -------------------------------------------------------------------------- */ +/* user:create */ +/* -------------------------------------------------------------------------- */ + + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ] + }, + }, + description: 'The unique identifier of the customer', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ] + }, + } + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'The table to create the row in.', + }, + ] + }, + { + displayName: 'Data', + name: 'dataAttributesUi', + placeholder: 'Add Data', + description: 'key value pairs that represent the custom user properties you want to update', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false + ], + }, + }, + options: [ + { + name: 'dataAttributesValues', + displayName: 'Data', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Name of the property to set.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property to set.', + }, + ] + }, + ], + }, + { + displayName: 'Data', + name: 'dataAttributesJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'key value pairs that represent the custom user properties you want to update', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + }, + +/* -------------------------------------------------------------------------- */ +/* user:alias */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'alias', + ] + }, + }, + description: 'The old unique identifier of the user', + }, + { + displayName: 'New ID', + name: 'newId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'alias', + ] + }, + }, + description: 'The new unique identifier of the user', + }, +/* -------------------------------------------------------------------------- */ +/* user:unsubscribe */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'unsubscribe', + ] + }, + }, + description: 'The unique identifier of the user', + }, +/* -------------------------------------------------------------------------- */ +/* user:resubscribe */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'resubscribe', + ] + }, + }, + description: 'The unique identifier of the user', + }, +/* -------------------------------------------------------------------------- */ +/* user:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'delete', + ] + }, + }, + description: 'The unique identifier of the user', + }, +/* -------------------------------------------------------------------------- */ +/* user:addTags */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'addTags', + ] + }, + }, + description: 'The unique identifier of the user', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'addTags', + ] + }, + }, + description: 'Tags to add separated by ,', + }, +/* -------------------------------------------------------------------------- */ +/* user:removeTags */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'removeTags', + ] + }, + }, + description: 'The unique identifier of the user', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'removeTags', + ] + }, + }, + description: 'Tags to remove separated by ,', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Vero/Vero.node.ts b/packages/nodes-base/nodes/Vero/Vero.node.ts new file mode 100644 index 0000000000..d17fbef375 --- /dev/null +++ b/packages/nodes-base/nodes/Vero/Vero.node.ts @@ -0,0 +1,232 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; +import { + veroApiRequest, + validateJSON, +} from './GenericFunctions'; +import { + userOperations, + userFields, +} from './UserDescription'; +import { + eventOperations, + eventFields +} from './EventDescripion'; + +export class Vero implements INodeType { + description: INodeTypeDescription = { + displayName: 'Vero', + name: 'Vero', + icon: 'file:vero.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Vero API', + defaults: { + name: 'Vero', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'veroApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'User', + value: 'user', + description: `Lets you create, update and manage the subscription status of your users.`, + }, + { + name: 'Event', + value: 'event', + description: `Lets you track events based on actions your customers take in real time.`, + }, + ], + default: 'user', + description: 'Resource to consume.', + }, + ...userOperations, + ...eventOperations, + ...userFields, + ...eventFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + //https://developers.getvero.com/?bash#users + if (resource === 'user') { + //https://developers.getvero.com/?bash#users-identify + if (operation === 'create') { + const id = this.getNodeParameter('id', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean; + const body = { + id, + }; + if (additionalFields.email) { + // @ts-ignore + body.email = additionalFields.email as string; + } + if (!jsonActive) { + const dataAttributesValues = (this.getNodeParameter('dataAttributesUi', i) as IDataObject).dataAttributesValues as IDataObject[]; + if (dataAttributesValues) { + const dataAttributes = {}; + for (let i = 0; i < dataAttributesValues.length; i++) { + // @ts-ignore + dataAttributes[dataAttributesValues[i].key] = dataAttributesValues[i].value; + // @ts-ignore + body.data = dataAttributes; + } + } + } else { + const dataAttributesJson = validateJSON(this.getNodeParameter('dataAttributesJson', i) as string); + if (dataAttributesJson) { + // @ts-ignore + body.data = dataAttributesJson; + } + } + try { + responseData = await veroApiRequest.call(this, 'POST', '/users/track', body); + } catch (err) { + throw new Error(`Vero Error: ${err}`); + } + } + //https://developers.getvero.com/?bash#users-alias + if (operation === 'alias') { + const id = this.getNodeParameter('id', i) as string; + const newId = this.getNodeParameter('newId', i) as string; + const body = { + id, + new_id: newId, + }; + try { + responseData = await veroApiRequest.call(this, 'PUT', '/users/reidentify', body); + } catch (err) { + throw new Error(`Vero Error: ${err}`); + } + } + //https://developers.getvero.com/?bash#users-unsubscribe + //https://developers.getvero.com/?bash#users-resubscribe + //https://developers.getvero.com/?bash#users-delete + if (operation === 'unsubscribe' || + operation === 'resubscribe' || + operation === 'delete') { + const id = this.getNodeParameter('id', i) as string; + const body = { + id, + }; + try { + responseData = await veroApiRequest.call(this, 'POST', `/users/${operation}`, body); + } catch (err) { + throw new Error(`Vero Error: ${err}`); + } + } + //https://developers.getvero.com/?bash#tags-add + //https://developers.getvero.com/?bash#tags-remove + if (operation === 'addTags' || + operation === 'removeTags') { + const id = this.getNodeParameter('id', i) as string; + const tags = (this.getNodeParameter('tags', i) as string).split(',') as string[]; + const body = { + id, + }; + if (operation === 'addTags') { + // @ts-ignore + body.add = JSON.stringify(tags); + } + if (operation === 'removeTags') { + // @ts-ignore + body.remove = JSON.stringify(tags); + } + try { + responseData = await veroApiRequest.call(this, 'PUT', '/users/tags/edit', body); + } catch (err) { + throw new Error(`Vero Error: ${err}`); + } + } + } + //https://developers.getvero.com/?bash#events + if (resource === 'event') { + //https://developers.getvero.com/?bash#events-track + if (operation === 'track') { + const id = this.getNodeParameter('id', i) as string; + const email = this.getNodeParameter('email', i) as string; + const eventName = this.getNodeParameter('eventName', i) as string; + const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean; + const body = { + identity: { id, email }, + event_name: eventName, + email, + }; + if (!jsonActive) { + const dataAttributesValues = (this.getNodeParameter('dataAttributesUi', i) as IDataObject).dataAttributesValues as IDataObject[]; + if (dataAttributesValues) { + const dataAttributes = {}; + for (let i = 0; i < dataAttributesValues.length; i++) { + // @ts-ignore + dataAttributes[dataAttributesValues[i].key] = dataAttributesValues[i].value; + // @ts-ignore + body.data = JSON.stringify(dataAttributes); + } + } + const extraAttributesValues = (this.getNodeParameter('extraAttributesUi', i) as IDataObject).extraAttributesValues as IDataObject[]; + if (extraAttributesValues) { + const extraAttributes = {}; + for (let i = 0; i < extraAttributesValues.length; i++) { + // @ts-ignore + extraAttributes[extraAttributesValues[i].key] = extraAttributesValues[i].value; + // @ts-ignore + body.extras = JSON.stringify(extraAttributes); + } + } + } else { + const dataAttributesJson = validateJSON(this.getNodeParameter('dataAttributesJson', i) as string); + if (dataAttributesJson) { + // @ts-ignore + body.data = JSON.stringify(dataAttributesJson); + } + const extraAttributesJson = validateJSON(this.getNodeParameter('extraAttributesJson', i) as string); + if (extraAttributesJson) { + // @ts-ignore + body.extras = JSON.stringify(extraAttributesJson); + } + } + try { + responseData = await veroApiRequest.call(this, 'POST', '/events/track', body); + } catch (err) { + throw new Error(`Vero Error: ${err}`); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Vero/vero.png b/packages/nodes-base/nodes/Vero/vero.png new file mode 100644 index 0000000000000000000000000000000000000000..bddebcf31b8e6aeed4176cad5ebe224829778423 GIT binary patch literal 4069 zcmY*cc|26@-=48$jfZSw8+#FBge)_Zu`k1jvBy}OY{QJqpptzZA!Q4ZU6Lg#WS1pm zm+WgL*?0NzJkR@k-}ihz_qp%;I@f(&-|xBpIG+<`jMQeL2hjrn047}>4U@AOc0Oon z&hE;`<30cY@IDrSFxEvNz{WmaE?9SG0N`>$UjIjZ3JlkU_19KhSe=#re%xcl(C~2+ z>mT5-kd&L~?#Z7Al-sa1q%4TX3dCz85v-H*5PUc0&m-{V;-a?t7dfH}{or!S4!iD~3D4(s*VTcg;ZwkQ;E@WY73`ThQ zID_S-<)meVRO!KBFwDovMcG6{>)+#NJGjst0s*HCf%y6PN&8)s_VRIsK$Vn~ATqKL zSy`zwh7_LUNx%@LJn_Q+i2RR^hBMyL2a6+My*$C^x)=v9Ujkf6=-lYP*FSXi&QWC}AFT74^SQn%6!tgs|6%|7z#!-5|EHLLI{h0x zYgLsV2Kn!6Q>8a(7PkZd81L(9+%&yI{mY6DXFbdg9~gb+)VU!xQ6X%NoEx)@je2fE zpZDU4Y`nedWC0zPigV|Ssxn3Pv3isVxVq<@1MD8(@g}KNB72IWs2@iwH7}ZO_HnwxG4v zLv|8+n>x3a(7^Cfsq!Kz?dizi6(Em|L8Vnc929yA`L#abOK5Z4pR8~gV*l_&6CeJJ zjp3&9D!Oh69_%)ujTwGrk>64N<_klOVGEziQNJv3`$7xx&{P0DYAyu2W?y+%pah-v zm?Pb)Jl87&Lrw6WR`pHfTpsE>9iyv+H#&QJSqt1DwK||+gdgg>zopRKS2q>kER62~ zL37oV^NJr-PgIdi7V9P#!}4iUHr#5mNe={HJJN29RyoayZH<)HgEFQum9uZ2PmcM6 zTThB-%ko62tV4C=^51R*&yi|lt=+A0yog&gIkB{Gri6T&4Ey@7Ku%7^pldnRevA#u z*E2?(J}0Kfxz8V;G|`n}d4ku<2U{>1ea25CD=iEi_h=AAlDi3aZu=`lK|}PbTR}*= zlnHZPgr@3MWMcS>+0~ju?^6w`?%D`P^~*|)lI}mtEjJdX1VfvTS>k4l#LcpslP`pX zp#QKP%zr%Uhc5_O>}pxwlx-9RFzu?Vkge2`}-3{wP%*K`XAO-i-&< z=)KnyyaG(`Oa1!#wiVedfRZT6rHyDHgU4UuqrdAi#U$2QS7v{CfsycRqGDfDf@SvK zSW7rsv~A7!T8I4e$P(OFh7sH={oE`i$KM`qrdm;PaeNd6jN*Ja;S;pA5-1VuYd9rFDO_Ra)1KuhRakGKMpkH9x-E@%bb!~V=|c4`oP2x{Vig2J-~*T@5&p`toeq7 zV;x=mzD_r5h-lA2!KSB~R0fR|9cH3k1mGSwLx29<_UxCzP29`BvgUmUYBOem>CNOX zc@Obz6lY%yHnHkzHkUgfYPxC9)$V-u3{72eJdV>~a*<-o$h+VR4y$oUtV(pS^Y>YNHJ#{-CVZgJ0U8wptXKcd6gc}`TJg%20tZ$QEJ!Xsc=wRk zYnv+j%>rA(dILtG4+bXferQvl`~C)l z-amIA#9n(9*{oit&58e%-~fvbc6?~5IYALN~7QTIkDGM4Q@!eP*){mV>eY26bIvHx` zQ(H*AUW&);utItz911JQ^>;eJyYWWF#&K5^Xd2$uQApZu%huy=y9L=A>g<$MNF@qA z8)zZ(ptQOG(VOmEbZs~`Qsd_elh_yjw!u>)dO5O%p5GdidpHy}!&VQ!H<|6~NoRGc z)a&Gn?18iBPYR2oXWN`xps>2!(TpHRl>bP}A3|>E+XaX4nUGiTyEd7Ida+$Gu*#Bn z4f!xcpr>szkN6Q>!ZfcqacE17G_!j@H~RWoA*5+@K**WVI!#FUU2nv6!7mKS;zq~2 z8dd9Cp>C<-nng?j;a?PFa=z-8+p}nAHH^i0nN=v-kJ*m?wXk0f;_QnmkWc&!)M`KI znXH!9VsP8}EuTCX;4qXTA?9>?YVga0&)_6tu%7@=r-BPNZkEfGunc{5!RQT`*|bbb z)t3e^nlb4v3o*6JC$?Jz1`!|JY|E8^q_Z0sC8p~BsfpLd{GRyrmk1VS+$ ztgeOZyMcvx+`>(pepoBeeWb$-54e0FDF z{E$uom&?~4ZXy^S}x-|aGpgW{DZgzqF6#ct@WeRLlX#+ zfEQLNttA_tw0W&*Kc67~DPmDjAfXB;*p^YvwBGJX^`A+L8`Y8$rYz|h(j~n%E{hn; z5%ueOEl>K2?}o5&rJ~8+0-nxCTEldxeVQ;>b*-~#2ZDx}t@pHmlXU;xYd{G{e}GS-X_9=SEXgP4dAE$s?A#FS>ASo| zuvURkPf2MUHFS|x>3-E#T7k&VX>R)o?RZ%yf%yY`0RbU5BL8`7cF6sQhN#kOf8tY! zCAIs=cxf&l^4nYWixdvT4P!2&xBAybF;bf5yk=Y9h%1Wx@1b_Toip!~vBh`-h4~r? z-RM=?*->!cqz9)W;{V-p4t7jL1G`Hljtf`5$8>vRmAb|G+ibpZG&a1UQ$Ramswd_Z z19Z82X9SgtIQ{N1&i{C(LNfG~qjXDODzOFEW#0=i$Pn#RwU-dHC5Oa*K>qclmYryf z9=;8!Qo(I?$$BeM5Kc;ReXcs$OMa}e`h=HNf+z51d2}k)cT}2&7bi>wq{0)HL^u=K8pZhO>}Dp^jg{R* zV#psXwPW@cd32$XF)vN$9gaQ;y@XuCfFvTs`(6lvIj7humuSijN^i8yLJ0GZX!1ET z)u*n@JW~7E9ey*;`il>aD?Pvxg_7P-_D<77ELG;e8!|@QnLJQ~O(d@vBw8>!eoMDI z>5yKs9bh4?(qyGikGYLCUn_;fq!Ml=@huNka4Hzd`YhTuCEaSQ2;!Z$ApIF?gA79+ zZopyJKShVAy;Mjpm=(diDsI2>$7D)UBe^m=NfXa(!2yLc#a{c}@E+3G{*V_lXWM%E zWHU@tw6Ld7B{2S5mVW2Eh@`IcOb(E~|Kp9aJ8_dw3FfgD#UQ2wrhBKN)& zQ=vwc^loZbOrxj+MX1TU(_|zspNHv{k#x$8X2p)y{)~uty6gFX4S}Rr%c8Ne&L!vn Obh?^IjWTun;Qs)EuV)$n literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5afa6b431c..f10dc78186 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -72,7 +72,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", - "dist/credentials/TypeformApi.credentials.js" + "dist/credentials/TypeformApi.credentials.js", + "dist/credentials/VeroApi.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -153,6 +154,7 @@ "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js", + "dist/nodes/Vero/Vero.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Xml.node.js", From 01983a9c8f706e33b06ade4d090f70fd73c0530b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 18 Dec 2019 12:30:16 -0500 Subject: [PATCH 02/13] :zap: removed deprecation warning --- packages/cli/bin/old.json | 1 + packages/nodes-base/nodes/MoveBinaryData.node.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 packages/cli/bin/old.json diff --git a/packages/cli/bin/old.json b/packages/cli/bin/old.json new file mode 100644 index 0000000000..42e0416825 --- /dev/null +++ b/packages/cli/bin/old.json @@ -0,0 +1 @@ +{"hola":1} \ No newline at end of file diff --git a/packages/nodes-base/nodes/MoveBinaryData.node.ts b/packages/nodes-base/nodes/MoveBinaryData.node.ts index 28a0866942..02b1ff98e8 100644 --- a/packages/nodes-base/nodes/MoveBinaryData.node.ts +++ b/packages/nodes-base/nodes/MoveBinaryData.node.ts @@ -267,7 +267,7 @@ export class MoveBinaryData implements INodeType { } const encoding = (options.encoding as string) || 'utf8'; - let convertedValue = new Buffer(value.data, 'base64').toString(encoding); + let convertedValue = Buffer.from(value.data, 'base64').toString(encoding); if (setAllData === true) { // Set the full data @@ -321,7 +321,7 @@ export class MoveBinaryData implements INodeType { } const convertedValue = { - data: new Buffer(value as string).toString('base64'), + data: Buffer.from(value as string).toString('base64'), mimeType: options.mimeType || 'application/json', }; set(newItem.binary!, destinationKey, convertedValue); From a8a21034d1a619b80c5c7a0c5b0c788258a90920 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 18 Dec 2019 12:32:04 -0500 Subject: [PATCH 03/13] deleted test file --- packages/cli/bin/old.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/cli/bin/old.json diff --git a/packages/cli/bin/old.json b/packages/cli/bin/old.json deleted file mode 100644 index 42e0416825..0000000000 --- a/packages/cli/bin/old.json +++ /dev/null @@ -1 +0,0 @@ -{"hola":1} \ No newline at end of file From 8acc3c5931805c686ea47e82e1fccafbde25378a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 19 Dec 2019 16:07:55 -0600 Subject: [PATCH 04/13] :sparkles: Add ExecuteWorkflow-Node --- packages/cli/src/ActiveWorkflowRunner.ts | 2 +- packages/cli/src/Interfaces.ts | 15 +- packages/cli/src/Server.ts | 5 +- packages/cli/src/WebhookHelpers.ts | 30 +- .../cli/src/WorkflowExecuteAdditionalData.ts | 260 ++++++++++++++---- packages/cli/src/WorkflowHelpers.ts | 111 ++++++++ packages/cli/src/WorkflowRunner.ts | 92 ++----- packages/cli/src/WorkflowRunnerProcess.ts | 14 +- packages/core/src/NodeExecuteFunctions.ts | 16 ++ packages/core/src/WorkflowExecute.ts | 19 +- .../nodes-base/nodes/ExecuteWorkflow.node.ts | 75 +++++ packages/nodes-base/package.json | 1 + packages/workflow/src/Interfaces.ts | 33 ++- packages/workflow/src/WorkflowHooks.ts | 48 ++++ packages/workflow/src/index.ts | 1 + 15 files changed, 525 insertions(+), 197 deletions(-) create mode 100644 packages/nodes-base/nodes/ExecuteWorkflow.node.ts create mode 100644 packages/workflow/src/WorkflowHooks.ts diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index cde03c20d3..6a0e5cb02b 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -301,7 +301,7 @@ export class ActiveWorkflowRunner { const mode = 'trigger'; const credentials = await WorkflowCredentials(workflowData.nodes); - const additionalData = await WorkflowExecuteAdditionalData.getBase(mode, credentials); + const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode); // Add the workflows which have webhooks defined diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 9d615d36a8..4000c30785 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -1,16 +1,14 @@ import { - IConnections, ICredentialsDecrypted, ICredentialsEncrypted, IDataObject, IExecutionError, - INode, IRun, IRunData, IRunExecutionData, ITaskData, + IWorkflowBase as IWorkflowBaseWorkflow, IWorkflowCredentials, - IWorkflowSettings, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -44,16 +42,9 @@ export interface IDatabaseCollections { } -export interface IWorkflowBase { +export interface IWorkflowBase extends IWorkflowBaseWorkflow { id?: number | string | ObjectID; - name: string; - active: boolean; - createdAt: Date; - updatedAt: Date; - nodes: INode[]; - connections: IConnections; - settings?: IWorkflowSettings; - staticData?: IDataObject; + } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index d40374915f..41fda80372 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -497,7 +497,7 @@ class App { if (WorkflowHelpers.isWorkflowIdValid(workflowData.id) === true && (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined)) { // Webhooks can only be tested with saved workflows const credentials = await WorkflowCredentials(workflowData.nodes); - const additionalData = await WorkflowExecuteAdditionalData.getBase(executionMode, credentials); + const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const nodeTypes = NodeTypes(); const workflowInstance = new Workflow(workflowData.id, workflowData.nodes, workflowData.connections, false, nodeTypes, undefined, workflowData.settings); const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, sessionId, destinationNode); @@ -544,13 +544,12 @@ class App { const methodName = req.query.methodName; const nodeTypes = NodeTypes(); - const executionMode = 'manual'; const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, credentials); const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase; const workflowCredentials = await WorkflowCredentials(workflowData.nodes); - const additionalData = await WorkflowExecuteAdditionalData.getBase(executionMode, workflowCredentials, currentNodeParameters); + const additionalData = await WorkflowExecuteAdditionalData.getBase(workflowCredentials, currentNodeParameters); return loadDataInstance.getOptions(methodName, additionalData); })); diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 77ef8087be..b13cc3b0ea 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -9,6 +9,7 @@ import { IWorkflowDb, IWorkflowExecutionDataProcess, ResponseHelper, + WorkflowHelpers, WorkflowRunner, WorkflowCredentials, WorkflowExecuteAdditionalData, @@ -24,9 +25,7 @@ import { IDataObject, IExecuteData, INode, - IRun, IRunExecutionData, - ITaskData, IWebhookData, IWebhookResponseData, IWorkflowExecuteAdditionalData, @@ -38,29 +37,6 @@ import { const activeExecutions = ActiveExecutions.getInstance(); -/** - * Returns the data of the last executed node - * - * @export - * @param {IRun} inputData - * @returns {(ITaskData | undefined)} - */ -export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined { - const runData = inputData.data.resultData.runData; - const lastNodeExecuted = inputData.data.resultData.lastNodeExecuted; - - if (lastNodeExecuted === undefined) { - return undefined; - } - - if (runData[lastNodeExecuted] === undefined) { - return undefined; - } - - return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1]; -} - - /** * Returns all the webhooks which should be created for the give workflow * @@ -132,7 +108,7 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo // Prepare everything that is needed to run the workflow const credentials = await WorkflowCredentials(workflowData.nodes); - const additionalData = await WorkflowExecuteAdditionalData.getBase(executionMode, credentials); + const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); // Add the Response and Request so that this data can be accessed in the node additionalData.httpRequest = req; @@ -286,7 +262,7 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo return undefined; } - const returnData = getDataLastExecutedNodeData(data); + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); if (returnData === undefined) { if (didSendResponse === false) { responseCallback(null, { diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 91821e607b..befa2c6680 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -5,6 +5,7 @@ import { IPushDataExecutionFinished, IWorkflowBase, IWorkflowExecutionDataProcess, + NodeTypes, Push, ResponseHelper, WebhookHelpers, @@ -13,17 +14,25 @@ import { import { UserSettings, + WorkflowExecute, } from 'n8n-core'; import { IDataObject, + IExecuteData, + INode, INodeParameters, + INodeExecutionData, IRun, + IRunExecutionData, ITaskData, IWorkflowCredentials, IWorkflowExecuteAdditionalData, IWorkflowExecuteHooks, + IWorkflowHooksOptionalParameters, + Workflow, WorkflowExecuteMode, + WorkflowHooks, } from 'n8n-workflow'; import * as config from '../config'; @@ -35,15 +44,10 @@ import * as config from '../config'; * * @param {IWorkflowBase} workflowData The workflow which got executed * @param {IRun} fullRunData The run which produced the error - * @param {WorkflowExecuteMode} mode The mode in which the workflow which did error got started in + * @param {WorkflowExecuteMode} mode The mode in which the workflow got started in * @param {string} [executionId] The id the execution got saved as */ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mode: WorkflowExecuteMode, executionId?: string, retryOf?: string): void { - if (mode === 'manual') { - // Do not call error workflow when executed manually - return; - } - // Check if there was an error and if so if an errorWorkflow is set if (fullRunData.data.resultData.error !== undefined && workflowData.settings !== undefined && workflowData.settings.errorWorkflow) { const workflowErrorData = { @@ -68,11 +72,12 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo /** * Pushes the execution out to all connected clients * + * @param {WorkflowExecuteMode} mode The mode in which the workflow got started in * @param {IRun} fullRunData The RunData of the finished execution * @param {string} executionIdActive The id of the finished execution * @param {string} [executionIdDb] The database id of finished execution */ -export function pushExecutionFinished(fullRunData: IRun, executionIdActive: string, executionIdDb?: string, retryOf?: string) { +export function pushExecutionFinished(mode: WorkflowExecuteMode, fullRunData: IRun, executionIdActive: string, executionIdDb?: string, retryOf?: string) { // Clone the object except the runData. That one is not supposed // to be send. Because that data got send piece by piece after // each node which finished executing @@ -101,100 +106,116 @@ export function pushExecutionFinished(fullRunData: IRun, executionIdActive: stri /** - * Returns the workflow execution hooks + * Returns hook functions to push data to Editor-UI * - * @param {WorkflowExecuteMode} mode - * @param {IWorkflowBase} workflowData - * @param {string} executionId - * @param {string} [sessionId] - * @param {string} [retryOf] * @returns {IWorkflowExecuteHooks} */ -const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, executionId: string, sessionId?: string, retryOf?: string): IWorkflowExecuteHooks => { +function hookFunctionsPush(): IWorkflowExecuteHooks { return { nodeExecuteBefore: [ - async (nodeName: string): Promise => { + async function (this: WorkflowHooks, nodeName: string): Promise { // Push data to session which started workflow before each // node which starts rendering - if (sessionId === undefined) { + if (this.sessionId === undefined) { return; } const pushInstance = Push.getInstance(); pushInstance.send('nodeExecuteBefore', { - executionId, + executionId: this.executionId, nodeName, - }, sessionId); + }, this.sessionId); }, ], nodeExecuteAfter: [ - async (nodeName: string, data: ITaskData): Promise => { + async function (this: WorkflowHooks, nodeName: string, data: ITaskData): Promise { // Push data to session which started workflow after each rendered node - if (sessionId === undefined) { + if (this.sessionId === undefined) { return; } const pushInstance = Push.getInstance(); pushInstance.send('nodeExecuteAfter', { - executionId, + executionId: this.executionId, nodeName, data, - }, sessionId); + }, this.sessionId); }, ], workflowExecuteBefore: [ - async (): Promise => { + async function (this: WorkflowHooks): Promise { // Push data to editor-ui once workflow finished const pushInstance = Push.getInstance(); pushInstance.send('executionStarted', { - executionId, - mode, + executionId: this.executionId, + mode: this.mode, startedAt: new Date(), - retryOf, - workflowId: workflowData.id as string, - workflowName: workflowData.name, + retryOf: this.retryOf, + workflowId: this.workflowData.id as string, + workflowName: this.workflowData.name, }); } ], workflowExecuteAfter: [ - async (fullRunData: IRun, newStaticData: IDataObject): Promise => { + async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { + pushExecutionFinished(this.mode, fullRunData, this.executionId, undefined, this.retryOf); + }, + ] + }; +} + + +/** + * Returns hook functions to save workflow execution and call error workflow + * + * @returns {IWorkflowExecuteHooks} + */ +function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { + return { + nodeExecuteBefore: [], + nodeExecuteAfter: [], + workflowExecuteBefore: [], + workflowExecuteAfter: [ + async function (this: WorkflowHooks, fullRunData: IRun, newStaticData: IDataObject): Promise { + + const isManualMode = [this.mode, parentProcessMode].includes('manual'); + try { - if (mode !== 'manual' && WorkflowHelpers.isWorkflowIdValid(workflowData.id as string) === true && newStaticData) { + if (!isManualMode && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id as string) === true && newStaticData) { // Workflow is saved so update in database try { - await WorkflowHelpers.saveStaticDataById(workflowData.id as string, newStaticData); + 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 "${workflowData.id}" to save changed staticData: ${e.message}`); + console.error(`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: ${e.message}`); } } let saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean; - if (workflowData.settings !== undefined && workflowData.settings.saveManualExecutions !== undefined) { + if (this.workflowData.settings !== undefined && this.workflowData.settings.saveManualExecutions !== undefined) { // Apply to workflow override - saveManualExecutions = workflowData.settings.saveManualExecutions as boolean; + saveManualExecutions = this.workflowData.settings.saveManualExecutions as boolean; } - if (mode === 'manual' && saveManualExecutions === false) { - pushExecutionFinished(fullRunData, executionId, undefined, retryOf); - executeErrorWorkflow(workflowData, fullRunData, mode, undefined, retryOf); + if (isManualMode && saveManualExecutions === false) { return; } // Check config to know if execution should be saved or not let saveDataErrorExecution = config.get('executions.saveDataOnError') as string; let saveDataSuccessExecution = config.get('executions.saveDataOnSuccess') as string; - if (workflowData.settings !== undefined) { - saveDataErrorExecution = (workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; - saveDataSuccessExecution = (workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; + if (this.workflowData.settings !== undefined) { + saveDataErrorExecution = (this.workflowData.settings.saveDataErrorExecution as string) || saveDataErrorExecution; + saveDataSuccessExecution = (this.workflowData.settings.saveDataSuccessExecution as string) || saveDataSuccessExecution; } const workflowDidSucceed = !fullRunData.data.resultData.error; if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' || workflowDidSucceed === false && saveDataErrorExecution === 'none' ) { - pushExecutionFinished(fullRunData, executionId, undefined, retryOf); - executeErrorWorkflow(workflowData, fullRunData, mode, undefined, retryOf); + if (!isManualMode) { + executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + } return; } @@ -204,15 +225,15 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, execution finished: fullRunData.finished ? fullRunData.finished : false, startedAt: fullRunData.startedAt, stoppedAt: fullRunData.stoppedAt, - workflowData, + workflowData: this.workflowData, }; - if (retryOf !== undefined) { - fullExecutionData.retryOf = retryOf.toString(); + if (this.retryOf !== undefined) { + fullExecutionData.retryOf = this.retryOf.toString(); } - if (workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(workflowData.id.toString()) === true) { - fullExecutionData.workflowId = workflowData.id.toString(); + if (this.workflowData.id !== undefined && WorkflowHelpers.isWorkflowIdValid(this.workflowData.id.toString()) === true) { + fullExecutionData.workflowId = this.workflowData.id.toString(); } const executionData = ResponseHelper.flattenExecutionData(fullExecutionData); @@ -220,33 +241,133 @@ const hooks = (mode: WorkflowExecuteMode, workflowData: IWorkflowBase, execution // Save the Execution in DB const executionResult = await Db.collections.Execution!.save(executionData as IExecutionFlattedDb); - if (fullRunData.finished === true && retryOf !== undefined) { + if (fullRunData.finished === true && this.retryOf !== undefined) { // If the retry was successful save the reference it on the original execution // await Db.collections.Execution!.save(executionData as IExecutionFlattedDb); - await Db.collections.Execution!.update(retryOf, { retrySuccessId: executionResult.id }); + await Db.collections.Execution!.update(this.retryOf, { retrySuccessId: executionResult.id }); } - pushExecutionFinished(fullRunData, executionId, executionResult.id as string, retryOf); - executeErrorWorkflow(workflowData, fullRunData, mode, executionResult ? executionResult.id as string : undefined, retryOf); + if (!isManualMode) { + executeErrorWorkflow(this.workflowData, fullRunData, this.mode, executionResult ? executionResult.id as string : undefined, this.retryOf); + } } catch (error) { - pushExecutionFinished(fullRunData, executionId, undefined, retryOf); - executeErrorWorkflow(workflowData, fullRunData, mode, undefined, retryOf); + if (!isManualMode) { + executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf); + } } }, ] }; -}; +} + + +/** + * Executes the workflow with the given ID + * + * @export + * @param {string} workflowId The id of the workflow to execute + * @param {IWorkflowExecuteAdditionalData} additionalData + * @param {INodeExecutionData[]} [inputData] + * @returns {(Promise>)} + */ +export async function executeWorkflow(workflowId: string, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]): Promise> { + const mode = 'integrated'; + + if (Db.collections!.Workflow === null) { + // The first time executeWorkflow gets called the Database has + // to get initialized first + await Db.init(); + } + + const workflowData = await Db.collections!.Workflow!.findOne(workflowId); + if (workflowData === undefined) { + throw new Error(`The workflow with the id "${workflowId}" does not exist.`); + } + + const nodeTypes = NodeTypes(); + + const workflow = new Workflow(workflowId as string | undefined, workflowData!.nodes, workflowData!.connections, workflowData!.active, nodeTypes, workflowData!.staticData); + + // Does not get used so set it simply to empty string + const executionId = ''; + + // Create new additionalData to have different workflow loaded and to call + // different webooks + const additionalDataIntegrated = await getBase(additionalData.credentials); + additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData, { parentProcessMode: additionalData.hooks!.mode }); + + // Find Start-Node + const requiredNodeTypes = ['n8n-nodes-base.start']; + let startNode: INode | undefined; + for (const node of workflowData!.nodes) { + if (requiredNodeTypes.includes(node.type)) { + startNode = node; + break; + } + } + if (startNode === undefined) { + // If the workflow does not contain a start-node we can not know what + // should be executed and with what data to start. + throw new Error(`The workflow does not contain a "Start" node and can so not be executed.`); + } + + // Always start with empty data if no inputData got supplied + inputData = inputData || [ + { + json: {} + } + ]; + + // Initialize the incoming data + const nodeExecutionStack: IExecuteData[] = []; + nodeExecutionStack.push( + { + node: startNode, + data: { + main: [inputData], + }, + }, + ); + + const runExecutionData: IRunExecutionData = { + startData: { + }, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack, + waitingExecution: {}, + }, + }; + + // Execute the workflow + const workflowExecute = new WorkflowExecute(additionalDataIntegrated, mode, runExecutionData); + const data = await workflowExecute.processRunExecutionData(workflow); + + if (data.finished === true) { + // Workflow did finish successfully + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); + return returnData!.data!.main; + } else { + // Workflow did fail + const error = new Error(data.data.resultData.error!.message); + error.stack = data.data.resultData.error!.stack; + throw error; + } +} /** * Returns the base additional data without webhooks * * @export - * @param {WorkflowExecuteMode} mode * @param {IWorkflowCredentials} credentials + * @param {INodeParameters[]} [currentNodeParameters=[]] * @returns {Promise} */ -export async function getBase(mode: WorkflowExecuteMode, credentials: IWorkflowCredentials, currentNodeParameters: INodeParameters[] = []): Promise { +export async function getBase(credentials: IWorkflowCredentials, currentNodeParameters: INodeParameters[] = []): Promise { const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const timezone = config.get('generic.timezone') as string; @@ -261,6 +382,8 @@ export async function getBase(mode: WorkflowExecuteMode, credentials: IWorkflowC return { credentials, encryptionKey, + executeWorkflow, + restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string, timezone, webhookBaseUrl, webhookTestBaseUrl, @@ -270,13 +393,30 @@ export async function getBase(mode: WorkflowExecuteMode, credentials: IWorkflowC /** - * Returns the workflow hooks + * Returns WorkflowHooks instance for running integrated workflows + * (Workflows which get started inside of another workflow) + */ +export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { + optionalParameters = optionalParameters || {}; + const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode); + return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); +} + + +/** + * Returns WorkflowHooks instance for running the main workflow * * @export * @param {IWorkflowExecutionDataProcess} data * @param {string} executionId - * @returns {IWorkflowExecuteHooks} + * @returns {WorkflowHooks} */ -export function getHookMethods(data: IWorkflowExecutionDataProcess, executionId: string): IWorkflowExecuteHooks { - return hooks(data.executionMode, data.workflowData, executionId, data.sessionId, data.retryOf as string | undefined); +export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string): WorkflowHooks { + const hookFunctions = hookFunctionsSave(); + const pushFunctions = hookFunctionsPush(); + for (const key of Object.keys(pushFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], pushFunctions[key]); + } + + return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string}); } diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index b3e1156fa9..ab723a95e5 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -1,5 +1,6 @@ import { Db, + ITransferNodeTypes, IWorkflowExecutionDataProcess, IWorkflowErrorData, NodeTypes, @@ -11,7 +12,9 @@ import { IDataObject, IExecuteData, INode, + IRun, IRunExecutionData, + ITaskData, Workflow, } from 'n8n-workflow'; @@ -19,6 +22,31 @@ import * as config from '../config'; const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; + +/** + * Returns the data of the last executed node + * + * @export + * @param {IRun} inputData + * @returns {(ITaskData | undefined)} + */ +export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefined { + const runData = inputData.data.resultData.runData; + const lastNodeExecuted = inputData.data.resultData.lastNodeExecuted; + + if (lastNodeExecuted === undefined) { + return undefined; + } + + if (runData[lastNodeExecuted] === undefined) { + return undefined; + } + + return runData[lastNodeExecuted][runData[lastNodeExecuted].length - 1]; +} + + + /** * Returns if the given id is a valid workflow id * @@ -129,6 +157,89 @@ export async function executeErrorWorkflow(workflowId: string, workflowErrorData +/** + * Returns all the defined NodeTypes + * + * @export + * @returns {ITransferNodeTypes} + */ +export function getAllNodeTypeData(): ITransferNodeTypes { + const nodeTypes = NodeTypes(); + + // Get the data of all thenode types that they + // can be loaded again in the process + const returnData: ITransferNodeTypes = {}; + for (const nodeTypeName of Object.keys(nodeTypes.nodeTypes)) { + if (nodeTypes.nodeTypes[nodeTypeName] === undefined) { + throw new Error(`The NodeType "${nodeTypeName}" could not be found!`); + } + + returnData[nodeTypeName] = { + className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name, + sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath, + }; + } + + return returnData; +} + + + +/** + * Returns the data of the node types that are needed + * to execute the given nodes + * + * @export + * @param {INode[]} nodes + * @returns {ITransferNodeTypes} + */ +export function getNodeTypeData(nodes: INode[]): ITransferNodeTypes { + const nodeTypes = NodeTypes(); + + // Check which node-types have to be loaded + const neededNodeTypes = getNeededNodeTypes(nodes); + + // Get all the data of the needed node types that they + // can be loaded again in the process + const returnData: ITransferNodeTypes = {}; + for (const nodeTypeName of neededNodeTypes) { + if (nodeTypes.nodeTypes[nodeTypeName] === undefined) { + throw new Error(`The NodeType "${nodeTypeName}" could not be found!`); + } + + returnData[nodeTypeName] = { + className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name, + sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath, + }; + } + + return returnData; +} + + + +/** + * Returns the names of the NodeTypes which are are needed + * to execute the gives nodes + * + * @export + * @param {INode[]} nodes + * @returns {string[]} + */ +export function getNeededNodeTypes(nodes: INode[]): string[] { + // Check which node-types have to be loaded + const neededNodeTypes: string[] = []; + for (const node of nodes) { + if (!neededNodeTypes.includes(node.type)) { + neededNodeTypes.push(node.type); + } + } + + return neededNodeTypes; +} + + + /** * Saves the static data if it changed * diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 5496ace2a4..8840d1cfb0 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -1,11 +1,9 @@ - import { ActiveExecutions, IProcessMessageDataHook, ITransferNodeTypes, IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcessWithExecution, - NodeTypes, Push, WorkflowExecuteAdditionalData, WorkflowHelpers, @@ -17,9 +15,8 @@ import { import { IExecutionError, - INode, IRun, - IWorkflowExecuteHooks, + WorkflowHooks, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -38,70 +35,15 @@ export class WorkflowRunner { } - /** - * Returns the data of the node types that are needed - * to execute the given nodes - * - * @param {INode[]} nodes - * @returns {ITransferNodeTypes} - * @memberof WorkflowRunner - */ - getNodeTypeData(nodes: INode[]): ITransferNodeTypes { - const nodeTypes = NodeTypes(); - - // Check which node-types have to be loaded - const neededNodeTypes: string[] = []; - for (const node of nodes) { - if (!neededNodeTypes.includes(node.type)) { - neededNodeTypes.push(node.type); - } - } - - // Get all the data of the needed node types that they - // can be loaded again in the process - const returnData: ITransferNodeTypes = {}; - for (const nodeTypeName of neededNodeTypes) { - if (nodeTypes.nodeTypes[nodeTypeName] === undefined) { - throw new Error(`The NodeType "${nodeTypeName}" could not be found!`); - } - - returnData[nodeTypeName] = { - className: nodeTypes.nodeTypes[nodeTypeName].type.constructor.name, - sourcePath: nodeTypes.nodeTypes[nodeTypeName].sourcePath, - }; - } - - return returnData; - } - - /** * The process did send a hook message so execute the appropiate hook * - * @param {IWorkflowExecuteHooks} hookFunctions + * @param {WorkflowHooks} workflowHooks * @param {IProcessMessageDataHook} hookData * @memberof WorkflowRunner */ - processHookMessage(hookFunctions: IWorkflowExecuteHooks, hookData: IProcessMessageDataHook) { - if (hookFunctions[hookData.hook] !== undefined && Array.isArray(hookFunctions[hookData.hook])) { - - for (const hookFunction of hookFunctions[hookData.hook]!) { - // TODO: Not sure if that is 100% correct or something is still missing like to wait - hookFunction.apply(this, hookData.parameters) - .catch((error: Error) => { - // Catch all errors here because when "executeHook" gets called - // we have the most time no "await" and so the errors would so - // not be uncaught by anything. - - // TODO: Add proper logging - console.error(`There was a problem executing hook: "${hookData.hook}"`); - console.error('Parameters:'); - console.error(hookData.parameters); - console.error('Error:'); - console.error(error); - }); - } - } + processHookMessage(workflowHooks: WorkflowHooks, hookData: IProcessMessageDataHook) { + workflowHooks.executeHookFunctions(hookData.hook, hookData.parameters); } @@ -133,7 +75,7 @@ export class WorkflowRunner { this.activeExecutions.remove(executionId, fullRunData); // Also send to Editor UI - WorkflowExecuteAdditionalData.pushExecutionFinished(fullRunData, executionId); + WorkflowExecuteAdditionalData.pushExecutionFinished(executionMode, fullRunData, executionId); } @@ -157,12 +99,30 @@ export class WorkflowRunner { // Register the active execution const executionId = this.activeExecutions.add(subprocess, data); - const nodeTypeData = this.getNodeTypeData(data.workflowData.nodes); + // Check if workflow contains a "executeWorkflow" Node as in this + // case we can not know which nodeTypes will be needed and so have + // to load all of them in the workflowRunnerProcess + let loadAllNodeTypes = false; + for (const node of data.workflowData.nodes) { + if (node.type === 'n8n-nodes-base.executeWorkflow') { + loadAllNodeTypes = true; + break; + } + } + + let nodeTypeData: ITransferNodeTypes; + if (loadAllNodeTypes === true) { + // Supply all nodeTypes + nodeTypeData = WorkflowHelpers.getAllNodeTypeData(); + } else { + // Supply only nodeTypes which the workflow needs + nodeTypeData = WorkflowHelpers.getNodeTypeData(data.workflowData.nodes); + } (data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId; (data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData; - const hookFunctions = WorkflowExecuteAdditionalData.getHookMethods(data, executionId); + const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); // Send all data to subprocess it needs to run the workflow subprocess.send({ type: 'startWorkflow', data } as IProcessMessage); @@ -178,7 +138,7 @@ export class WorkflowRunner { this.processError(executionError, startedAt, data.executionMode, executionId); } else if (message.type === 'processHook') { - this.processHookMessage(hookFunctions, message.data as IProcessMessageDataHook); + this.processHookMessage(workflowHooks, message.data as IProcessMessageDataHook); } }); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 528c92f471..4d8ef4d000 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -1,6 +1,5 @@ import { - IProcessMessageDataHook, IWorkflowExecutionDataProcessWithExecution, NodeTypes, WorkflowExecuteAdditionalData, @@ -18,10 +17,9 @@ import { INodeTypeData, IRun, ITaskData, - IWorkflowExecuteHooks, Workflow, + WorkflowHooks, } from 'n8n-workflow'; -import { ChildProcess } from 'child_process'; export class WorkflowRunnerProcess { data: IWorkflowExecutionDataProcessWithExecution | undefined; @@ -61,10 +59,9 @@ export class WorkflowRunnerProcess { await nodeTypes.init(nodeTypesData); this.workflow = new Workflow(this.data.workflowData.id as string | undefined, this.data.workflowData!.nodes, this.data.workflowData!.connections, this.data.workflowData!.active, nodeTypes, this.data.workflowData!.staticData); - const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.executionMode, this.data.credentials); + const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); additionalData.hooks = this.getProcessForwardHooks(); - if (this.data.executionData !== undefined) { this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode, this.data.executionData); return this.workflowExecute.processRunExecutionData(this.workflow); @@ -111,11 +108,10 @@ export class WorkflowRunnerProcess { * the parent process where they then can be executed with access * to database and to PushService * - * @param {ChildProcess} process * @returns */ - getProcessForwardHooks(): IWorkflowExecuteHooks { - return { + getProcessForwardHooks(): WorkflowHooks { + const hookFunctions = { nodeExecuteBefore: [ async (nodeName: string): Promise => { this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]); @@ -137,6 +133,8 @@ export class WorkflowRunnerProcess { }, ] }; + + return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }); } } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 344abf9f83..775c2fd754 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -341,6 +341,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); }, + getRestApiUrl: (): string => { + return additionalData.restApiUrl; + }, getTimezone: (): string => { return getTimezone(workflow, additionalData); }, @@ -375,6 +378,10 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions { return ((workflow, runExecutionData, connectionInputData, inputData, node) => { return { + async executeWorkflow(workflowId: string, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any + // return additionalData.executeWorkflow(workflowId, additionalData, inputData); + return additionalData.executeWorkflow(workflowId, additionalData, inputData); + }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); }, @@ -408,6 +415,9 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx getMode: (): WorkflowExecuteMode => { return mode; }, + getRestApiUrl: (): string => { + return additionalData.restApiUrl; + }, getTimezone: (): string => { return getTimezone(workflow, additionalData); }, @@ -482,6 +492,9 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: getMode: (): WorkflowExecuteMode => { return mode; }, + getRestApiUrl: (): string => { + return additionalData.restApiUrl; + }, getTimezone: (): string => { return getTimezone(workflow, additionalData); }, @@ -540,6 +553,9 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio getTimezone: (): string => { return getTimezone(workflow, additionalData); }, + getRestApiUrl: (): string => { + return additionalData.restApiUrl; + }, helpers: { request: requestPromise, }, diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 83fd2f4296..d8ca8cd50c 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -226,25 +226,8 @@ export class WorkflowExecute { if (this.additionalData.hooks === undefined) { return; } - if (this.additionalData.hooks[hookName] === undefined || this.additionalData.hooks[hookName]!.length === 0) { - return; - } - for (const hookFunction of this.additionalData.hooks[hookName]!) { - await hookFunction.apply(this, parameters as [IRun, IWaitingForExecution]) - .catch((error) => { - // Catch all errors here because when "executeHook" gets called - // we have the most time no "await" and so the errors would so - // not be caught by anything. - - // TODO: Add proper logging - console.error(`There was a problem executing hook: "${hookName}"`); - console.error('Parameters:'); - console.error(parameters); - console.error('Error:'); - console.error(error); - }); - } + return this.additionalData.hooks.executeHookFunctions(hookName, parameters); } diff --git a/packages/nodes-base/nodes/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow.node.ts new file mode 100644 index 0000000000..bf5090e4c4 --- /dev/null +++ b/packages/nodes-base/nodes/ExecuteWorkflow.node.ts @@ -0,0 +1,75 @@ +import { OptionsWithUri } from 'request'; + +import { IExecuteFunctions } from 'n8n-core'; +import { + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + + +export class ExecuteWorkflow implements INodeType { + description: INodeTypeDescription = { + displayName: 'Execute Workflow', + name: 'executeWorkflow', + icon: 'fa:network-wired', + group: ['transform'], + version: 1, + subtitle: '={{"Workflow: " + $parameter["workflowId"]}}', + description: 'Execute another workflow', + defaults: { + name: 'Execute Workflow', + color: '#ff6d5a', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Workflow', + name: 'workflowId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkflows', + }, + default: '', + required: true, + description: 'The workflow to execute.', + }, + ] + }; + + methods = { + loadOptions: { + async getWorkflows(this: ILoadOptionsFunctions): Promise { + const options: OptionsWithUri = { + method: 'GET', + uri: this.getRestApiUrl() + '/workflows', + json: true + }; + + const returnData: INodePropertyOptions[] = []; + + const responseData = await this.helpers.request!(options); + for (const workflowData of responseData.data) { + returnData.push({ + name: workflowData.name, + value: workflowData.id, + }); + } + + return returnData; + } + }, + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const workflowId = this.getNodeParameter('workflowId', 0) as string; + const receivedData = await this.executeWorkflow(workflowId, items); + + return receivedData; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5afa6b431c..a5c3e02ce6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -94,6 +94,7 @@ "dist/nodes/EmailSend.node.js", "dist/nodes/ErrorTrigger.node.js", "dist/nodes/ExecuteCommand.node.js", + "dist/nodes/ExecuteWorkflow.node.js", "dist/nodes/FileMaker/FileMaker.node.js", "dist/nodes/Freshdesk/Freshdesk.node.js", "dist/nodes/Flow/Flow.node.js", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 660096d7c1..7824ee52cb 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1,4 +1,5 @@ import { Workflow } from './Workflow'; +import { WorkflowHooks } from './WorkflowHooks'; import * as express from 'express'; export interface IBinaryData { @@ -149,6 +150,7 @@ export interface IExecuteContextData { export interface IExecuteFunctions { + executeWorkflow(workflowId: string, inputData?: INodeExecutionData[]): Promise; // tslint:disable-line:no-any getContext(type: string): IContextObject; getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[]; @@ -156,6 +158,7 @@ export interface IExecuteFunctions { getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData; getWorkflowStaticData(type: string): IDataObject; + getRestApiUrl(): string; getTimezone(): string; prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise; helpers: { @@ -170,6 +173,7 @@ export interface IExecuteSingleFunctions { getInputData(inputIndex?: number, inputName?: string): INodeExecutionData; getMode(): WorkflowExecuteMode; getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any + getRestApiUrl(): string; getTimezone(): string; getWorkflowDataProxy(): IWorkflowDataProxyData; getWorkflowStaticData(type: string): IDataObject; @@ -184,6 +188,7 @@ export interface ILoadOptionsFunctions { getCurrentNodeParameter(parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined; getCurrentNodeParameters(): INodeParameters | undefined; getTimezone(): string; + getRestApiUrl(): string; helpers: { [key: string]: ((...args: any[]) => any) | undefined; //tslint:disable-line:no-any }; @@ -208,6 +213,7 @@ export interface ITriggerFunctions { getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getMode(): WorkflowExecuteMode; getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any + getRestApiUrl(): string; getTimezone(): string; getWorkflowStaticData(type: string): IDataObject; helpers: { @@ -574,6 +580,18 @@ export interface IWaitingForExecution { } +export interface IWorkflowBase { + id?: number | string | any; // tslint:disable-line:no-any + name: string; + active: boolean; + createdAt: Date; + updatedAt: Date; + nodes: INode[]; + connections: IConnections; + settings?: IWorkflowSettings; + staticData?: IDataObject; +} + export interface IWorkflowCredentials { // Credential type [key: string]: { @@ -582,6 +600,7 @@ export interface IWorkflowCredentials { }; } + export interface IWorkflowExecuteHooks { [key: string]: Array<((...args: any[]) => Promise)> | undefined; // tslint:disable-line:no-any nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData) => Promise)>; @@ -593,16 +612,26 @@ export interface IWorkflowExecuteHooks { export interface IWorkflowExecuteAdditionalData { credentials: IWorkflowCredentials; encryptionKey: string; - hooks?: IWorkflowExecuteHooks; + executeWorkflow: (workflowId: string, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]) => Promise; // tslint:disable-line:no-any + // hooks?: IWorkflowExecuteHooks; + hooks?: WorkflowHooks; httpResponse?: express.Response; httpRequest?: express.Request; + restApiUrl: string; timezone: string; webhookBaseUrl: string; webhookTestBaseUrl: string; currentNodeParameters? : INodeParameters[]; } -export type WorkflowExecuteMode = 'cli' | 'error' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook'; +export type WorkflowExecuteMode = 'cli' | 'error' | 'integrated' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook'; + +export interface IWorkflowHooksOptionalParameters { + parentProcessMode?: string; + retryOf?: string; + sessionId?: string; +} + export interface IWorkflowSettings { [key: string]: IDataObject | string | number | boolean | undefined; diff --git a/packages/workflow/src/WorkflowHooks.ts b/packages/workflow/src/WorkflowHooks.ts new file mode 100644 index 0000000000..94a02abddc --- /dev/null +++ b/packages/workflow/src/WorkflowHooks.ts @@ -0,0 +1,48 @@ +import { + IWorkflowBase, + IWorkflowExecuteHooks, + IWorkflowHooksOptionalParameters, + WorkflowExecuteMode, +} from './Interfaces'; + + +export class WorkflowHooks { + mode: WorkflowExecuteMode; + workflowData: IWorkflowBase; + executionId: string; + sessionId?: string; + retryOf?: string; + hookFunctions: IWorkflowExecuteHooks; + + constructor(hookFunctions: IWorkflowExecuteHooks, mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters) { + optionalParameters = optionalParameters || {}; + + this.hookFunctions = hookFunctions; + this.mode = mode; + this.executionId = executionId; + this.workflowData = workflowData; + this.sessionId = optionalParameters.sessionId; + this.retryOf = optionalParameters.retryOf; + } + + async executeHookFunctions(hookName: string, parameters: any[]) { // tslint:disable-line:no-any + if (this.hookFunctions[hookName] !== undefined && Array.isArray(this.hookFunctions[hookName])) { + for (const hookFunction of this.hookFunctions[hookName]!) { + await hookFunction.apply(this, parameters) + .catch((error: Error) => { + // Catch all errors here because when "executeHook" gets called + // we have the most time no "await" and so the errors would so + // not be uncaught by anything. + + // TODO: Add proper logging + console.error(`There was a problem executing hook: "${hookName}"`); + console.error('Parameters:'); + console.error(parameters); + console.error('Error:'); + console.error(error); + }); + } + } + } + +} diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 42a5cf77c1..89d01313d6 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -1,6 +1,7 @@ export * from './Interfaces'; export * from './Workflow'; export * from './WorkflowDataProxy'; +export * from './WorkflowHooks'; import * as NodeHelpers from './NodeHelpers'; import * as ObservableObject from './ObservableObject'; From 108ffb0d86effa1439a08818b6c3769b2acfbd43 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 19 Dec 2019 16:15:19 -0600 Subject: [PATCH 05/13] :zap: Automatically parse response in GraphQL node to JSON --- packages/nodes-base/nodes/GraphQL/GraphQL.node.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts index 7fb9b6c0b1..0f7c25240a 100644 --- a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts +++ b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts @@ -221,10 +221,14 @@ export class GraphQL implements INodeType { }); } else { if (typeof response === 'string') { - throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + try { + returnItems.push({ json: JSON.parse(response) }); + } catch (e) { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + } else { + returnItems.push({ json: response }); } - - returnItems.push({ json: response }); } } From f2236ba38c106cbb457764b973fc92ead3b207fa Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 19 Dec 2019 17:58:55 -0600 Subject: [PATCH 06/13] :zap: Make WriteBinaryFile-Node error when data is missing --- packages/nodes-base/nodes/WriteBinaryFile.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/WriteBinaryFile.node.ts b/packages/nodes-base/nodes/WriteBinaryFile.node.ts index faabd99475..2bcb8f8e9b 100644 --- a/packages/nodes-base/nodes/WriteBinaryFile.node.ts +++ b/packages/nodes-base/nodes/WriteBinaryFile.node.ts @@ -60,11 +60,11 @@ export class WriteBinaryFile implements INodeType { const fileName = this.getNodeParameter('fileName') as string; if (item.binary === undefined) { - return item; + throw new Error('No binary data set. So file can not be written!'); } if (item.binary[dataPropertyName] === undefined) { - return item; + throw new Error(`The binary property "${dataPropertyName}" does not exist. So no file can be written!`); } // Write the file to disk From 9c2ff708c54b91d60804c68a822ccbfc87abb8f3 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 19 Dec 2019 22:33:37 -0600 Subject: [PATCH 07/13] :zap: Small fix for Vero-Node --- packages/nodes-base/nodes/Vero/EventDescripion.ts | 4 ++-- packages/nodes-base/nodes/Vero/UserDescription.ts | 6 +++--- packages/nodes-base/nodes/Vero/Vero.node.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Vero/EventDescripion.ts b/packages/nodes-base/nodes/Vero/EventDescripion.ts index 8df87fd072..bfa6ef4464 100644 --- a/packages/nodes-base/nodes/Vero/EventDescripion.ts +++ b/packages/nodes-base/nodes/Vero/EventDescripion.ts @@ -121,7 +121,7 @@ export const eventFields = [ 'track', ], jsonParameters: [ - false + false, ], }, }, @@ -167,7 +167,7 @@ export const eventFields = [ 'track', ], jsonParameters: [ - false + false, ], }, }, diff --git a/packages/nodes-base/nodes/Vero/UserDescription.ts b/packages/nodes-base/nodes/Vero/UserDescription.ts index f90cb49a8c..02c484cd50 100644 --- a/packages/nodes-base/nodes/Vero/UserDescription.ts +++ b/packages/nodes-base/nodes/Vero/UserDescription.ts @@ -141,7 +141,7 @@ export const userFields = [ 'create', ], jsonParameters: [ - false + false, ], }, }, @@ -332,7 +332,7 @@ export const userFields = [ ] }, }, - description: 'Tags to add separated by ,', + description: 'Tags to add separated by ","', }, /* -------------------------------------------------------------------------- */ /* user:removeTags */ @@ -371,6 +371,6 @@ export const userFields = [ ] }, }, - description: 'Tags to remove separated by ,', + description: 'Tags to remove separated by ","', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Vero/Vero.node.ts b/packages/nodes-base/nodes/Vero/Vero.node.ts index d17fbef375..226a230d95 100644 --- a/packages/nodes-base/nodes/Vero/Vero.node.ts +++ b/packages/nodes-base/nodes/Vero/Vero.node.ts @@ -23,7 +23,7 @@ import { export class Vero implements INodeType { description: INodeTypeDescription = { displayName: 'Vero', - name: 'Vero', + name: 'vero', icon: 'file:vero.png', group: ['output'], version: 1, From 3f748e3aa8182314b1b38e83b0818fc853637f74 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 20 Dec 2019 12:53:52 -0600 Subject: [PATCH 08/13] :heavy_check_mark: Fix tests --- packages/core/test/Helpers.ts | 39 ++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index d7d9e8617f..f0483ec02f 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -8,7 +8,9 @@ import { INodeTypeData, IRun, ITaskData, + IWorkflowBase, IWorkflowExecuteAdditionalData, + WorkflowHooks, } from 'n8n-workflow'; import { @@ -248,20 +250,33 @@ export function NodeTypes(): NodeTypesClass { export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise, nodeExecutionOrder: string[]): IWorkflowExecuteAdditionalData { + const hookFunctions = { + nodeExecuteAfter: [ + async (nodeName: string, data: ITaskData): Promise => { + nodeExecutionOrder.push(nodeName); + }, + ], + workflowExecuteAfter: [ + async (fullRunData: IRun): Promise => { + waitPromise.resolve(fullRunData); + }, + ], + }; + + const workflowData: IWorkflowBase = { + name: '', + createdAt: new Date(), + updatedAt: new Date(), + active: true, + nodes: [], + connections: {}, + }; + return { credentials: {}, - hooks: { - nodeExecuteAfter: [ - async (nodeName: string, data: ITaskData): Promise => { - nodeExecutionOrder.push(nodeName); - }, - ], - workflowExecuteAfter: [ - async (fullRunData: IRun): Promise => { - waitPromise.resolve(fullRunData); - }, - ], - }, + hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), + executeWorkflow: async (workflowId: string): Promise => {}, // tslint:disable-line:no-any + restApiUrl: '', encryptionKey: 'test', timezone: 'America/New_York', webhookBaseUrl: 'webhook', From e0752b861de8f21795dd2494ca0167b3091e2483 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 20 Dec 2019 16:58:36 -0600 Subject: [PATCH 09/13] :sparkles: Make it possible to rename workflows --- .../editor-ui/src/components/MainSidebar.vue | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 9b333e9021..0fc72a8e21 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -56,6 +56,12 @@ Save As + + +