2021-08-29 11:58:11 -07:00
/* eslint-disable consistent-return */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-param-reassign */
2019-06-23 03:35:23 -07:00
import * as express from 'express' ;
2021-08-29 11:58:11 -07:00
import { ActiveWebhooks } from 'n8n-core' ;
2019-06-23 03:35:23 -07:00
import {
IWebhookData ,
IWorkflowExecuteAdditionalData ,
WebhookHttpMethod ,
Workflow ,
2021-03-23 11:08:47 -07:00
WorkflowActivateMode ,
2019-06-23 03:35:23 -07:00
WorkflowExecuteMode ,
} from 'n8n-workflow' ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line import/no-cycle
import { IResponseCallbackData , IWorkflowDb , Push , ResponseHelper , WebhookHelpers } from '.' ;
2019-06-23 03:35:23 -07:00
2021-07-23 11:56:18 -07:00
const WEBHOOK_TEST_UNREGISTERED_HINT = ` Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button) ` ;
2019-06-23 03:35:23 -07:00
export class TestWebhooks {
private testWebhookData : {
[ key : string ] : {
sessionId? : string ;
2021-08-29 11:58:11 -07:00
timeout : NodeJS.Timeout ;
2019-06-23 03:35:23 -07:00
workflowData : IWorkflowDb ;
2020-09-16 14:55:34 -07:00
workflow : Workflow ;
2019-06-23 03:35:23 -07:00
} ;
} = { } ;
2021-08-29 11:58:11 -07:00
private activeWebhooks : ActiveWebhooks | null = null ;
2019-06-23 03:35:23 -07:00
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
* /
2021-08-29 11:58:11 -07:00
async callTestWebhook (
httpMethod : WebhookHttpMethod ,
path : string ,
request : express.Request ,
response : express.Response ,
) : Promise < IResponseCallbackData > {
2021-01-28 06:44:10 -08:00
// Reset request parameters
request . params = { } ;
2021-02-09 00:14:40 -08:00
// Remove trailing slash
if ( path . endsWith ( '/' ) ) {
path = path . slice ( 0 , - 1 ) ;
}
let webhookData : IWebhookData | undefined = this . activeWebhooks ! . get ( httpMethod , path ) ;
2021-01-23 11:00:32 -08:00
// check if path is dynamic
2019-06-23 03:35:23 -07:00
if ( webhookData === undefined ) {
2021-01-23 11:00:32 -08:00
const pathElements = path . split ( '/' ) ;
const webhookId = pathElements . shift ( ) ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2021-01-23 11:00:32 -08:00
webhookData = this . activeWebhooks ! . get ( httpMethod , pathElements . join ( '/' ) , webhookId ) ;
if ( webhookData === undefined ) {
// The requested webhook is not registered
2021-08-29 11:58:11 -07:00
throw new ResponseHelper . ResponseError (
` The requested webhook " ${ httpMethod } ${ path } " is not registered. ` ,
404 ,
404 ,
WEBHOOK_TEST_UNREGISTERED_HINT ,
) ;
2021-01-23 11:00:32 -08:00
}
2021-01-28 06:44:10 -08:00
2021-01-23 11:00:32 -08:00
path = webhookData . path ;
// extracting params from path
path . split ( '/' ) . forEach ( ( ele , index ) = > {
if ( ele . startsWith ( ':' ) ) {
// write params to req.params
request . params [ ele . slice ( 1 ) ] = pathElements [ index ] ;
}
} ) ;
2019-06-23 03:35:23 -07:00
}
2021-08-29 11:58:11 -07:00
const webhookKey = ` ${ this . activeWebhooks ! . getWebhookKey (
webhookData . httpMethod ,
webhookData . path ,
webhookData . webhookId ,
) } | $ { webhookData . workflowId } ` ;
2020-02-10 17:52:15 -08:00
2020-09-16 14:55:34 -07:00
// TODO: Clean that duplication up one day and improve code generally
if ( this . testWebhookData [ webhookKey ] === undefined ) {
// The requested webhook is not registered
2021-08-29 11:58:11 -07:00
throw new ResponseHelper . ResponseError (
` The requested webhook " ${ httpMethod } ${ path } " is not registered. ` ,
404 ,
404 ,
WEBHOOK_TEST_UNREGISTERED_HINT ,
) ;
2020-09-16 14:55:34 -07:00
}
2020-01-22 15:06:43 -08:00
2021-08-29 11:58:11 -07:00
const { workflow } = this . testWebhookData [ webhookKey ] ;
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
}
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-async-promise-executor
2019-06-23 03:35:23 -07:00
return new Promise ( async ( resolve , reject ) = > {
try {
const executionMode = 'manual' ;
2021-08-29 11:58:11 -07:00
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 ) ;
}
resolve ( data ) ;
} ,
) ;
2019-06-23 03:35:23 -07:00
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 ( ) ;
2021-08-29 11:58:11 -07: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 ] ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2020-01-22 15:06:43 -08:00
this . activeWebhooks ! . removeWorkflow ( workflow ) ;
2019-06-23 03:35:23 -07:00
} ) ;
}
2020-07-24 07:24:18 -07:00
/ * *
* Gets all request methods associated with a single test webhook
* @param path webhook path
* /
2021-08-29 11:58:11 -07:00
async getWebhookMethods ( path : string ) : Promise < string [ ] > {
2020-07-24 07:24:18 -07:00
const webhookMethods : string [ ] = this . activeWebhooks ! . getWebhookMethods ( path ) ;
if ( webhookMethods === undefined ) {
// The requested webhook is not registered
2021-08-29 11:58:11 -07:00
throw new ResponseHelper . ResponseError (
` The requested webhook " ${ path } " is not registered. ` ,
404 ,
404 ,
WEBHOOK_TEST_UNREGISTERED_HINT ,
) ;
2020-07-24 07:24:18 -07:00
}
return webhookMethods ;
}
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
* /
2021-08-29 11:58:11 -07:00
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 ,
true ,
) ;
if ( ! webhooks . find ( ( webhook ) = > webhook . webhookDescription . restartWebhook !== true ) ) {
2021-08-21 05:11:32 -07:00
// No webhooks found to start a workflow
2019-06-23 03:35:23 -07:00
return false ;
}
2020-05-03 08:55:14 -07:00
if ( workflow . id === undefined ) {
throw new Error ( 'Webhooks can only be added for saved workflows as an id is needed!' ) ;
}
2019-06-23 03:35:23 -07:00
// 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 ;
2020-10-21 08:50:23 -07:00
const activatedKey : string [ ] = [ ] ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-restricted-syntax
2019-06-23 03:35:23 -07:00
for ( const webhookData of webhooks ) {
2021-08-29 11:58:11 -07:00
key = ` ${ this . activeWebhooks ! . getWebhookKey (
webhookData . httpMethod ,
webhookData . path ,
webhookData . webhookId ,
) } | $ { workflowData . id } ` ;
2020-05-30 16:03:58 -07:00
2020-10-21 08:50:23 -07:00
activatedKey . push ( key ) ;
2019-06-23 03:35:23 -07:00
this . testWebhookData [ key ] = {
sessionId ,
timeout ,
2020-09-16 14:55:34 -07:00
workflow ,
2019-06-23 03:35:23 -07:00
workflowData ,
} ;
2020-10-21 08:50:23 -07:00
try {
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-await-in-loop
2021-03-23 11:08:47 -07:00
await this . activeWebhooks ! . add ( workflow , webhookData , mode , activation ) ;
2020-10-21 08:50:23 -07:00
} catch ( error ) {
2021-08-29 11:58:11 -07:00
activatedKey . forEach ( ( deleteKey ) = > delete this . testWebhookData [ deleteKey ] ) ;
// eslint-disable-next-line no-await-in-loop
2020-10-21 08:50:23 -07:00
await this . activeWebhooks ! . removeWorkflow ( workflow ) ;
throw error ;
}
2020-03-20 16:30:03 -07:00
}
2020-01-22 15:06:43 -08:00
2019-06-23 03:35:23 -07:00
return true ;
2021-08-29 11:58:11 -07:00
}
2019-06-23 03:35:23 -07:00
/ * *
* 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 {
2019-06-23 03:35:23 -07:00
let foundWebhook = false ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-restricted-syntax
2019-06-23 03:35:23 -07:00
for ( const webhookKey of Object . keys ( this . testWebhookData ) ) {
const webhookData = this . testWebhookData [ webhookKey ] ;
if ( webhookData . workflowData . id . toString ( ) !== workflowId ) {
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-continue
2019-06-23 03:35:23 -07:00
continue ;
}
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 ( ) ;
2021-08-29 11:58:11 -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.
}
}
2021-08-29 11:58:11 -07:00
const { workflow } = this . testWebhookData [ webhookKey ] ;
2020-03-20 16:30:03 -07:00
2019-06-23 03:35:23 -07:00
// Remove the webhook
delete this . testWebhookData [ webhookKey ] ;
2020-10-21 08:50:23 -07:00
2021-08-29 11:58:11 -07:00
if ( ! foundWebhook ) {
2020-10-21 08:50:23 -07:00
// As it removes all webhooks of the workflow execute only once
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2020-10-21 08:50:23 -07:00
this . activeWebhooks ! . removeWorkflow ( workflow ) ;
}
foundWebhook = true ;
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
let workflow : Workflow ;
2020-01-22 15:06:43 -08:00
const workflows : Workflow [ ] = [ ] ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-restricted-syntax
2020-02-10 17:52:15 -08:00
for ( const webhookKey of Object . keys ( this . testWebhookData ) ) {
2020-09-16 14:55:34 -07:00
workflow = this . testWebhookData [ webhookKey ] . workflow ;
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 ;
}