Add polling support to Trigger-Nodes

This commit is contained in:
Jan Oberhauser 2019-12-31 14:19:37 -06:00
parent d072321ad4
commit 584033ab4a
12 changed files with 600 additions and 289 deletions

View file

@ -21,6 +21,7 @@ import {
import { import {
IExecuteData, IExecuteData,
IGetExecutePollFunctions,
IGetExecuteTriggerFunctions, IGetExecuteTriggerFunctions,
INode, INode,
INodeExecutionData, INodeExecutionData,
@ -219,20 +220,17 @@ export class ActiveWorkflowRunner {
/** /**
* Return trigger function which gets the global functions from n8n-core * Runs the given workflow
* and overwrites the emit to be able to start it in subprocess
* *
* @param {IWorkflowDb} workflowData * @param {IWorkflowDb} workflowData
* @param {INode} node
* @param {INodeExecutionData[][]} data
* @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData * @param {IWorkflowExecuteAdditionalDataWorkflow} additionalData
* @param {WorkflowExecuteMode} mode * @param {WorkflowExecuteMode} mode
* @returns {IGetExecuteTriggerFunctions} * @returns
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecuteTriggerFunctions{ runWorkflow(workflowData: IWorkflowDb, node: INode, data: INodeExecutionData[][], additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode) {
return ((workflow: Workflow, node: INode) => {
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode);
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
const nodeExecutionStack: IExecuteData[] = [ const nodeExecutionStack: IExecuteData[] = [
{ {
node, node,
@ -263,12 +261,52 @@ export class ActiveWorkflowRunner {
}; };
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
workflowRunner.run(runData, true); 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 returnFunctions;
}); });
} }
/**
* Return trigger 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 {IGetExecuteTriggerFunctions}
* @memberof ActiveWorkflowRunner
*/
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): IGetExecuteTriggerFunctions{
return ((workflow: Workflow, node: INode) => {
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode);
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
this.runWorkflow(workflowData, node, data, additionalData, mode);
};
return returnFunctions;
});
}
/** /**
* Makes a workflow active * Makes a workflow active
* *
@ -303,10 +341,11 @@ export class ActiveWorkflowRunner {
const credentials = await WorkflowCredentials(workflowData.nodes); const credentials = await WorkflowCredentials(workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode); const getTriggerFunctions = this.getExecuteTriggerFunctions(workflowData, additionalData, mode);
const getPollFunctions = this.getExecutePollFunctions(workflowData, additionalData, mode);
// Add the workflows which have webhooks defined // Add the workflows which have webhooks defined
await this.addWorkflowWebhooks(workflowInstance, additionalData, mode); 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 (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them // If there were any activation errors delete them

View file

@ -2,6 +2,7 @@ import {
INodeType, INodeType,
INodeTypes, INodeTypes,
INodeTypeData, INodeTypeData,
NodeHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -11,6 +12,15 @@ class NodeTypesClass implements INodeTypes {
async init(nodeTypes: INodeTypeData): Promise<void> { async init(nodeTypes: INodeTypeData): Promise<void> {
// 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; this.nodeTypes = nodeTypes;
} }

View file

@ -39,6 +39,7 @@
"typescript": "~3.7.4" "typescript": "~3.7.4"
}, },
"dependencies": { "dependencies": {
"cron": "^1.7.2",
"crypto-js": "^3.1.9-1", "crypto-js": "^3.1.9-1",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mmmagic": "^0.5.2", "mmmagic": "^0.5.2",

View file

@ -1,19 +1,23 @@
import { CronJob } from 'cron';
import { import {
IGetExecutePollFunctions,
IGetExecuteTriggerFunctions, IGetExecuteTriggerFunctions,
INode,
IPollResponse,
ITriggerResponse, ITriggerResponse,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import {
export interface WorkflowData { ITriggerTime,
workflow: Workflow; IWorkflowData,
triggerResponse?: ITriggerResponse; } from './';
}
export class ActiveWorkflows { export class ActiveWorkflows {
private workflowData: { private workflowData: {
[key: string]: WorkflowData; [key: string]: IWorkflowData;
} = {}; } = {};
@ -48,7 +52,7 @@ export class ActiveWorkflows {
* @returns {(WorkflowData | undefined)} * @returns {(WorkflowData | undefined)}
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
get(id: string): WorkflowData | undefined { get(id: string): IWorkflowData | undefined {
return this.workflowData[id]; return this.workflowData[id];
} }
@ -62,7 +66,7 @@ export class ActiveWorkflows {
* @returns {Promise<void>} * @returns {Promise<void>}
* @memberof ActiveWorkflows * @memberof ActiveWorkflows
*/ */
async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions): Promise<void> { async add(id: string, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getTriggerFunctions: IGetExecuteTriggerFunctions, getPollFunctions: IGetExecutePollFunctions): Promise<void> {
console.log('ADD ID (active): ' + id); console.log('ADD ID (active): ' + id);
this.workflowData[id] = { this.workflowData[id] = {
@ -78,9 +82,110 @@ export class ActiveWorkflows {
this.workflowData[id].triggerResponse = triggerResponse; 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<IPollResponse>}
* @memberof ActiveWorkflows
*/
async activatePolling(node: INode, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, getPollFunctions: IGetExecutePollFunctions): Promise<IPollResponse> {
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 * Makes a workflow inactive
@ -103,6 +208,10 @@ export class ActiveWorkflows {
await workflowData.triggerResponse.closeFunction(); await workflowData.triggerResponse.closeFunction();
} }
if (workflowData.pollResponse && workflowData.pollResponse.closeFunction) {
await workflowData.pollResponse.closeFunction();
}
delete this.workflowData[id]; delete this.workflowData[id];
} }

View file

@ -8,13 +8,16 @@ import {
ILoadOptionsFunctions as ILoadOptionsFunctionsBase, ILoadOptionsFunctions as ILoadOptionsFunctionsBase,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
IPollFunctions as IPollFunctionsBase,
IPollResponse,
ITriggerFunctions as ITriggerFunctionsBase, ITriggerFunctions as ITriggerFunctionsBase,
ITriggerResponse,
IWebhookFunctions as IWebhookFunctionsBase, IWebhookFunctions as IWebhookFunctionsBase,
IWorkflowSettings as IWorkflowSettingsWorkflow, IWorkflowSettings as IWorkflowSettingsWorkflow,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as request from 'request';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
interface Constructable<T> { interface Constructable<T> {
@ -31,7 +34,7 @@ export interface IProcessMessage {
export interface IExecuteFunctions extends IExecuteFunctionsBase { export interface IExecuteFunctions extends IExecuteFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: request.RequestAPI<requestPromise.RequestPromise, requestPromise.RequestPromiseOptions, request.RequiredUriUrl>, request: requestPromise.RequestPromiseAPI,
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
@ -40,7 +43,16 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase {
export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
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<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
@ -48,12 +60,22 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
export interface ITriggerFunctions extends ITriggerFunctionsBase { export interface ITriggerFunctions extends ITriggerFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: request.RequestAPI<requestPromise.RequestPromise, requestPromise.RequestPromiseOptions, request.RequiredUriUrl>, request: requestPromise.RequestPromiseAPI,
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; 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 { export interface IUserSettings {
encryptionKey?: string; encryptionKey?: string;
tunnelSubdomain?: string; tunnelSubdomain?: string;
@ -61,14 +83,14 @@ export interface IUserSettings {
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
helpers: { helpers: {
request?: request.RequestAPI<requestPromise.RequestPromise, requestPromise.RequestPromiseOptions, request.RequiredUriUrl>, request?: requestPromise.RequestPromiseAPI,
}; };
} }
export interface IHookFunctions extends IHookFunctionsBase { export interface IHookFunctions extends IHookFunctionsBase {
helpers: { helpers: {
request: request.RequestAPI<requestPromise.RequestPromise, requestPromise.RequestPromiseOptions, request.RequiredUriUrl>, request: requestPromise.RequestPromiseAPI,
}; };
} }
@ -76,7 +98,7 @@ export interface IHookFunctions extends IHookFunctionsBase {
export interface IWebhookFunctions extends IWebhookFunctionsBase { export interface IWebhookFunctions extends IWebhookFunctionsBase {
helpers: { helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>; prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: request.RequestAPI<requestPromise.RequestPromise, requestPromise.RequestPromiseOptions, request.RequiredUriUrl>, request: requestPromise.RequestPromiseAPI,
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
}; };
} }
@ -98,3 +120,10 @@ export interface INodeDefinitionFile {
export interface INodeInputDataConnections { export interface INodeInputDataConnections {
[key: string]: INodeExecutionData[][]; [key: string]: INodeExecutionData[][];
} }
export interface IWorkflowData {
pollResponse?: IPollResponse;
triggerResponse?: ITriggerResponse;
workflow: Workflow;
}

View file

@ -17,6 +17,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
INodeType, INodeType,
IPollFunctions,
IRunExecutionData, IRunExecutionData,
ITaskDataConnections, ITaskDataConnections,
ITriggerFunctions, 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. * Returns the execute functions the trigger nodes have access to.
* *

View file

@ -5,22 +5,21 @@ import {
IHookFunctions, IHookFunctions,
ILoadOptionsFunctions, ILoadOptionsFunctions,
IExecuteSingleFunctions, IExecuteSingleFunctions,
IPollFunctions,
ITriggerFunctions, ITriggerFunctions,
BINARY_ENCODING,
getLoadOptionsFunctions
} from 'n8n-core'; } from 'n8n-core';
import { import {
IDataObject, IDataObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function togglApiRequest(this: ITriggerFunctions | IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // 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<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('togglApi'); const credentials = this.getCredentials('togglApi');
if (credentials === undefined) { if (credentials === undefined) {
throw new Error('No credentials got returned!'); throw new Error('No credentials got returned!');
} }
const headerWithAuthentication = Object.assign({}, 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 = { const options: OptionsWithUri = {
headers: headerWithAuthentication, headers: headerWithAuthentication,
@ -36,11 +35,15 @@ export async function togglApiRequest(this: ITriggerFunctions | IHookFunctions |
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message; if (error.statusCode === 403) {
throw new Error('The Toggle credentials are probably invalid!');
if (errorMessage !== undefined) {
throw errorMessage;
} }
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;
} }
} }

View file

@ -1,19 +1,18 @@
import { ITriggerFunctions } from 'n8n-core'; import { IPollFunctions } from 'n8n-core';
import { import {
INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
ITriggerResponse,
IDataObject, IDataObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CronJob } from 'cron';
import * as moment from 'moment'; import * as moment from 'moment';
import { togglApiRequest } from './GenericFunctions'; import { togglApiRequest } from './GenericFunctions';
export class TogglTrigger implements INodeType { export class TogglTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Toggl', displayName: 'Toggl',
name: 'Toggl', name: 'toggl',
icon: 'file:toggl.png', icon: 'file:toggl.png',
group: ['trigger'], group: ['trigger'],
version: 1, version: 1,
@ -28,6 +27,7 @@ export class TogglTrigger implements INodeType {
required: true, required: true,
} }
], ],
polling: true,
inputs: [], inputs: [],
outputs: ['main'], outputs: ['main'],
properties: [ properties: [
@ -44,232 +44,36 @@ export class TogglTrigger implements INodeType {
required: true, required: true,
default: 'newTimeEntry', 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:<ul><li>Seconds: 0-59</li><li>Minutes: 0 - 59</li><li>Hours: 0 - 23</li><li>Day of Month: 1 - 31</li><li>Months: 0 - 11 (Jan - Dec)</li><li>Day of Week: 0 - 6 (Sun - Sat)</li></ul>',
},
] ]
}; };
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> { async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const webhookData = this.getWorkflowStaticData('node'); const webhookData = this.getWorkflowStaticData('node');
const mode = this.getNodeParameter('mode') as string;
const event = this.getNodeParameter('event') as string; const event = this.getNodeParameter('event') as string;
// Get all the trigger times
let cronTime;
let endpoint: string; 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') { if (event === 'newTimeEntry') {
endpoint = '/time_entries'; endpoint = '/time_entries';
} else {
throw new Error(`The defined event "${event}" is not supported`);
} }
const executeTrigger = async () => {
const qs: IDataObject = {}; const qs: IDataObject = {};
let timeEntries = []; let timeEntries = [];
qs.start_date = webhookData.lastTimeChecked; qs.start_date = webhookData.lastTimeChecked;
qs.end_date = moment().format(); qs.end_date = moment().format();
try { try {
timeEntries = await togglApiRequest.call(this, 'GET', endpoint, {}, qs); timeEntries = await togglApiRequest.call(this, 'GET', endpoint, {}, qs);
webhookData.lastTimeChecked = qs.end_date;
} catch (err) { } catch (err) {
throw new Error(`Toggl Trigger Error: ${err}`); throw new Error(`Toggl Trigger Error: ${err}`);
} }
if (Array.isArray(timeEntries) && timeEntries.length !== 0) { if (Array.isArray(timeEntries) && timeEntries.length !== 0) {
this.emit([this.helpers.returnJsonArray(timeEntries)]); return [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() { return null;
executeTrigger();
}
if (webhookData.lastTimeChecked === undefined) {
webhookData.lastTimeChecked = moment().format();
}
return {
closeFunction,
manualTriggerFunction,
};
} }
} }

View file

@ -194,7 +194,7 @@
"aws4": "^1.8.0", "aws4": "^1.8.0",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"cron": "^1.6.0", "cron": "^1.7.2",
"glob-promise": "^3.4.0", "glob-promise": "^3.4.0",
"gm": "^1.23.1", "gm": "^1.23.1",
"googleapis": "^46.0.0", "googleapis": "^46.0.0",

View file

@ -107,6 +107,10 @@ export interface IDataObject {
} }
export interface IGetExecutePollFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IPollFunctions;
}
export interface IGetExecuteTriggerFunctions { export interface IGetExecuteTriggerFunctions {
(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions; (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 { export interface ITriggerFunctions {
emit(data: INodeExecutionData[][]): void; emit(data: INodeExecutionData[][]): void;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
@ -285,6 +302,7 @@ export interface INodeExecutionData {
export interface INodeExecuteFunctions { export interface INodeExecuteFunctions {
getExecutePollFunctions: IGetExecutePollFunctions;
getExecuteTriggerFunctions: IGetExecuteTriggerFunctions; getExecuteTriggerFunctions: IGetExecuteTriggerFunctions;
getExecuteFunctions: IGetExecuteFunctions; getExecuteFunctions: IGetExecuteFunctions;
getExecuteSingleFunctions: IGetExecuteSingleFunctions; getExecuteSingleFunctions: IGetExecuteSingleFunctions;
@ -363,6 +381,10 @@ export interface IParameterDependencies {
[key: string]: string[]; [key: string]: string[];
} }
export interface IPollResponse {
closeFunction?: () => Promise<void>;
}
export interface ITriggerResponse { export interface ITriggerResponse {
closeFunction?: () => Promise<void>; closeFunction?: () => Promise<void>;
// To manually trigger the run // To manually trigger the run
@ -376,6 +398,7 @@ export interface INodeType {
description: INodeTypeDescription; description: INodeTypeDescription;
execute?(this: IExecuteFunctions): Promise<INodeExecutionData[][] | null>; execute?(this: IExecuteFunctions): Promise<INodeExecutionData[][] | null>;
executeSingle?(this: IExecuteSingleFunctions): Promise<INodeExecutionData>; executeSingle?(this: IExecuteSingleFunctions): Promise<INodeExecutionData>;
poll?(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
trigger?(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>; trigger?(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>;
webhook?(this: IWebhookFunctions): Promise<IWebhookResponseData>; webhook?(this: IWebhookFunctions): Promise<IWebhookResponseData>;
hooks?: { hooks?: {
@ -447,6 +470,7 @@ export interface INodeTypeDescription {
properties: INodeProperties[]; properties: INodeProperties[];
credentials?: INodeCredentialDescription[]; credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow maxNodes?: number; // How many nodes of that type can be created in a workflow
polling?: boolean;
subtitle?: string; subtitle?: string;
hooks?: { hooks?: {
[key: string]: INodeHookDescription[] | undefined; [key: string]: INodeHookDescription[] | undefined;

View file

@ -23,6 +23,194 @@ import {
import { get } from 'lodash'; 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:<ul><li>Seconds: 0-59</li><li>Minutes: 0 - 59</li><li>Hours: 0 - 23</li><li>Day of Month: 1 - 31</li><li>Months: 0 - 11 (Jan - Dec)</li><li>Day of Week: 0 - 6 (Sun - Sat)</li></ul>',
},
],
},
],
},
];
}
return [];
}
/** /**
* Returns if the parameter should be displayed or not * Returns if the parameter should be displayed or not
* *

View file

@ -3,27 +3,28 @@ import {
IConnections, IConnections,
IGetExecuteTriggerFunctions, IGetExecuteTriggerFunctions,
INode, INode,
NodeHelpers,
INodes, INodes,
INodeExecuteFunctions, INodeExecuteFunctions,
INodeExecutionData, INodeExecutionData,
INodeParameters,
INodeIssues, INodeIssues,
NodeParameterValue, INodeParameters,
INodeType, INodeType,
INodeTypes, INodeTypes,
ObservableObject, IPollFunctions,
IRunExecutionData, IRunExecutionData,
ITaskDataConnections, ITaskDataConnections,
ITriggerResponse, ITriggerResponse,
IWebhookData, IWebhookData,
IWebhookResponseData, IWebhookResponseData,
WebhookSetupMethodNames,
WorkflowDataProxy,
IWorfklowIssues, IWorfklowIssues,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
IWorkflowSettings, IWorkflowSettings,
NodeHelpers,
NodeParameterValue,
ObservableObject,
WebhookSetupMethodNames,
WorkflowDataProxy,
WorkflowExecuteMode,
} from './'; } from './';
// @ts-ignore // @ts-ignore
@ -188,7 +189,7 @@ export class Workflow {
continue; 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. // Is a trigger node. So workflow can be activated.
return true; return true;
} }
@ -289,6 +290,30 @@ export class Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
getTriggerNodes(): INode[] { 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[] = []; const returnNodes: INode[] = [];
// Check if it has any of them // Check if it has any of them
@ -304,7 +329,7 @@ export class Workflow {
nodeType = this.nodeTypes.getByName(node.type); nodeType = this.nodeTypes.getByName(node.type);
if (nodeType !== undefined && nodeType.trigger) { if (nodeType !== undefined && checkFunction(nodeType)) {
returnNodes.push(node); returnNodes.push(node);
} }
} }
@ -729,14 +754,14 @@ export class Workflow {
// Check which node to return as start node // 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 node: INode;
let nodeType: INodeType; let nodeType: INodeType;
for (const nodeName of nodeNames) { for (const nodeName of nodeNames) {
node = this.nodes[nodeName]; node = this.nodes[nodeName];
nodeType = this.nodeTypes.getByName(node.type) as INodeType; nodeType = this.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.trigger !== undefined) { if (nodeType.trigger !== undefined || nodeType.poll !== undefined) {
return node; 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<INodeExecutionData[][] | null> {
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 * Executes the webhook data to see what it should return and if the
* workflow should be started or not * workflow should be started or not
@ -1096,6 +1145,9 @@ export class Workflow {
} else if (nodeType.execute) { } else if (nodeType.execute) {
const thisArgs = nodeExecuteFunctions.getExecuteFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, additionalData, mode); const thisArgs = nodeExecuteFunctions.getExecuteFunctions(this, runExecutionData, runIndex, connectionInputData, inputData, node, additionalData, mode);
return nodeType.execute.call(thisArgs); 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) { } else if (nodeType.trigger) {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode start the trigger // In manual mode start the trigger