2019-06-23 03:35:23 -07:00
|
|
|
import * as express from 'express';
|
|
|
|
|
|
|
|
import {
|
|
|
|
IResponseCallbackData,
|
2020-01-22 15:06:43 -08:00
|
|
|
IWorkflowDb,
|
|
|
|
NodeTypes,
|
2019-06-23 03:35:23 -07:00
|
|
|
Push,
|
|
|
|
ResponseHelper,
|
|
|
|
WebhookHelpers,
|
2020-01-22 15:06:43 -08:00
|
|
|
WorkflowHelpers,
|
2019-06-23 03:35:23 -07:00
|
|
|
} from './';
|
|
|
|
|
|
|
|
import {
|
|
|
|
ActiveWebhooks,
|
|
|
|
} from 'n8n-core';
|
|
|
|
|
|
|
|
import {
|
|
|
|
IWebhookData,
|
|
|
|
IWorkflowExecuteAdditionalData,
|
|
|
|
WebhookHttpMethod,
|
|
|
|
Workflow,
|
|
|
|
WorkflowExecuteMode,
|
|
|
|
} from 'n8n-workflow';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class TestWebhooks {
|
|
|
|
|
|
|
|
private testWebhookData: {
|
|
|
|
[key: string]: {
|
|
|
|
sessionId?: string;
|
|
|
|
timeout: NodeJS.Timeout,
|
|
|
|
workflowData: IWorkflowDb;
|
|
|
|
};
|
|
|
|
} = {};
|
|
|
|
private activeWebhooks: ActiveWebhooks | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.activeWebhooks = new ActiveWebhooks();
|
|
|
|
this.activeWebhooks.testWebhooks = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executes a test-webhook and returns the data. It also makes sure that the
|
|
|
|
* data gets additionally send to the UI. After the request got handled it
|
|
|
|
* automatically remove the test-webhook.
|
|
|
|
*
|
|
|
|
* @param {WebhookHttpMethod} httpMethod
|
|
|
|
* @param {string} path
|
|
|
|
* @param {express.Request} request
|
|
|
|
* @param {express.Response} response
|
|
|
|
* @returns {Promise<object>}
|
|
|
|
* @memberof TestWebhooks
|
|
|
|
*/
|
|
|
|
async callTestWebhook(httpMethod: WebhookHttpMethod, path: string, request: express.Request, response: express.Response): Promise<IResponseCallbackData> {
|
|
|
|
const webhookData: IWebhookData | undefined = this.activeWebhooks!.get(httpMethod, path);
|
|
|
|
|
|
|
|
if (webhookData === undefined) {
|
|
|
|
// The requested webhook is not registred
|
2019-08-28 08:16:09 -07:00
|
|
|
throw new ResponseHelper.ResponseError('The requested webhook is not registred.', 404, 404);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
|
2020-02-10 17:52:15 -08:00
|
|
|
const webhookKey = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path);
|
|
|
|
|
|
|
|
const workflowData = this.testWebhookData[webhookKey].workflowData;
|
2020-01-22 15:06:43 -08:00
|
|
|
|
|
|
|
const nodeTypes = NodeTypes();
|
2020-02-15 17:07:01 -08:00
|
|
|
const workflow = new Workflow({ id: webhookData.workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings});
|
2020-01-22 15:06:43 -08:00
|
|
|
|
2019-06-23 03:35:23 -07:00
|
|
|
// Get the node which has the webhook defined to know where to start from and to
|
|
|
|
// get additional data
|
2020-01-22 15:06:43 -08:00
|
|
|
const workflowStartNode = workflow.getNode(webhookData.node);
|
2019-06-23 03:35:23 -07:00
|
|
|
if (workflowStartNode === null) {
|
2019-08-28 08:16:09 -07:00
|
|
|
throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
const executionMode = 'manual';
|
2020-01-22 15:06:43 -08:00
|
|
|
const executionId = await WebhookHelpers.executeWebhook(workflow, webhookData, this.testWebhookData[webhookKey].workflowData, workflowStartNode, executionMode, this.testWebhookData[webhookKey].sessionId, request, response, (error: Error | null, data: IResponseCallbackData) => {
|
2019-06-23 03:35:23 -07:00
|
|
|
if (error !== null) {
|
|
|
|
return reject(error);
|
|
|
|
}
|
|
|
|
resolve(data);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (executionId === undefined) {
|
|
|
|
// The workflow did not run as the request was probably setup related
|
|
|
|
// or a ping so do not resolve the promise and wait for the real webhook
|
|
|
|
// request instead.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inform editor-ui that webhook got received
|
|
|
|
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
2019-08-28 06:28:47 -07:00
|
|
|
const pushInstance = Push.getInstance();
|
2020-01-22 15:06:43 -08:00
|
|
|
pushInstance.send('testWebhookReceived', { workflowId: webhookData.workflowId, executionId }, this.testWebhookData[webhookKey].sessionId!);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
// Delete webhook also if an error is thrown
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the webhook
|
|
|
|
clearTimeout(this.testWebhookData[webhookKey].timeout);
|
|
|
|
delete this.testWebhookData[webhookKey];
|
2020-01-22 15:06:43 -08:00
|
|
|
this.activeWebhooks!.removeWorkflow(workflow);
|
2019-06-23 03:35:23 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if it has to wait for webhook data to execute the workflow. If yes it waits
|
|
|
|
* for it and resolves with the result of the workflow if not it simply resolves
|
|
|
|
* with undefined
|
|
|
|
*
|
|
|
|
* @param {IWorkflowDb} workflowData
|
|
|
|
* @param {Workflow} workflow
|
|
|
|
* @returns {(Promise<IExecutionDb | undefined>)}
|
|
|
|
* @memberof TestWebhooks
|
|
|
|
*/
|
|
|
|
async needsWebhookData(workflowData: IWorkflowDb, workflow: Workflow, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, sessionId?: string, destinationNode?: string): Promise<boolean> {
|
|
|
|
const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData, destinationNode);
|
|
|
|
|
|
|
|
if (webhooks.length === 0) {
|
|
|
|
// No Webhooks found
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove test-webhooks automatically if they do not get called (after 120 seconds)
|
|
|
|
const timeout = setTimeout(() => {
|
2020-03-20 16:30:03 -07:00
|
|
|
this.cancelTestWebhook(workflowData.id.toString());
|
2019-06-23 03:35:23 -07:00
|
|
|
}, 120000);
|
|
|
|
|
|
|
|
let key: string;
|
|
|
|
for (const webhookData of webhooks) {
|
|
|
|
key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path);
|
|
|
|
this.testWebhookData[key] = {
|
|
|
|
sessionId,
|
|
|
|
timeout,
|
|
|
|
workflowData,
|
|
|
|
};
|
2020-01-22 15:06:43 -08:00
|
|
|
await this.activeWebhooks!.add(workflow, webhookData, mode);
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2020-03-20 16:30:03 -07:00
|
|
|
// Save static data!
|
|
|
|
this.testWebhookData[key].workflowData.staticData = workflow.staticData;
|
|
|
|
}
|
2020-01-22 15:06:43 -08:00
|
|
|
|
2019-06-23 03:35:23 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes a test webhook of the workflow with the given id
|
|
|
|
*
|
|
|
|
* @param {string} workflowId
|
|
|
|
* @returns {boolean}
|
|
|
|
* @memberof TestWebhooks
|
|
|
|
*/
|
2020-03-20 16:30:03 -07:00
|
|
|
cancelTestWebhook(workflowId: string): boolean {
|
|
|
|
const nodeTypes = NodeTypes();
|
|
|
|
|
2019-06-23 03:35:23 -07:00
|
|
|
let foundWebhook = false;
|
|
|
|
for (const webhookKey of Object.keys(this.testWebhookData)) {
|
|
|
|
const webhookData = this.testWebhookData[webhookKey];
|
|
|
|
|
|
|
|
if (webhookData.workflowData.id.toString() !== workflowId) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foundWebhook = true;
|
|
|
|
|
|
|
|
clearTimeout(this.testWebhookData[webhookKey].timeout);
|
|
|
|
|
|
|
|
// Inform editor-ui that webhook got received
|
|
|
|
if (this.testWebhookData[webhookKey].sessionId !== undefined) {
|
|
|
|
try {
|
2019-08-28 06:28:47 -07:00
|
|
|
const pushInstance = Push.getInstance();
|
2019-07-24 05:25:30 -07:00
|
|
|
pushInstance.send('testWebhookDeleted', { workflowId }, this.testWebhookData[webhookKey].sessionId!);
|
2019-06-23 03:35:23 -07:00
|
|
|
} catch (error) {
|
|
|
|
// Could not inform editor, probably is not connected anymore. So sipmly go on.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-20 16:30:03 -07:00
|
|
|
const workflowData = webhookData.workflowData;
|
|
|
|
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 });
|
|
|
|
|
2019-06-23 03:35:23 -07:00
|
|
|
// Remove the webhook
|
|
|
|
delete this.testWebhookData[webhookKey];
|
2020-01-22 15:06:43 -08:00
|
|
|
this.activeWebhooks!.removeWorkflow(workflow);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return foundWebhook;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes all the currently active test webhooks
|
|
|
|
*/
|
|
|
|
async removeAll(): Promise<void> {
|
|
|
|
if (this.activeWebhooks === null) {
|
|
|
|
return;
|
|
|
|
}
|
2020-01-22 15:06:43 -08:00
|
|
|
|
2020-02-10 17:52:15 -08:00
|
|
|
const nodeTypes = NodeTypes();
|
2020-01-22 15:06:43 -08:00
|
|
|
|
2020-02-10 17:52:15 -08:00
|
|
|
let workflowData: IWorkflowDb;
|
|
|
|
let workflow: Workflow;
|
2020-01-22 15:06:43 -08:00
|
|
|
const workflows: Workflow[] = [];
|
2020-02-10 17:52:15 -08:00
|
|
|
for (const webhookKey of Object.keys(this.testWebhookData)) {
|
|
|
|
workflowData = this.testWebhookData[webhookKey].workflowData;
|
2020-02-15 17:07:01 -08:00
|
|
|
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 });
|
2020-03-20 16:30:03 -07:00
|
|
|
workflows.push(workflow);
|
2020-01-22 15:06:43 -08:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2020-01-22 15:06:43 -08:00
|
|
|
return this.activeWebhooks.removeAll(workflows);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let testWebhooksInstance: TestWebhooks | undefined;
|
|
|
|
|
|
|
|
export function getInstance(): TestWebhooks {
|
|
|
|
if (testWebhooksInstance === undefined) {
|
|
|
|
testWebhooksInstance = new TestWebhooks();
|
|
|
|
}
|
|
|
|
|
|
|
|
return testWebhooksInstance;
|
|
|
|
}
|