refactor(core): Extract webhook context out of NodeExecutionFunctions (no-changelog) (#11455)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-11-04 11:43:19 +01:00
committed by GitHub
parent 1be7de6180
commit 097c2542d0
4 changed files with 413 additions and 170 deletions

View File

@@ -23,12 +23,11 @@ import type {
} from 'axios';
import axios from 'axios';
import crypto, { createHmac } from 'crypto';
import type { Request, Response } from 'express';
import FileType from 'file-type';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import { access as fsAccess, writeFile as fsWriteFile } from 'fs/promises';
import { IncomingMessage, type IncomingHttpHeaders } from 'http';
import { IncomingMessage } from 'http';
import { Agent, type AgentOptions } from 'https';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
@@ -167,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 } from './node-execution-context';
import { PollContext, TriggerContext, WebhookContext } from './node-execution-context';
import { getSecretsProxy } from './Secrets';
import { SSHClientsManager } from './SSHClientsManager';
@@ -2800,7 +2799,7 @@ const addExecutionDataFunctions = async (
}
};
async function getInputConnectionData(
export async function getInputConnectionData(
this: IAllExecuteFunctions,
workflow: Workflow,
runExecutionData: IRunExecutionData,
@@ -4491,170 +4490,13 @@ export function getExecuteWebhookFunctions(
closeFunctions: CloseFunction[],
runExecutionData: IRunExecutionData | null,
): IWebhookFunctions {
return ((workflow: Workflow, node: INode, runExecutionData: IRunExecutionData | null) => {
return {
...getCommonWorkflowFunctions(workflow, node, additionalData),
getBodyData(): IDataObject {
if (additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.body;
},
getCredentials: async (type) =>
await getCredentials(workflow, node, type, additionalData, mode),
getHeaderData(): IncomingHttpHeaders {
if (additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.headers;
},
async getInputConnectionData(
inputName: NodeConnectionType,
itemIndex: number,
): Promise<unknown> {
// To be able to use expressions like "$json.sessionId" set the
// body data the webhook received to what is normally used for
// incoming node data.
const connectionInputData: INodeExecutionData[] = [
{ json: additionalData.httpRequest?.body || {} },
];
const runExecutionData: IRunExecutionData = {
resultData: {
runData: {},
},
};
const executeData: IExecuteData = {
data: {
main: [connectionInputData],
},
node,
source: null,
};
const runIndex = 0;
return await getInputConnectionData.call(
this,
return new WebhookContext(
workflow,
runExecutionData,
runIndex,
connectionInputData,
{} as ITaskDataConnections,
node,
additionalData,
executeData,
mode,
webhookData,
closeFunctions,
inputName,
itemIndex,
);
},
getMode: () => mode,
evaluateExpression: (expression: string, evaluateItemIndex?: number) => {
const itemIndex = evaluateItemIndex === undefined ? 0 : evaluateItemIndex;
const runIndex = 0;
let connectionInputData: INodeExecutionData[] = [];
let executionData: IExecuteData | undefined;
if (runExecutionData?.executionData !== undefined) {
executionData = runExecutionData.executionData.nodeExecutionStack[0];
if (executionData !== undefined) {
connectionInputData = executionData.data.main[0]!;
}
}
const additionalKeys = getAdditionalKeys(additionalData, mode, runExecutionData);
return workflow.expression.resolveSimpleParameterValue(
`=${expression}`,
{},
runExecutionData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
additionalKeys,
executionData,
);
},
getNodeParameter: (
parameterName: string,
fallbackValue?: any,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object => {
const itemIndex = 0;
const runIndex = 0;
let connectionInputData: INodeExecutionData[] = [];
let executionData: IExecuteData | undefined;
if (runExecutionData?.executionData !== undefined) {
executionData = runExecutionData.executionData.nodeExecutionStack[0];
if (executionData !== undefined) {
connectionInputData = executionData.data.main[0]!;
}
}
const additionalKeys = getAdditionalKeys(additionalData, mode, runExecutionData);
return getNodeParameter(
workflow,
runExecutionData,
runIndex,
connectionInputData,
node,
parameterName,
itemIndex,
mode,
additionalKeys,
executionData,
fallbackValue,
options,
);
},
getParamsData(): object {
if (additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.params;
},
getQueryData(): object {
if (additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.query;
},
getRequestObject(): Request {
if (additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest;
},
getResponseObject(): Response {
if (additionalData.httpResponse === undefined) {
throw new ApplicationError('Response is missing');
}
return additionalData.httpResponse;
},
getNodeWebhookUrl: (name: string): string | undefined =>
getNodeWebhookUrl(
name,
workflow,
node,
additionalData,
mode,
getAdditionalKeys(additionalData, mode, null),
),
getWebhookName: () => webhookData.webhookDescription.name,
helpers: {
createDeferredPromise,
...getRequestHelperFunctions(workflow, node, additionalData),
...getBinaryHelperFunctions(additionalData, workflow.id),
returnJsonArray,
},
nodeHelpers: getNodeHelperFunctions(additionalData, workflow.id),
};
})(workflow, node, runExecutionData);
}

View File

@@ -0,0 +1,161 @@
import type { Request, Response } from 'express';
import { mock } from 'jest-mock-extended';
import type {
Expression,
ICredentialDataDecryptedObject,
ICredentialsHelper,
INode,
INodeType,
INodeTypes,
IWebhookData,
IWorkflowExecuteAdditionalData,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { WebhookContext } from '../webhook-context';
describe('WebhookContext', () => {
const testCredentialType = 'testCredential';
const nodeType = mock<INodeType>({
description: {
credentials: [
{
name: testCredentialType,
required: true,
},
],
properties: [
{
name: 'testParameter',
required: true,
},
],
},
});
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,
});
additionalData.httpRequest = {
body: { test: 'body' },
headers: { test: 'header' },
params: { test: 'param' },
query: { test: 'query' },
} as unknown as Request;
additionalData.httpResponse = mock<Response>();
const mode: WorkflowExecuteMode = 'manual';
const webhookData = mock<IWebhookData>({
webhookDescription: {
name: 'default',
},
});
const runExecutionData = null;
const webhookContext = new WebhookContext(
workflow,
node,
additionalData,
mode,
webhookData,
[],
runExecutionData,
);
beforeEach(() => {
jest.clearAllMocks();
});
describe('getCredentials', () => {
it('should get decrypted credentials', async () => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
const credentials =
await webhookContext.getCredentials<ICredentialDataDecryptedObject>(testCredentialType);
expect(credentials).toEqual({ secret: 'token' });
});
});
describe('getBodyData', () => {
it('should return the body data of the request', () => {
const bodyData = webhookContext.getBodyData();
expect(bodyData).toEqual({ test: 'body' });
});
});
describe('getHeaderData', () => {
it('should return the header data of the request', () => {
const headerData = webhookContext.getHeaderData();
expect(headerData).toEqual({ test: 'header' });
});
});
describe('getParamsData', () => {
it('should return the params data of the request', () => {
const paramsData = webhookContext.getParamsData();
expect(paramsData).toEqual({ test: 'param' });
});
});
describe('getQueryData', () => {
it('should return the query data of the request', () => {
const queryData = webhookContext.getQueryData();
expect(queryData).toEqual({ test: 'query' });
});
});
describe('getRequestObject', () => {
it('should return the request object', () => {
const request = webhookContext.getRequestObject();
expect(request).toBe(additionalData.httpRequest);
});
});
describe('getResponseObject', () => {
it('should return the response object', () => {
const response = webhookContext.getResponseObject();
expect(response).toBe(additionalData.httpResponse);
});
});
describe('getWebhookName', () => {
it('should return the name of the webhook', () => {
const webhookName = webhookContext.getWebhookName();
expect(webhookName).toBe('default');
});
});
describe('getNodeParameter', () => {
beforeEach(() => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
expression.getParameterValue.mockImplementation((value) => value);
});
it('should return parameter value when it exists', () => {
const parameter = webhookContext.getNodeParameter('testParameter');
expect(parameter).toBe('testValue');
});
it('should return the fallback value when the parameter does not exist', () => {
const parameter = webhookContext.getNodeParameter('otherParameter', 'fallback');
expect(parameter).toBe('fallback');
});
});
});

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-cycle
export { PollContext } from './poll-context';
export { TriggerContext } from './trigger-context';
export { WebhookContext } from './webhook-context';

View File

@@ -0,0 +1,239 @@
import type { Request, Response } from 'express';
import type {
CloseFunction,
ICredentialDataDecryptedObject,
IDataObject,
IExecuteData,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
IRunExecutionData,
ITaskDataConnections,
IWebhookData,
IWebhookFunctions,
IWorkflowExecuteAdditionalData,
NodeConnectionType,
NodeParameterValueType,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
copyBinaryFile,
getAdditionalKeys,
getCredentials,
getInputConnectionData,
getNodeParameter,
getNodeWebhookUrl,
returnJsonArray,
} from '@/NodeExecuteFunctions';
import { BinaryHelpers } from './helpers/binary-helpers';
import { RequestHelpers } from './helpers/request-helpers';
import { NodeExecutionContext } from './node-execution-context';
export class WebhookContext extends NodeExecutionContext implements IWebhookFunctions {
readonly helpers: IWebhookFunctions['helpers'];
readonly nodeHelpers: IWebhookFunctions['nodeHelpers'];
constructor(
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
private readonly webhookData: IWebhookData,
private readonly closeFunctions: CloseFunction[],
private readonly runExecutionData: IRunExecutionData | null,
) {
super(workflow, node, additionalData, mode);
this.helpers = {
createDeferredPromise,
returnJsonArray,
...new BinaryHelpers(workflow, additionalData).exported,
...new RequestHelpers(this, workflow, node, additionalData).exported,
};
this.nodeHelpers = {
copyBinaryFile: async (filePath, fileName, mimeType) =>
await copyBinaryFile(
this.workflow.id,
this.additionalData.executionId!,
filePath,
fileName,
mimeType,
),
};
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
}
getBodyData() {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.body as IDataObject;
}
getHeaderData() {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.headers;
}
getParamsData(): object {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.params;
}
getQueryData(): object {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.query;
}
getRequestObject(): Request {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest;
}
getResponseObject(): Response {
if (this.additionalData.httpResponse === undefined) {
throw new ApplicationError('Response is missing');
}
return this.additionalData.httpResponse;
}
getNodeWebhookUrl(name: string): string | undefined {
return getNodeWebhookUrl(
name,
this.workflow,
this.node,
this.additionalData,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, null),
);
}
getWebhookName() {
return this.webhookData.webhookDescription.name;
}
async getInputConnectionData(inputName: NodeConnectionType, itemIndex: number): Promise<unknown> {
// To be able to use expressions like "$json.sessionId" set the
// body data the webhook received to what is normally used for
// incoming node data.
const connectionInputData: INodeExecutionData[] = [
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
{ json: this.additionalData.httpRequest?.body || {} },
];
const runExecutionData: IRunExecutionData = {
resultData: {
runData: {},
},
};
const executeData: IExecuteData = {
data: {
main: [connectionInputData],
},
node: this.node,
source: null,
};
const runIndex = 0;
return await getInputConnectionData.call(
this,
this.workflow,
runExecutionData,
runIndex,
connectionInputData,
{} as ITaskDataConnections,
this.additionalData,
executeData,
this.mode,
this.closeFunctions,
inputName,
itemIndex,
);
}
evaluateExpression(expression: string, evaluateItemIndex?: number) {
const itemIndex = evaluateItemIndex ?? 0;
const runIndex = 0;
let connectionInputData: INodeExecutionData[] = [];
let executionData: IExecuteData | undefined;
if (this.runExecutionData?.executionData !== undefined) {
executionData = this.runExecutionData.executionData.nodeExecutionStack[0];
if (executionData !== undefined) {
connectionInputData = executionData.data.main[0]!;
}
}
const additionalKeys = getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
return this.workflow.expression.resolveSimpleParameterValue(
`=${expression}`,
{},
this.runExecutionData,
runIndex,
itemIndex,
this.node.name,
connectionInputData,
this.mode,
additionalKeys,
executionData,
);
}
getNodeParameter(
parameterName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallbackValue?: any,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object {
const itemIndex = 0;
const runIndex = 0;
let connectionInputData: INodeExecutionData[] = [];
let executionData: IExecuteData | undefined;
if (this.runExecutionData?.executionData !== undefined) {
executionData = this.runExecutionData.executionData.nodeExecutionStack[0];
if (executionData !== undefined) {
connectionInputData = executionData.data.main[0]!;
}
}
const additionalKeys = getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
return getNodeParameter(
this.workflow,
this.runExecutionData,
runIndex,
connectionInputData,
this.node,
parameterName,
itemIndex,
this.mode,
additionalKeys,
executionData,
fallbackValue,
options,
);
}
}