diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 9fef6ed243..a72cb065c7 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -11,11 +11,11 @@ import { WorkflowHelpers, WorkflowRunner, WorkflowExecuteAdditionalData, + IWebhookDb, } from './'; import { ActiveWorkflows, - ActiveWebhooks, NodeExecuteFunctions, } from 'n8n-core'; @@ -26,7 +26,7 @@ import { INode, INodeExecutionData, IRunExecutionData, - IWebhookData, + NodeHelpers, IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow, WebhookHttpMethod, Workflow, @@ -38,19 +38,21 @@ import * as express from 'express'; export class ActiveWorkflowRunner { private activeWorkflows: ActiveWorkflows | null = null; - private activeWebhooks: ActiveWebhooks | null = null; + private activationErrors: { [key: string]: IActivationError; } = {}; async init() { + // Get the active workflows from database + + // NOTE + // Here I guess we can have a flag on the workflow table like hasTrigger + // so intead of pulling all the active wehhooks just pull the actives that have a trigger const workflowsData: IWorkflowDb[] = await Db.collections.Workflow!.find({ active: true }) as IWorkflowDb[]; - this.activeWebhooks = new ActiveWebhooks(); - - // Add them as active workflows this.activeWorkflows = new ActiveWorkflows(); if (workflowsData.length !== 0) { @@ -58,20 +60,27 @@ export class ActiveWorkflowRunner { console.log(' Start Active Workflows:'); console.log(' ================================'); + const nodeTypes = NodeTypes(); + for (const workflowData of workflowsData) { - console.log(` - ${workflowData.name}`); - try { - await this.add(workflowData.id.toString(), workflowData); - console.log(` => Started`); - } catch (error) { - console.log(` => ERROR: Workflow could not be activated:`); - console.log(` ${error.message}`); + + 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}); + + if (workflow.getTriggerNodes().length !== 0 + || workflow.getPollNodes().length !== 0) { + console.log(` - ${workflowData.name}`); + try { + await this.add(workflowData.id.toString(), workflowData); + console.log(` => Started`); + } catch (error) { + console.log(` => ERROR: Workflow could not be activated:`); + console.log(` ${error.message}`); + } } } } } - /** * Removes all the currently active workflows * @@ -94,7 +103,6 @@ export class ActiveWorkflowRunner { return; } - /** * Checks if a webhook for the given method and path exists and executes the workflow. * @@ -110,30 +118,41 @@ export class ActiveWorkflowRunner { throw new ResponseHelper.ResponseError('The "activeWorkflows" instance did not get initialized yet.', 404, 404); } - const webhookData: IWebhookData | undefined = this.activeWebhooks!.get(httpMethod, path); + const webhook = await Db.collections.Webhook?.findOne({ webhookPath: path, method: httpMethod }) as IWebhookDb; - if (webhookData === undefined) { + // check if something exist + if (webhook === undefined) { // The requested webhook is not registered throw new ResponseHelper.ResponseError(`The requested webhook "${httpMethod} ${path}" is not registered.`, 404, 404); } - const workflowData = await Db.collections.Workflow!.findOne(webhookData.workflowId); + const workflowData = await Db.collections.Workflow!.findOne(webhook.workflowId); if (workflowData === undefined) { - throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhookData.workflowId}"`, 404, 404); + throw new ResponseHelper.ResponseError(`Could not find workflow with id "${webhook.workflowId}"`, 404, 404); } const nodeTypes = NodeTypes(); - 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}); + const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings}); + + const credentials = await WorkflowCredentials([workflow.getNode(webhook.node as string) as INode]); + + const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); + + const webhookData = NodeHelpers.getNodeWebhooks(workflow, workflow.getNode(webhook.node as string) as INode, additionalData).filter((webhook) => { + return (webhook.httpMethod === httpMethod && webhook.path === path); + })[0]; // Get the node which has the webhook defined to know where to start from and to // get additional data const workflowStartNode = workflow.getNode(webhookData.node); + if (workflowStartNode === null) { throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); } 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) => { if (error !== null) { return reject(error); @@ -143,19 +162,14 @@ export class ActiveWorkflowRunner { }); } - /** * Returns the ids of the currently active workflows * * @returns {string[]} * @memberof ActiveWorkflowRunner */ - getActiveWorkflows(): string[] { - if (this.activeWorkflows === null) { - return []; - } - - return this.activeWorkflows.allActiveWorkflows(); + getActiveWorkflows(): Promise { + return Db.collections.Workflow?.find({ select: ['id'] }) as Promise; } @@ -166,15 +180,11 @@ export class ActiveWorkflowRunner { * @returns {boolean} * @memberof ActiveWorkflowRunner */ - isActive(id: string): boolean { - if (this.activeWorkflows !== null) { - return this.activeWorkflows.isActive(id); - } - - return false; + async isActive(id: string): Promise { + const workflow = await Db.collections.Workflow?.findOne({ id }) as IWorkflowDb; + return workflow?.active as boolean; } - /** * Return error if there was a problem activating the workflow * @@ -190,7 +200,6 @@ export class ActiveWorkflowRunner { return this.activationErrors[id]; } - /** * Adds all the webhooks of the workflow * @@ -202,12 +211,60 @@ export class ActiveWorkflowRunner { */ async addWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode): Promise { const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData); + let path = ''; for (const webhookData of webhooks) { - await this.activeWebhooks!.add(workflow, webhookData, mode); - // Save static data! - await WorkflowHelpers.saveStaticData(workflow); + + const node = workflow.getNode(webhookData.node) as INode; + node.name = webhookData.node; + + path = node.parameters.path as string; + + if (node.parameters.path === undefined) { + path = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['path'], 'GET') as string; + } + + const webhook = { + workflowId: webhookData.workflowId, + webhookPath: NodeHelpers.getNodeWebhookPath(workflow.id as string, node, path), + node: node.name, + method: webhookData.httpMethod, + } as IWebhookDb; + + try { + await Db.collections.Webhook?.insert(webhook); + + const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, false); + if (webhookExists === false) { + // If webhook does not exist yet create it + await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, false); + } + + } catch (error) { + + let errorMessage = ''; + + await Db.collections.Webhook?.delete({ workflowId: workflow.id }); + + // if it's a workflow from the the insert + // TODO check if there is standard error code for deplicate key violation that works + // with all databases + if (error.name === 'QueryFailedError') { + + errorMessage = `The webhook path [${webhook.webhookPath}] and method [${webhook.method}] already exist.`; + + } else if (error.detail) { + // it's a error runnig the webhook methods (checkExists, create) + errorMessage = error.detail; + } else { + errorMessage = error; + } + + throw new Error(errorMessage); + } } + // Save static data! + await WorkflowHelpers.saveStaticData(workflow); } @@ -227,10 +284,22 @@ export class ActiveWorkflowRunner { const nodeTypes = NodeTypes(); const workflow = new Workflow({ id: workflowId, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings }); - await this.activeWebhooks!.removeWorkflow(workflow); + const mode = 'internal'; - // Save the static workflow data if needed - await WorkflowHelpers.saveStaticData(workflow); + const credentials = await WorkflowCredentials(workflowData.nodes); + const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials); + + const webhooks = WebhookHelpers.getWorkflowWebhooks(workflow, additionalData); + + for (const webhookData of webhooks) { + await workflow.runWebhookMethod('delete', webhookData, NodeExecuteFunctions, mode, false); + } + + const webhook = { + workflowId: workflowData.id, + } as IWebhookDb; + + await Db.collections.Webhook?.delete(webhook); } @@ -322,7 +391,6 @@ export class ActiveWorkflowRunner { }); } - /** * Makes a workflow active * @@ -361,7 +429,11 @@ export class ActiveWorkflowRunner { // Add the workflows which have webhooks defined await this.addWorkflowWebhooks(workflowInstance, additionalData, mode); - await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions, getPollFunctions); + + if (workflowInstance.getTriggerNodes().length !== 0 + || workflowInstance.getPollNodes().length !== 0) { + await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, getTriggerFunctions, getPollFunctions); + } if (this.activationErrors[workflowId] !== undefined) { // If there were activation errors delete them @@ -386,7 +458,6 @@ export class ActiveWorkflowRunner { await WorkflowHelpers.saveStaticData(workflowInstance!); } - /** * Makes a workflow inactive * @@ -395,6 +466,7 @@ export class ActiveWorkflowRunner { * @memberof ActiveWorkflowRunner */ async remove(workflowId: string): Promise { + if (this.activeWorkflows !== null) { // Remove all the webhooks of the workflow await this.removeWorkflowWebhooks(workflowId); @@ -404,8 +476,13 @@ export class ActiveWorkflowRunner { delete this.activationErrors[workflowId]; } - // Remove the workflow from the "list" of active workflows - return this.activeWorkflows.remove(workflowId); + // if it's active in memory then it's a trigger + // so remove from list of actives workflows + if (this.activeWorkflows.isActive(workflowId)) { + this.activeWorkflows.remove(workflowId); + } + + return; } throw new Error(`The "activeWorkflows" instance did not get initialized yet.`); diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 54633adb13..9077922a5c 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -27,10 +27,12 @@ export let collections: IDatabaseCollections = { Credentials: null, Execution: null, Workflow: null, + Webhook: null, }; import { - InitialMigration1587669153312 + InitialMigration1587669153312, + WebhookModel1589476000887, } from './databases/postgresdb/migrations'; import { @@ -81,7 +83,7 @@ export async function init(): Promise { port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number, username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string, schema: config.get('database.postgresdb.schema'), - migrations: [InitialMigration1587669153312], + migrations: [InitialMigration1587669153312, WebhookModel1589476000887], migrationsRun: true, migrationsTableName: `${entityPrefix}migrations`, }; @@ -135,6 +137,7 @@ export async function init(): Promise { collections.Credentials = getRepository(entities.CredentialsEntity); collections.Execution = getRepository(entities.ExecutionEntity); collections.Workflow = getRepository(entities.WorkflowEntity); + collections.Webhook = getRepository(entities.WebhookEntity); return collections; } diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 225e02885f..fd56ad3ada 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -40,8 +40,15 @@ export interface IDatabaseCollections { Credentials: Repository | null; Execution: Repository | null; Workflow: Repository | null; + Webhook: Repository | null; } +export interface IWebhookDb { + workflowId: number | string | ObjectID; + webhookPath: string; + method: string; + node: string; +} export interface IWorkflowBase extends IWorkflowBaseWorkflow { id?: number | string | ObjectID; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index d078c8594e..4e27568983 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -427,7 +427,9 @@ class App { const newWorkflowData = req.body; const id = req.params.id; - if (this.activeWorkflowRunner.isActive(id)) { + const isActive = await this.activeWorkflowRunner.isActive(id); + + if (isActive) { // When workflow gets saved always remove it as the triggers could have been // changed and so the changes would not take effect await this.activeWorkflowRunner.remove(id); @@ -492,7 +494,9 @@ class App { this.app.delete('/rest/workflows/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const id = req.params.id; - if (this.activeWorkflowRunner.isActive(id)) { + const isActive = await this.activeWorkflowRunner.isActive(id); + + if (isActive) { // Before deleting a workflow deactivate it await this.activeWorkflowRunner.remove(id); } @@ -503,6 +507,7 @@ class App { })); + this.app.post('/rest/workflows/run', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const workflowData = req.body.workflowData; const runData: IRunData | undefined = req.body.runData; @@ -632,7 +637,8 @@ class App { // Returns the active workflow ids this.app.get('/rest/active', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { - return this.activeWorkflowRunner.getActiveWorkflows(); + const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(); + return activeWorkflows.map(workflow => workflow.id.toString()) as string[]; })); diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 45ae624e2b..a20fa76c88 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -141,12 +141,14 @@ export class TestWebhooks { let key: string; for (const webhookData of webhooks) { key = this.activeWebhooks!.getWebhookKey(webhookData.httpMethod, webhookData.path); + + await this.activeWebhooks!.add(workflow, webhookData, mode); + this.testWebhookData[key] = { sessionId, timeout, workflowData, }; - await this.activeWebhooks!.add(workflow, webhookData, mode); // Save static data! this.testWebhookData[key].workflowData.staticData = workflow.staticData; diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index d39e3f5dee..0badd1c9d1 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -69,6 +69,33 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo return returnData; } +/** + * Returns all the webhooks which should be created for the give workflow + * + * @export + * @param {string} workflowId + * @param {Workflow} workflow + * @returns {IWebhookData[]} + */ +export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { + // Check all the nodes in the workflow if they have webhooks + + const returnData: IWebhookData[] = []; + + let parentNodes: string[] | undefined; + + for (const node of Object.values(workflow.nodes)) { + if (parentNodes !== undefined && !parentNodes.includes(node.name)) { + // If parentNodes are given check only them if they have webhooks + // and no other ones + continue; + } + returnData.push.apply(returnData, NodeHelpers.getNodeWebhooksBasic(workflow, node)); + } + + return returnData; +} + /** * Executes a webhook diff --git a/packages/cli/src/databases/mongodb/WebhookEntity.ts b/packages/cli/src/databases/mongodb/WebhookEntity.ts new file mode 100644 index 0000000000..a78fd34ae9 --- /dev/null +++ b/packages/cli/src/databases/mongodb/WebhookEntity.ts @@ -0,0 +1,25 @@ +import { + Column, + Entity, + PrimaryColumn, +} from 'typeorm'; + +import { + IWebhookDb, + } from '../../Interfaces'; + +@Entity() +export class WebhookEntity implements IWebhookDb { + + @Column() + workflowId: number; + + @PrimaryColumn() + webhookPath: string; + + @PrimaryColumn() + method: string; + + @Column() + node: string; +} diff --git a/packages/cli/src/databases/mongodb/index.ts b/packages/cli/src/databases/mongodb/index.ts index 164d67fd0c..bd6b9abd60 100644 --- a/packages/cli/src/databases/mongodb/index.ts +++ b/packages/cli/src/databases/mongodb/index.ts @@ -1,3 +1,5 @@ export * from './CredentialsEntity'; export * from './ExecutionEntity'; export * from './WorkflowEntity'; +export * from './WebhookEntity'; + diff --git a/packages/cli/src/databases/mysqldb/WebhookEntity.ts b/packages/cli/src/databases/mysqldb/WebhookEntity.ts new file mode 100644 index 0000000000..a78fd34ae9 --- /dev/null +++ b/packages/cli/src/databases/mysqldb/WebhookEntity.ts @@ -0,0 +1,25 @@ +import { + Column, + Entity, + PrimaryColumn, +} from 'typeorm'; + +import { + IWebhookDb, + } from '../../Interfaces'; + +@Entity() +export class WebhookEntity implements IWebhookDb { + + @Column() + workflowId: number; + + @PrimaryColumn() + webhookPath: string; + + @PrimaryColumn() + method: string; + + @Column() + node: string; +} diff --git a/packages/cli/src/databases/mysqldb/index.ts b/packages/cli/src/databases/mysqldb/index.ts index 164d67fd0c..a3494531db 100644 --- a/packages/cli/src/databases/mysqldb/index.ts +++ b/packages/cli/src/databases/mysqldb/index.ts @@ -1,3 +1,4 @@ export * from './CredentialsEntity'; export * from './ExecutionEntity'; export * from './WorkflowEntity'; +export * from './WebhookEntity'; diff --git a/packages/cli/src/databases/postgresdb/WebhookEntity.ts b/packages/cli/src/databases/postgresdb/WebhookEntity.ts new file mode 100644 index 0000000000..6e511cde74 --- /dev/null +++ b/packages/cli/src/databases/postgresdb/WebhookEntity.ts @@ -0,0 +1,25 @@ +import { + Column, + Entity, + PrimaryColumn, +} from 'typeorm'; + +import { + IWebhookDb, + } from '../../'; + +@Entity() +export class WebhookEntity implements IWebhookDb { + + @Column() + workflowId: number; + + @PrimaryColumn() + webhookPath: string; + + @PrimaryColumn() + method: string; + + @Column() + node: string; +} diff --git a/packages/cli/src/databases/postgresdb/index.ts b/packages/cli/src/databases/postgresdb/index.ts index 164d67fd0c..bd6b9abd60 100644 --- a/packages/cli/src/databases/postgresdb/index.ts +++ b/packages/cli/src/databases/postgresdb/index.ts @@ -1,3 +1,5 @@ export * from './CredentialsEntity'; export * from './ExecutionEntity'; export * from './WorkflowEntity'; +export * from './WebhookEntity'; + diff --git a/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts b/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts index 555015c10d..5f3798ca66 100644 --- a/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts +++ b/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts @@ -1,4 +1,5 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { + MigrationInterface, QueryRunner } from 'typeorm'; import * as config from '../../../../config'; diff --git a/packages/cli/src/databases/postgresdb/migrations/1589476000887-WebhookModel.ts b/packages/cli/src/databases/postgresdb/migrations/1589476000887-WebhookModel.ts new file mode 100644 index 0000000000..a521d227d0 --- /dev/null +++ b/packages/cli/src/databases/postgresdb/migrations/1589476000887-WebhookModel.ts @@ -0,0 +1,73 @@ +import { + MigrationInterface, + QueryRunner, +} from 'typeorm'; + +import { + IWorkflowDb, + NodeTypes, + WebhookHelpers, + } from '../../..'; + +import { + Workflow, +} from 'n8n-workflow/dist/src/Workflow'; + +import { + IWebhookDb, + } from '../../../Interfaces'; + + import * as config from '../../../../config'; + +export class WebhookModel1589476000887 implements MigrationInterface { + name = 'WebhookModel1589476000887'; + + async up(queryRunner: QueryRunner): Promise { + let tablePrefix = config.get('database.tablePrefix'); + const schema = config.get('database.postgresdb.schema'); + if (schema) { + tablePrefix = schema + '.' + tablePrefix; + } + + await queryRunner.query(`CREATE TABLE ${tablePrefix}webhook_entity ("workflowId" integer NOT NULL, "webhookPath" character varying NOT NULL, "method" character varying NOT NULL, "node" character varying NOT NULL, CONSTRAINT "PK_b21ace2e13596ccd87dc9bf4ea6" PRIMARY KEY ("webhookPath", "method"))`, undefined); + + const workflows = await queryRunner.query(`SELECT * FROM ${tablePrefix}workflow_entity WHERE active=true`) as IWorkflowDb[]; + const data: IWebhookDb[] = []; + const nodeTypes = NodeTypes(); + for (const workflow of workflows) { + const workflowInstance = new Workflow({ id: workflow.id as string, name: workflow.name, nodes: workflow.nodes, connections: workflow.connections, active: workflow.active, nodeTypes, staticData: workflow.staticData, settings: workflow.settings }); + // I'm writing something more simple than this. I tried to use the built in method + // getWorkflowWebhooks but it needs additionaldata and to get it I need the credentials + // and for some reason when I use + // const credentials = await WorkflowCredentials(node); + // to get the credentials I got an error I think is cuz the database is yet not ready. + const webhooks = WebhookHelpers.getWorkflowWebhooksBasic(workflowInstance); + for (const webhook of webhooks) { + data.push({ + workflowId: workflowInstance.id as string, + webhookPath: webhook.path, + method: webhook.httpMethod, + node: webhook.node, + }); + } + } + + if (data.length !== 0) { + await queryRunner.manager.createQueryBuilder() + .insert() + .into(`${tablePrefix}webhook_entity`) + .values(data) + .execute(); + } + } + + async down(queryRunner: QueryRunner): Promise { + let tablePrefix = config.get('database.tablePrefix'); + const schema = config.get('database.postgresdb.schema'); + if (schema) { + tablePrefix = schema + '.' + tablePrefix; + } + await queryRunner.query(`DROP TABLE ${tablePrefix}webhook_entity`, undefined); + } + +} diff --git a/packages/cli/src/databases/postgresdb/migrations/index.ts b/packages/cli/src/databases/postgresdb/migrations/index.ts index 5bb6551492..827f01796c 100644 --- a/packages/cli/src/databases/postgresdb/migrations/index.ts +++ b/packages/cli/src/databases/postgresdb/migrations/index.ts @@ -1 +1,3 @@ export * from './1587669153312-InitialMigration'; +export * from './1589476000887-WebhookModel'; + diff --git a/packages/cli/src/databases/sqlite/WebhookEntity.ts b/packages/cli/src/databases/sqlite/WebhookEntity.ts new file mode 100644 index 0000000000..a78fd34ae9 --- /dev/null +++ b/packages/cli/src/databases/sqlite/WebhookEntity.ts @@ -0,0 +1,25 @@ +import { + Column, + Entity, + PrimaryColumn, +} from 'typeorm'; + +import { + IWebhookDb, + } from '../../Interfaces'; + +@Entity() +export class WebhookEntity implements IWebhookDb { + + @Column() + workflowId: number; + + @PrimaryColumn() + webhookPath: string; + + @PrimaryColumn() + method: string; + + @Column() + node: string; +} diff --git a/packages/cli/src/databases/sqlite/index.ts b/packages/cli/src/databases/sqlite/index.ts index 2c7d6e25e9..a3494531db 100644 --- a/packages/cli/src/databases/sqlite/index.ts +++ b/packages/cli/src/databases/sqlite/index.ts @@ -1,4 +1,4 @@ export * from './CredentialsEntity'; export * from './ExecutionEntity'; export * from './WorkflowEntity'; - +export * from './WebhookEntity'; diff --git a/packages/core/src/ActiveWebhooks.ts b/packages/core/src/ActiveWebhooks.ts index 17cf753830..7f6d705ccd 100644 --- a/packages/core/src/ActiveWebhooks.ts +++ b/packages/core/src/ActiveWebhooks.ts @@ -35,13 +35,20 @@ export class ActiveWebhooks { throw new Error('Webhooks can only be added for saved workflows as an id is needed!'); } + const webhookKey = this.getWebhookKey(webhookData.httpMethod, webhookData.path); + + //check that there is not a webhook already registed with that path/method + if (this.webhookUrls[webhookKey] !== undefined) { + throw new Error('There is test wenhook registered on that path'); + } + if (this.workflowWebhooks[webhookData.workflowId] === undefined) { this.workflowWebhooks[webhookData.workflowId] = []; } // Make the webhook available directly because sometimes to create it successfully // it gets called - this.webhookUrls[this.getWebhookKey(webhookData.httpMethod, webhookData.path)] = webhookData; + this.webhookUrls[webhookKey] = webhookData; const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks); if (webhookExists === false) { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index ec505a172c..783a6ef1dc 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -23,7 +23,9 @@ "test:e2e": "vue-cli-service test:e2e", "test:unit": "vue-cli-service test:unit" }, - "dependencies": {}, + "dependencies": { + "uuid": "^8.1.0" + }, "devDependencies": { "@beyonk/google-fonts-webpack-plugin": "^1.2.3", "@fortawesome/fontawesome-svg-core": "^1.2.19", diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index de89534e09..65ff1b5267 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -126,6 +126,8 @@ import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; +import { v4 as uuidv4 } from 'uuid'; + import { debounce } from 'lodash'; import axios from 'axios'; import { @@ -946,6 +948,10 @@ export default mixins( // Check if node-name is unique else find one that is newNodeData.name = this.getUniqueNodeName(newNodeData.name); + if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { + newNodeData.webhookPath = uuidv4(); + } + await this.addNodes([newNodeData]); // Automatically deselect all nodes and select the current one and also active @@ -1579,6 +1585,11 @@ export default mixins( console.error(e); // eslint-disable-line no-console } node.parameters = nodeParameters !== null ? nodeParameters : {}; + + // if it's a webhook and the path is empty set the UUID as the default path + if (node.type === 'n8n-nodes-base.webhook' && node.parameters.path === '') { + node.parameters.path = node.webhookPath as string; + } } foundNodeIssues = this.getNodeIssues(nodeType, node); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ce75635fc0..b5f143e3f6 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -297,6 +297,7 @@ export interface INode { continueOnFail?: boolean; parameters: INodeParameters; credentials?: INodeCredentials; + webhookPath?: string; } diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index a72ee565de..87e219699d 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -791,6 +791,59 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: return returnData; } +export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookData[] { + if (node.disabled === true) { + // Node is disabled so webhooks will also not be enabled + return []; + } + + const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType; + + if (nodeType.description.webhooks === undefined) { + // Node does not have any webhooks so return + return []; + } + + const workflowId = workflow.id || '__UNSAVED__'; + + const returnData: IWebhookData[] = []; + for (const webhookDescription of nodeType.description.webhooks) { + let nodeWebhookPath = workflow.getSimpleParameterValue(node, webhookDescription['path'], 'GET'); + if (nodeWebhookPath === undefined) { + // TODO: Use a proper logger + console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); + continue; + } + + nodeWebhookPath = nodeWebhookPath.toString(); + + if (nodeWebhookPath.charAt(0) === '/') { + nodeWebhookPath = nodeWebhookPath.slice(1); + } + + const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath); + + const httpMethod = workflow.getSimpleParameterValue(node, webhookDescription['httpMethod'], 'GET'); + + if (httpMethod === undefined) { + // TODO: Use a proper logger + console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`); + continue; + } + + //@ts-ignore + returnData.push({ + httpMethod: httpMethod.toString() as WebhookHttpMethod, + node: node.name, + path, + webhookDescription, + workflowId, + }); + } + + return returnData; +} + /** * Returns the webhook path @@ -802,7 +855,16 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: * @returns {string} */ export function getNodeWebhookPath(workflowId: string, node: INode, path: string): string { - return `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`; + let webhookPath = ''; + if (node.webhookPath === undefined) { + webhookPath = `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`; + } else { + if (node.type === 'n8n-nodes-base.webhook') { + return path; + } + webhookPath = `${node.webhookPath}/${path}`; + } + return webhookPath; }