refactor(core): Extract hook context out of NodeExecutionFunctions (no-changelog) (#11537)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-11-04 16:27:23 +01:00 committed by GitHub
parent 0817f8571a
commit 19a5c2fcf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 265 additions and 62 deletions

View file

@ -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);
}
/**

View file

@ -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<INodeType>({
description: {
credentials: [
{
name: testCredentialType,
required: true,
},
],
properties: [
{
name: 'testParameter',
required: true,
},
],
},
});
nodeType.description.webhooks = [webhookDescription];
const nodeTypes = mock<INodeTypes>();
const expression = mock<Expression>();
const workflow = mock<Workflow>({ expression, nodeTypes });
const node = mock<INode>({
credentials: {
[testCredentialType]: {
id: 'testCredentialId',
},
},
});
node.parameters = {
testParameter: 'testValue',
};
const credentialsHelper = mock<ICredentialsHelper>();
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
const mode: WorkflowExecuteMode = 'manual';
const activation: WorkflowActivateMode = 'init';
const webhookData = mock<IWebhookData>({
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<ICredentialDataDecryptedObject>(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<IWebhookDescription>(webhookDescription);
});
});
});

View file

@ -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<T extends object = ICredentialDataDecryptedObject>(type: string) {
return await getCredentials<T>(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);
}
}

View file

@ -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';

View file

@ -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,

View file

@ -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<boolean>;
};
};
@ -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;