Implement Wait functionality (#1817)

* refactor saving

* refactor api layer to be stateless

* refactor header details

* set variable for menu height

* clean up scss

* clean up indentation

* clean up dropdown impl

* refactor no tags view

* split away header

* Fix tslint issues

* Refactor tag manager

* add tags to patch request

* clean up scss

*  Refactor types to entities

* fix issues

* update no workflow error

* clean up tagscontainer

* use getters instead of state

* remove imports

* use custom colors

* clean up tags container

* clean up dropdown

* clean up focusoncreate

*  Ignore mistaken ID in POST /workflows

*  Fix undefined tag ID in PATCH /workflows

*  Shorten response for POST /tags

* remove scss mixins

* clean up imports

*  Implement validation with class-validator

* address ivan's comments

* implement modals

* Fix lint issues

* fix disabling shortcuts

* fix focus issues

* fix focus issues

* fix focus issues with modal

* fix linting issues

* use dispatch

* use constants for modal keys

* fix focus

* fix lint issues

* remove unused prop

* add modal root

* fix lint issues

* remove unused methods

* fix shortcut

* remove max width

*  Fix duplicate entry error for pg and MySQL

* update rename messaging

* update order of buttons

* fix firefox overflow on windows

* fix dropdown height

* 🔨 refactor tag crud controllers

* 🧹 remove unused imports

* use variable for number of items

* fix dropdown spacing

*  Restore type to fix build

*  Fix post-refactor PATCH /workflows/:id

*  Fix PATCH /workflows/:id for zero tags

*  Fix usage count becoming stringified

* address max's comments

* fix filter spacing

* fix blur bug

* address most of ivan's comments

* address tags type concern

* remove defaults

*  return tag id as string

* 🔨 add hooks to tag CUD operations

* 🏎 simplify timestamp pruning

* remove blur event

* fix onblur bug

*  Fix fs import to fix build

* address max's comments

* implement responsive tag container

* fix lint issues

* update tag limits

* address ivan's comments

* remove rename, refactor header, implement new designs for save, remove responsive tag container

* update styling

* update styling

* implement responsive tag container

* implement header tags edit

* implement header tags edit

* fix lint issues

* implement expandable input

* minor fixes

* minor fixes

* use variable

* rename save as

* duplicate fixes

* minor edit fixes

* lint fixes

* style fixes

* hook up saving name

* hook up tags

* clean up impl

* fix dirty state bug

* update limit

* update notification messages

* on click outside

* fix minor bug with count

* lint fixes

* handle minor edge cases

* handle minor edge cases

* handle minor bugs; fix firefox dropdown issue

* Fix min width

* apply tags only after api success

* remove count fix

* clean up workflow tags impl, fix tags delete bug

* fix minor issue

* fix minor spacing issue

* disable wrap for ops

* fix viewport root; save on click in dropdown

* save button loading when saving name/tags

* implement max width on tags container

* implement cleaner create experience

* disable edit while updating

* codacy hex color

* refactor tags container

* fix clickability

* fix workflow open and count

* clean up structure

* fix up lint issues

* fix button size

* increase workflow name limit for larger screen

* tslint fixes

* disable responsiveness for workflow modal

* rename event

* change min width for tags

* clean up pr

* address max's comments on styles

* remove success toasts

* add hover mode to name

* minor fixes

* refactor name preview

* fix name input not to jiggle

* finish up name input

* Fix up add tags

* clean up param

* clean up scss

* fix resizing name

* fix resizing name

* fix resize bug

* clean up edit spacing

* ignore on esc

* fix input bug

* focus input on clear

* build

* fix up add tags clickablity

* remove scrollbars

* move into folders

* clean up multiple patch req

* remove padding top from edit

* update tags on enter

* build

* rollout blur on enter behavior

* rollout esc behavior

* fix tags bug when duplicating tags

* move key to reload tags

* update header spacing

* build

* update hex case

* refactor workflow title

* remove unusued prop

* keep focus on error, fix bug on error

* Fix bug with name / tags toggle on error

* fix connection push bug

* :spakles: Implement wait functionality

* 🐛 Do not delete waiting executions with prune

*  Improve SQLite migration to not lose execution data anymore

*  Make it possible to restart waiting execution via webhook

*  Add missing file

* 🐛 Some more merge fixes

*  Do not show error for Wait-Nodes if in time-mode

*  Make $executionId available in expressions

* 👕 Fix lint issue

* 👕 Fix lint issue

* 👕 Fix lint issue

*  Set the unlimited sleep time as a variable

*  Add also sleeping webhook path to config

*  Make it possible to retrieve restartUrl in workflow

*  Add authentication to Wait-Node in Webhook-Mode

*  Return 404 when trying to restart execution via webhook which does
not support it

*  Make it possible to set absolute time on Wait-Node

*  Remove not needed imports

*  Fix description format

*  Implement missing webhook features on Wait-Node

*  Display webhook variable in NodeWebhooks

*  Include also date in displayed sleep time

*  Make it possible to see sleep time on node

*  Make sure that no executions does get executed twice

*  Add comment

*  Further improvements

*  Make Wait-Node easier to use

*  Add support for "notice" parameter type

* Fixing wait node to work with queue, improved logging and execution view

* Added support for mysql and pg

*  Add support for webhook postfix path

*  Make it possible to stop sleeping executions

*  Fix issue with webhook paths in not webhook mode

*  Remove not needed console.log

*  Update TODOs

*  Increase min time of workflow staying active to descrease possible issue
with overlap

* 👕 Fix lint issue

* 🐛 Fix issues with webhooks

*  Make error message clearer

*  Fix issue with missing execution ID in scaling mode

* Fixed execution list to correctly display waiting executins

* Feature: enable webhook wait workflows to continue after specified time

* Fixed linting

*  Improve waiting description text

*  Fix parameter display issue and rename

*  Remove comment

*  Do not display webhooks on Wait-Node

* Changed wording from restart to resume on wait node

* Fixed wording and inconsistent screen when changing resume modes

* Removed dots from the descriptions

* Changed docs url and renaming postfix to suffix

* Changed names from sleep to wait

*  Apply suggestions from ben

Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>

* Some fixes by Ben

*  Remove console.logs

*  Fixes and improvements

Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
Jan 2021-08-21 14:11:32 +02:00 committed by GitHub
parent 12417ea323
commit 5a179cd5ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1823 additions and 192 deletions

View file

@ -22,6 +22,7 @@ import {
NodeTypes,
Server,
TestWebhooks,
WaitTracker,
} from '../src';
import { IDataObject } from 'n8n-workflow';
@ -284,6 +285,8 @@ export class Start extends Command {
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
await activeWorkflowRunner.init();
const waitTracker = WaitTracker();
const editorUrl = GenericHelpers.getBaseUrl();
this.log(`\nEditor is now accessible via:\n${editorUrl}`);

View file

@ -150,6 +150,7 @@ export class Worker extends Command {
const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, executionTimeoutTimestamp);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
additionalData.executionId = jobData.executionId;
let workflowExecute: WorkflowExecute;
let workflowRun: PCancelable<IRun>;

View file

@ -489,6 +489,12 @@ const config = convict({
env: 'N8N_ENDPOINT_WEBHOOK',
doc: 'Path for webhook endpoint',
},
webhookWaiting: {
format: String,
default: 'webhook-waiting',
env: 'N8N_ENDPOINT_WEBHOOK_WAIT',
doc: 'Path for waiting-webhook endpoint',
},
webhookTest: {
format: String,
default: 'webhook-test',

View file

@ -35,7 +35,10 @@ export class ActiveExecutions {
* @returns {string}
* @memberof ActiveExecutions
*/
async add(executionData: IWorkflowExecutionDataProcess, process?: ChildProcess): Promise<string> {
async add(executionData: IWorkflowExecutionDataProcess, process?: ChildProcess, executionId?: string): Promise<string> {
if (executionId === undefined) {
// Is a new execution so save in DB
const fullExecutionData: IExecutionDb = {
data: executionData.executionData!,
@ -55,10 +58,19 @@ export class ActiveExecutions {
const execution = ResponseHelper.flattenExecutionData(fullExecutionData);
// Save the Execution in DB
const executionResult = await Db.collections.Execution!.save(execution as IExecutionFlattedDb);
executionId = typeof executionResult.id === "object" ? executionResult.id!.toString() : executionResult.id + "";
} else {
// Is an existing execution we want to finish so update in DB
const executionId = typeof executionResult.id === "object" ? executionResult.id!.toString() : executionResult.id + "";
const execution = {
id: executionId,
waitTill: null,
};
// @ts-ignore
await Db.collections.Execution!.update(executionId, execution);
}
this.activeExecutions[executionId] = {
executionData,

View file

@ -209,7 +209,7 @@ export class ActiveWorkflowRunner {
return new Promise((resolve, reject) => {
const executionMode = 'webhook';
//@ts-ignore
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData, workflowStartNode, executionMode, undefined, req, res, (error: Error | null, data: object) => {
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData, workflowStartNode, executionMode, undefined, undefined, undefined, req, res, (error: Error | null, data: object) => {
if (error !== null) {
return reject(error);
}
@ -282,7 +282,7 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner
*/
async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): Promise<void> {
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true);
let path = '' as string | undefined;
for (const webhookData of webhooks) {
@ -368,7 +368,7 @@ export class ActiveWorkflowRunner {
const additionalData = await WorkflowExecuteAdditionalData.getBase();
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData);
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, undefined, true);
for (const webhookData of webhooks) {
await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, 'update', false);

View file

@ -143,7 +143,7 @@ export class CredentialsHelper extends ICredentialsHelper {
if (expressionResolveValues) {
try {
const workflow = new Workflow({ nodes: Object.values(expressionResolveValues.workflow.nodes), connections: expressionResolveValues.workflow.connectionsBySourceNode, active: false, nodeTypes: expressionResolveValues.workflow.nodeTypes });
decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, mode, false, decryptedData) as ICredentialDataDecryptedObject;
decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, mode, {}, false, decryptedData) as ICredentialDataDecryptedObject;
} catch (e) {
e.message += ' [Error resolving credentials]';
throw e;
@ -160,7 +160,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const workflow = new Workflow({ nodes: [node!], connections: {}, active: false, nodeTypes: mockNodeTypes });
// Resolve expressions if any are set
decryptedData = workflow.expression.getComplexParameterValue(node!, decryptedData as INodeParameters, mode, undefined, decryptedData) as ICredentialDataDecryptedObject;
decryptedData = workflow.expression.getComplexParameterValue(node!, decryptedData as INodeParameters, mode, {}, undefined, decryptedData) as ICredentialDataDecryptedObject;
}
// Load and apply the credentials overwrites if any exist

View file

@ -150,6 +150,7 @@ export interface IExecutionBase {
// Data in regular format with references
export interface IExecutionDb extends IExecutionBase {
data: IRunExecutionData;
waitTill?: Date;
workflowData?: IWorkflowBase;
}
@ -163,6 +164,7 @@ export interface IExecutionResponse extends IExecutionBase {
data: IRunExecutionData;
retryOf?: string;
retrySuccessId?: string;
waitTill?: Date;
workflowData: IWorkflowBase;
}
@ -176,6 +178,7 @@ export interface IExecutionFlatted extends IExecutionBase {
export interface IExecutionFlattedDb extends IExecutionBase {
id: number | string;
data: string;
waitTill?: Date | null;
workflowData: IWorkflowBase;
}
@ -204,6 +207,7 @@ export interface IExecutionsSummary {
mode: WorkflowExecuteMode;
retryOf?: string;
retrySuccessId?: string;
waitTill?: Date;
startedAt: Date;
stoppedAt?: Date;
workflowId: string;

View file

@ -163,6 +163,7 @@ export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutio
const returnData: IExecutionFlatted = Object.assign({}, {
data: stringify(fullExecutionData.data),
mode: fullExecutionData.mode,
waitTill: fullExecutionData.waitTill,
startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
@ -200,6 +201,7 @@ export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb):
workflowData: fullExecutionData.workflowData as IWorkflowDb,
data: parse(fullExecutionData.data),
mode: fullExecutionData.mode,
waitTill: fullExecutionData.waitTill ? fullExecutionData.waitTill : undefined,
startedAt: fullExecutionData.startedAt,
stoppedAt: fullExecutionData.stoppedAt,
finished: fullExecutionData.finished ? fullExecutionData.finished : false,

View file

@ -64,6 +64,9 @@ import {
Push,
ResponseHelper,
TestWebhooks,
WaitingWebhooks,
WaitTracker,
WaitTrackerClass,
WebhookHelpers,
WebhookServer,
WorkflowExecuteAdditionalData,
@ -96,6 +99,7 @@ import {
import {
FindManyOptions,
FindOneOptions,
IsNull,
LessThanOrEqual,
Not,
} from 'typeorm';
@ -124,9 +128,11 @@ class App {
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
testWebhooks: TestWebhooks.TestWebhooks;
endpointWebhook: string;
endpointWebhookWaiting: string;
endpointWebhookTest: string;
endpointPresetCredentials: string;
externalHooks: IExternalHooksClass;
waitTracker: WaitTrackerClass;
defaultWorkflowName: string;
saveDataErrorExecution: string;
saveDataSuccessExecution: string;
@ -150,6 +156,7 @@ class App {
this.app = express();
this.endpointWebhook = config.get('endpoints.webhook') as string;
this.endpointWebhookWaiting = config.get('endpoints.webhookWaiting') as string;
this.endpointWebhookTest = config.get('endpoints.webhookTest') as string;
this.defaultWorkflowName = config.get('workflows.defaultName') as string;
@ -168,6 +175,7 @@ class App {
this.push = Push.getInstance();
this.activeExecutionsInstance = ActiveExecutions.getInstance();
this.waitTracker = WaitTracker();
this.protocol = config.get('protocol');
this.sslKey = config.get('ssl_key');
@ -620,7 +628,6 @@ class App {
return { name: `${nameToReturn} ${maxSuffix + 1}` };
}));
// Returns a specific workflow
this.app.get(`/${this.restEndpoint}/workflows/:id`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<WorkflowEntity | undefined> => {
const workflow = await Db.collections.Workflow!.findOne(req.params.id, { relations: ['tags'] });
@ -1621,6 +1628,9 @@ class App {
executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]);
const countFilter = JSON.parse(JSON.stringify(filter));
if (countFilter.waitTill !== undefined) {
countFilter.waitTill = Not(IsNull());
}
countFilter.id = Not(In(executingWorkflowIds));
const resultsQuery = await Db.collections.Execution!
@ -1631,6 +1641,7 @@ class App {
'execution.mode',
'execution.retryOf',
'execution.retrySuccessId',
'execution.waitTill',
'execution.startedAt',
'execution.stoppedAt',
'execution.workflowData',
@ -1639,7 +1650,14 @@ class App {
.take(limit);
Object.keys(filter).forEach((filterField) => {
if (filterField === 'waitTill') {
resultsQuery.andWhere(`execution.${filterField} is not null`);
} else if(filterField === 'finished' && filter[filterField] === false) {
resultsQuery.andWhere(`execution.${filterField} = :${filterField}`, {[filterField]: filter[filterField]});
resultsQuery.andWhere(`execution.waitTill is null`);
} else {
resultsQuery.andWhere(`execution.${filterField} = :${filterField}`, {[filterField]: filter[filterField]});
}
});
if (req.query.lastId) {
resultsQuery.andWhere(`execution.id < :lastId`, {lastId: req.query.lastId});
@ -1667,6 +1685,7 @@ class App {
mode: result.mode,
retryOf: result.retryOf ? result.retryOf.toString() : undefined,
retrySuccessId: result.retrySuccessId ? result.retrySuccessId.toString() : undefined,
waitTill: result.waitTill as Date | undefined,
startedAt: result.startedAt,
stoppedAt: result.stoppedAt,
workflowId: result.workflowData!.id ? result.workflowData!.id!.toString() : '',
@ -1893,15 +1912,22 @@ class App {
// Manual executions should still be stoppable, so
// try notifying the `activeExecutions` to stop it.
const result = await this.activeExecutionsInstance.stopExecution(req.params.id);
if (result !== undefined) {
const returnData: IExecutionsStopData = {
if (result === undefined) {
// If active execution could not be found check if it is a waiting one
try {
return await this.waitTracker.stopExecution(req.params.id);
} catch (error) {
// Ignore, if it errors as then it is probably a currently running
// execution
}
} else {
return {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
};
return returnData;
} as IExecutionsStopData;
}
const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']);
@ -1932,16 +1958,18 @@ class App {
// Stopt he execution and wait till it is done and we got the data
const result = await this.activeExecutionsInstance.stopExecution(executionId);
let returnData: IExecutionsStopData;
if (result === undefined) {
throw new Error(`The execution id "${executionId}" could not be found.`);
}
const returnData: IExecutionsStopData = {
// If active execution could not be found check if it is a waiting one
returnData = await this.waitTracker.stopExecution(executionId);
} else {
returnData = {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
};
}
return returnData;
}
@ -1988,6 +2016,76 @@ class App {
WebhookServer.registerProductionWebhooks.apply(this);
}
// ----------------------------------------
// Waiting Webhooks
// ----------------------------------------
const waitingWebhooks = new WaitingWebhooks();
// HEAD webhook-waiting requests
this.app.head(`/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookWaiting.length + 2);
let response;
try {
response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// GET webhook-waiting requests
this.app.get(`/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookWaiting.length + 2);
let response;
try {
response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// POST webhook-waiting requests
this.app.post(`/${this.endpointWebhookWaiting}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookWaiting.length + 2);
let response;
try {
response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// HEAD webhook requests (test for UI)
this.app.head(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url

View file

@ -105,7 +105,7 @@ export class TestWebhooks {
return new Promise(async (resolve, reject) => {
try {
const executionMode = 'manual';
const executionId = await WebhookHelpers.executeWebhook(workflow, webhookData!, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, request, response, (error: Error | null, data: IResponseCallbackData) => {
const executionId = await WebhookHelpers.executeWebhook(workflow, webhookData!, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, undefined, undefined, request, response, (error: Error | null, data: IResponseCallbackData) => {
if (error !== null) {
return reject(error);
}
@ -163,10 +163,9 @@ export class TestWebhooks {
* @memberof TestWebhooks
*/
async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, activation: WorkflowActivateMode, sessionId?: string, destinationNode?: string): Promise<boolean> {
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode);
if (webhooks.length === 0) {
// No Webhooks found
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode, true);
if (!webhooks.find(webhook => webhook.webhookDescription.restartWebhook !== true)) {
// No webhooks found to start a workflow
return false;
}

View file

@ -0,0 +1,181 @@
import {
ActiveExecutions,
DatabaseType,
Db,
GenericHelpers,
IExecutionFlattedDb,
IExecutionsStopData,
IWorkflowExecutionDataProcess,
ResponseHelper,
WorkflowCredentials,
WorkflowRunner,
} from '.';
import {
IRun,
LoggerProxy as Logger,
WorkflowOperationError,
} from 'n8n-workflow';
import {
FindManyOptions,
LessThanOrEqual,
ObjectLiteral,
} from 'typeorm';
import { DateUtils } from 'typeorm/util/DateUtils';
export class WaitTrackerClass {
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
private waitingExecutions: {
[key: string]: {
executionId: string,
timer: NodeJS.Timeout,
};
} = {};
mainTimer: NodeJS.Timeout;
constructor() {
this.activeExecutionsInstance = ActiveExecutions.getInstance();
// Poll every 60 seconds a list of upcoming executions
this.mainTimer = setInterval(() => {
this.getwaitingExecutions();
}, 60000);
this.getwaitingExecutions();
}
async getwaitingExecutions() {
Logger.debug('Wait tracker querying database for waiting executions');
// Find all the executions which should be triggered in the next 70 seconds
const findQuery: FindManyOptions<IExecutionFlattedDb> = {
select: ['id', 'waitTill'],
where: {
waitTill: LessThanOrEqual(new Date(Date.now() + 70000)),
},
order: {
waitTill: 'ASC',
},
};
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
if (dbType === 'sqlite') {
// This is needed because of issue in TypeORM <> SQLite:
// https://github.com/typeorm/typeorm/issues/2286
(findQuery.where! as ObjectLiteral).waitTill = LessThanOrEqual(DateUtils.mixedDateToUtcDatetimeString(new Date(Date.now() + 70000)));
}
const executions = await Db.collections.Execution!.find(findQuery);
if (executions.length === 0) {
return;
}
const executionIds = executions.map(execution => execution.id.toString()).join(', ');
Logger.debug(`Wait tracker found ${executions.length} executions. Setting timer for IDs: ${executionIds}`);
// Add timers for each waiting execution that they get started at the correct time
for (const execution of executions) {
const executionId = execution.id.toString();
if (this.waitingExecutions[executionId] === undefined) {
const triggerTime = execution.waitTill!.getTime() - new Date().getTime();
this.waitingExecutions[executionId] = {
executionId,
timer: setTimeout(() => {
this.startExecution(executionId);
}, triggerTime),
};
}
}
}
async stopExecution(executionId: string): Promise<IExecutionsStopData> {
if (this.waitingExecutions[executionId] !== undefined) {
// The waiting execution was already sheduled to execute.
// So stop timer and remove.
clearTimeout(this.waitingExecutions[executionId].timer);
delete this.waitingExecutions[executionId];
}
// Also check in database
const execution = await Db.collections.Execution!.findOne(executionId);
if (execution === undefined || !execution.waitTill) {
throw new Error(`The execution ID "${executionId}" could not be found.`);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
// Set in execution in DB as failed and remove waitTill time
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
fullExecutionData.data.resultData.error = {
...error,
message: error.message,
stack: error.stack,
};
fullExecutionData.stoppedAt = new Date();
fullExecutionData.waitTill = undefined;
await Db.collections.Execution!.update(executionId, ResponseHelper.flattenExecutionData(fullExecutionData));
return {
mode: fullExecutionData.mode,
startedAt: new Date(fullExecutionData.startedAt),
stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined,
finished: fullExecutionData.finished,
};
}
startExecution(executionId: string) {
Logger.debug(`Wait tracker resuming execution ${executionId}`, {executionId});
delete this.waitingExecutions[executionId];
(async () => {
// Get the data to execute
const fullExecutionDataFlatted = await Db.collections.Execution!.findOne(executionId);
if (fullExecutionDataFlatted === undefined) {
throw new Error(`The execution with the id "${executionId}" does not exist.`);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(fullExecutionDataFlatted);
if (fullExecutionData.finished === true) {
throw new Error('The execution did succeed and can so not be started again.');
}
const data: IWorkflowExecutionDataProcess = {
executionMode: fullExecutionData.mode,
executionData: fullExecutionData.data,
workflowData: fullExecutionData.workflowData,
};
// Start the execution again
const workflowRunner = new WorkflowRunner();
await workflowRunner.run(data, false, false, executionId);
})().catch((error) => {
Logger.error(`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`, { executionId });
});
}
}
let waitTrackerInstance: WaitTrackerClass | undefined;
export function WaitTracker(): WaitTrackerClass {
if (waitTrackerInstance === undefined) {
waitTrackerInstance = new WaitTrackerClass();
}
return waitTrackerInstance;
}

View file

@ -0,0 +1,117 @@
import {
Db,
IExecutionResponse,
IResponseCallbackData,
IWorkflowDb,
NodeTypes,
ResponseHelper,
WebhookHelpers,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
} from '.';
import {
INode,
IRunExecutionData,
NodeHelpers,
WebhookHttpMethod,
Workflow,
} from 'n8n-workflow';
import * as express from 'express';
import {
LoggerProxy as Logger,
} from 'n8n-workflow';
export class WaitingWebhooks {
async executeWebhook(httpMethod: WebhookHttpMethod, fullPath: string, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
Logger.debug(`Received waiting-webhoook "${httpMethod}" for path "${fullPath}"`);
// Reset request parameters
req.params = {};
// Remove trailing slash
if (fullPath.endsWith('/')) {
fullPath = fullPath.slice(0, -1);
}
const pathParts = fullPath.split('/');
const executionId = pathParts.shift();
const path = pathParts.join('/');
const execution = await Db.collections.Execution?.findOne(executionId);
if (execution === undefined) {
throw new ResponseHelper.ResponseError(`The execution "${executionId} does not exist.`, 404, 404);
}
const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
if (fullExecutionData.finished === true || fullExecutionData.data.resultData.error) {
throw new ResponseHelper.ResponseError(`The execution "${executionId} has finished already.`, 409, 409);
}
return this.startExecution(httpMethod, path, fullExecutionData, req, res);
}
async startExecution(httpMethod: WebhookHttpMethod, path: string, fullExecutionData: IExecutionResponse, req: express.Request, res: express.Response): Promise<IResponseCallbackData> {
const executionId = fullExecutionData.id;
if (fullExecutionData.finished === true) {
throw new Error('The execution did succeed and can so not be started again.');
}
const lastNodeExecuted = fullExecutionData!.data.resultData.lastNodeExecuted as string;
// Set the node as disabled so that the data does not get executed again as it would result
// in starting the wait all over again
fullExecutionData!.data.executionData!.nodeExecutionStack[0].node.disabled = true;
// Remove waitTill information else the execution would stop
fullExecutionData!.data.waitTill = undefined;
// Remove the data of the node execution again else it will display the node as executed twice
fullExecutionData!.data.resultData.runData[lastNodeExecuted].pop();
const workflowData = fullExecutionData.workflowData;
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: workflowData.id!.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
const additionalData = await WorkflowExecuteAdditionalData.getBase();
const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(lastNodeExecuted) as INode, additionalData).filter((webhook) => {
return (webhook.httpMethod === httpMethod && webhook.path === path && webhook.webhookDescription.restartWebhook === true);
})[0];
if (webhookData === undefined) {
// If no data got found it means that the execution can not be started via a webhook.
// Return 404 because we do not want to give any data if the execution exists or not.
const errorMessage = `The execution "${executionId}" with webhook suffix path "${path}" is not known.`;
throw new ResponseHelper.ResponseError(errorMessage, 404, 404);
}
const workflowStartNode = workflow.getNode(lastNodeExecuted);
if (workflowStartNode === null) {
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
}
const runExecutionData = fullExecutionData.data as IRunExecutionData;
return new Promise((resolve, reject) => {
const executionMode = 'webhook';
WebhookHelpers.executeWebhook(workflow, webhookData, workflowData as IWorkflowDb, workflowStartNode, executionMode, undefined, runExecutionData, fullExecutionData.id, req, res, (error: Error | null, data: object) => {
if (error !== null) {
return reject(error);
}
resolve(data);
});
});
}
}

View file

@ -3,7 +3,6 @@ import { get } from 'lodash';
import {
ActiveExecutions,
ExternalHooks,
GenericHelpers,
IExecutionDb,
IResponseCallbackData,
@ -29,6 +28,7 @@ import {
IRunExecutionData,
IWebhookData,
IWebhookResponseData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
NodeHelpers,
@ -47,7 +47,7 @@ const activeExecutions = ActiveExecutions.getInstance();
* @param {Workflow} workflow
* @returns {IWebhookData[]}
*/
export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, destinationNode?: string): IWebhookData[] {
export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, destinationNode?: string, ignoreRestartWehbooks = false): IWebhookData[] {
// Check all the nodes in the workflow if they have webhooks
const returnData: IWebhookData[] = [];
@ -65,7 +65,7 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
// and no other ones
continue;
}
returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData));
returnData.push.apply(returnData, NodeHelpers.getNodeWebhooks(workflow, node, additionalData, ignoreRestartWehbooks));
}
return returnData;
@ -106,7 +106,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
* @param {((error: Error | null, data: IResponseCallbackData) => void)} responseCallback
* @returns {(Promise<string | undefined>)}
*/
export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> {
export async function executeWebhook(workflow: Workflow, webhookData: IWebhookData, workflowData: IWorkflowDb, workflowStartNode: INode, executionMode: WorkflowExecuteMode, sessionId: string | undefined, runExecutionData: IRunExecutionData | undefined, executionId: string | undefined, req: express.Request, res: express.Response, responseCallback: (error: Error | null, data: IResponseCallbackData) => void): Promise<string | undefined> {
// Get the nodeType to know which responseMode is set
const nodeType = workflow.nodeTypes.getByName(workflowStartNode.type);
if (nodeType === undefined) {
@ -115,9 +115,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
throw new ResponseHelper.ResponseError(errorMessage, 500, 500);
}
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
// Get the responseMode
const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, 'onReceived');
const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, 200) as number;
const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, additionalKeys, 'onReceived');
const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, additionalKeys, 200) as number;
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
// If the mode is not known we error. Is probably best like that instead of using
@ -174,8 +178,12 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Save static data if it changed
await WorkflowHelpers.saveStaticData(workflow);
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
if (webhookData.webhookDescription['responseHeaders'] !== undefined) {
const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, undefined) as {
const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, additionalKeys, undefined) as {
entries?: Array<{
name: string;
value: string;
@ -256,7 +264,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
);
const runExecutionData: IRunExecutionData = {
runExecutionData = runExecutionData || {
startData: {
},
resultData: {
@ -267,7 +275,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
nodeExecutionStack,
waitingExecution: {},
},
};
} as IRunExecutionData;
if (executionId !== undefined) {
// Set the data the webhook node did return on the waiting node if executionId
// already exists as it means that we are restarting an existing execution.
runExecutionData.executionData!.nodeExecutionStack[0].data.main = webhookResultData.workflowData;
}
if (Object.keys(runExecutionDataMerge).length !== 0) {
// If data to merge got defined add it to the execution data
@ -283,7 +297,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
// Start now to run the workflow
const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData, true, !didSendResponse);
executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId);
Logger.verbose(`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`, { executionId });
@ -330,7 +344,11 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return data;
}
const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, 'firstEntryJson');
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId,
};
const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, additionalKeys, 'firstEntryJson');
if (didSendResponse === false) {
let data: IDataObject | IDataObject[];
@ -345,13 +363,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
data = returnData.data!.main[0]![0].json;
const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, undefined);
const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, additionalKeys, undefined);
if (responsePropertyName !== undefined) {
data = get(data, responsePropertyName as string) as IDataObject;
}
const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, undefined);
const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, additionalKeys, undefined);
if (responseContentType !== undefined) {
// Send the webhook response manually to be able to set the content-type
@ -384,7 +402,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
didSendResponse = true;
}
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, 'data');
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, additionalKeys, 'data');
if (responseBinaryPropertyName === undefined && didSendResponse === false) {
responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {});

View file

@ -26,6 +26,11 @@ import * as config from '../config';
import * as parseUrl from 'parseurl';
export function registerProductionWebhooks() {
// ----------------------------------------
// Regular Webhooks
// ----------------------------------------
// HEAD webhook requests
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url

View file

@ -256,7 +256,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
if (execution === undefined) {
// Something went badly wrong if this happens.
// This check is here mostly to make typescript happy.
return undefined;
return;
}
const fullExecutionData: IExecutionResponse = ResponseHelper.unflattenExecutionData(execution);
@ -267,11 +267,9 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
return;
}
if (fullExecutionData.data === undefined) {
fullExecutionData.data = {
startData: {
},
startData: {},
resultData: {
runData: {},
},
@ -351,7 +349,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
saveManualExecutions = this.workflowData.settings.saveManualExecutions as boolean;
}
if (isManualMode && saveManualExecutions === false) {
if (isManualMode && saveManualExecutions === false && !fullRunData.waitTill) {
// Data is always saved, so we remove from database
await Db.collections.Execution!.delete(this.executionId);
return;
@ -369,6 +367,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
if (workflowDidSucceed === true && saveDataSuccessExecution === 'none' ||
workflowDidSucceed === false && saveDataErrorExecution === 'none'
) {
if (!fullRunData.waitTill) {
if (!isManualMode) {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
}
@ -376,6 +375,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
await Db.collections.Execution!.delete(this.executionId);
return;
}
}
const fullExecutionData: IExecutionDb = {
data: fullRunData.data,
@ -384,6 +384,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
startedAt: fullRunData.startedAt,
stoppedAt: fullRunData.stoppedAt,
workflowData: this.workflowData,
waitTill: fullRunData.waitTill,
};
if (this.retryOf !== undefined) {
@ -469,6 +470,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
startedAt: fullRunData.startedAt,
stoppedAt: fullRunData.stoppedAt,
workflowData: this.workflowData,
waitTill: fullRunData.data.waitTill,
};
if (this.retryOf !== undefined) {
@ -731,6 +733,7 @@ export async function getBase(currentNodeParameters?: INodeParameters, execution
const timezone = config.get('generic.timezone') as string;
const webhookBaseUrl = urlBaseWebhook + config.get('endpoints.webhook') as string;
const webhookWaitingBaseUrl = urlBaseWebhook + config.get('endpoints.webhookWaiting') as string;
const webhookTestBaseUrl = urlBaseWebhook + config.get('endpoints.webhookTest') as string;
const encryptionKey = await UserSettings.getEncryptionKey();
@ -745,6 +748,7 @@ export async function getBase(currentNodeParameters?: INodeParameters, execution
restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string,
timezone,
webhookBaseUrl,
webhookWaitingBaseUrl,
webhookTestBaseUrl,
currentNodeParameters,
executionTimeoutTimestamp,

View file

@ -123,19 +123,18 @@ export class WorkflowRunner {
* @returns {Promise<string>}
* @memberof WorkflowRunner
*/
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean): Promise<string> {
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean, executionId?: string): Promise<string> {
const executionsProcess = config.get('executions.process') as string;
const executionsMode = config.get('executions.mode') as string;
let executionId: string;
if (executionsMode === 'queue' && data.executionMode !== 'manual') {
// Do not run "manual" executions in bull because sending events to the
// frontend would not be possible
executionId = await this.runBull(data, loadStaticData, realtime);
executionId = await this.runBull(data, loadStaticData, realtime, executionId);
} else if (executionsProcess === 'main') {
executionId = await this.runMainProcess(data, loadStaticData);
executionId = await this.runMainProcess(data, loadStaticData, executionId);
} else {
executionId = await this.runSubprocess(data, loadStaticData);
executionId = await this.runSubprocess(data, loadStaticData, executionId);
}
const externalHooks = ExternalHooks();
@ -162,7 +161,7 @@ export class WorkflowRunner {
* @returns {Promise<string>}
* @memberof WorkflowRunner
*/
async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
async runMainProcess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, restartExecutionId?: string): Promise<string> {
if (loadStaticData === true && data.workflowData.id) {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(data.workflowData.id as string);
}
@ -186,7 +185,10 @@ export class WorkflowRunner {
const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
// Register the active execution
const executionId = await this.activeExecutions.add(data, undefined);
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId) as string;
additionalData.executionId = executionId;
Logger.verbose(`Execution for workflow ${data.workflowData.name} was assigned id ${executionId}`, {executionId});
let workflowExecution: PCancelable<IRun>;
try {
@ -240,12 +242,12 @@ export class WorkflowRunner {
return executionId;
}
async runBull(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean): Promise<string> {
async runBull(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, realtime?: boolean, restartExecutionId?: string): Promise<string> {
// TODO: If "loadStaticData" is set to true it has to load data new on worker
// Register the active execution
const executionId = await this.activeExecutions.add(data, undefined);
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
const jobData: IBullJobData = {
executionId,
@ -412,7 +414,7 @@ export class WorkflowRunner {
* @returns {Promise<string>}
* @memberof WorkflowRunner
*/
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean, restartExecutionId?: string): Promise<string> {
let startedAt = new Date();
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
@ -421,7 +423,7 @@ export class WorkflowRunner {
}
// Register the active execution
const executionId = await this.activeExecutions.add(data, subprocess);
const executionId = await this.activeExecutions.add(data, subprocess, restartExecutionId);
// Supply all nodeTypes and credentialTypes
const nodeTypeData = WorkflowHelpers.getAllNodeTypeData() as ITransferNodeTypes;

View file

@ -150,6 +150,7 @@ export class WorkflowRunnerProcess {
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings });
const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
additionalData.hooks = this.getProcessForwardHooks();
additionalData.executionId = inputData.executionId;
additionalData.sendMessageToUI = async (source: string, message: any) => { // tslint:disable-line:no-any
if (workflowRunner.data!.executionMode !== 'manual') {

View file

@ -53,4 +53,8 @@ export class ExecutionEntity implements IExecutionFlattedDb {
@Index()
@Column({ nullable: true })
workflowId: string;
@Index()
@Column({ type: resolveDataType('datetime') as ColumnOptions['type'], nullable: true })
waitTill: Date;
}

View file

@ -0,0 +1,22 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class AddWaitColumnId1626183952959 implements MigrationInterface {
name = 'AddWaitColumnId1626183952959';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` ADD `waitTill` DATETIME NULL');
await queryRunner.query('CREATE INDEX `IDX_' + tablePrefix + 'ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity` (`waitTill`)');
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query(
'DROP INDEX `IDX_' + tablePrefix + 'ca4a71b47f28ac6ea88293a8e2` ON `' + tablePrefix + 'execution_entity`'
);
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'execution_entity` DROP COLUMN `waitTill`');
}
}

View file

@ -8,6 +8,7 @@ import { ChangeCredentialDataSize1620729500000 } from './1620729500000-ChangeCre
import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
export const mysqlMigrations = [
InitialMigration1588157391238,
@ -20,4 +21,5 @@ export const mysqlMigrations = [
CreateTagEntity1617268711084,
UniqueWorkflowNames1620826335440,
CertifyCorrectCollation1623936588000,
AddWaitColumnId1626183952959,
];

View file

@ -0,0 +1,31 @@
import {MigrationInterface, QueryRunner} from "typeorm";
import * as config from '../../../../config';
export class AddwaitTill1626176912946 implements MigrationInterface {
name = 'AddwaitTill1626176912946';
async up(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
await queryRunner.query(`ALTER TABLE ${tablePrefix}execution_entity ADD "waitTill" TIMESTAMP`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2 ON ${tablePrefix}execution_entity ("waitTill")`);
}
async down(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.get('database.tablePrefix');
const tablePrefixPure = tablePrefix;
const schema = config.get('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}
await queryRunner.query(`DROP INDEX IDX_${tablePrefixPure}ca4a71b47f28ac6ea88293a8e2`);
await queryRunner.query(`ALTER TABLE ${tablePrefix}webhook_entity DROP COLUMN "waitTill"`);
}
}

View file

@ -5,6 +5,7 @@ import { AddWebhookId1611144599516 } from './1611144599516-AddWebhookId';
import { MakeStoppedAtNullable1607431743768 } from './1607431743768-MakeStoppedAtNullable';
import { CreateTagEntity1617270242566 } from './1617270242566-CreateTagEntity';
import { UniqueWorkflowNames1620824779533 } from './1620824779533-UniqueWorkflowNames';
import { AddwaitTill1626176912946 } from './1626176912946-AddwaitTill';
export const postgresMigrations = [
InitialMigration1587669153312,
@ -14,4 +15,5 @@ export const postgresMigrations = [
MakeStoppedAtNullable1607431743768,
CreateTagEntity1617270242566,
UniqueWorkflowNames1620824779533,
AddwaitTill1626176912946,
];

View file

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
export class AddWaitColumn1621707690587 implements MigrationInterface {
name = 'AddWaitColumn1621707690587';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}temporary_execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime, "workflowData" text NOT NULL, "workflowId" varchar, "waitTill" DATETIME)`, undefined);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_execution_entity"("id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId") SELECT "id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId" FROM "${tablePrefix}execution_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_execution_entity" RENAME TO "${tablePrefix}execution_entity"`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}ca4a71b47f28ac6ea88293a8e2" ON "${tablePrefix}execution_entity" ("waitTill")`);
await queryRunner.query(`VACUUM;`);
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query(`CREATE TABLE IF NOT EXISTS "${tablePrefix}temporary_execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime, "workflowData" text NOT NULL, "workflowId" varchar)`, undefined);
await queryRunner.query(`INSERT INTO "${tablePrefix}temporary_execution_entity"("id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId") SELECT "id", "data", "finished", "mode", "retryOf", "retrySuccessId", "startedAt", "stoppedAt", "workflowData", "workflowId" FROM "${tablePrefix}execution_entity"`);
await queryRunner.query(`DROP TABLE "${tablePrefix}execution_entity"`);
await queryRunner.query(`ALTER TABLE "${tablePrefix}temporary_execution_entity" RENAME TO "${tablePrefix}execution_entity"`);
await queryRunner.query(`CREATE INDEX "IDX_${tablePrefix}cefb067df2402f6aed0638a6c1" ON "${tablePrefix}execution_entity" ("stoppedAt")`);
await queryRunner.query(`VACUUM;`);
}
}

View file

@ -5,6 +5,7 @@ import { AddWebhookId1611071044839 } from './1611071044839-AddWebhookId';
import { MakeStoppedAtNullable1607431743769 } from './1607431743769-MakeStoppedAtNullable';
import { CreateTagEntity1617213344594 } from './1617213344594-CreateTagEntity';
import { UniqueWorkflowNames1620821879465 } from './1620821879465-UniqueWorkflowNames';
import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
export const sqliteMigrations = [
InitialMigration1588102412422,
@ -14,4 +15,5 @@ export const sqliteMigrations = [
MakeStoppedAtNullable1607431743769,
CreateTagEntity1617213344594,
UniqueWorkflowNames1620821879465,
AddWaitColumn1621707690587,
];

View file

@ -5,6 +5,8 @@ export * from './ExternalHooks';
export * from './Interfaces';
export * from './LoadNodesAndCredentials';
export * from './NodeTypes';
export * from './WaitTracker';
export * from './WaitingWebhooks';
export * from './WorkflowCredentials';
export * from './WorkflowRunner';

View file

@ -5,4 +5,6 @@ export const EXTENSIONS_SUBDIRECTORY = 'custom';
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
export const USER_SETTINGS_FILE_NAME = 'config';
export const USER_SETTINGS_SUBFOLDER = '.n8n';
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKOWN__';
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';

View file

@ -4,6 +4,7 @@ import {
ILoadOptionsFunctions,
IResponseError,
IWorkflowSettings,
PLACEHOLDER_EMPTY_EXECUTION_ID,
} from './';
import {
@ -28,6 +29,7 @@ import {
IWebhookData,
IWebhookDescription,
IWebhookFunctions,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
IWorkflowExecuteAdditionalData,
IWorkflowMetadata,
@ -322,6 +324,23 @@ export function returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExe
/**
* Returns the additional keys for Expressions and Function-Nodes
*
* @export
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns {(IWorkflowDataProxyAdditionalKeys)}
*/
export function getAdditionalKeys(additionalData: IWorkflowExecuteAdditionalData): IWorkflowDataProxyAdditionalKeys {
const executionId = additionalData.executionId || PLACEHOLDER_EMPTY_EXECUTION_ID;
return {
$executionId: executionId,
$resumeWebhookUrl: `${additionalData.webhookWaitingBaseUrl}/${executionId}`,
};
}
/**
* Returns the requested decrypted credentials if the node has access to them.
*
@ -420,7 +439,7 @@ export function getNode(node: INode): INode {
* @param {*} [fallbackValue]
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object)}
*/
export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, connectionInputData: INodeExecutionData[], node: INode, parameterName: string, itemIndex: number, mode: WorkflowExecuteMode, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { //tslint:disable-line:no-any
export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, connectionInputData: INodeExecutionData[], node: INode, parameterName: string, itemIndex: number, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { //tslint:disable-line:no-any
const nodeType = workflow.nodeTypes.getByName(node.type);
if (nodeType === undefined) {
throw new Error(`Node type "${node.type}" is not known so can not return paramter value!`);
@ -434,7 +453,7 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu
let returnData;
try {
returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys);
} catch (e) {
e.message += ` [Error in parameter: "${parameterName}"]`;
throw e;
@ -469,7 +488,7 @@ export function continueOnFail(node: INode): boolean {
* @param {boolean} [isTest]
* @returns {(string | undefined)}
*/
export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean): string | undefined {
export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, isTest?: boolean): string | undefined {
let baseUrl = additionalData.webhookBaseUrl;
if (isTest === true) {
baseUrl = additionalData.webhookTestBaseUrl;
@ -480,12 +499,12 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode,
return undefined;
}
const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode);
const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, additionalKeys);
if (path === undefined) {
return undefined;
}
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, false) as boolean;
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, additionalKeys, false) as boolean;
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath);
}
@ -588,7 +607,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
const runIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue);
},
getRestApiUrl: (): string => {
return additionalData.restApiUrl;
@ -654,7 +673,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
const runIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue);
},
getRestApiUrl: (): string => {
return additionalData.restApiUrl;
@ -706,7 +725,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return continueOnFail(node);
},
evaluateExpression: (expression: string, itemIndex: number) => {
return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode, getAdditionalKeys(additionalData));
},
async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
@ -717,6 +736,9 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
async getCredentials(type: string, itemIndex?: number): Promise<ICredentialDataDecryptedObject | undefined> {
return await getCredentials(workflow, node, type, additionalData, mode, runExecutionData, runIndex, connectionInputData, itemIndex);
},
getExecutionId: (): string => {
return additionalData.executionId!;
},
getInputData: (inputIndex = 0, inputName = 'main') => {
if (!inputData.hasOwnProperty(inputName)) {
@ -729,17 +751,15 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
}
if (inputData[inputName][inputIndex] === null) {
// return [];
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
}
// TODO: Maybe do clone of data only here so it only clones the data that is really needed
return inputData[inputName][inputIndex] as INodeExecutionData[];
},
getNodeParameter: (parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue);
},
getMode: (): WorkflowExecuteMode => {
return mode;
@ -757,13 +777,16 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return getWorkflowMetadata(workflow);
},
getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode);
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode, getAdditionalKeys(additionalData));
return dataProxy.getDataProxy();
},
getWorkflowStaticData(type: string): IDataObject {
return workflow.getStaticData(type, node);
},
prepareOutputData: NodeHelpers.prepareOutputData,
async putExecutionToWait(waitTill: Date): Promise<void> {
runExecutionData.waitTill = waitTill;
},
sendMessageToUI(message : any): void { // tslint:disable-line:no-any
if (mode !== 'manual') {
return;
@ -819,7 +842,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex;
return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode);
return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode, getAdditionalKeys(additionalData));
},
getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node);
@ -865,13 +888,13 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
return getTimezone(workflow, additionalData);
},
getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue);
},
getWorkflow: () => {
return getWorkflowMetadata(workflow);
},
getWorkflowDataProxy: (): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode);
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode, getAdditionalKeys(additionalData));
return dataProxy.getDataProxy();
},
getWorkflowStaticData(type: string): IDataObject {
@ -928,7 +951,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: s
const runIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, 'internal' as WorkflowExecuteMode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, 'internal' as WorkflowExecuteMode, getAdditionalKeys(additionalData), fallbackValue);
},
getTimezone: (): string => {
return getTimezone(workflow, additionalData);
@ -983,10 +1006,10 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
const runIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue);
},
getNodeWebhookUrl: (name: string): string | undefined => {
return getNodeWebhookUrl(name, workflow, node, additionalData, mode, isTest);
return getNodeWebhookUrl(name, workflow, node, additionalData, mode, getAdditionalKeys(additionalData), isTest);
},
getTimezone: (): string => {
return getTimezone(workflow, additionalData);
@ -1063,7 +1086,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
const runIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue);
return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, getAdditionalKeys(additionalData), fallbackValue);
},
getParamsData(): object {
if (additionalData.httpRequest === undefined) {
@ -1090,7 +1113,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
return additionalData.httpResponse;
},
getNodeWebhookUrl: (name: string): string | undefined => {
return getNodeWebhookUrl(name, workflow, node, additionalData, mode);
return getNodeWebhookUrl(name, workflow, node, additionalData, mode, getAdditionalKeys(additionalData));
},
getTimezone: (): string => {
return getTimezone(workflow, additionalData);

View file

@ -31,7 +31,6 @@ export class WorkflowExecute {
private additionalData: IWorkflowExecuteAdditionalData;
private mode: WorkflowExecuteMode;
constructor(additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData) {
this.additionalData = additionalData;
this.mode = mode;
@ -512,6 +511,13 @@ export class WorkflowExecute {
this.runExecutionData.startData = {};
}
if (this.runExecutionData.waitTill) {
const lastNodeExecuted = this.runExecutionData.resultData.lastNodeExecuted as string;
this.runExecutionData.executionData!.nodeExecutionStack[0].node.disabled = true;
this.runExecutionData.waitTill = undefined;
this.runExecutionData.resultData.runData[lastNodeExecuted].pop();
}
let currentExecutionTry = '';
let lastExecutionTry = '';
@ -693,7 +699,7 @@ export class WorkflowExecute {
}
}
if (nodeSuccessData === null) {
if (nodeSuccessData === null && !this.runExecutionData.waitTill!!) {
// If null gets returned it means that the node did succeed
// but did not have any data. So the branch should end
// (meaning the nodes afterwards should not be processed)
@ -767,6 +773,15 @@ export class WorkflowExecute {
continue;
}
if (this.runExecutionData.waitTill!!) {
await this.executeHook('nodeExecuteAfter', [executionNode.name, taskData, this.runExecutionData]);
// Add the node back to the stack that the workflow can start to execute again from that node
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionData);
break;
}
// Add the nodes to which the current node has an output connection to that they can
// be executed next
if (workflow.connectionsBySourceNode.hasOwnProperty(executionNode.name)) {
@ -849,6 +864,9 @@ export class WorkflowExecute {
message: executionError.message,
stack: executionError.stack,
} as ExecutionError;
} else if (this.runExecutionData.waitTill!!) {
Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, { workflowId: workflow.id });
fullRunData.waitTill = this.runExecutionData.waitTill;
} else {
Logger.verbose(`Workflow execution finished successfully`, { workflowId: workflow.id });
fullRunData.finished = true;

View file

@ -758,6 +758,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
encryptionKey: 'test',
timezone: 'America/New_York',
webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting',
webhookTestBaseUrl: 'webhook-test',
};
}

View file

@ -353,6 +353,7 @@ export interface IExecutionsSummary {
finished?: boolean;
retryOf?: string;
retrySuccessId?: string;
waitTill?: Date;
startedAt: Date;
stoppedAt?: Date;
workflowId: string;

View file

@ -82,7 +82,10 @@
<el-tooltip placement="top" effect="light">
<div slot="content" v-html="statusTooltipText(scope.row)"></div>
<span class="status-badge running" v-if="scope.row.stoppedAt === undefined">
<span class="status-badge running" v-if="scope.row.waitTill">
Waiting
</span>
<span class="status-badge running" v-else-if="scope.row.stoppedAt === undefined">
Running
</span>
<span class="status-badge success" v-else-if="scope.row.finished">
@ -98,7 +101,7 @@
<el-dropdown trigger="click" @command="handleRetryClick">
<span class="el-dropdown-link">
<el-button class="retry-button" v-bind:class="{ warning: scope.row.stoppedAt === null }" circle v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined" type="text" size="small" title="Retry execution">
<el-button class="retry-button" v-bind:class="{ warning: scope.row.stoppedAt === null }" circle v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined && scope.row.waitTill === undefined" type="text" size="small" title="Retry execution">
<font-awesome-icon icon="redo" />
</el-button>
</span>
@ -128,12 +131,12 @@
</el-table-column>
<el-table-column label="" width="100" align="center">
<template slot-scope="scope">
<span v-if="scope.row.stoppedAt === undefined">
<span v-if="scope.row.stoppedAt === undefined || scope.row.waitTill" class="execution-actions">
<el-button circle title="Stop Execution" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" size="mini">
<font-awesome-icon icon="stop" />
</el-button>
</span>
<span v-else-if="scope.row.id">
<span v-if="scope.row.stoppedAt !== undefined && scope.row.id" class="execution-actions">
<el-button circle title="Open Past Execution" @click.stop="displayExecution(scope.row)" size="mini">
<font-awesome-icon icon="folder-open" />
</el-button>
@ -159,6 +162,8 @@ import ExecutionTime from '@/components/ExecutionTime.vue';
import WorkflowActivator from '@/components/WorkflowActivator.vue';
import { externalHooks } from '@/components/mixins/externalHooks';
import { WAIT_TIME_UNLIMITED } from '@/constants';
import { restApi } from '@/components/mixins/restApi';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { showMessage } from '@/components/mixins/showMessage';
@ -235,6 +240,10 @@ export default mixins(
id: 'success',
name: 'Success',
},
{
id: 'waiting',
name: 'Waiting',
},
],
};
@ -249,7 +258,7 @@ export default mixins(
if (['ALL', 'running'].includes(this.filter.status)) {
returnData.push.apply(returnData, this.activeExecutions);
}
if (['ALL', 'error', 'success'].includes(this.filter.status)) {
if (['ALL', 'error', 'success', 'waiting'].includes(this.filter.status)) {
returnData.push.apply(returnData, this.finishedExecutions);
}
@ -287,7 +296,9 @@ export default mixins(
if (this.filter.workflowId !== 'ALL') {
filter.workflowId = this.filter.workflowId;
}
if (['error', 'success'].includes(this.filter.status)) {
if (this.filter.status === 'waiting') {
filter.waitTill = true;
} else if (['error', 'success'].includes(this.filter.status)) {
filter.finished = this.filter.status === 'success';
}
return filter;
@ -609,7 +620,13 @@ export default mixins(
this.isDataLoading = false;
},
statusTooltipText (entry: IExecutionsSummary): string {
if (entry.stoppedAt === undefined) {
if (entry.waitTill) {
const waitDate = new Date(entry.waitTill);
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
return 'The workflow is waiting indefinitely for an incoming webhook call.';
}
return `The worklow is waiting till ${waitDate.toLocaleDateString()} ${waitDate.toLocaleTimeString()}.`;
} else if (entry.stoppedAt === undefined) {
return 'The worklow is currently executing.';
} else if (entry.finished === true && entry.retryOf !== undefined) {
return `The workflow execution was a retry of "${entry.retryOf}" and it was successful.`;
@ -659,6 +676,12 @@ export default mixins(
text-align: right;
}
.execution-actions {
button {
margin: 0 0.25em;
}
}
.filters {
line-height: 2em;
.refresh-button {

View file

@ -11,6 +11,12 @@
v-if="executionFinished"
title="Execution was successful"
/>
<font-awesome-icon
icon="clock"
class="execution-icon warning"
v-else-if="executionWaiting"
title="Execution waiting"
/>
<font-awesome-icon
icon="times"
class="execution-icon error"
@ -59,6 +65,11 @@ export default mixins(titleChange).extend({
return !!fullExecution && fullExecution.finished;
},
executionWaiting(): boolean {
const fullExecution = this.$store.getters.getWorkflowExecution;
return !!fullExecution && !!fullExecution.waitTill;
},
workflowExecution(): IExecutionResponse | null {
return this.$store.getters.getWorkflowExecution;
},
@ -84,9 +95,14 @@ export default mixins(titleChange).extend({
box-sizing: border-box;
}
.execution-icon.success {
.execution-icon {
&.success {
color: $--custom-success-text-light;
}
&.warning {
color: $--custom-running-text;
}
}
.container {
width: 100%;

View file

@ -9,6 +9,13 @@
</div>
<el-badge v-else :hidden="workflowDataItems === 0" class="node-info-icon data-count" :value="workflowDataItems"></el-badge>
<div v-if="waiting" class="node-info-icon waiting">
<el-tooltip placement="top" effect="light">
<div slot="content" v-html="waiting"></div>
<font-awesome-icon icon="clock" />
</el-tooltip>
</div>
<div class="node-executing-info" title="Node is executing">
<font-awesome-icon icon="sync-alt" spin />
</div>
@ -46,6 +53,7 @@
<script lang="ts">
import Vue from 'vue';
import { WAIT_TIME_UNLIMITED } from '@/constants';
import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeBase } from '@/components/mixins/nodeBase';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
@ -60,6 +68,8 @@ import NodeIcon from '@/components/NodeIcon.vue';
import mixins from 'vue-typed-mixins';
import { get } from 'lodash';
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
name: 'Node',
components: {
@ -125,6 +135,22 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
return 'play';
}
},
waiting (): string | undefined {
const workflowExecution = this.$store.getters.getWorkflowExecution;
if (workflowExecution && workflowExecution.waitTill) {
const lastNodeExecuted = get(workflowExecution, 'data.resultData.lastNodeExecuted');
if (this.name === lastNodeExecuted) {
const waitDate = new Date(workflowExecution.waitTill);
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
return 'The node is waiting indefinitely for an incoming webhook call.';
}
return `Node is waiting till ${waitDate.toLocaleDateString()} ${waitDate.toLocaleTimeString()}`;
}
}
return;
},
workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
@ -283,12 +309,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
position: absolute;
top: -18px;
right: 12px;
z-index: 10;
z-index: 11;
&.data-count {
font-weight: 600;
top: -12px;
}
&.waiting {
left: 10px;
top: -12px;
}
}
.node-issues {
@ -298,6 +329,13 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
color: #ff0000;
}
.waiting {
width: 25px;
height: 25px;
font-size: 20px;
color: #5e5efa;
}
.node-options {
display: none;
position: absolute;

View file

@ -26,7 +26,7 @@
</div>
<div class="url-field">
<div class="webhook-url left-ellipsis clickable" @click="copyWebhookUrl(webhook)">
{{getWebhookUrl(webhook, 'path')}}<br />
{{getWebhookUrlDisplay(webhook)}}<br />
</div>
</div>
</div>
@ -39,6 +39,7 @@
<script lang="ts">
import {
INodeTypeDescription,
IWebhookDescription,
NodeHelpers,
} from 'n8n-workflow';
@ -59,7 +60,7 @@ export default mixins(
name: 'NodeWebhooks',
props: [
'node', // NodeUi
'nodeType', // NodeTypeDescription
'nodeType', // INodeTypeDescription
],
data () {
return {
@ -73,7 +74,7 @@ export default mixins(
return [];
}
return this.nodeType.webhooks;
return (this.nodeType as INodeTypeDescription).webhooks!.filter(webhookData => webhookData.restartWebhook !== true);
},
},
methods: {
@ -98,6 +99,9 @@ export default mixins(
}
},
getWebhookUrl (webhookData: IWebhookDescription): string {
if (webhookData.restartWebhook === true) {
return '$resumeWebhookUrl';
}
let baseUrl = this.$store.getters.getWebhookUrl;
if (this.showUrlFor === 'test') {
baseUrl = this.$store.getters.getWebhookTestUrl;
@ -109,6 +113,9 @@ export default mixins(
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflowId, this.node, path, isFullPath);
},
getWebhookUrlDisplay (webhookData: IWebhookDescription): string {
return this.getWebhookUrl(webhookData);
},
},
watch: {
node () {

View file

@ -14,6 +14,8 @@
/>
</div>
<div v-else-if="parameter.type === 'notice'" v-html="parameter.displayName" class="parameter-item parameter-notice"></div>
<div
v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"
class="multi-parameter"
@ -299,6 +301,17 @@ export default mixins(
.parameter-name:hover > .delete-option {
display: block;
}
.parameter-notice {
background-color: #fff5d3;
color: $--custom-font-black;
margin: 0.3em 0;
padding: 0.8em;
& a {
color: $--color-primary;
}
}
}
</style>

View file

@ -12,12 +12,17 @@
<script lang="ts">
import {
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants';
import {
GenericValue,
IContextObject,
IDataObject,
IRunData,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
Workflow,
WorkflowDataProxy,
} from 'n8n-workflow';
@ -376,7 +381,12 @@ export default mixins(
return returnData;
}
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, {}, 'manual');
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, {}, 'manual', additionalKeys);
const proxy = dataProxy.getDataProxy();
// @ts-ignore

View file

@ -1,3 +1,7 @@
import {
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants';
import {
IBinaryKeyData,
ICredentialType,
@ -330,7 +334,7 @@ export const nodeHelpers = mixins(
}
if (nodeType !== null && nodeType.subtitle !== undefined) {
return workflow.expression.getSimpleParameterValue(data as INode, nodeType.subtitle, 'internal') as string | undefined;
return workflow.expression.getSimpleParameterValue(data as INode, nodeType.subtitle, 'internal', PLACEHOLDER_FILLED_AT_EXECUTION_TIME) as string | undefined;
}
if (data.parameters.operation !== undefined) {

View file

@ -217,7 +217,15 @@ export const pushConnection = mixins(
// @ts-ignore
const workflow = this.getWorkflow();
if (runDataExecuted.finished !== true) {
if (runDataExecuted.waitTill !== undefined) {
// Workflow did start but had been put to wait
this.$titleSet(workflow.name as string, 'IDLE');
this.$showMessage({
title: 'Workflow got started',
message: 'Workflow execution has started and is now waiting!',
type: 'success',
});
} else if (runDataExecuted.finished !== true) {
this.$titleSet(workflow.name as string, 'ERROR');
this.$showMessage({

View file

@ -1,4 +1,7 @@
import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import {
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
} from '@/constants';
import {
IConnections,
@ -8,6 +11,7 @@ import {
INodeIssues,
INodeParameters,
NodeParameterValue,
INodeCredentials,
INodeType,
INodeTypes,
INodeTypeData,
@ -15,7 +19,7 @@ import {
IRunData,
IRunExecutionData,
IWorfklowIssues,
INodeCredentials,
IWorkflowDataProxyAdditionalKeys,
Workflow,
NodeHelpers,
} from 'n8n-workflow';
@ -368,7 +372,12 @@ export const workflowHelpers = mixins(
connectionInputData = [];
}
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', false) as IDataObject;
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', additionalKeys, false) as IDataObject;
},
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) {

View file

@ -2,6 +2,8 @@ export const MAX_DISPLAY_DATA_SIZE = 204800;
export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250;
export const NODE_NAME_PREFIX = 'node-';
export const PLACEHOLDER_FILLED_AT_EXECUTION_TIME = '[filled at execution time]';
// workflows
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
export const DEFAULT_NEW_WORKFLOW_NAME = 'My workflow';
@ -52,3 +54,4 @@ export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fB
// General
export const INSTANCE_ID_HEADER = 'n8n-instance-id';
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';

View file

@ -42,6 +42,7 @@ import {
faCodeBranch,
faCog,
faCogs,
faClock,
faClone,
faCloud,
faCloudDownloadAlt,
@ -75,6 +76,7 @@ import {
faMapSigns,
faNetworkWired,
faPause,
faPauseCircle,
faPen,
faPlay,
faPlayCircle,
@ -104,7 +106,6 @@ import {
faTrash,
faUndo,
faUsers,
faClock,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@ -132,6 +133,7 @@ library.add(faCode);
library.add(faCodeBranch);
library.add(faCog);
library.add(faCogs);
library.add(faClock);
library.add(faClone);
library.add(faCloud);
library.add(faCloudDownloadAlt);
@ -165,6 +167,7 @@ library.add(faKey);
library.add(faMapSigns);
library.add(faNetworkWired);
library.add(faPause);
library.add(faPauseCircle);
library.add(faPen);
library.add(faPlay);
library.add(faPlayCircle);
@ -194,7 +197,6 @@ library.add(faTimes);
library.add(faTrash);
library.add(faUndo);
library.add(faUsers);
library.add(faClock);
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.use(Fragment.Plugin);

View file

@ -54,7 +54,7 @@ export class FormIoTrigger implements INodeType {
},
required: true,
default: '',
description: `Choose from the list or specify an ID. You can also specify the ID using an <a href="https://docs.n8n.io/nodes/expressions.html#expressions" target="_blank" >expression</a>`
description: `Choose from the list or specify an ID. You can also specify the ID using an <a href="https://docs.n8n.io/nodes/expressions.html#expressions" target="_blank" >expression</a>`,
},
{
displayName: 'Form Name/ID',
@ -68,7 +68,7 @@ export class FormIoTrigger implements INodeType {
},
required: true,
default: '',
description: `Choose from the list or specify an ID. You can also specify the ID using an <a href="https://docs.n8n.io/nodes/expressions.html#expressions" target="_blank" >expression</a>`
description: `Choose from the list or specify an ID. You can also specify the ID using an <a href="https://docs.n8n.io/nodes/expressions.html#expressions" target="_blank" >expression</a>`,
},
{
displayName: 'Trigger Events',

View file

@ -4,7 +4,6 @@ import {
} from 'n8n-core';
import {
IDataObject,
IHookFunctions,
IWebhookFunctions,
NodeApiError,
@ -14,8 +13,8 @@ import {
interface IFormIoCredentials {
environment: 'cloudHosted' | ' selfHosted';
domain?: string;
email: string,
password: string,
email: string;
password: string;
}
/**
@ -40,9 +39,6 @@ async function getToken(this: IExecuteFunctions | IWebhookFunctions | IHookFunct
resolveWithFullResponse: true,
};
console.log('options');
console.log(JSON.stringify(options, null, 2));
try {
const responseObject = await this.helpers.request!(options);
return responseObject.headers['x-jwt-token'];

View file

@ -0,0 +1,887 @@
import {
IExecuteFunctions,
WAIT_TIME_UNLIMITED,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IWebhookFunctions,
IWebhookResponseData,
NodeOperationError,
} from 'n8n-workflow';
import * as basicAuth from 'basic-auth';
import { Response } from 'express';
import * as fs from 'fs';
import * as formidable from 'formidable';
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
if (message === undefined) {
message = 'Authorization problem!';
if (responseCode === 401) {
message = 'Authorization is required!';
} else if (responseCode === 403) {
message = 'Authorization data is wrong!';
}
}
resp.writeHead(responseCode, { 'WWW-Authenticate': `Basic realm="${realm}"` });
resp.end(message);
return {
noWebhookResponse: true,
};
}
export class Wait implements INodeType {
description: INodeTypeDescription = {
displayName: 'Wait',
name: 'wait',
icon: 'fa:pause-circle',
group: ['organization'],
version: 1,
description: 'Wait before continue with execution',
defaults: {
name: 'Wait',
color: '#804050',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
incomingAuthentication: [
'basicAuth',
],
},
},
},
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
incomingAuthentication: [
'headerAuth',
],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: '={{$parameter["httpMethod"]}}',
isFullPath: true,
responseCode: '={{$parameter["responseCode"]}}',
responseMode: '={{$parameter["responseMode"]}}',
responseData: '={{$parameter["responseData"]}}',
responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}',
responseContentType: '={{$parameter["options"]["responseContentType"]}}',
responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}',
responseHeaders: '={{$parameter["options"]["responseHeaders"]}}',
path: '={{$parameter["options"]["webhookSuffix"] || ""}}',
restartWebhook: true,
},
],
properties: [
{
displayName: 'Webhook authentication',
name: 'incomingAuthentication',
type: 'options',
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'Header Auth',
value: 'headerAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description: 'If and how incoming resume-webhook-requests to $resumeWebhookUrl should be authenticated for additional security.',
},
{
displayName: 'Resume',
name: 'resume',
type: 'options',
options: [
{
name: 'After time interval',
value: 'timeInterval',
description: 'Waits for a certain amount of time',
},
{
name: 'At specified time',
value: 'specificTime',
description: 'Waits until the set date time to continue',
},
{
name: 'On webhook call',
value: 'webhook',
description: 'Waits for a webhook call',
},
],
default: 'timeInterval',
description: 'For what the node should wait for before to continue with the execution',
},
// ----------------------------------
// resume:specificTime
// ----------------------------------
{
displayName: 'Date and Time',
name: 'dateTime',
type: 'dateTime',
displayOptions: {
show: {
resume: [
'specificTime',
],
},
},
default: '',
description: 'The date and time to wait for before continuing',
},
// ----------------------------------
// resume:timeInterval
// ----------------------------------
{
displayName: 'Amount',
name: 'amount',
type: 'number',
displayOptions: {
show: {
resume: [
'timeInterval',
],
},
},
typeOptions: {
minValue: 0,
numberPrecision: 2,
},
default: 1,
description: 'The time to wait',
},
{
displayName: 'Unit',
name: 'unit',
type: 'options',
displayOptions: {
show: {
resume: [
'timeInterval',
],
},
},
options: [
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Days',
value: 'days',
},
],
default: 'hours',
description: 'Unit of the interval value',
},
// ----------------------------------
// resume:webhook
// ----------------------------------
{
displayName: 'The URL to call will be generated at run time, and can be referenced under <strong>$resumeWebhookUrl</strong>. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
name: 'webhookNotice',
type: 'notice',
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
default: '',
},
{
displayName: 'HTTP Method',
name: 'httpMethod',
type: 'options',
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
options: [
{
name: 'GET',
value: 'GET',
},
{
name: 'HEAD',
value: 'HEAD',
},
{
name: 'POST',
value: 'POST',
},
],
default: 'GET',
description: 'The HTTP method to liste to',
},
{
displayName: 'Response Code',
name: 'responseCode',
type: 'number',
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
typeOptions: {
minValue: 100,
maxValue: 599,
},
default: 200,
description: 'The HTTP Response code to return',
},
{
displayName: 'Response Mode',
name: 'responseMode',
type: 'options',
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
options: [
{
name: 'On Received',
value: 'onReceived',
description: 'Returns directly with defined Response Code',
},
{
name: 'Last Node',
value: 'lastNode',
description: 'Returns data of the last executed node',
},
],
default: 'onReceived',
description: 'When and how to respond to the webhook',
},
{
displayName: 'Response Data',
name: 'responseData',
type: 'options',
displayOptions: {
show: {
resume: [
'webhook',
],
responseMode: [
'lastNode',
],
},
},
options: [
{
name: 'All Entries',
value: 'allEntries',
description: 'Returns all the entries of the last node. Always returns an array',
},
{
name: 'First Entry JSON',
value: 'firstEntryJson',
description: 'Returns the JSON data of the first entry of the last node. Always returns a JSON object',
},
{
name: 'First Entry Binary',
value: 'firstEntryBinary',
description: 'Returns the binary data of the first entry of the last node. Always returns a binary file',
},
],
default: 'firstEntryJson',
description: 'What data should be returned. If it should return<br />all the itemsas array or only the first item as object',
},
{
displayName: 'Property Name',
name: 'responseBinaryPropertyName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
resume: [
'webhook',
],
responseData: [
'firstEntryBinary',
],
},
},
description: 'Name of the binary property to return',
},
{
displayName: 'Limit wait time',
name: 'limitWaitTime',
type: 'boolean',
default: false,
description: `If no webhook call is received, the workflow will automatically<br />
resume execution after specified condition.`,
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
},
{
displayName: 'Limit type',
name: 'limitType',
type: 'options',
default: 'afterTimeInterval',
description: `Sets the condition for the execution to resume.<br />
Can be a specified date or after some time.`,
displayOptions: {
show: {
limitWaitTime: [
true,
],
resume: [
'webhook',
],
},
},
options: [
{
name: 'After time interval',
value: 'afterTimeInterval',
},
{
name: 'At specified time',
value: 'atSpecifiedTime',
},
],
},
{
displayName: 'Amount',
name: 'resumeAmount',
type: 'number',
displayOptions: {
show: {
limitType: [
'afterTimeInterval',
],
limitWaitTime: [
true,
],
resume: [
'webhook',
],
},
},
typeOptions: {
minValue: 0,
numberPrecision: 2,
},
default: 1,
description: 'The time to wait',
},
{
displayName: 'Unit',
name: 'resumeUnit',
type: 'options',
displayOptions: {
show: {
limitType: [
'afterTimeInterval',
],
limitWaitTime: [
true,
],
resume: [
'webhook',
],
},
},
options: [
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Days',
value: 'days',
},
],
default: 'hours',
description: 'Unit of the interval value',
},
{
displayName: 'Max Date and Time',
name: 'maxDateAndTime',
type: 'dateTime',
displayOptions: {
show: {
limitType: [
'atSpecifiedTime',
],
limitWaitTime: [
true,
],
resume: [
'webhook',
],
},
},
default: '',
description: 'Continue execution on the informed date',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
resume: [
'webhook',
],
},
},
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
displayOptions: {
show: {
'/httpMethod': [
'POST',
],
},
},
default: false,
description: 'Set to true if webhook will receive binary data',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
binaryData: [
true,
],
},
},
description: `Name of the binary property to which to write the data of<br />
the received file. If the data gets received via "Form-Data Multipart"<br />
it will be the prefix and a number starting with 0 will be attached to it.`,
},
{
displayName: 'Response Data',
name: 'responseData',
type: 'string',
displayOptions: {
show: {
'/responseMode': [
'onReceived',
],
},
},
default: '',
placeholder: 'success',
description: 'Custom response data to send',
},
{
displayName: 'Response Content-Type',
name: 'responseContentType',
type: 'string',
displayOptions: {
show: {
'/responseData': [
'firstEntryJson',
],
'/responseMode': [
'lastNode',
],
},
},
default: '',
placeholder: 'application/xml',
description: 'Set a custom content-type to return if another one as the "application/json" should be returned',
},
{
displayName: 'Response Headers',
name: 'responseHeaders',
placeholder: 'Add Response Header',
description: 'Add headers to the webhook response',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'entries',
displayName: 'Entries',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the header',
},
],
},
],
},
{
displayName: 'Property Name',
name: 'responsePropertyName',
type: 'string',
displayOptions: {
show: {
'/responseData': [
'firstEntryJson',
],
'/responseMode': [
'lastNode',
],
},
},
default: 'data',
description: 'Name of the property to return the data of instead of the whole JSON',
},
{
displayName: 'Webhook Suffix',
name: 'webhookSuffix',
type: 'string',
default: '',
placeholder: 'webhook',
description: 'The webhook suffix path that will be appended to the restart URL. Important: Does currently not support expressions.',
},
// {
// displayName: 'Raw Body',
// name: 'rawBody',
// type: 'boolean',
// displayOptions: {
// hide: {
// binaryData: [
// true,
// ],
// },
// },
// default: false,
// description: 'Raw body (binary)',
// },
],
},
],
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// INFO: Currently (20.06.2021) 100% identical with Webook-Node
const incomingAuthentication = this.getNodeParameter('incomingAuthentication') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
const req = this.getRequestObject();
const resp = this.getResponseObject();
const headers = this.getHeaderData();
const realm = 'Webhook';
if (incomingAuthentication === 'basicAuth') {
// Basic authorization is needed to call webhook
const httpBasicAuth = await this.getCredentials('httpBasicAuth');
if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) {
// Data is not defined on node so can not authenticate
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
}
const basicAuthData = basicAuth(req);
if (basicAuthData === undefined) {
// Authorization data is missing
return authorizationError(resp, realm, 401);
}
if (basicAuthData.name !== httpBasicAuth!.user || basicAuthData.pass !== httpBasicAuth!.password) {
// Provided authentication data is wrong
return authorizationError(resp, realm, 403);
}
} else if (incomingAuthentication === 'headerAuth') {
// Special header with value is needed to call webhook
const httpHeaderAuth = await this.getCredentials('httpHeaderAuth');
if (httpHeaderAuth === undefined || !httpHeaderAuth.name || !httpHeaderAuth.value) {
// Data is not defined on node so can not authenticate
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
}
const headerName = (httpHeaderAuth.name as string).toLowerCase();
const headerValue = (httpHeaderAuth.value as string);
if (!headers.hasOwnProperty(headerName) || (headers as IDataObject)[headerName] !== headerValue) {
// Provided authentication data is wrong
return authorizationError(resp, realm, 403);
}
}
// @ts-ignore
const mimeType = headers['content-type'] || 'application/json';
if (mimeType.includes('multipart/form-data')) {
const form = new formidable.IncomingForm({});
return new Promise((resolve, reject) => {
form.parse(req, async (err, data, files) => {
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: data,
},
};
let count = 0;
for (const file of Object.keys(files)) {
let binaryPropertyName = file;
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName}${count}`;
}
const fileJson = (files[file] as formidable.File).toJSON() as unknown as IDataObject;
const fileContent = await fs.promises.readFile((files[file] as formidable.File).path);
returnItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(Buffer.from(fileContent), fileJson.name as string, fileJson.type as string);
count += 1;
}
resolve({
workflowData: [
[
returnItem,
],
],
});
});
});
}
if (options.binaryData === true) {
return new Promise((resolve, reject) => {
const binaryPropertyName = options.binaryPropertyName || 'data';
const data: Buffer[] = [];
req.on('data', (chunk) => {
data.push(chunk);
});
req.on('end', async () => {
const returnItem: INodeExecutionData = {
binary: {},
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: this.getBodyData(),
},
};
returnItem.binary![binaryPropertyName as string] = await this.helpers.prepareBinaryData(Buffer.concat(data));
return resolve({
workflowData: [
[
returnItem,
],
],
});
});
req.on('error', (error) => {
throw new NodeOperationError(this.getNode(), error);
});
});
}
const response: INodeExecutionData = {
json: {
headers,
params: this.getParamsData(),
query: this.getQueryData(),
body: this.getBodyData(),
},
};
if (options.rawBody) {
response.binary = {
data: {
// @ts-ignore
data: req.rawBody.toString(BINARY_ENCODING),
mimeType,
},
};
}
let webhookResponse: string | undefined;
if (options.responseData) {
webhookResponse = options.responseData as string;
}
return {
webhookResponse,
workflowData: [
[
response,
],
],
};
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const resume = this.getNodeParameter('resume', 0) as string;
if (resume === 'webhook') {
let waitTill = new Date(WAIT_TIME_UNLIMITED);
const limitWaitTime = this.getNodeParameter('limitWaitTime', 0);
if (limitWaitTime === true) {
const limitType = this.getNodeParameter('limitType', 0);
if (limitType === 'afterTimeInterval') {
let waitAmount = this.getNodeParameter('resumeAmount', 0) as number;
const resumeUnit = this.getNodeParameter('resumeUnit', 0);
if (resumeUnit === 'minutes') {
waitAmount *= 60;
}
if (resumeUnit === 'hours') {
waitAmount *= 60 * 60;
}
if (resumeUnit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
waitTill = new Date(new Date().getTime() + waitAmount);
} else {
waitTill = new Date(this.getNodeParameter('maxDateAndTime', 0) as string);
}
}
await this.putExecutionToWait(waitTill);
return [this.getInputData()];
}
let waitTill: Date;
if (resume === 'timeInterval') {
const unit = this.getNodeParameter('unit', 0) as string;
let waitAmount = this.getNodeParameter('amount', 0) as number;
if (unit === 'minutes') {
waitAmount *= 60;
}
if (unit === 'hours') {
waitAmount *= 60 * 60;
}
if (unit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
waitTill = new Date(new Date().getTime() + waitAmount);
} else {
// resume: dateTime
const dateTime = this.getNodeParameter('dateTime', 0) as string;
console.log('dateTime', dateTime);
waitTill = new Date(dateTime);
}
const waitValue = Math.max(waitTill.getTime() - new Date().getTime(), 0);
if (waitValue < 65000) {
// If wait time is shorter than 65 seconds leave execution active because
// we just check the database every 60 seconds.
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([this.getInputData()]);
}, waitValue);
});
}
// If longer than 60 seconds put execution to wait
await this.putExecutionToWait(waitTill);
return [this.getInputData()];
}
}

View file

@ -585,6 +585,7 @@
"dist/nodes/UptimeRobot/UptimeRobot.node.js",
"dist/nodes/Vero/Vero.node.js",
"dist/nodes/Vonage/Vonage.node.js",
"dist/nodes/Wait.node.js",
"dist/nodes/Webflow/Webflow.node.js",
"dist/nodes/Webflow/WebflowTrigger.node.js",
"dist/nodes/Webhook.node.js",

View file

@ -4,6 +4,7 @@ import {
INodeExecutionData,
INodeParameters,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
NodeParameterValue,
Workflow,
WorkflowDataProxy,
@ -59,7 +60,7 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @memberof Workflow
*/
resolveSimpleParameterValue(parameterValue: NodeParameterValue, siblingParameters: INodeParameters, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
resolveSimpleParameterValue(parameterValue: NodeParameterValue, siblingParameters: INodeParameters, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
// Check if it is an expression
if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') {
// Is no expression so return value
@ -72,7 +73,7 @@ export class Expression {
parameterValue = parameterValue.substr(1);
// Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, siblingParameters, mode, -1, selfData);
const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, siblingParameters, mode, additionalKeys, -1, selfData);
const data = dataProxy.getDataProxy();
// Execute the expression
@ -102,7 +103,7 @@ export class Expression {
* @returns {(string | undefined)}
* @memberof Workflow
*/
getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, mode: WorkflowExecuteMode, defaultValue?: boolean | number | string): boolean | number | string | undefined {
getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, defaultValue?: boolean | number | string): boolean | number | string | undefined {
if (parameterValue === undefined) {
// Value is not set so return the default
return defaultValue;
@ -118,7 +119,7 @@ export class Expression {
},
};
return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode) as boolean | number | string | undefined;
return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys) as boolean | number | string | undefined;
}
@ -132,7 +133,7 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)}
* @memberof Workflow
*/
getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], mode: WorkflowExecuteMode, defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined {
getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined {
if (parameterValue === undefined) {
// Value is not set so return the default
return defaultValue;
@ -149,10 +150,10 @@ export class Expression {
};
// Resolve the "outer" main values
const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode, false, selfData);
const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys, false, selfData);
// Resolve the "inner" values
return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData, mode, false, selfData);
return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData, mode, additionalKeys, false, selfData);
}
@ -172,7 +173,7 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @memberof Workflow
*/
getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
// Helper function which returns true when the parameter is a complex one or array
const isComplexParameter = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => {
return typeof value === 'object';
@ -181,15 +182,15 @@ export class Expression {
// Helper function which resolves a parameter value depending on if it is simply or not
const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], siblingParameters: INodeParameters) => {
if (isComplexParameter(value)) {
return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData);
} else {
return this.resolveSimpleParameterValue(value as NodeParameterValue, siblingParameters, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
return this.resolveSimpleParameterValue(value as NodeParameterValue, siblingParameters, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData);
}
};
// Check if it value is a simple one that we can get it resolved directly
if (!isComplexParameter(parameterValue)) {
return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, {}, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, {}, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, additionalKeys, returnObjectAsString, selfData);
}
// The parameter value is complex so resolve depending on type

View file

@ -221,6 +221,7 @@ export interface IExecuteFunctions {
getTimezone(): string;
getWorkflow(): IWorkflowMetadata;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
putExecutionToWait(waitTill: Date): Promise<void>;
sendMessageToUI(message: any): void; // tslint:disable-line:no-any
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
@ -403,8 +404,7 @@ export interface INodeParameters {
[key: string]: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
}
export type NodePropertyTypes = 'boolean' | 'collection' | 'color' | 'dateTime' | 'fixedCollection' | 'hidden' | 'json' | 'multiOptions' | 'number' | 'options' | 'string';
export type NodePropertyTypes = 'boolean' | 'collection' | 'color' | 'dateTime' | 'fixedCollection' | 'hidden' | 'json' | 'notice' | 'multiOptions' | 'number' | 'options' | 'string';
export type EditorTypes = 'code';
@ -450,8 +450,6 @@ export interface INodeProperties {
noDataExpression?: boolean;
required?: boolean;
}
export interface INodePropertyOptions {
name: string;
value: string | number;
@ -594,6 +592,7 @@ export interface IWebhookDescription {
responsePropertyName?: string;
responseMode?: WebhookResponseMode | string;
responseData?: WebhookResponseData | string;
restartWebhook?: boolean;
}
export interface IWorkflowDataProxyData {
@ -610,6 +609,10 @@ export interface IWorkflowDataProxyData {
$workflow: any; // tslint:disable-line:no-any
}
export interface IWorkflowDataProxyAdditionalKeys {
[key: string]: string | number | undefined;
}
export interface IWorkflowMetadata {
id?: number | string;
name?: string;
@ -646,6 +649,7 @@ export interface IRun {
data: IRunExecutionData;
finished?: boolean;
mode: WorkflowExecuteMode;
waitTill?: Date;
startedAt: Date;
stoppedAt?: Date;
}
@ -669,6 +673,7 @@ export interface IRunExecutionData {
nodeExecutionStack: IExecuteData[];
waitingExecution: IWaitingForExecution;
};
waitTill?: Date;
}
@ -741,6 +746,7 @@ export interface IWorkflowExecuteAdditionalData {
encryptionKey: string;
executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: any) => Promise<any>; // tslint:disable-line:no-any
// hooks?: IWorkflowExecuteHooks;
executionId?: string;
hooks?: WorkflowHooks;
httpResponse?: express.Response;
httpRequest?: express.Request;
@ -748,6 +754,7 @@ export interface IWorkflowExecuteAdditionalData {
sendMessageToUI?: (source: string, message: any) => void; // tslint:disable-line:no-any
timezone: string;
webhookBaseUrl: string;
webhookWaitingBaseUrl: string;
webhookTestBaseUrl: string;
currentNodeParameters?: INodeParameters;
executionTimeoutTimestamp?: number;

View file

@ -749,7 +749,7 @@ export async function prepareOutputData(outputData: INodeExecutionData[], output
* @param {INode} node
* @returns {IWebhookData[]}
*/
export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData): IWebhookData[] {
export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, ignoreRestartWehbooks = false): IWebhookData[] {
if (node.disabled === true) {
// Node is disabled so webhooks will also not be enabled
return [];
@ -767,7 +767,12 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) {
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode);
if (ignoreRestartWehbooks === true && webhookDescription.restartWebhook === true) {
continue;
}
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, {});
if (nodeWebhookPath === undefined) {
// TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`);
@ -783,10 +788,11 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
nodeWebhookPath = nodeWebhookPath.slice(0, -1);
}
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], 'internal', false) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath);
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], 'internal', {}, false) as boolean;
const restartWebhook: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['restartWebhook'], 'internal', {}, false) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath, restartWebhook);
const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, 'GET');
const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, {}, 'GET');
if (httpMethod === undefined) {
// TODO: Use a proper logger
@ -832,7 +838,7 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) {
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode);
let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode, {});
if (nodeWebhookPath === undefined) {
// TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`);
@ -848,11 +854,11 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
nodeWebhookPath = nodeWebhookPath.slice(0, -1);
}
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, false) as boolean;
const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, {}, false) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath);
const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode);
const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, {});
if (httpMethod === undefined) {
// TODO: Use a proper logger
@ -883,9 +889,11 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD
* @param {string} path
* @returns {string}
*/
export function getNodeWebhookPath(workflowId: string, node: INode, path: string, isFullPath?: boolean): string {
export function getNodeWebhookPath(workflowId: string, node: INode, path: string, isFullPath?: boolean, restartWebhook?: boolean): string {
let webhookPath = '';
if (node.webhookId === undefined) {
if (restartWebhook === true) {
return path;
} else if (node.webhookId === undefined) {
webhookPath = `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`;
} else {
if (isFullPath === true) {

View file

@ -3,6 +3,7 @@ import {
INodeExecutionData,
INodeParameters,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
NodeHelpers,
NodeParameterValue,
@ -23,10 +24,11 @@ export class WorkflowDataProxy {
private siblingParameters: INodeParameters;
private mode: WorkflowExecuteMode;
private selfData: IDataObject;
private additionalKeys: IWorkflowDataProxyAdditionalKeys;
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], siblingParameters: INodeParameters, mode: WorkflowExecuteMode, defaultReturnRunIndex = -1, selfData = {}) {
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], siblingParameters: INodeParameters, mode: WorkflowExecuteMode, additionalKeys: IWorkflowDataProxyAdditionalKeys, defaultReturnRunIndex = -1, selfData = {}) {
this.workflow = workflow;
this.runExecutionData = runExecutionData;
this.defaultReturnRunIndex = defaultReturnRunIndex;
@ -37,6 +39,7 @@ export class WorkflowDataProxy {
this.siblingParameters = siblingParameters;
this.mode = mode;
this.selfData = selfData;
this.additionalKeys = additionalKeys;
}
@ -131,7 +134,7 @@ export class WorkflowDataProxy {
if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') {
// The found value is an expression so resolve it
return that.workflow.expression.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData, that.mode);
return that.workflow.expression.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData, that.mode, that.additionalKeys);
}
return returnValue;
@ -371,11 +374,11 @@ export class WorkflowDataProxy {
$env: this.envGetter(),
$evaluateExpression: (expression: string, itemIndex?: number) => {
itemIndex = itemIndex || that.itemIndex;
return that.workflow.expression.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData, that.mode);
return that.workflow.expression.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData, that.mode, that.additionalKeys);
},
$item: (itemIndex: number, runIndex?: number) => {
const defaultReturnRunIndex = runIndex === undefined ? -1 : runIndex;
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, that.siblingParameters, that.mode, defaultReturnRunIndex);
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, that.siblingParameters, that.mode, that.additionalKeys, defaultReturnRunIndex);
return dataProxy.getDataProxy();
},
$items: (nodeName?: string, outputIndex?: number, runIndex?: number) => {
@ -399,6 +402,7 @@ export class WorkflowDataProxy {
$runIndex: this.runIndex,
$mode: this.mode,
$workflow: this.workflowGetter(),
...that.additionalKeys,
};
return new Proxy(base, {

View file

@ -1097,7 +1097,7 @@ describe('Workflow', () => {
for (const parameterName of Object.keys(testData.output)) {
const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName];
const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, 'manual');
const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, 'manual', {});
// @ts-ignore
expect(result).toEqual(testData.output[parameterName]);
}
@ -1247,7 +1247,7 @@ describe('Workflow', () => {
const parameterName = 'values';
const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName];
const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, 'manual');
const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, 'manual', {});
expect(result).toEqual({
string: [