Skip to content

Commit

Permalink
feat(Webhook Node): Setting to enable multiple outputs/methods (#9086)
Browse files Browse the repository at this point in the history
Co-authored-by: Giulio Andreini <[email protected]>
  • Loading branch information
2 people authored and despairblue committed Apr 25, 2024
1 parent 4aed020 commit 5d0f212
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 29 deletions.
11 changes: 9 additions & 2 deletions packages/cli/src/TestWebhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,20 @@ export class TestWebhooks implements IWebhookManager {

for (const webhook of webhooks) {
const key = this.registrations.toKey(webhook);
const isAlreadyRegistered = await this.registrations.get(key);
const registrationByKey = await this.registrations.get(key);

if (runData && webhook.node in runData) {
return false;
}

if (isAlreadyRegistered && !webhook.webhookId) {
// if registration already exists and is not a test webhook created by this user in this workflow throw an error
if (
registrationByKey &&
!webhook.webhookId &&
!registrationByKey.webhook.isTest &&
registrationByKey.webhook.userId !== userId &&
registrationByKey.webhook.workflowId !== workflow.id
) {
throw new WebhookPathTakenError(webhook.node);
}

Expand Down
19 changes: 16 additions & 3 deletions packages/editor-ui/src/components/NodeWebhooks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
>
<div v-if="isWebhookMethodVisible(webhook)" class="webhook-wrapper">
<div class="http-field">
<div class="http-method">
{{ workflowHelpers.getWebhookExpressionValue(webhook, 'httpMethod') }}<br />
</div>
<div class="http-method">{{ getWebhookHttpMethod(webhook) }}<br /></div>
</div>
<div class="url-field">
<div class="webhook-url left-ellipsis clickable" @click="copyWebhookUrl(webhook)">
Expand Down Expand Up @@ -195,12 +193,27 @@ export default defineComponent({
return '';
},
isWebhookMethodVisible(webhook: IWebhookDescription): boolean {
try {
const method = this.workflowHelpers.getWebhookExpressionValue(webhook, 'httpMethod', false);
if (Array.isArray(method) && method.length !== 1) {
return false;
}
} catch (error) {}
if (typeof webhook.ndvHideMethod === 'string') {
return !this.workflowHelpers.getWebhookExpressionValue(webhook, 'ndvHideMethod');
}
return !webhook.ndvHideMethod;
},
getWebhookHttpMethod(webhook: IWebhookDescription): string {
const method = this.workflowHelpers.getWebhookExpressionValue(webhook, 'httpMethod', false);
if (Array.isArray(method)) {
return method[0];
}
return method;
},
},
});
</script>
Expand Down
9 changes: 8 additions & 1 deletion packages/editor-ui/src/components/TriggerPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,17 @@ export default defineComponent({
return undefined;
}
return this.workflowHelpers.getWebhookExpressionValue(
const httpMethod = this.workflowHelpers.getWebhookExpressionValue(
this.nodeType.webhooks[0],
'httpMethod',
false,
);
if (Array.isArray(httpMethod)) {
return httpMethod.join(', ');
}
return httpMethod;
},
webhookTestUrl(): string | undefined {
if (!this.node || !this.nodeType?.webhooks?.length) {
Expand Down
16 changes: 13 additions & 3 deletions packages/editor-ui/src/composables/useWorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -737,12 +737,21 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
return nodeData;
}

function getWebhookExpressionValue(webhookData: IWebhookDescription, key: string): string {
function getWebhookExpressionValue(
webhookData: IWebhookDescription,
key: string,
stringify = true,
): string {
if (webhookData[key] === undefined) {
return 'empty';
}
try {
return resolveExpression(webhookData[key] as string) as string;
return resolveExpression(
webhookData[key] as string,
undefined,
undefined,
stringify,
) as string;
} catch (e) {
return i18n.baseText('nodeWebhooks.invalidExpression');
}
Expand Down Expand Up @@ -784,6 +793,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
c?: number;
additionalKeys?: IWorkflowDataProxyAdditionalKeys;
} = {},
stringifyObject = true,
) {
const parameters = {
__xxxxxxx__: expression,
Expand All @@ -795,7 +805,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
}

const obj = returnData.__xxxxxxx__;
if (typeof obj === 'object') {
if (typeof obj === 'object' && stringifyObject) {
const proxy = obj as { isProxy: boolean; toJSON?: () => unknown } | null;
if (proxy?.isProxy && proxy.toJSON) return JSON.stringify(proxy.toJSON());
const workflow = getCurrentWorkflow();
Expand Down
58 changes: 56 additions & 2 deletions packages/nodes-base/nodes/Webhook/Webhook.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,60 @@ export class Webhook extends Node {
credentials: credentialsProperty(this.authPropertyName),
webhooks: [defaultWebhookDescription],
properties: [
httpMethodsProperty,
{
displayName: 'Allow Multiple HTTP Methods',
name: 'multipleMethods',
type: 'boolean',
default: false,
isNodeSetting: true,
description: 'Whether to allow the webhook to listen for multiple HTTP methods',
},
{
...httpMethodsProperty,
displayOptions: {
show: {
multipleMethods: [false],
},
},
},
{
displayName: 'HTTP Methods',
name: 'httpMethod',
type: 'multiOptions',
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'HEAD',
value: 'HEAD',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: ['GET', 'POST'],
description: 'The HTTP methods to listen to',
displayOptions: {
show: {
multipleMethods: [true],
},
},
},
{
displayName: 'Path',
name: 'path',
Expand Down Expand Up @@ -144,6 +197,7 @@ export class Webhook extends Node {
};
const req = context.getRequestObject();
const resp = context.getResponseObject();
const requestMethod = context.getRequestObject().method;

if (!isIpWhitelisted(options.ipWhitelist, req.ips, req.ip)) {
resp.writeHead(403);
Expand All @@ -165,7 +219,7 @@ export class Webhook extends Node {
throw error;
}

const prepareOutput = setupOutputConnection(context, {
const prepareOutput = setupOutputConnection(context, requestMethod, {
jwtPayload: validationData,
});

Expand Down
42 changes: 35 additions & 7 deletions packages/nodes-base/nodes/Webhook/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,36 +50,64 @@ export const getResponseData = (parameters: WebhookParameters) => {
};

export const configuredOutputs = (parameters: WebhookParameters) => {
const httpMethod = parameters.httpMethod;
const httpMethod = parameters.httpMethod as string | string[];

return [
{
if (!Array.isArray(httpMethod))
return [
{
type: `${NodeConnectionType.Main}`,
displayName: httpMethod,
},
];

const outputs = httpMethod.map((method) => {
return {
type: `${NodeConnectionType.Main}`,
displayName: httpMethod,
},
];
displayName: method,
};
});

return outputs;
};

export const setupOutputConnection = (
ctx: IWebhookFunctions,
method: string,
additionalData: {
jwtPayload?: IDataObject;
},
) => {
const httpMethod = ctx.getNodeParameter('httpMethod', []) as string[] | string;
let webhookUrl = ctx.getNodeWebhookUrl('default') as string;
const executionMode = ctx.getMode() === 'manual' ? 'test' : 'production';

if (executionMode === 'test') {
webhookUrl = webhookUrl.replace('/webhook/', '/webhook-test/');
}

// multi methods could be set in settings of node, so we need to check if it's an array
if (!Array.isArray(httpMethod)) {
return (outputData: INodeExecutionData): INodeExecutionData[][] => {
outputData.json.webhookUrl = webhookUrl;
outputData.json.executionMode = executionMode;
if (additionalData?.jwtPayload) {
outputData.json.jwtPayload = additionalData.jwtPayload;
}
return [[outputData]];
};
}

const outputIndex = httpMethod.indexOf(method.toUpperCase());
const outputs: INodeExecutionData[][] = httpMethod.map(() => []);

return (outputData: INodeExecutionData): INodeExecutionData[][] => {
outputData.json.webhookUrl = webhookUrl;
outputData.json.executionMode = executionMode;
if (additionalData?.jwtPayload) {
outputData.json.jwtPayload = additionalData.jwtPayload;
}
return [[outputData]];
outputs[outputIndex] = [outputData];
return outputs;
};
};

Expand Down
27 changes: 16 additions & 11 deletions packages/workflow/src/NodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,7 @@ export function getNodeWebhooks(
) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath, restartWebhook);

const httpMethod = workflow.expression.getSimpleParameterValue(
const webhookMethods = workflow.expression.getSimpleParameterValue(
node,
webhookDescription.httpMethod,
mode,
Expand All @@ -1005,7 +1005,7 @@ export function getNodeWebhooks(
'GET',
);

if (httpMethod === undefined) {
if (webhookMethods === 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.`,
Expand All @@ -1018,15 +1018,20 @@ export function getNodeWebhooks(
webhookId = node.webhookId;
}

returnData.push({
httpMethod: httpMethod.toString() as IHttpRequestMethods,
node: node.name,
path,
webhookDescription,
workflowId,
workflowExecuteAdditionalData: additionalData,
webhookId,
});
String(webhookMethods)
.split(',')
.forEach((httpMethod) => {
if (!httpMethod) return;
returnData.push({
httpMethod: httpMethod.trim() as IHttpRequestMethods,
node: node.name,
path,
webhookDescription,
workflowId,
workflowExecuteAdditionalData: additionalData,
webhookId,
});
});
}

return returnData;
Expand Down

0 comments on commit 5d0f212

Please sign in to comment.