refactor(core): Extract load-options context out of NodeExecutionFunctions (no-changelog) (#11461)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-11-04 12:35:07 +01:00 committed by GitHub
parent a092b8e972
commit da18e1ad29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 220 additions and 102 deletions

View file

@ -1,4 +1,4 @@
import { NodeExecuteFunctions } from 'n8n-core';
import { LoadOptionsContext, NodeExecuteFunctions } from 'n8n-core';
import type {
ILoadOptions,
ILoadOptionsFunctions,
@ -253,6 +253,6 @@ export class DynamicNodeParametersService {
workflow: Workflow,
) {
const node = workflow.nodes['Temp-Node'];
return NodeExecuteFunctions.getLoadOptionsFunctions(workflow, node, path, additionalData);
return new LoadOptionsContext(workflow, node, additionalData, path);
}
}

View file

@ -59,7 +59,6 @@ import type {
IGetNodeParameterOptions,
IHookFunctions,
IHttpRequestOptions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
IN8nHttpResponse,
INode,
@ -4332,85 +4331,6 @@ export function getCredentialTestFunctions(): ICredentialTestFunctions {
};
}
/**
* Returns the execute functions regular nodes have access to in load-options-function.
*/
export function getLoadOptionsFunctions(
workflow: Workflow,
node: INode,
path: string,
additionalData: IWorkflowExecuteAdditionalData,
): ILoadOptionsFunctions {
return ((workflow: Workflow, node: INode, path: string) => {
return {
...getCommonWorkflowFunctions(workflow, node, additionalData),
getCredentials: async (type) =>
await getCredentials(workflow, node, type, additionalData, 'internal'),
getCurrentNodeParameter: (
parameterPath: string,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object | undefined => {
const nodeParameters = additionalData.currentNodeParameters;
if (parameterPath.charAt(0) === '&') {
parameterPath = `${path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`;
}
let returnData = get(nodeParameters, parameterPath);
// This is outside the try/catch because it throws errors with proper messages
if (options?.extractValue) {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new ApplicationError('Node type is not known so cannot return parameter value', {
tags: { nodeType: node.type },
});
}
returnData = extractValue(
returnData,
parameterPath,
node,
nodeType,
) as NodeParameterValueType;
}
return returnData;
},
getCurrentNodeParameters: () => additionalData.currentNodeParameters,
getNodeParameter: (
parameterName: string,
fallbackValue?: any,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object => {
const runExecutionData: IRunExecutionData | null = null;
const itemIndex = 0;
const runIndex = 0;
const mode = 'internal' as WorkflowExecuteMode;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(
workflow,
runExecutionData,
runIndex,
connectionInputData,
node,
parameterName,
itemIndex,
mode,
getAdditionalKeys(additionalData, mode, runExecutionData),
undefined,
fallbackValue,
options,
);
},
helpers: {
...getSSHTunnelFunctions(),
...getRequestHelperFunctions(workflow, node, additionalData),
},
};
})(workflow, node, path);
}
/**
* Returns the execute functions regular nodes have access to in hook-function.
*/

View file

@ -0,0 +1,102 @@
import { mock } from 'jest-mock-extended';
import type {
Expression,
ICredentialDataDecryptedObject,
ICredentialsHelper,
INode,
INodeType,
INodeTypes,
IWorkflowExecuteAdditionalData,
Workflow,
} from 'n8n-workflow';
import { LoadOptionsContext } from '../load-options-context';
describe('LoadOptionsContext', () => {
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 });
const path = 'testPath';
const loadOptionsContext = new LoadOptionsContext(workflow, node, additionalData, path);
beforeEach(() => {
jest.clearAllMocks();
});
describe('getCredentials', () => {
it('should get decrypted credentials', async () => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
const credentials =
await loadOptionsContext.getCredentials<ICredentialDataDecryptedObject>(testCredentialType);
expect(credentials).toEqual({ secret: 'token' });
});
});
describe('getCurrentNodeParameter', () => {
beforeEach(() => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
});
it('should return parameter value when it exists', () => {
additionalData.currentNodeParameters = {
testParameter: 'testValue',
};
const parameter = loadOptionsContext.getCurrentNodeParameter('testParameter');
expect(parameter).toBe('testValue');
});
});
describe('getNodeParameter', () => {
beforeEach(() => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
expression.getParameterValue.mockImplementation((value) => value);
});
it('should return parameter value when it exists', () => {
const parameter = loadOptionsContext.getNodeParameter('testParameter');
expect(parameter).toBe('testValue');
});
it('should return the fallback value when the parameter does not exist', () => {
const parameter = loadOptionsContext.getNodeParameter('otherParameter', 'fallback');
expect(parameter).toBe('fallback');
});
});
});

View file

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

View file

@ -0,0 +1,102 @@
import { get } from 'lodash';
import type {
ICredentialDataDecryptedObject,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
ILoadOptionsFunctions,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
Workflow,
} from 'n8n-workflow';
import { extractValue } from '@/ExtractValue';
// eslint-disable-next-line import/no-cycle
import { getAdditionalKeys, getCredentials, getNodeParameter } from '@/NodeExecuteFunctions';
import { RequestHelpers } from './helpers/request-helpers';
import { SSHTunnelHelpers } from './helpers/ssh-tunnel-helpers';
import { NodeExecutionContext } from './node-execution-context';
export class LoadOptionsContext extends NodeExecutionContext implements ILoadOptionsFunctions {
readonly helpers: ILoadOptionsFunctions['helpers'];
constructor(
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
private readonly path: string,
) {
super(workflow, node, additionalData, 'internal');
this.helpers = {
...new RequestHelpers(this, workflow, node, additionalData).exported,
...new SSHTunnelHelpers().exported,
};
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
}
getCurrentNodeParameter(
parameterPath: string,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object | undefined {
const nodeParameters = this.additionalData.currentNodeParameters;
if (parameterPath.charAt(0) === '&') {
parameterPath = `${this.path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`;
}
let returnData = get(nodeParameters, parameterPath);
// This is outside the try/catch because it throws errors with proper messages
if (options?.extractValue) {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
this.node.type,
this.node.typeVersion,
);
returnData = extractValue(
returnData,
parameterPath,
this.node,
nodeType,
) as NodeParameterValueType;
}
return returnData;
}
getCurrentNodeParameters() {
return this.additionalData.currentNodeParameters;
}
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,
);
}
}

View file

@ -74,38 +74,23 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
}
getBodyData() {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.body as IDataObject;
return this.assertHttpRequest().body as IDataObject;
}
getHeaderData() {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.headers;
return this.assertHttpRequest().headers;
}
getParamsData(): object {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.params;
return this.assertHttpRequest().params;
}
getQueryData(): object {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest.query;
return this.assertHttpRequest().query;
}
getRequestObject(): Request {
if (this.additionalData.httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return this.additionalData.httpRequest;
return this.assertHttpRequest();
}
getResponseObject(): Response {
@ -115,6 +100,14 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
return this.additionalData.httpResponse;
}
private assertHttpRequest() {
const { httpRequest } = this.additionalData;
if (httpRequest === undefined) {
throw new ApplicationError('Request is missing');
}
return httpRequest;
}
getNodeWebhookUrl(name: string): string | undefined {
return getNodeWebhookUrl(
name,