2019-06-23 03:35:23 -07:00
import {
IWebhookData ,
WebhookHttpMethod ,
2020-01-22 15:06:43 -08:00
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 { NodeExecuteFunctions } from '.' ;
2019-06-23 03:35:23 -07:00
export class ActiveWebhooks {
private workflowWebhooks : {
[ key : string ] : IWebhookData [ ] ;
} = { } ;
private webhookUrls : {
2021-01-23 11:00:32 -08:00
[ key : string ] : IWebhookData [ ] ;
2019-06-23 03:35:23 -07:00
} = { } ;
testWebhooks = false ;
/ * *
* Adds a new webhook
*
* @param { IWebhookData } webhookData
* @param { WorkflowExecuteMode } mode
* @returns { Promise < void > }
* @memberof ActiveWebhooks
* /
2021-08-29 11:58:11 -07:00
async add (
workflow : Workflow ,
webhookData : IWebhookData ,
mode : WorkflowExecuteMode ,
activation : WorkflowActivateMode ,
) : Promise < void > {
2020-01-22 15:06:43 -08:00
if ( workflow . id === undefined ) {
2019-06-23 03:35:23 -07:00
throw new Error ( 'Webhooks can only be added for saved workflows as an id is needed!' ) ;
}
2021-02-09 00:14:40 -08:00
if ( webhookData . path . endsWith ( '/' ) ) {
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-param-reassign
2021-02-09 00:14:40 -08:00
webhookData . path = webhookData . path . slice ( 0 , - 1 ) ;
}
2019-06-23 03:35:23 -07:00
2021-08-29 11:58:11 -07:00
const webhookKey = this . getWebhookKey (
webhookData . httpMethod ,
webhookData . path ,
webhookData . webhookId ,
) ;
2020-05-30 16:03:58 -07:00
2021-08-29 11:58:11 -07:00
// check that there is not a webhook already registed with that path/method
2021-01-23 11:00:32 -08:00
if ( this . webhookUrls [ webhookKey ] && ! webhookData . webhookId ) {
2021-08-29 11:58:11 -07:00
throw new Error (
` Test-Webhook can not be activated because another one with the same method " ${ webhookData . httpMethod } " and path " ${ webhookData . path } " is already active! ` ,
) ;
2020-05-30 16:03:58 -07:00
}
2020-01-22 15:06:43 -08:00
if ( this . workflowWebhooks [ webhookData . workflowId ] === undefined ) {
this . workflowWebhooks [ webhookData . workflowId ] = [ ] ;
2019-06-23 03:35:23 -07:00
}
// Make the webhook available directly because sometimes to create it successfully
// it gets called
2021-01-23 11:00:32 -08:00
if ( ! this . webhookUrls [ webhookKey ] ) {
this . webhookUrls [ webhookKey ] = [ ] ;
}
this . webhookUrls [ webhookKey ] . push ( webhookData ) ;
2019-06-23 03:35:23 -07:00
2020-08-27 04:37:17 -07:00
try {
2021-08-29 11:58:11 -07:00
const webhookExists = await workflow . runWebhookMethod (
'checkExists' ,
webhookData ,
NodeExecuteFunctions ,
mode ,
activation ,
this . testWebhooks ,
) ;
2020-10-21 08:50:23 -07:00
if ( webhookExists !== true ) {
2020-08-27 04:37:17 -07:00
// If webhook does not exist yet create it
2021-08-29 11:58:11 -07:00
await workflow . runWebhookMethod (
'create' ,
webhookData ,
NodeExecuteFunctions ,
mode ,
activation ,
this . testWebhooks ,
) ;
2020-07-24 04:56:20 -07:00
}
2020-08-27 04:37:17 -07:00
} catch ( error ) {
// If there was a problem unregister the webhook again
2021-01-23 11:00:32 -08:00
if ( this . webhookUrls [ webhookKey ] . length <= 1 ) {
delete this . webhookUrls [ webhookKey ] ;
} else {
2021-08-29 11:58:11 -07:00
this . webhookUrls [ webhookKey ] = this . webhookUrls [ webhookKey ] . filter (
( webhook ) = > webhook . path !== webhookData . path ,
) ;
2021-01-23 11:00:32 -08:00
}
2019-06-23 03:35:23 -07:00
2020-08-27 04:37:17 -07:00
throw error ;
}
2020-01-22 15:06:43 -08:00
this . workflowWebhooks [ webhookData . workflowId ] . push ( webhookData ) ;
2019-06-23 03:35:23 -07:00
}
/ * *
* Returns webhookData if a webhook with matches is currently registered
*
* @param { WebhookHttpMethod } httpMethod
* @param { string } path
2021-01-23 11:00:32 -08:00
* @param { ( string | undefined ) } webhookId
2019-06-23 03:35:23 -07:00
* @returns { ( IWebhookData | undefined ) }
* @memberof ActiveWebhooks
* /
2021-01-23 11:00:32 -08:00
get ( httpMethod : WebhookHttpMethod , path : string , webhookId? : string ) : IWebhookData | undefined {
const webhookKey = this . getWebhookKey ( httpMethod , path , webhookId ) ;
2019-06-23 03:35:23 -07:00
if ( this . webhookUrls [ webhookKey ] === undefined ) {
return undefined ;
}
2021-02-09 00:14:40 -08:00
let webhook : IWebhookData | undefined ;
let maxMatches = 0 ;
const pathElementsSet = new Set ( path . split ( '/' ) ) ;
// check if static elements match in path
// if more results have been returned choose the one with the most static-route matches
2021-08-29 11:58:11 -07:00
this . webhookUrls [ webhookKey ] . forEach ( ( dynamicWebhook ) = > {
const staticElements = dynamicWebhook . path . split ( '/' ) . filter ( ( ele ) = > ! ele . startsWith ( ':' ) ) ;
const allStaticExist = staticElements . every ( ( staticEle ) = > pathElementsSet . has ( staticEle ) ) ;
2021-02-09 00:14:40 -08:00
if ( allStaticExist && staticElements . length > maxMatches ) {
maxMatches = staticElements . length ;
webhook = dynamicWebhook ;
2021-01-23 11:00:32 -08:00
}
2021-02-09 00:14:40 -08:00
// handle routes with no static elements
else if ( staticElements . length === 0 && ! webhook ) {
webhook = dynamicWebhook ;
}
} ) ;
2021-01-23 11:00:32 -08:00
return webhook ;
2019-06-23 03:35:23 -07:00
}
2020-07-24 07:24:18 -07:00
/ * *
* Gets all request methods associated with a single webhook
2020-07-24 07:43:23 -07:00
* @param path
2020-07-24 07:24:18 -07:00
* /
getWebhookMethods ( path : string ) : string [ ] {
2021-08-29 11:58:11 -07:00
const methods : string [ ] = [ ] ;
2020-07-24 07:24:18 -07:00
Object . keys ( this . webhookUrls )
2021-08-29 11:58:11 -07:00
. filter ( ( key ) = > key . includes ( path ) )
// eslint-disable-next-line array-callback-return
. map ( ( key ) = > {
methods . push ( key . split ( '|' ) [ 0 ] ) ;
} ) ;
2020-07-24 07:24:18 -07:00
return methods ;
}
2019-06-23 03:35:23 -07:00
2020-01-22 15:06:43 -08:00
/ * *
* Returns the ids of all the workflows which have active webhooks
*
* @returns { string [ ] }
* @memberof ActiveWebhooks
* /
getWorkflowIds ( ) : string [ ] {
return Object . keys ( this . workflowWebhooks ) ;
}
2019-06-23 03:35:23 -07:00
/ * *
* Returns key to uniquely identify a webhook
*
* @param { WebhookHttpMethod } httpMethod
* @param { string } path
2021-01-23 11:00:32 -08:00
* @param { ( string | undefined ) } webhookId
2019-06-23 03:35:23 -07:00
* @returns { string }
* @memberof ActiveWebhooks
* /
2021-01-23 11:00:32 -08:00
getWebhookKey ( httpMethod : WebhookHttpMethod , path : string , webhookId? : string ) : string {
if ( webhookId ) {
if ( path . startsWith ( webhookId ) ) {
const cutFromIndex = path . indexOf ( '/' ) + 1 ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-param-reassign
2021-01-23 11:00:32 -08:00
path = path . slice ( cutFromIndex ) ;
}
return ` ${ httpMethod } | ${ webhookId } | ${ path . split ( '/' ) . length } ` ;
}
2019-06-23 03:35:23 -07:00
return ` ${ httpMethod } | ${ path } ` ;
}
/ * *
* Removes all webhooks of a workflow
*
2020-01-22 15:06:43 -08:00
* @param { Workflow } workflow
2019-06-23 03:35:23 -07:00
* @returns { boolean }
* @memberof ActiveWebhooks
* /
2020-01-22 15:06:43 -08:00
async removeWorkflow ( workflow : Workflow ) : Promise < boolean > {
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2020-01-22 15:06:43 -08:00
const workflowId = workflow . id ! . toString ( ) ;
2019-06-23 03:35:23 -07:00
if ( this . workflowWebhooks [ workflowId ] === undefined ) {
// If it did not exist then there is nothing to remove
return false ;
}
const webhooks = this . workflowWebhooks [ workflowId ] ;
const mode = 'internal' ;
// Go through all the registered webhooks of the workflow and remove them
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
// eslint-disable-next-line no-await-in-loop
await workflow . runWebhookMethod (
'delete' ,
webhookData ,
NodeExecuteFunctions ,
mode ,
'update' ,
this . testWebhooks ,
) ;
delete this . webhookUrls [
this . getWebhookKey ( webhookData . httpMethod , webhookData . path , webhookData . webhookId )
] ;
2019-06-23 03:35:23 -07:00
}
// Remove also the workflow-webhook entry
delete this . workflowWebhooks [ workflowId ] ;
return true ;
}
/ * *
2020-10-21 08:50:23 -07:00
* Removes all the webhooks of the given workflows
2019-06-23 03:35:23 -07:00
* /
2020-01-22 15:06:43 -08:00
async removeAll ( workflows : Workflow [ ] ) : Promise < void > {
2019-06-23 03:35:23 -07:00
const removePromises = [ ] ;
2021-08-29 11:58:11 -07:00
// eslint-disable-next-line no-restricted-syntax
2020-01-22 15:06:43 -08:00
for ( const workflow of workflows ) {
removePromises . push ( this . removeWorkflow ( workflow ) ) ;
2019-06-23 03:35:23 -07:00
}
await Promise . all ( removePromises ) ;
}
}