From a4acd034af66a43f4694c799d8df5d017bff6d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 29 Oct 2024 17:06:17 +0100 Subject: [PATCH] refactor(core): Extract hook context out of NodeExecutionFunctions (no-changelog) --- packages/core/src/NodeExecuteFunctions.ts | 61 +------- .../__tests__/hook-context.test.ts | 147 ++++++++++++++++++ .../node-execution-context/hook-context.ts | 103 ++++++++++++ .../core/src/node-execution-context/index.ts | 1 + .../node-execution-context/webhook-context.ts | 3 +- packages/workflow/src/Interfaces.ts | 12 +- 6 files changed, 265 insertions(+), 62 deletions(-) create mode 100644 packages/core/src/node-execution-context/__tests__/hook-context.test.ts create mode 100644 packages/core/src/node-execution-context/hook-context.ts diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 4e80e1a153130..e50d049f59c29 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -108,6 +108,7 @@ import type { ICheckProcessedContextData, AiEvent, ISupplyDataFunctions, + WebhookType, } from 'n8n-workflow'; import { NodeConnectionType, @@ -165,7 +166,7 @@ import { extractValue } from './ExtractValue'; import { InstanceSettings } from './InstanceSettings'; import type { ExtendedValidationResult, IResponseError } from './Interfaces'; // eslint-disable-next-line import/no-cycle -import { PollContext, TriggerContext, WebhookContext } from './node-execution-context'; +import { HookContext, PollContext, TriggerContext, WebhookContext } from './node-execution-context'; import { getSecretsProxy } from './Secrets'; import { SSHClientsManager } from './SSHClientsManager'; @@ -2628,7 +2629,7 @@ export function continueOnFail(node: INode): boolean { * */ export function getNodeWebhookUrl( - name: string, + name: WebhookType, workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, @@ -2673,7 +2674,7 @@ export function getNodeWebhookUrl( * */ export function getWebhookDescription( - name: string, + name: WebhookType, workflow: Workflow, node: INode, ): IWebhookDescription | undefined { @@ -4342,59 +4343,7 @@ export function getExecuteHookFunctions( activation: WorkflowActivateMode, webhookData?: IWebhookData, ): IHookFunctions { - return ((workflow: Workflow, node: INode) => { - return { - ...getCommonWorkflowFunctions(workflow, node, additionalData), - getCredentials: async (type) => - await getCredentials(workflow, node, type, additionalData, mode), - getMode: () => mode, - getActivationMode: () => activation, - getNodeParameter: ( - parameterName: string, - fallbackValue?: any, - options?: IGetNodeParameterOptions, - ): NodeParameterValueType | object => { - const runExecutionData: IRunExecutionData | null = null; - const itemIndex = 0; - const runIndex = 0; - const connectionInputData: INodeExecutionData[] = []; - - return getNodeParameter( - workflow, - runExecutionData, - runIndex, - connectionInputData, - node, - parameterName, - itemIndex, - mode, - getAdditionalKeys(additionalData, mode, runExecutionData), - undefined, - fallbackValue, - options, - ); - }, - getNodeWebhookUrl: (name: string): string | undefined => { - return getNodeWebhookUrl( - name, - workflow, - node, - additionalData, - mode, - getAdditionalKeys(additionalData, mode, null), - webhookData?.isTest, - ); - }, - getWebhookName(): string { - if (webhookData === undefined) { - throw new ApplicationError('Only supported in webhook functions'); - } - return webhookData.webhookDescription.name; - }, - getWebhookDescription: (name) => getWebhookDescription(name, workflow, node), - helpers: getRequestHelperFunctions(workflow, node, additionalData), - }; - })(workflow, node); + return new HookContext(workflow, node, additionalData, mode, activation, webhookData); } /** diff --git a/packages/core/src/node-execution-context/__tests__/hook-context.test.ts b/packages/core/src/node-execution-context/__tests__/hook-context.test.ts new file mode 100644 index 0000000000000..1092198a23bc9 --- /dev/null +++ b/packages/core/src/node-execution-context/__tests__/hook-context.test.ts @@ -0,0 +1,147 @@ +import { mock } from 'jest-mock-extended'; +import type { + Expression, + ICredentialDataDecryptedObject, + ICredentialsHelper, + INode, + INodeType, + INodeTypes, + IWebhookDescription, + IWebhookData, + IWorkflowExecuteAdditionalData, + Workflow, + WorkflowActivateMode, + WorkflowExecuteMode, +} from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; + +import { HookContext } from '../hook-context'; + +describe('HookContext', () => { + const testCredentialType = 'testCredential'; + const webhookDescription: IWebhookDescription = { + name: 'default', + httpMethod: 'GET', + responseMode: 'onReceived', + path: 'testPath', + }; + const nodeType = mock({ + description: { + credentials: [ + { + name: testCredentialType, + required: true, + }, + ], + properties: [ + { + name: 'testParameter', + required: true, + }, + ], + }, + }); + nodeType.description.webhooks = [webhookDescription]; + const nodeTypes = mock(); + const expression = mock(); + const workflow = mock({ expression, nodeTypes }); + const node = mock({ + credentials: { + [testCredentialType]: { + id: 'testCredentialId', + }, + }, + }); + node.parameters = { + testParameter: 'testValue', + }; + const credentialsHelper = mock(); + const additionalData = mock({ credentialsHelper }); + const mode: WorkflowExecuteMode = 'manual'; + const activation: WorkflowActivateMode = 'init'; + const webhookData = mock({ + webhookDescription: { + name: 'default', + isFullPath: true, + }, + }); + + const hookContext = new HookContext( + workflow, + node, + additionalData, + mode, + activation, + webhookData, + ); + + beforeEach(() => { + jest.clearAllMocks(); + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + expression.getParameterValue.mockImplementation((value) => value); + expression.getSimpleParameterValue.mockImplementation((_, value) => value); + }); + + describe('getActivationMode', () => { + it('should return the activation property', () => { + const result = hookContext.getActivationMode(); + expect(result).toBe(activation); + }); + }); + + describe('getCredentials', () => { + it('should get decrypted credentials', async () => { + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' }); + + const credentials = + await hookContext.getCredentials(testCredentialType); + + expect(credentials).toEqual({ secret: 'token' }); + }); + }); + + describe('getNodeParameter', () => { + it('should return parameter value when it exists', () => { + const parameter = hookContext.getNodeParameter('testParameter'); + + expect(parameter).toBe('testValue'); + }); + }); + + describe('getNodeWebhookUrl', () => { + it('should return node webhook url', () => { + const url = hookContext.getNodeWebhookUrl('default'); + + expect(url).toContain('testPath'); + }); + }); + + describe('getWebhookName', () => { + it('should return webhook name', () => { + const name = hookContext.getWebhookName(); + + expect(name).toBe('default'); + }); + + it('should throw an error if webhookData is undefined', () => { + const hookContextWithoutWebhookData = new HookContext( + workflow, + node, + additionalData, + mode, + activation, + ); + + expect(() => hookContextWithoutWebhookData.getWebhookName()).toThrow(ApplicationError); + }); + }); + + describe('getWebhookDescription', () => { + it('should return webhook description', () => { + const description = hookContext.getWebhookDescription('default'); + + expect(description).toEqual(webhookDescription); + }); + }); +}); diff --git a/packages/core/src/node-execution-context/hook-context.ts b/packages/core/src/node-execution-context/hook-context.ts new file mode 100644 index 0000000000000..7cc6567779e53 --- /dev/null +++ b/packages/core/src/node-execution-context/hook-context.ts @@ -0,0 +1,103 @@ +import type { + ICredentialDataDecryptedObject, + IGetNodeParameterOptions, + INode, + INodeExecutionData, + IHookFunctions, + IRunExecutionData, + IWorkflowExecuteAdditionalData, + NodeParameterValueType, + Workflow, + WorkflowActivateMode, + WorkflowExecuteMode, + IWebhookData, + WebhookType, +} from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; + +// eslint-disable-next-line import/no-cycle +import { + getAdditionalKeys, + getCredentials, + getNodeParameter, + getNodeWebhookUrl, + getWebhookDescription, +} from '@/NodeExecuteFunctions'; + +import { RequestHelpers } from './helpers/request-helpers'; +import { NodeExecutionContext } from './node-execution-context'; + +export class HookContext extends NodeExecutionContext implements IHookFunctions { + readonly helpers: IHookFunctions['helpers']; + + constructor( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + private readonly activation: WorkflowActivateMode, + private readonly webhookData?: IWebhookData, + ) { + super(workflow, node, additionalData, mode); + + this.helpers = new RequestHelpers(this, workflow, node, additionalData); + } + + getActivationMode() { + return this.activation; + } + + async getCredentials(type: string) { + return await getCredentials(this.workflow, this.node, type, this.additionalData, this.mode); + } + + getNodeParameter( + parameterName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fallbackValue?: any, + options?: IGetNodeParameterOptions, + ): NodeParameterValueType | object { + const runExecutionData: IRunExecutionData | null = null; + const itemIndex = 0; + const runIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + + return getNodeParameter( + this.workflow, + runExecutionData, + runIndex, + connectionInputData, + this.node, + parameterName, + itemIndex, + this.mode, + getAdditionalKeys(this.additionalData, this.mode, runExecutionData), + undefined, + fallbackValue, + options, + ); + } + + getNodeWebhookUrl(name: WebhookType): string | undefined { + return getNodeWebhookUrl( + name, + this.workflow, + this.node, + this.additionalData, + this.mode, + getAdditionalKeys(this.additionalData, this.mode, null), + this.webhookData?.isTest, + ); + } + + getWebhookName(): string { + if (this.webhookData === undefined) { + throw new ApplicationError('Only supported in webhook functions'); + } + return this.webhookData.webhookDescription.name; + } + + getWebhookDescription(name: WebhookType) { + return getWebhookDescription(name, this.workflow, this.node); + } +} diff --git a/packages/core/src/node-execution-context/index.ts b/packages/core/src/node-execution-context/index.ts index cc12f1927d3e6..a6397c60ced9e 100644 --- a/packages/core/src/node-execution-context/index.ts +++ b/packages/core/src/node-execution-context/index.ts @@ -1,4 +1,5 @@ // eslint-disable-next-line import/no-cycle +export { HookContext } from './hook-context'; export { LoadOptionsContext } from './load-options-context'; export { PollContext } from './poll-context'; export { TriggerContext } from './trigger-context'; diff --git a/packages/core/src/node-execution-context/webhook-context.ts b/packages/core/src/node-execution-context/webhook-context.ts index 6b2d8c0d8922b..a7fa7203c8703 100644 --- a/packages/core/src/node-execution-context/webhook-context.ts +++ b/packages/core/src/node-execution-context/webhook-context.ts @@ -14,6 +14,7 @@ import type { IWorkflowExecuteAdditionalData, NodeConnectionType, NodeParameterValueType, + WebhookType, Workflow, WorkflowExecuteMode, } from 'n8n-workflow'; @@ -108,7 +109,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc return httpRequest; } - getNodeWebhookUrl(name: string): string | undefined { + getNodeWebhookUrl(name: WebhookType): string | undefined { return getNodeWebhookUrl( name, this.workflow, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 80140d93a2092..57ec23b54494f 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1104,8 +1104,8 @@ export interface ITriggerFunctions export interface IHookFunctions extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> { getWebhookName(): string; - getWebhookDescription(name: string): IWebhookDescription | undefined; - getNodeWebhookUrl: (name: string) => string | undefined; + getWebhookDescription(name: WebhookType): IWebhookDescription | undefined; + getNodeWebhookUrl: (name: WebhookType) => string | undefined; getNodeParameter( parameterName: string, fallbackValue?: any, @@ -1127,7 +1127,7 @@ export interface IWebhookFunctions extends FunctionsBaseWithRequiredKeys<'getMod fallbackValue?: any, options?: IGetNodeParameterOptions, ): NodeParameterValueType | object; - getNodeWebhookUrl: (name: string) => string | undefined; + getNodeWebhookUrl: (name: WebhookType) => string | undefined; evaluateExpression(expression: string, itemIndex?: number): NodeParameterValueType; getParamsData(): object; getQueryData(): object; @@ -1619,7 +1619,7 @@ export interface INodeType { }; }; webhookMethods?: { - [name in IWebhookDescription['name']]?: { + [name in WebhookType]?: { [method in WebhookSetupMethodNames]: (this: IHookFunctions) => Promise; }; }; @@ -1972,11 +1972,13 @@ export interface IWebhookData { staticData?: Workflow['staticData']; } +export type WebhookType = 'default' | 'setup'; + export interface IWebhookDescription { [key: string]: IHttpRequestMethods | WebhookResponseMode | boolean | string | undefined; httpMethod: IHttpRequestMethods | string; isFullPath?: boolean; - name: 'default' | 'setup'; + name: WebhookType; path: string; responseBinaryPropertyName?: string; responseContentType?: string;