From 0e340a1e2b843331ec3039aca5538de3730dfdd6 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 21 Dec 2019 21:44:56 -0500 Subject: [PATCH 1/3] done done --- .../credentials/TogglApi.credentials.ts | 24 ++ .../nodes/Toggl/GenericFunctions.ts | 46 +++ .../nodes/Toggl/TogglTrigger.node.ts | 275 ++++++++++++++++++ packages/nodes-base/nodes/Toggl/toggl.png | Bin 0 -> 36857 bytes packages/nodes-base/package.json | 6 +- 5 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/credentials/TogglApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Toggl/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Toggl/toggl.png diff --git a/packages/nodes-base/credentials/TogglApi.credentials.ts b/packages/nodes-base/credentials/TogglApi.credentials.ts new file mode 100644 index 0000000000..d2af640c46 --- /dev/null +++ b/packages/nodes-base/credentials/TogglApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class TogglApi implements ICredentialType { + name = 'togglApi'; + displayName = 'Toggl API'; + properties = [ + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Toggl/GenericFunctions.ts b/packages/nodes-base/nodes/Toggl/GenericFunctions.ts new file mode 100644 index 0000000000..c971d7e2bb --- /dev/null +++ b/packages/nodes-base/nodes/Toggl/GenericFunctions.ts @@ -0,0 +1,46 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, + ITriggerFunctions, + BINARY_ENCODING, + getLoadOptionsFunctions +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function togglApiRequest(this: ITriggerFunctions | IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('togglApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const headerWithAuthentication = Object.assign({}, + { Authorization: ` Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString(BINARY_ENCODING)}` }); + + const options: OptionsWithUri = { + headers: headerWithAuthentication, + method, + qs: query, + uri: uri || `https://www.toggl.com/api/v8${resource}`, + body, + json: true + }; + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.response.body.message || error.response.body.Message; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} diff --git a/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts b/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts new file mode 100644 index 0000000000..4b515ff26d --- /dev/null +++ b/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts @@ -0,0 +1,275 @@ +import { ITriggerFunctions } from 'n8n-core'; +import { + INodeType, + INodeTypeDescription, + ITriggerResponse, + IDataObject, +} from 'n8n-workflow'; + +import { CronJob } from 'cron'; +import * as moment from 'moment'; +import { togglApiRequest } from './GenericFunctions'; + +export class TogglTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Toggl', + name: 'Toggl', + icon: 'file:toggl.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Toggl events occure', + defaults: { + name: 'Toggl', + color: '#00FF00', + }, + credentials: [ + { + name: 'togglApi', + required: true, + } + ], + inputs: [], + outputs: ['main'], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { + name: 'New Time Entry', + value: 'newTimeEntry', + } + ], + required: true, + default: 'newTimeEntry', + }, + { + displayName: 'Mode', + name: 'mode', + type: 'options', + options: [ + { + name: 'Every Minute', + value: 'everyMinute' + }, + { + name: 'Every Hour', + value: 'everyHour' + }, + { + name: 'Every Day', + value: 'everyDay' + }, + { + name: 'Every Week', + value: 'everyWeek' + }, + { + name: 'Every Month', + value: 'everyMonth' + }, + { + name: 'Custom', + value: 'custom' + }, + ], + default: 'everyDay', + description: 'How often to trigger.', + }, + { + displayName: 'Hour', + name: 'hour', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 23, + }, + displayOptions: { + hide: { + mode: [ + 'custom', + 'everyHour', + 'everyMinute' + ], + }, + }, + default: 14, + description: 'The hour of the day to trigger (24h format).', + }, + { + displayName: 'Minute', + name: 'minute', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 59, + }, + displayOptions: { + hide: { + mode: [ + 'custom', + 'everyMinute' + ], + }, + }, + default: 0, + description: 'The minute of the day to trigger.', + }, + { + displayName: 'Day of Month', + name: 'dayOfMonth', + type: 'number', + displayOptions: { + show: { + mode: [ + 'everyMonth', + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 31, + }, + default: 1, + description: 'The day of the month to trigger.', + }, + { + displayName: 'Weekday', + name: 'weekday', + type: 'options', + displayOptions: { + show: { + mode: [ + 'everyWeek', + ], + }, + }, + options: [ + { + name: 'Monday', + value: '1', + }, + { + name: 'Tuesday', + value: '2', + }, + { + name: 'Wednesday', + value: '3', + }, + { + name: 'Thursday', + value: '4', + }, + { + name: 'Friday', + value: '5', + }, + { + name: 'Saturday', + value: '6', + }, + { + name: 'Sunday', + value: '0', + }, + ], + default: '1', + description: 'The weekday to trigger.', + }, + { + displayName: 'Cron Expression', + name: 'cronExpression', + type: 'string', + displayOptions: { + show: { + mode: [ + 'custom', + ], + }, + }, + default: '* * * * * *', + description: 'Use custom cron expression. Values and ranges as follows:', + }, + ] + }; + + async trigger(this: ITriggerFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const mode = this.getNodeParameter('mode') as string; + const event = this.getNodeParameter('event') as string; + // Get all the trigger times + let cronTime; + let endpoint: string; + //let parameterName: string; + if (mode === 'custom') { + const cronExpression = this.getNodeParameter('cronExpression') as string; + cronTime = cronExpression as string; + } + if (mode === 'everyMinute') { + cronTime = `* * * * *`; + } + if (mode === 'everyHour') { + const minute = this.getNodeParameter('minute') as string; + cronTime = `${minute} * * * *`; + } + if (mode === 'everyDay') { + const hour = this.getNodeParameter('hour') as string; + const minute = this.getNodeParameter('minute') as string; + cronTime = `${minute} ${hour} * * *`; + } + if (mode === 'everyWeek') { + const weekday = this.getNodeParameter('weekday') as string; + const hour = this.getNodeParameter('hour') as string; + const minute = this.getNodeParameter('minute') as string; + cronTime = `${minute} ${hour} * * ${weekday}`; + } + if (mode === 'everyMonth') { + const dayOfMonth = this.getNodeParameter('dayOfMonth') as string; + const hour = this.getNodeParameter('hour') as string; + const minute = this.getNodeParameter('minute') as string; + cronTime = `${minute} ${hour} ${dayOfMonth} * *`; + } + if (event === 'newTimeEntry') { + endpoint = '/time_entries'; + } + + const executeTrigger = async () => { + const qs: IDataObject = {}; + let timeEntries = []; + qs.start_date = webhookData.lastTimeChecked; + qs.end_date = moment().format(); + try { + timeEntries = await togglApiRequest.call(this, 'GET', endpoint, {}, qs); + } catch (err) { + throw new Error(`Toggl Trigger Error: ${err}`); + } + if (Array.isArray(timeEntries) && timeEntries.length !== 0) { + this.emit([this.helpers.returnJsonArray(timeEntries)]); + } + webhookData.lastTimeChecked = qs.end_date; + }; + + const timezone = this.getTimezone(); + + // Start the cron-jobs + const cronJob = new CronJob(cronTime as string, executeTrigger, undefined, true, timezone); + + // Stop the cron-jobs + async function closeFunction() { + cronJob.stop(); + } + + async function manualTriggerFunction() { + executeTrigger(); + } + if (webhookData.lastTimeChecked === undefined) { + webhookData.lastTimeChecked = moment().format(); + } + return { + closeFunction, + manualTriggerFunction, + }; + } +} diff --git a/packages/nodes-base/nodes/Toggl/toggl.png b/packages/nodes-base/nodes/Toggl/toggl.png new file mode 100644 index 0000000000000000000000000000000000000000..06cbdb7caac83739e48625b22b8d57615f3072f1 GIT binary patch literal 36857 zcmeHQ2|SeB-=897r%frcM3G^3GZV6J*+QkR%FGxH!_1u-TdSpoNGd5y+!n1$g-Tj1 zrA@Raty)zSH>J|@KF^HjnW;LU;o z*Nj3%Vo<0T0Vou8BMPO#%{@8|OhumSJd2M)nd*bT17ZrMM4?cN`7B>QfuFk@mB9%& zr3G^6Ow*WfE)W~YIfe@S8_pEabYjBU5qxTlrJgVb75EiM~Q>DY02 zOdX;r!4#usrJ$puW5EjyqI%mqcEf>hmU5^+kz5>DV**3?7RsU~wXJz;tPJPNcw6PY=up{^>4fI2X<+g5O;$ zpnhgCG_D!e6l2y4$ld+Nu<-C6k$izSQ(UtiGvxcma+zk{Og<-)$6x}{^?ND;U*Hf0 zRH?g({zgI$QOpQIk4XW<`FaKaI4yx0H@L?N0br*eLwm;Y1@^4)9|J`V0Yqa)@Bt

{@K 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 J`)mUe{s(k^K)C<_ literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5afa6b431c..a657584302 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/TogglApi.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -152,7 +153,8 @@ "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", - "dist/nodes/Typeform/TypeformTrigger.node.js", + "dist/nodes/Typeform/TypeformTrigger.node.js", + "dist/nodes/Toggl/TogglTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Xml.node.js", From c630a3583c5504ecb28c38c668e2ce80fcff2411 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 22 Dec 2019 08:12:12 -0500 Subject: [PATCH 2/3] fix --- packages/nodes-base/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d767f6beed..7912bf3169 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -72,13 +72,9 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", -<<<<<<< HEAD - "dist/credentials/TypeformApi.credentials.js", - "dist/credentials/TogglApi.credentials.js" -======= "dist/credentials/TypeformApi.credentials.js", + "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js" ->>>>>>> master ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", From 584033ab4a99c6047b31df50ca038d071b7a01ac Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 31 Dec 2019 14:19:37 -0600 Subject: [PATCH 3/3] :sparkles: Add polling support to Trigger-Nodes --- packages/cli/src/ActiveWorkflowRunner.ts | 105 +++++--- packages/cli/src/NodeTypes.ts | 10 + packages/core/package.json | 1 + packages/core/src/ActiveWorkflows.ts | 125 ++++++++- packages/core/src/Interfaces.ts | 43 +++- packages/core/src/NodeExecuteFunctions.ts | 52 ++++ .../nodes/Toggl/GenericFunctions.ts | 21 +- .../nodes/Toggl/TogglTrigger.node.ts | 240 ++---------------- packages/nodes-base/package.json | 6 +- packages/workflow/src/Interfaces.ts | 24 ++ packages/workflow/src/NodeHelpers.ts | 188 ++++++++++++++ packages/workflow/src/Workflow.ts | 74 +++++- 12 files changed, 600 insertions(+), 289 deletions(-) diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 6a0e5cb02b..9604fd9de6 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -21,6 +21,7 @@ import { import { IExecuteData, + IGetExecutePollFunctions, IGetExecuteTriggerFunctions, INode, INodeExecutionData, @@ -218,6 +219,73 @@ export class ActiveWorkflowRunner { } + /** + * Runs the given workflow + * + * @param {IWorkflowDb} workflowData + * @param {INode} node + * @param {INodeExecutionData[][]} data + * @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData + * @param {WorkflowExecuteMode} mode + * @returns + * @memberof ActiveWorkflowRunner + */ + runWorkflow(workflowData: IWorkflowDb, node: INode, data: INodeExecutionData[][], additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode) { + const nodeExecutionStack: IExecuteData[] = [ + { + node, + data: { + main: data, + } + } + ]; + + const executionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack, + waitingExecution: {}, + }, + }; + + // Start the workflow + const runData: IWorkflowExecutionDataProcess = { + credentials: additionalData.credentials, + executionMode: mode, + executionData, + workflowData, + }; + + const workflowRunner = new WorkflowRunner(); + return workflowRunner.run(runData, true); + } + + + /** + * Return poll function which gets the global functions from n8n-core + * and overwrites the __emit to be able to start it in subprocess + * + * @param {IWorkflowDb} workflowData + * @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData + * @param {WorkflowExecuteMode} mode + * @returns {IGetExecutePollFunctions} + * @memberof ActiveWorkflowRunner + */ + getExecutePollFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecutePollFunctions { + return ((workflow: Workflow, node: INode) => { + const returnFunctions = NodeExecuteFunctions.getExecutePollFunctions(workflow, node, additionalData, mode); + returnFunctions.__emit = (data: INodeExecutionData[][]): void => { + this.runWorkflow(workflowData, node, data, additionalData, mode); + }; + return returnFunctions; + }); + } + + /** * Return trigger function which gets the global functions from n8n-core * and overwrites the emit to be able to start it in subprocess @@ -232,43 +300,13 @@ export class ActiveWorkflowRunner { return ((workflow: Workflow, node: INode) => { const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode); returnFunctions.emit = (data: INodeExecutionData[][]): void => { - - const nodeExecutionStack: IExecuteData[] = [ - { - node, - data: { - main: data, - } - } - ]; - - const executionData: IRunExecutionData = { - startData: {}, - resultData: { - runData: {}, - }, - executionData: { - contextData: {}, - nodeExecutionStack, - waitingExecution: {}, - }, - }; - - // Start the workflow - const runData: IWorkflowExecutionDataProcess = { - credentials: additionalData.credentials, - executionMode: mode, - executionData, - workflowData, - }; - - const workflowRunner = new WorkflowRunner(); - workflowRunner.run(runData, true); + this.runWorkflow(workflowData, node, data, additionalData, mode); }; return returnFunctions; }); } + /** * Makes a workflow active * @@ -303,10 +341,11 @@ export class ActiveWorkflowRunner { const credentials = await WorkflowCredentials(workflowData.nodes); const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode); + const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode); // Add the workflows which have webhooks defined await this.addWorkflowWebhooks(workflowInstance, additionalData, mode); - await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions); + await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions, getPollFunctions); if (this.activationErrors[workflowId] !== undefined) { // If there were any activation errors delete them diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index aaae63effd..66b2363bfe 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -2,6 +2,7 @@ import { INodeType, INodeTypes, INodeTypeData, + NodeHelpers, } from 'n8n-workflow'; @@ -11,6 +12,15 @@ class NodeTypesClass implements INodeTypes { async init(nodeTypes: INodeTypeData): Promise { + // Some nodeTypes need to get special parameters applied like the + // polling nodes the polling times + for (const nodeTypeData of Object.values(nodeTypes)) { + const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type) + + if (applyParameters.length) { + nodeTypeData.type.description.properties.unshift.apply(nodeTypeData.type.description.properties, applyParameters); + } + } this.nodeTypes = nodeTypes; } diff --git a/packages/core/package.json b/packages/core/package.json index 716f466f02..7186564ec2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,7 @@ "typescript": "~3.7.4" }, "dependencies": { + "cron": "^1.7.2", "crypto-js": "^3.1.9-1", "lodash.get": "^4.4.2", "mmmagic": "^0.5.2", diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 0a7b3df268..9cd4f0079a 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -1,19 +1,23 @@ +import { CronJob } from 'cron'; + import { + IGetExecutePollFunctions, IGetExecuteTriggerFunctions, + INode, + IPollResponse, ITriggerResponse, IWorkflowExecuteAdditionalData, Workflow, } from 'n8n-workflow'; - -export interface WorkflowData { - workflow: Workflow; - triggerResponse?: ITriggerResponse; -} +import { + ITriggerTime, + IWorkflowData, +} from './'; export class ActiveWorkflows { private workflowData: { - [key: string]: WorkflowData; + [key: string]: IWorkflowData; } = {}; @@ -48,7 +52,7 @@ export class ActiveWorkflows { * @returns {(WorkflowData | undefined)} * @memberof ActiveWorkflows */ - get(id: string): WorkflowData | undefined { + get(id: string): IWorkflowData | undefined { return this.workflowData[id]; } @@ -62,7 +66,7 @@ export class ActiveWorkflows { * @returns {Promise} * @memberof ActiveWorkflows */ - async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions): Promise { + async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise { console.log('ADD ID (active): ' + id); this.workflowData[id] = { @@ -78,9 +82,110 @@ export class ActiveWorkflows { this.workflowData[id].triggerResponse = triggerResponse; } } + + const pollNodes = workflow.getPollNodes(); + for (const pollNode of pollNodes) { + this.workflowData[id].pollResponse = await this.activatePolling(pollNode, workflow, additionalData, getPollFunctions); + } } + /** + * Activates polling for the given node + * + * @param {INode} node + * @param {Workflow} workflow + * @param {IWorkflowExecuteAdditionalData} additionalData + * @param {IGetExecutePollFunctions} getPollFunctions + * @returns {Promise} + * @memberof ActiveWorkflows + */ + async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions): Promise { + const mode = 'trigger'; + + const pollFunctions = getPollFunctions(workflow, node, additionalData, mode); + + const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as { + item: ITriggerTime[]; + }; + + // Define the order the cron-time-parameter appear + const parameterOrder = [ + 'second', // 0 - 59 + 'minute', // 0 - 59 + 'hour', // 0 - 23 + 'dayOfMonth', // 1 - 31 + 'month', // 0 - 11(Jan - Dec) + 'weekday', // 0 - 6(Sun - Sat) + ]; + + // Get all the trigger times + const cronTimes: string[] = []; + let cronTime: string[]; + let parameterName: string; + if (pollTimes.item !== undefined) { + for (const item of pollTimes.item) { + cronTime = []; + if (item.mode === 'custom') { + cronTimes.push(item.cronExpression as string); + continue; + } + if (item.mode === 'everyMinute') { + cronTimes.push(`${Math.floor(Math.random() * 60).toString()} * * * * *`); + continue; + } + + for (parameterName of parameterOrder) { + if (item[parameterName] !== undefined) { + // Value is set so use it + cronTime.push(item[parameterName] as string); + } else if (parameterName === 'second') { + // For seconds we use by default a random one to make sure to + // balance the load a little bit over time + cronTime.push(Math.floor(Math.random() * 60).toString()); + } else { + // For all others set "any" + cronTime.push('*'); + } + } + + cronTimes.push(cronTime.join(' ')); + } + } + + // The trigger function to execute when the cron-time got reached + const executeTrigger = async () => { + const pollResponse = await workflow.runPoll(node, pollFunctions); + + if (pollResponse !== null) { + // TODO: Run workflow + pollFunctions.__emit(pollResponse); + } + }; + + // Execute the trigger directly to be able to know if it works + await executeTrigger(); + + const timezone = pollFunctions.getTimezone(); + + // Start the cron-jobs + const cronJobs: CronJob[] = []; + for (const cronTime of cronTimes) { + cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone)); + } + + // Stop the cron-jobs + async function closeFunction() { + for (const cronJob of cronJobs) { + cronJob.stop(); + } + } + + return { + closeFunction, + }; + } + /** * Makes a workflow inactive @@ -103,6 +208,10 @@ export class ActiveWorkflows { await workflowData.triggerResponse.closeFunction(); } + if (workflowData.pollResponse && workflowData.pollResponse.closeFunction) { + await workflowData.pollResponse.closeFunction(); + } + delete this.workflowData[id]; } diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 76b6eea932..3b9a501969 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -8,13 +8,16 @@ import { ILoadOptionsFunctions as ILoadOptionsFunctionsBase, INodeExecutionData, INodeType, + IPollFunctions as IPollFunctionsBase, + IPollResponse, ITriggerFunctions as ITriggerFunctionsBase, + ITriggerResponse, IWebhookFunctions as IWebhookFunctionsBase, IWorkflowSettings as IWorkflowSettingsWorkflow, + Workflow, } from 'n8n-workflow'; -import * as request from 'request'; import * as requestPromise from 'request-promise-native'; interface Constructable { @@ -31,7 +34,7 @@ export interface IProcessMessage { export interface IExecuteFunctions extends IExecuteFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: request.RequestAPI, + request: requestPromise.RequestPromiseAPI, returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -40,7 +43,16 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase { export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: request.RequestAPI < requestPromise.RequestPromise, requestPromise.RequestPromiseOptions, request.RequiredUriUrl >, + request: requestPromise.RequestPromiseAPI, + }; +} + + +export interface IPollFunctions extends IPollFunctionsBase { + helpers: { + prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; + request: requestPromise.RequestPromiseAPI, + returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -48,12 +60,22 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { export interface ITriggerFunctions extends ITriggerFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: request.RequestAPI, + request: requestPromise.RequestPromiseAPI, returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } +export interface ITriggerTime { + mode: string; + hour: number; + minute: number; + dayOfMonth: number; + weekeday: number; + [key: string]: string | number; +} + + export interface IUserSettings { encryptionKey?: string; tunnelSubdomain?: string; @@ -61,14 +83,14 @@ export interface IUserSettings { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { - request?: request.RequestAPI, + request?: requestPromise.RequestPromiseAPI, }; } export interface IHookFunctions extends IHookFunctionsBase { helpers: { - request: request.RequestAPI, + request: requestPromise.RequestPromiseAPI, }; } @@ -76,7 +98,7 @@ export interface IHookFunctions extends IHookFunctionsBase { export interface IWebhookFunctions extends IWebhookFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; - request: request.RequestAPI, + request: requestPromise.RequestPromiseAPI, returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -98,3 +120,10 @@ export interface INodeDefinitionFile { export interface INodeInputDataConnections { [key: string]: INodeExecutionData[][]; } + + +export interface IWorkflowData { + pollResponse?: IPollResponse; + triggerResponse?: ITriggerResponse; + workflow: Workflow; +} diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 775c2fd754..4fc7c2dd40 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -17,6 +17,7 @@ import { INodeExecutionData, INodeParameters, INodeType, + IPollFunctions, IRunExecutionData, ITaskDataConnections, ITriggerFunctions, @@ -310,6 +311,57 @@ export function getWebhookDescription(name: string, workflow: Workflow, node: IN +/** + * Returns the execute functions the poll nodes have access to. + * + * @export + * @param {Workflow} workflow + * @param {INode} node + * @param {IWorkflowExecuteAdditionalData} additionalData + * @param {WorkflowExecuteMode} mode + * @returns {ITriggerFunctions} + */ +// TODO: Check if I can get rid of: additionalData, and so then maybe also at ActiveWorkflowRunner.add +export function getExecutePollFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IPollFunctions { + return ((workflow: Workflow, node: INode) => { + return { + __emit: (data: INodeExecutionData[][]): void => { + throw new Error('Overwrite NodeExecuteFunctions.getExecutePullFunctions.__emit function!'); + }, + getCredentials(type: string): ICredentialDataDecryptedObject | undefined { + return getCredentials(workflow, node, type, additionalData); + }, + getMode: (): WorkflowExecuteMode => { + return mode; + }, + getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any + const runExecutionData: IRunExecutionData | null = null; + const itemIndex = 0; + const runIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + }, + getRestApiUrl: (): string => { + return additionalData.restApiUrl; + }, + getTimezone: (): string => { + return getTimezone(workflow, additionalData); + }, + getWorkflowStaticData(type: string): IDataObject { + return workflow.getStaticData(type, node); + }, + helpers: { + prepareBinaryData, + request: requestPromise, + returnJsonArray, + }, + }; + })(workflow, node); +} + + + /** * Returns the execute functions the trigger nodes have access to. * diff --git a/packages/nodes-base/nodes/Toggl/GenericFunctions.ts b/packages/nodes-base/nodes/Toggl/GenericFunctions.ts index c971d7e2bb..37c6bdb80b 100644 --- a/packages/nodes-base/nodes/Toggl/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Toggl/GenericFunctions.ts @@ -5,22 +5,21 @@ import { IHookFunctions, ILoadOptionsFunctions, IExecuteSingleFunctions, + IPollFunctions, ITriggerFunctions, - BINARY_ENCODING, - getLoadOptionsFunctions } from 'n8n-core'; import { IDataObject, } from 'n8n-workflow'; -export async function togglApiRequest(this: ITriggerFunctions | IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any +export async function togglApiRequest(this: ITriggerFunctions | IPollFunctions | IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('togglApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); } const headerWithAuthentication = Object.assign({}, - { Authorization: ` Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString(BINARY_ENCODING)}` }); + { Authorization: ` Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}` }); const options: OptionsWithUri = { headers: headerWithAuthentication, @@ -36,11 +35,15 @@ export async function togglApiRequest(this: ITriggerFunctions | IHookFunctions | try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message; - - if (errorMessage !== undefined) { - throw errorMessage; + if (error.statusCode === 403) { + throw new Error('The Toggle credentials are probably invalid!'); } - throw error.response.body; + + const errorMessage = error.response.body && (error.response.body.message || error.response.body.Message); + if (errorMessage !== undefined) { + throw new Error(errorMessage); + } + + throw error; } } diff --git a/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts b/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts index 4b515ff26d..6080f13421 100644 --- a/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts +++ b/packages/nodes-base/nodes/Toggl/TogglTrigger.node.ts @@ -1,19 +1,18 @@ -import { ITriggerFunctions } from 'n8n-core'; +import { IPollFunctions } from 'n8n-core'; import { + INodeExecutionData, INodeType, INodeTypeDescription, - ITriggerResponse, IDataObject, } from 'n8n-workflow'; -import { CronJob } from 'cron'; import * as moment from 'moment'; import { togglApiRequest } from './GenericFunctions'; export class TogglTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Toggl', - name: 'Toggl', + name: 'toggl', icon: 'file:toggl.png', group: ['trigger'], version: 1, @@ -28,6 +27,7 @@ export class TogglTrigger implements INodeType { required: true, } ], + polling: true, inputs: [], outputs: ['main'], properties: [ @@ -44,232 +44,36 @@ export class TogglTrigger implements INodeType { required: true, default: 'newTimeEntry', }, - { - displayName: 'Mode', - name: 'mode', - type: 'options', - options: [ - { - name: 'Every Minute', - value: 'everyMinute' - }, - { - name: 'Every Hour', - value: 'everyHour' - }, - { - name: 'Every Day', - value: 'everyDay' - }, - { - name: 'Every Week', - value: 'everyWeek' - }, - { - name: 'Every Month', - value: 'everyMonth' - }, - { - name: 'Custom', - value: 'custom' - }, - ], - default: 'everyDay', - description: 'How often to trigger.', - }, - { - displayName: 'Hour', - name: 'hour', - type: 'number', - typeOptions: { - minValue: 0, - maxValue: 23, - }, - displayOptions: { - hide: { - mode: [ - 'custom', - 'everyHour', - 'everyMinute' - ], - }, - }, - default: 14, - description: 'The hour of the day to trigger (24h format).', - }, - { - displayName: 'Minute', - name: 'minute', - type: 'number', - typeOptions: { - minValue: 0, - maxValue: 59, - }, - displayOptions: { - hide: { - mode: [ - 'custom', - 'everyMinute' - ], - }, - }, - default: 0, - description: 'The minute of the day to trigger.', - }, - { - displayName: 'Day of Month', - name: 'dayOfMonth', - type: 'number', - displayOptions: { - show: { - mode: [ - 'everyMonth', - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 31, - }, - default: 1, - description: 'The day of the month to trigger.', - }, - { - displayName: 'Weekday', - name: 'weekday', - type: 'options', - displayOptions: { - show: { - mode: [ - 'everyWeek', - ], - }, - }, - options: [ - { - name: 'Monday', - value: '1', - }, - { - name: 'Tuesday', - value: '2', - }, - { - name: 'Wednesday', - value: '3', - }, - { - name: 'Thursday', - value: '4', - }, - { - name: 'Friday', - value: '5', - }, - { - name: 'Saturday', - value: '6', - }, - { - name: 'Sunday', - value: '0', - }, - ], - default: '1', - description: 'The weekday to trigger.', - }, - { - displayName: 'Cron Expression', - name: 'cronExpression', - type: 'string', - displayOptions: { - show: { - mode: [ - 'custom', - ], - }, - }, - default: '* * * * * *', - description: 'Use custom cron expression. Values and ranges as follows:

  • Seconds: 0-59
  • Minutes: 0 - 59
  • Hours: 0 - 23
  • Day of Month: 1 - 31
  • Months: 0 - 11 (Jan - Dec)
  • Day of Week: 0 - 6 (Sun - Sat)
', - }, ] }; - async trigger(this: ITriggerFunctions): Promise { + async poll(this: IPollFunctions): Promise { const webhookData = this.getWorkflowStaticData('node'); - const mode = this.getNodeParameter('mode') as string; const event = this.getNodeParameter('event') as string; - // Get all the trigger times - let cronTime; let endpoint: string; - //let parameterName: string; - if (mode === 'custom') { - const cronExpression = this.getNodeParameter('cronExpression') as string; - cronTime = cronExpression as string; - } - if (mode === 'everyMinute') { - cronTime = `* * * * *`; - } - if (mode === 'everyHour') { - const minute = this.getNodeParameter('minute') as string; - cronTime = `${minute} * * * *`; - } - if (mode === 'everyDay') { - const hour = this.getNodeParameter('hour') as string; - const minute = this.getNodeParameter('minute') as string; - cronTime = `${minute} ${hour} * * *`; - } - if (mode === 'everyWeek') { - const weekday = this.getNodeParameter('weekday') as string; - const hour = this.getNodeParameter('hour') as string; - const minute = this.getNodeParameter('minute') as string; - cronTime = `${minute} ${hour} * * ${weekday}`; - } - if (mode === 'everyMonth') { - const dayOfMonth = this.getNodeParameter('dayOfMonth') as string; - const hour = this.getNodeParameter('hour') as string; - const minute = this.getNodeParameter('minute') as string; - cronTime = `${minute} ${hour} ${dayOfMonth} * *`; - } + if (event === 'newTimeEntry') { endpoint = '/time_entries'; + } else { + throw new Error(`The defined event "${event}" is not supported`); } - const executeTrigger = async () => { - const qs: IDataObject = {}; - let timeEntries = []; - qs.start_date = webhookData.lastTimeChecked; - qs.end_date = moment().format(); - try { - timeEntries = await togglApiRequest.call(this, 'GET', endpoint, {}, qs); - } catch (err) { - throw new Error(`Toggl Trigger Error: ${err}`); - } - if (Array.isArray(timeEntries) && timeEntries.length !== 0) { - this.emit([this.helpers.returnJsonArray(timeEntries)]); - } + const qs: IDataObject = {}; + let timeEntries = []; + qs.start_date = webhookData.lastTimeChecked; + qs.end_date = moment().format(); + + try { + timeEntries = await togglApiRequest.call(this, 'GET', endpoint, {}, qs); webhookData.lastTimeChecked = qs.end_date; - }; - - const timezone = this.getTimezone(); - - // Start the cron-jobs - const cronJob = new CronJob(cronTime as string, executeTrigger, undefined, true, timezone); - - // Stop the cron-jobs - async function closeFunction() { - cronJob.stop(); + } catch (err) { + throw new Error(`Toggl Trigger Error: ${err}`); + } + if (Array.isArray(timeEntries) && timeEntries.length !== 0) { + return [this.helpers.returnJsonArray(timeEntries)]; } - async function manualTriggerFunction() { - executeTrigger(); - } - if (webhookData.lastTimeChecked === undefined) { - webhookData.lastTimeChecked = moment().format(); - } - return { - closeFunction, - manualTriggerFunction, - }; + return null; } + } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ac4cf7b610..8280bf53e7 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -156,8 +156,8 @@ "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", - "dist/nodes/Typeform/TypeformTrigger.node.js", - "dist/nodes/Toggl/TogglTrigger.node.js", + "dist/nodes/Typeform/TypeformTrigger.node.js", + "dist/nodes/Toggl/TogglTrigger.node.js", "dist/nodes/Vero/Vero.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", @@ -194,7 +194,7 @@ "aws4": "^1.8.0", "basic-auth": "^2.0.1", "cheerio": "^1.0.0-rc.3", - "cron": "^1.6.0", + "cron": "^1.7.2", "glob-promise": "^3.4.0", "gm": "^1.23.1", "googleapis": "^46.0.0", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 7824ee52cb..be0330d810 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -107,6 +107,10 @@ export interface IDataObject { } +export interface IGetExecutePollFunctions { + (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IPollFunctions; +} + export interface IGetExecuteTriggerFunctions { (workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions; } @@ -208,6 +212,19 @@ export interface IHookFunctions { }; } +export interface IPollFunctions { + __emit(data: INodeExecutionData[][]): void; + 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: { + [key: string]: (...args: any[]) => any //tslint:disable-line:no-any + }; +} + export interface ITriggerFunctions { emit(data: INodeExecutionData[][]): void; getCredentials(type: string): ICredentialDataDecryptedObject | undefined; @@ -285,6 +302,7 @@ export interface INodeExecutionData { export interface INodeExecuteFunctions { + getExecutePollFunctions: IGetExecutePollFunctions; getExecuteTriggerFunctions: IGetExecuteTriggerFunctions; getExecuteFunctions: IGetExecuteFunctions; getExecuteSingleFunctions: IGetExecuteSingleFunctions; @@ -363,6 +381,10 @@ export interface IParameterDependencies { [key: string]: string[]; } +export interface IPollResponse { + closeFunction?: () => Promise; +} + export interface ITriggerResponse { closeFunction?: () => Promise; // To manually trigger the run @@ -376,6 +398,7 @@ export interface INodeType { description: INodeTypeDescription; execute?(this: IExecuteFunctions): Promise; executeSingle?(this: IExecuteSingleFunctions): Promise; + poll?(this: IPollFunctions): Promise; trigger?(this: ITriggerFunctions): Promise; webhook?(this: IWebhookFunctions): Promise; hooks?: { @@ -447,6 +470,7 @@ export interface INodeTypeDescription { properties: INodeProperties[]; credentials?: INodeCredentialDescription[]; maxNodes?: number; // How many nodes of that type can be created in a workflow + polling?: boolean; subtitle?: string; hooks?: { [key: string]: INodeHookDescription[] | undefined; diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index a9f22441da..d398be8394 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -23,6 +23,194 @@ import { import { get } from 'lodash'; + + +/** + * Gets special parameters which should be added to nodeTypes depending + * on their type or configuration + * + * @export + * @param {INodeType} nodeType + * @returns + */ +export function getSpecialNodeParameters(nodeType: INodeType) { + if (nodeType.description.polling === true) { + return [ + { + displayName: 'Poll Times', + name: 'pollTimes', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Poll Time', + }, + default: {}, + description: 'Time at which polling should occur.', + placeholder: 'Add Poll Time', + options: [ + { + name: 'item', + displayName: 'Item', + values: [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + options: [ + { + name: 'Every Minute', + value: 'everyMinute', + }, + { + name: 'Every Hour', + value: 'everyHour', + }, + { + name: 'Every Day', + value: 'everyDay', + }, + { + name: 'Every Week', + value: 'everyWeek', + }, + { + name: 'Every Month', + value: 'everyMonth', + }, + { + name: 'Custom', + value: 'custom', + }, + ], + default: 'everyDay', + description: 'How often to trigger.', + }, + { + displayName: 'Hour', + name: 'hour', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 23, + }, + displayOptions: { + hide: { + mode: [ + 'custom', + 'everyHour', + 'everyMinute', + ], + }, + }, + default: 14, + description: 'The hour of the day to trigger (24h format).', + }, + { + displayName: 'Minute', + name: 'minute', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 59, + }, + displayOptions: { + hide: { + mode: [ + 'custom', + 'everyMinute', + ], + }, + }, + default: 0, + description: 'The minute of the day to trigger.', + }, + { + displayName: 'Day of Month', + name: 'dayOfMonth', + type: 'number', + displayOptions: { + show: { + mode: [ + 'everyMonth', + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 31, + }, + default: 1, + description: 'The day of the month to trigger.', + }, + { + displayName: 'Weekday', + name: 'weekday', + type: 'options', + displayOptions: { + show: { + mode: [ + 'everyWeek', + ], + }, + }, + options: [ + { + name: 'Monday', + value: '1', + }, + { + name: 'Tuesday', + value: '2', + }, + { + name: 'Wednesday', + value: '3', + }, + { + name: 'Thursday', + value: '4', + }, + { + name: 'Friday', + value: '5', + }, + { + name: 'Saturday', + value: '6', + }, + { + name: 'Sunday', + value: '0', + }, + ], + default: '1', + description: 'The weekday to trigger.', + }, + { + displayName: 'Cron Expression', + name: 'cronExpression', + type: 'string', + displayOptions: { + show: { + mode: [ + 'custom', + ], + }, + }, + default: '* * * * * *', + description: 'Use custom cron expression. Values and ranges as follows:
  • Seconds: 0-59
  • Minutes: 0 - 59
  • Hours: 0 - 23
  • Day of Month: 1 - 31
  • Months: 0 - 11 (Jan - Dec)
  • Day of Week: 0 - 6 (Sun - Sat)
', + }, + ], + }, + ], + }, + ]; + } + + return []; +} + + /** * Returns if the parameter should be displayed or not * diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 9efaaa67ec..1ee7991391 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -3,27 +3,28 @@ import { IConnections, IGetExecuteTriggerFunctions, INode, - NodeHelpers, INodes, INodeExecuteFunctions, INodeExecutionData, - INodeParameters, INodeIssues, - NodeParameterValue, + INodeParameters, INodeType, INodeTypes, - ObservableObject, + IPollFunctions, IRunExecutionData, ITaskDataConnections, ITriggerResponse, IWebhookData, IWebhookResponseData, - WebhookSetupMethodNames, - WorkflowDataProxy, IWorfklowIssues, IWorkflowExecuteAdditionalData, - WorkflowExecuteMode, IWorkflowSettings, + NodeHelpers, + NodeParameterValue, + ObservableObject, + WebhookSetupMethodNames, + WorkflowDataProxy, + WorkflowExecuteMode, } from './'; // @ts-ignore @@ -188,7 +189,7 @@ export class Workflow { continue; } - if (nodeType.trigger !== undefined || nodeType.webhook !== undefined) { + if (nodeType.poll !== undefined || nodeType.trigger !== undefined || nodeType.webhook !== undefined) { // Is a trigger node. So workflow can be activated. return true; } @@ -289,6 +290,30 @@ export class Workflow { * @memberof Workflow */ getTriggerNodes(): INode[] { + return this.queryNodes((nodeType: INodeType) => !!nodeType.trigger ); + } + + + /** + * Returns all the poll nodes in the workflow + * + * @returns {INode[]} + * @memberof Workflow + */ + getPollNodes(): INode[] { + return this.queryNodes((nodeType: INodeType) => !!nodeType.poll ); + } + + + /** + * Returns all the nodes in the workflow for which the given + * checkFunction return true + * + * @param {(nodeType: INodeType) => boolean} checkFunction + * @returns {INode[]} + * @memberof Workflow + */ + queryNodes(checkFunction: (nodeType: INodeType) => boolean): INode[] { const returnNodes: INode[] = []; // Check if it has any of them @@ -304,7 +329,7 @@ export class Workflow { nodeType = this.nodeTypes.getByName(node.type); - if (nodeType !== undefined && nodeType.trigger) { + if (nodeType !== undefined && checkFunction(nodeType)) { returnNodes.push(node); } } @@ -729,14 +754,14 @@ export class Workflow { // Check which node to return as start node - // Check if there are any trigger nodes and then return the first one + // Check if there are any trigger or poll nodes and then return the first one let node: INode; let nodeType: INodeType; for (const nodeName of nodeNames) { node = this.nodes[nodeName]; nodeType = this.nodeTypes.getByName(node.type) as INodeType; - if (nodeType.trigger !== undefined) { + if (nodeType.trigger !== undefined || nodeType.poll !== undefined) { return node; } } @@ -994,6 +1019,30 @@ export class Workflow { } + /** + * Runs the given trigger node so that it can trigger the workflow + * when the node has data. + * + * @param {INode} node + * @param {IPollFunctions} pollFunctions + * @returns + * @memberof Workflow + */ + async runPoll(node: INode, pollFunctions: IPollFunctions): Promise { + const nodeType = this.nodeTypes.getByName(node.type); + + if (nodeType === undefined) { + throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`); + } + + if (!nodeType.poll) { + throw new Error(`The node type "${node.type}" of node "${node.name}" does not have a poll function defined.`); + } + + return nodeType.poll!.call(pollFunctions); + } + + /** * Executes the webhook data to see what it should return and if the * workflow should be started or not @@ -1096,6 +1145,9 @@ export class Workflow { } else if (nodeType.execute) { const thisArgs = nodeExecuteFunctions.getExecuteFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, additionalData, mode); return nodeType.execute.call(thisArgs); + } else if (nodeType.poll) { + const thisArgs = nodeExecuteFunctions.getExecutePollFunctions(this, node, additionalData, mode); + return nodeType.poll.call(thisArgs); } else if (nodeType.trigger) { if (mode === 'manual') { // In manual mode start the trigger