refactor(core): Extract ExecuteContext out of NodeExecutionFunctions (no-changelog) (#11853)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-11-26 12:10:42 +01:00 committed by GitHub
parent 3aa72f613f
commit 75e2b6fd9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1813 additions and 1940 deletions

View file

@ -4,7 +4,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
import { Credentials, getAdditionalKeys } from 'n8n-core';
import type {
ICredentialDataDecryptedObject,
ICredentialsExpressionResolveValues,
@ -379,7 +379,7 @@ export class CredentialsHelper extends ICredentialsHelper {
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
}
const additionalKeys = NodeExecuteFunctions.getAdditionalKeys(additionalData, mode, null, {
const additionalKeys = getAdditionalKeys(additionalData, mode, null, {
secretsEnabled: canUseSecrets,
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,212 @@
import { mock } from 'jest-mock-extended';
import type {
INode,
IWorkflowExecuteAdditionalData,
IRunExecutionData,
INodeExecutionData,
ITaskDataConnections,
IExecuteData,
Workflow,
WorkflowExecuteMode,
ICredentialsHelper,
Expression,
INodeType,
INodeTypes,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { ApplicationError, ExpressionError } from 'n8n-workflow';
import { describeCommonTests } from './shared-tests';
import { ExecuteContext } from '../execute-context';
describe('ExecuteContext', () => {
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>({
name: 'Test Node',
credentials: {
[testCredentialType]: {
id: 'testCredentialId',
},
},
});
node.parameters = {
testParameter: 'testValue',
nullParameter: null,
};
const credentialsHelper = mock<ICredentialsHelper>();
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
const mode: WorkflowExecuteMode = 'manual';
const runExecutionData = mock<IRunExecutionData>();
const connectionInputData: INodeExecutionData[] = [];
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
const executeData = mock<IExecuteData>();
const runIndex = 0;
const closeFn = jest.fn();
const abortSignal = mock<AbortSignal>();
const executeContext = new ExecuteContext(
workflow,
node,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executeData,
[closeFn],
abortSignal,
);
beforeEach(() => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
expression.getParameterValue.mockImplementation((value) => value);
});
describeCommonTests(executeContext, {
abortSignal,
node,
workflow,
executeData,
runExecutionData,
});
describe('getInputData', () => {
const inputIndex = 0;
const inputName = 'main';
afterEach(() => {
inputData[inputName] = [[{ json: { test: 'data' } }]];
});
it('should return the input data correctly', () => {
const expectedData = [{ json: { test: 'data' } }];
expect(executeContext.getInputData(inputIndex, inputName)).toEqual(expectedData);
});
it('should return an empty array if the input name does not exist', () => {
const inputName = 'nonExistent';
expect(executeContext.getInputData(inputIndex, inputName)).toEqual([]);
});
it('should throw an error if the input index is out of range', () => {
const inputIndex = 2;
expect(() => executeContext.getInputData(inputIndex, inputName)).toThrow(ApplicationError);
});
it('should throw an error if the input index was not set', () => {
inputData.main[inputIndex] = null;
expect(() => executeContext.getInputData(inputIndex, inputName)).toThrow(ApplicationError);
});
});
describe('getNodeParameter', () => {
beforeEach(() => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
expression.getParameterValue.mockImplementation((value) => value);
});
it('should throw if parameter is not defined on the node.parameters', () => {
expect(() => executeContext.getNodeParameter('invalidParameter', 0)).toThrow(
'Could not get parameter',
);
});
it('should return null if the parameter exists but has a null value', () => {
const parameter = executeContext.getNodeParameter('nullParameter', 0);
expect(parameter).toBeNull();
});
it('should return parameter value when it exists', () => {
const parameter = executeContext.getNodeParameter('testParameter', 0);
expect(parameter).toBe('testValue');
});
it('should return the fallback value when the parameter does not exist', () => {
const parameter = executeContext.getNodeParameter('otherParameter', 0, 'fallback');
expect(parameter).toBe('fallback');
});
it('should handle expression evaluation errors', () => {
const error = new ExpressionError('Invalid expression');
expression.getParameterValue.mockImplementationOnce(() => {
throw error;
});
expect(() => executeContext.getNodeParameter('testParameter', 0)).toThrow(error);
expect(error.context.parameter).toEqual('testParameter');
});
it('should handle expression errors on Set nodes (Ticket #PAY-684)', () => {
node.type = 'n8n-nodes-base.set';
node.continueOnFail = true;
expression.getParameterValue.mockImplementationOnce(() => {
throw new ExpressionError('Invalid expression');
});
const parameter = executeContext.getNodeParameter('testParameter', 0);
expect(parameter).toEqual([{ name: undefined, value: undefined }]);
});
});
describe('getCredentials', () => {
it('should get decrypted credentials', async () => {
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
const credentials = await executeContext.getCredentials<ICredentialDataDecryptedObject>(
testCredentialType,
0,
);
expect(credentials).toEqual({ secret: 'token' });
});
});
describe('getExecuteData', () => {
it('should return the execute data correctly', () => {
expect(executeContext.getExecuteData()).toEqual(executeData);
});
});
describe('getWorkflowDataProxy', () => {
it('should return the workflow data proxy correctly', () => {
const workflowDataProxy = executeContext.getWorkflowDataProxy(0);
expect(workflowDataProxy.isProxy).toBe(true);
expect(Object.keys(workflowDataProxy.$input)).toEqual([
'all',
'context',
'first',
'item',
'last',
'params',
]);
});
});
});

View file

@ -12,15 +12,11 @@ import type {
Expression,
INodeType,
INodeTypes,
OnError,
ContextType,
IContextObject,
ICredentialDataDecryptedObject,
ISourceData,
ITaskMetadata,
} from 'n8n-workflow';
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
import { ApplicationError } from 'n8n-workflow';
import { describeCommonTests } from './shared-tests';
import { ExecuteSingleContext } from '../execute-single-context';
describe('ExecuteSingleContext', () => {
@ -45,6 +41,7 @@ describe('ExecuteSingleContext', () => {
const expression = mock<Expression>();
const workflow = mock<Workflow>({ expression, nodeTypes });
const node = mock<INode>({
name: 'Test Node',
credentials: {
[testCredentialType]: {
id: 'testCredentialId',
@ -58,7 +55,7 @@ describe('ExecuteSingleContext', () => {
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
const mode: WorkflowExecuteMode = 'manual';
const runExecutionData = mock<IRunExecutionData>();
const connectionInputData = mock<INodeExecutionData[]>();
const connectionInputData: INodeExecutionData[] = [];
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
const executeData = mock<IExecuteData>();
const runIndex = 0;
@ -84,81 +81,12 @@ describe('ExecuteSingleContext', () => {
expression.getParameterValue.mockImplementation((value) => value);
});
describe('getExecutionCancelSignal', () => {
it('should return the abort signal', () => {
expect(executeSingleContext.getExecutionCancelSignal()).toBe(abortSignal);
});
});
describe('continueOnFail', () => {
afterEach(() => {
node.onError = undefined;
node.continueOnFail = false;
});
it('should return false for nodes by default', () => {
expect(executeSingleContext.continueOnFail()).toEqual(false);
});
it('should return true if node has continueOnFail set to true', () => {
node.continueOnFail = true;
expect(executeSingleContext.continueOnFail()).toEqual(true);
});
test.each([
['continueRegularOutput', true],
['continueErrorOutput', true],
['stopWorkflow', false],
])('if node has onError set to %s, it should return %s', (onError, expected) => {
node.onError = onError as OnError;
expect(executeSingleContext.continueOnFail()).toEqual(expected);
});
});
describe('evaluateExpression', () => {
it('should evaluate the expression correctly', () => {
const expression = '$json.test';
const expectedResult = 'data';
const resolveSimpleParameterValueSpy = jest.spyOn(
workflow.expression,
'resolveSimpleParameterValue',
);
resolveSimpleParameterValueSpy.mockReturnValue(expectedResult);
expect(executeSingleContext.evaluateExpression(expression, itemIndex)).toEqual(
expectedResult,
);
expect(resolveSimpleParameterValueSpy).toHaveBeenCalledWith(
`=${expression}`,
{},
runExecutionData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
expect.objectContaining({}),
executeData,
);
resolveSimpleParameterValueSpy.mockRestore();
});
});
describe('getContext', () => {
it('should return the context object', () => {
const contextType: ContextType = 'node';
const expectedContext = mock<IContextObject>();
const getContextSpy = jest.spyOn(NodeHelpers, 'getContext');
getContextSpy.mockReturnValue(expectedContext);
expect(executeSingleContext.getContext(contextType)).toEqual(expectedContext);
expect(getContextSpy).toHaveBeenCalledWith(runExecutionData, contextType, node);
getContextSpy.mockRestore();
});
describeCommonTests(executeSingleContext, {
abortSignal,
node,
workflow,
executeData,
runExecutionData,
});
describe('getInputData', () => {
@ -266,54 +194,4 @@ describe('ExecuteSingleContext', () => {
]);
});
});
describe('getInputSourceData', () => {
it('should return the input source data correctly', () => {
const inputSourceData = mock<ISourceData>();
executeData.source = { main: [inputSourceData] };
expect(executeSingleContext.getInputSourceData()).toEqual(inputSourceData);
});
it('should throw an error if the source data is missing', () => {
executeData.source = null;
expect(() => executeSingleContext.getInputSourceData()).toThrow(ApplicationError);
});
});
describe('logAiEvent', () => {
it('should log the AI event correctly', () => {
const eventName = 'ai-tool-called';
const msg = 'test message';
executeSingleContext.logAiEvent(eventName, msg);
expect(additionalData.logAiEvent).toHaveBeenCalledWith(eventName, {
executionId: additionalData.executionId,
nodeName: node.name,
workflowName: workflow.name,
nodeType: node.type,
workflowId: workflow.id,
msg,
});
});
});
describe('setMetadata', () => {
it('sets metadata on execution data', () => {
const metadata: ITaskMetadata = {
subExecution: {
workflowId: '123',
executionId: 'xyz',
},
};
expect(executeSingleContext.getExecuteData().metadata?.subExecution).toEqual(undefined);
executeSingleContext.setMetadata(metadata);
expect(executeSingleContext.getExecuteData().metadata?.subExecution).toEqual(
metadata.subExecution,
);
});
});
});

View file

@ -14,7 +14,7 @@ import { NodeExecutionContext } from '../node-execution-context';
class TestContext extends NodeExecutionContext {}
describe('BaseContext', () => {
describe('NodeExecutionContext', () => {
const instanceSettings = mock<InstanceSettings>({ instanceId: 'abc123' });
Container.set(InstanceSettings, instanceSettings);

View file

@ -0,0 +1,181 @@
import { captor, mock } from 'jest-mock-extended';
import type {
IRunExecutionData,
ContextType,
IContextObject,
INode,
OnError,
Workflow,
ITaskMetadata,
ISourceData,
IExecuteData,
} from 'n8n-workflow';
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
import type { BaseExecuteContext } from '../base-execute-context';
export const describeCommonTests = (
context: BaseExecuteContext,
{
abortSignal,
node,
workflow,
runExecutionData,
executeData,
}: {
abortSignal: AbortSignal;
node: INode;
workflow: Workflow;
runExecutionData: IRunExecutionData;
executeData: IExecuteData;
},
) => {
// @ts-expect-error `additionalData` is private
const { additionalData } = context;
describe('getExecutionCancelSignal', () => {
it('should return the abort signal', () => {
expect(context.getExecutionCancelSignal()).toBe(abortSignal);
});
});
describe('onExecutionCancellation', () => {
const handler = jest.fn();
context.onExecutionCancellation(handler);
const fnCaptor = captor<() => void>();
expect(abortSignal.addEventListener).toHaveBeenCalledWith('abort', fnCaptor);
expect(handler).not.toHaveBeenCalled();
fnCaptor.value();
expect(abortSignal.removeEventListener).toHaveBeenCalledWith('abort', fnCaptor);
expect(handler).toHaveBeenCalled();
});
describe('continueOnFail', () => {
afterEach(() => {
node.onError = undefined;
node.continueOnFail = false;
});
it('should return false for nodes by default', () => {
expect(context.continueOnFail()).toEqual(false);
});
it('should return true if node has continueOnFail set to true', () => {
node.continueOnFail = true;
expect(context.continueOnFail()).toEqual(true);
});
test.each([
['continueRegularOutput', true],
['continueErrorOutput', true],
['stopWorkflow', false],
])('if node has onError set to %s, it should return %s', (onError, expected) => {
node.onError = onError as OnError;
expect(context.continueOnFail()).toEqual(expected);
});
});
describe('getContext', () => {
it('should return the context object', () => {
const contextType: ContextType = 'node';
const expectedContext = mock<IContextObject>();
const getContextSpy = jest.spyOn(NodeHelpers, 'getContext');
getContextSpy.mockReturnValue(expectedContext);
expect(context.getContext(contextType)).toEqual(expectedContext);
expect(getContextSpy).toHaveBeenCalledWith(runExecutionData, contextType, node);
getContextSpy.mockRestore();
});
});
describe('sendMessageToUI', () => {
it('should send console messages to the frontend', () => {
context.sendMessageToUI('Testing', 1, 2, {});
expect(additionalData.sendDataToUI).toHaveBeenCalledWith('sendConsoleMessage', {
source: '[Node: "Test Node"]',
messages: ['Testing', 1, 2, {}],
});
});
});
describe('logAiEvent', () => {
it('should log the AI event correctly', () => {
const eventName = 'ai-tool-called';
const msg = 'test message';
context.logAiEvent(eventName, msg);
expect(additionalData.logAiEvent).toHaveBeenCalledWith(eventName, {
executionId: additionalData.executionId,
nodeName: node.name,
workflowName: workflow.name,
nodeType: node.type,
workflowId: workflow.id,
msg,
});
});
});
describe('getInputSourceData', () => {
it('should return the input source data correctly', () => {
const inputSourceData = mock<ISourceData>();
executeData.source = { main: [inputSourceData] };
expect(context.getInputSourceData()).toEqual(inputSourceData);
});
it('should throw an error if the source data is missing', () => {
executeData.source = null;
expect(() => context.getInputSourceData()).toThrow(ApplicationError);
});
});
describe('setMetadata', () => {
it('sets metadata on execution data', () => {
const metadata: ITaskMetadata = {
subExecution: {
workflowId: '123',
executionId: 'xyz',
},
};
expect(context.getExecuteData().metadata?.subExecution).toEqual(undefined);
context.setMetadata(metadata);
expect(context.getExecuteData().metadata?.subExecution).toEqual(metadata.subExecution);
});
});
describe('evaluateExpression', () => {
it('should evaluate the expression correctly', () => {
const expression = '$json.test';
const expectedResult = 'data';
const resolveSimpleParameterValueSpy = jest.spyOn(
workflow.expression,
'resolveSimpleParameterValue',
);
resolveSimpleParameterValueSpy.mockReturnValue(expectedResult);
expect(context.evaluateExpression(expression, 0)).toEqual(expectedResult);
expect(resolveSimpleParameterValueSpy).toHaveBeenCalledWith(
`=${expression}`,
{},
runExecutionData,
0,
0,
node.name,
[],
'manual',
expect.objectContaining({}),
executeData,
);
resolveSimpleParameterValueSpy.mockRestore();
});
});
};

View file

@ -12,11 +12,11 @@ import type {
Expression,
INodeType,
INodeTypes,
OnError,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { ApplicationError } from 'n8n-workflow';
import { describeCommonTests } from './shared-tests';
import { SupplyDataContext } from '../supply-data-context';
describe('SupplyDataContext', () => {
@ -41,6 +41,7 @@ describe('SupplyDataContext', () => {
const expression = mock<Expression>();
const workflow = mock<Workflow>({ expression, nodeTypes });
const node = mock<INode>({
name: 'Test Node',
credentials: {
[testCredentialType]: {
id: 'testCredentialId',
@ -54,7 +55,7 @@ describe('SupplyDataContext', () => {
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
const mode: WorkflowExecuteMode = 'manual';
const runExecutionData = mock<IRunExecutionData>();
const connectionInputData = mock<INodeExecutionData[]>();
const connectionInputData: INodeExecutionData[] = [];
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
const executeData = mock<IExecuteData>();
const runIndex = 0;
@ -80,64 +81,12 @@ describe('SupplyDataContext', () => {
expression.getParameterValue.mockImplementation((value) => value);
});
describe('getExecutionCancelSignal', () => {
it('should return the abort signal', () => {
expect(supplyDataContext.getExecutionCancelSignal()).toBe(abortSignal);
});
});
describe('continueOnFail', () => {
afterEach(() => {
node.onError = undefined;
node.continueOnFail = false;
});
it('should return false for nodes by default', () => {
expect(supplyDataContext.continueOnFail()).toEqual(false);
});
it('should return true if node has continueOnFail set to true', () => {
node.continueOnFail = true;
expect(supplyDataContext.continueOnFail()).toEqual(true);
});
test.each([
['continueRegularOutput', true],
['continueErrorOutput', true],
['stopWorkflow', false],
])('if node has onError set to %s, it should return %s', (onError, expected) => {
node.onError = onError as OnError;
expect(supplyDataContext.continueOnFail()).toEqual(expected);
});
});
describe('evaluateExpression', () => {
it('should evaluate the expression correctly', () => {
const expression = '$json.test';
const expectedResult = 'data';
const resolveSimpleParameterValueSpy = jest.spyOn(
workflow.expression,
'resolveSimpleParameterValue',
);
resolveSimpleParameterValueSpy.mockReturnValue(expectedResult);
expect(supplyDataContext.evaluateExpression(expression, 0)).toEqual(expectedResult);
expect(resolveSimpleParameterValueSpy).toHaveBeenCalledWith(
`=${expression}`,
{},
runExecutionData,
runIndex,
0,
node.name,
connectionInputData,
mode,
expect.objectContaining({}),
executeData,
);
resolveSimpleParameterValueSpy.mockRestore();
});
describeCommonTests(supplyDataContext, {
abortSignal,
node,
workflow,
executeData,
runExecutionData,
});
describe('getInputData', () => {
@ -219,22 +168,4 @@ describe('SupplyDataContext', () => {
]);
});
});
describe('logAiEvent', () => {
it('should log the AI event correctly', () => {
const eventName = 'ai-tool-called';
const msg = 'test message';
supplyDataContext.logAiEvent(eventName, msg);
expect(additionalData.logAiEvent).toHaveBeenCalledWith(eventName, {
executionId: additionalData.executionId,
nodeName: node.name,
workflowName: workflow.name,
nodeType: node.type,
workflowId: workflow.id,
msg,
});
});
});
});

View file

@ -1,8 +1,121 @@
import type { IDataObject, INode, INodeType } from 'n8n-workflow';
import toPlainObject from 'lodash/toPlainObject';
import { DateTime } from 'luxon';
import type { IDataObject, INode, INodeType, NodeParameterValue } from 'n8n-workflow';
import { ExpressionError } from 'n8n-workflow';
import { validateValueAgainstSchema } from '@/NodeExecuteFunctions';
import { cleanupParameterData, ensureType, validateValueAgainstSchema } from '../utils';
describe('Validation', () => {
describe('cleanupParameterData', () => {
it('should stringify Luxon dates in-place', () => {
const input = { x: 1, y: DateTime.now() as unknown as NodeParameterValue };
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('string');
});
it('should stringify plain Luxon dates in-place', () => {
const input = {
x: 1,
y: toPlainObject(DateTime.now()),
};
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('string');
});
it('should handle objects with nameless constructors', () => {
const input = { x: 1, y: { constructor: {} } as NodeParameterValue };
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('object');
});
it('should handle objects without a constructor', () => {
const input = { x: 1, y: { constructor: undefined } as unknown as NodeParameterValue };
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('object');
});
});
describe('ensureType', () => {
it('throws error for null value', () => {
expect(() => ensureType('string', null, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' must not be null"),
);
});
it('throws error for undefined value', () => {
expect(() => ensureType('string', undefined, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' could not be 'undefined'"),
);
});
it('returns string value without modification', () => {
const value = 'hello';
const expectedValue = value;
const result = ensureType('string', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('returns number value without modification', () => {
const value = 42;
const expectedValue = value;
const result = ensureType('number', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('returns boolean value without modification', () => {
const value = true;
const expectedValue = value;
const result = ensureType('boolean', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('converts object to string if toType is string', () => {
const value = { name: 'John' };
const expectedValue = JSON.stringify(value);
const result = ensureType('string', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('converts string to number if toType is number', () => {
const value = '10';
const expectedValue = 10;
const result = ensureType('number', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('throws error for invalid conversion to number', () => {
const value = 'invalid';
expect(() => ensureType('number', value, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"),
);
});
it('parses valid JSON string to object if toType is object', () => {
const value = '{"name": "Alice"}';
const expectedValue = JSON.parse(value);
const result = ensureType('object', value, 'myParam');
expect(result).toEqual(expectedValue);
});
it('throws error for invalid JSON string to object conversion', () => {
const value = 'invalid_json';
expect(() => ensureType('object', value, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' could not be parsed"),
);
});
it('throws error for non-array value if toType is array', () => {
const value = { name: 'Alice' };
expect(() => ensureType('array', value, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' must be an array, but we got object"),
);
});
});
describe('validateValueAgainstSchema', () => {
test('should validate fixedCollection values parameter', () => {
const nodeType = {
description: {

View file

@ -0,0 +1,213 @@
import { get } from 'lodash';
import type {
Workflow,
INode,
IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
IRunExecutionData,
INodeExecutionData,
ITaskDataConnections,
IExecuteData,
ICredentialDataDecryptedObject,
CallbackManager,
IExecuteWorkflowInfo,
RelatedExecution,
ExecuteWorkflowData,
ITaskMetadata,
ContextType,
IContextObject,
INodeInputConfiguration,
INodeOutputConfiguration,
IWorkflowDataProxyData,
ISourceData,
AiEvent,
} from 'n8n-workflow';
import { ApplicationError, NodeHelpers, WorkflowDataProxy } from 'n8n-workflow';
import { Container } from 'typedi';
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
import { NodeExecutionContext } from './node-execution-context';
export class BaseExecuteContext extends NodeExecutionContext {
protected readonly binaryDataService = Container.get(BinaryDataService);
constructor(
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
protected readonly runExecutionData: IRunExecutionData,
runIndex: number,
protected readonly connectionInputData: INodeExecutionData[],
protected readonly inputData: ITaskDataConnections,
protected readonly executeData: IExecuteData,
protected readonly abortSignal?: AbortSignal,
) {
super(workflow, node, additionalData, mode, runExecutionData, runIndex);
}
getExecutionCancelSignal() {
return this.abortSignal;
}
onExecutionCancellation(handler: () => unknown) {
const fn = () => {
this.abortSignal?.removeEventListener('abort', fn);
handler();
};
this.abortSignal?.addEventListener('abort', fn);
}
getExecuteData() {
return this.executeData;
}
setMetadata(metadata: ITaskMetadata): void {
this.executeData.metadata = {
...(this.executeData.metadata ?? {}),
...metadata,
};
}
getContext(type: ContextType): IContextObject {
return NodeHelpers.getContext(this.runExecutionData, type, this.node);
}
/** Returns if execution should be continued even if there was an error */
continueOnFail(): boolean {
const onError = get(this.node, 'onError', undefined);
if (onError === undefined) {
return get(this.node, 'continueOnFail', false);
}
return ['continueRegularOutput', 'continueErrorOutput'].includes(onError);
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(
type: string,
itemIndex: number,
) {
return await this._getCredentials<T>(
type,
this.executeData,
this.connectionInputData,
itemIndex,
);
}
async executeWorkflow(
workflowInfo: IExecuteWorkflowInfo,
inputData?: INodeExecutionData[],
parentCallbackManager?: CallbackManager,
options?: {
doNotWaitToFinish?: boolean;
parentExecution?: RelatedExecution;
},
): Promise<ExecuteWorkflowData> {
return await this.additionalData
.executeWorkflow(workflowInfo, this.additionalData, {
...options,
parentWorkflowId: this.workflow.id?.toString(),
inputData,
parentWorkflowSettings: this.workflow.settings,
node: this.node,
parentCallbackManager,
})
.then(async (result) => {
const data = await this.binaryDataService.duplicateBinaryData(
this.workflow.id,
this.additionalData.executionId!,
result.data,
);
return { ...result, data };
});
}
getNodeInputs(): INodeInputConfiguration[] {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
this.node.type,
this.node.typeVersion,
);
return NodeHelpers.getNodeInputs(this.workflow, this.node, nodeType.description).map((input) =>
typeof input === 'string' ? { type: input } : input,
);
}
getNodeOutputs(): INodeOutputConfiguration[] {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
this.node.type,
this.node.typeVersion,
);
return NodeHelpers.getNodeOutputs(this.workflow, this.node, nodeType.description).map(
(output) => (typeof output === 'string' ? { type: output } : output),
);
}
getInputSourceData(inputIndex = 0, inputName = 'main'): ISourceData {
if (this.executeData?.source === null) {
// Should never happen as n8n sets it automatically
throw new ApplicationError('Source data is missing');
}
return this.executeData.source[inputName][inputIndex]!;
}
getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData {
return new WorkflowDataProxy(
this.workflow,
this.runExecutionData,
this.runIndex,
itemIndex,
this.node.name,
this.connectionInputData,
{},
this.mode,
this.additionalKeys,
this.executeData,
).getDataProxy();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessageToUI(...args: any[]): void {
if (this.mode !== 'manual') {
return;
}
try {
if (this.additionalData.sendDataToUI) {
args = args.map((arg) => {
// prevent invalid dates from being logged as null
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
if (arg.isLuxonDateTime && arg.invalidReason) return { ...arg };
// log valid dates in human readable format, as in browser
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
if (arg.isLuxonDateTime) return new Date(arg.ts).toString();
if (arg instanceof Date) return arg.toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return arg;
});
this.additionalData.sendDataToUI('sendConsoleMessage', {
source: `[Node: "${this.node.name}"]`,
messages: args,
});
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logger.warn(`There was a problem sending message to UI: ${error.message}`);
}
}
logAiEvent(eventName: AiEvent, msg: string) {
return this.additionalData.logAiEvent(eventName, {
executionId: this.additionalData.executionId ?? 'unsaved-execution',
nodeName: this.node.name,
workflowName: this.workflow.name ?? 'Unnamed workflow',
nodeType: this.node.type,
workflowId: this.workflow.id ?? 'unsaved-workflow',
msg,
});
}
}

View file

@ -0,0 +1,263 @@
import type {
CallbackManager,
CloseFunction,
ExecutionBaseError,
IExecuteData,
IExecuteFunctions,
IExecuteResponsePromiseData,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
IRunExecutionData,
ITaskDataConnections,
ITaskMetadata,
IWorkflowExecuteAdditionalData,
NodeConnectionType,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
import { createAgentStartJob } from '@/Agent';
// eslint-disable-next-line import/no-cycle
import {
returnJsonArray,
copyInputItems,
normalizeItems,
constructExecutionMetaData,
getInputConnectionData,
addExecutionDataFunctions,
assertBinaryData,
getBinaryDataBuffer,
copyBinaryFile,
getRequestHelperFunctions,
getBinaryHelperFunctions,
getSSHTunnelFunctions,
getFileSystemHelperFunctions,
getCheckProcessedHelperFunctions,
} from '@/NodeExecuteFunctions';
import { BaseExecuteContext } from './base-execute-context';
export class ExecuteContext extends BaseExecuteContext implements IExecuteFunctions {
readonly helpers: IExecuteFunctions['helpers'];
readonly nodeHelpers: IExecuteFunctions['nodeHelpers'];
readonly getNodeParameter: IExecuteFunctions['getNodeParameter'];
readonly startJob: IExecuteFunctions['startJob'];
constructor(
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
executeData: IExecuteData,
private readonly closeFunctions: CloseFunction[],
abortSignal?: AbortSignal,
) {
super(
workflow,
node,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executeData,
abortSignal,
);
this.helpers = {
createDeferredPromise,
returnJsonArray,
copyInputItems,
normalizeItems,
constructExecutionMetaData,
...getRequestHelperFunctions(
workflow,
node,
additionalData,
runExecutionData,
connectionInputData,
),
...getBinaryHelperFunctions(additionalData, workflow.id),
...getSSHTunnelFunctions(),
...getFileSystemHelperFunctions(node),
...getCheckProcessedHelperFunctions(workflow, node),
assertBinaryData: (itemIndex, propertyName) =>
assertBinaryData(inputData, node, itemIndex, propertyName, 0),
getBinaryDataBuffer: async (itemIndex, propertyName) =>
await getBinaryDataBuffer(inputData, itemIndex, propertyName, 0),
};
this.nodeHelpers = {
copyBinaryFile: async (filePath, fileName, mimeType) =>
await copyBinaryFile(
this.workflow.id,
this.additionalData.executionId!,
filePath,
fileName,
mimeType,
),
};
this.getNodeParameter = ((
parameterName: string,
itemIndex: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallbackValue?: any,
options?: IGetNodeParameterOptions,
) =>
this._getNodeParameter(
parameterName,
itemIndex,
fallbackValue,
options,
)) as IExecuteFunctions['getNodeParameter'];
this.startJob = createAgentStartJob(
this.additionalData,
this.inputData,
this.node,
this.workflow,
this.runExecutionData,
this.runIndex,
this.node.name,
this.connectionInputData,
{},
this.mode,
this.executeData,
);
}
async getInputConnectionData(inputName: NodeConnectionType, itemIndex: number): Promise<unknown> {
return await getInputConnectionData.call(
this,
this.workflow,
this.runExecutionData,
this.runIndex,
this.connectionInputData,
this.inputData,
this.additionalData,
this.executeData,
this.mode,
this.closeFunctions,
inputName,
itemIndex,
this.abortSignal,
);
}
getInputData(inputIndex = 0, inputName = 'main') {
if (!this.inputData.hasOwnProperty(inputName)) {
// Return empty array because else it would throw error when nothing is connected to input
return [];
}
const inputData = this.inputData[inputName];
// TODO: Check if nodeType has input with that index defined
if (inputData.length < inputIndex) {
throw new ApplicationError('Could not get input with given index', {
extra: { inputIndex, inputName },
});
}
if (inputData[inputIndex] === null) {
throw new ApplicationError('Value of input was not set', {
extra: { inputIndex, inputName },
});
}
return inputData[inputIndex];
}
async putExecutionToWait(waitTill: Date): Promise<void> {
this.runExecutionData.waitTill = waitTill;
if (this.additionalData.setExecutionStatus) {
this.additionalData.setExecutionStatus('waiting');
}
}
logNodeOutput(...args: unknown[]): void {
if (this.mode === 'manual') {
this.sendMessageToUI(...args);
return;
}
if (process.env.CODE_ENABLE_STDOUT === 'true') {
console.log(`[Workflow "${this.getWorkflow().id}"][Node "${this.node.name}"]`, ...args);
}
}
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
await this.additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
}
addInputData(
connectionType: NodeConnectionType,
data: INodeExecutionData[][] | ExecutionBaseError,
): { index: number } {
const nodeName = this.node.name;
let currentNodeRunIndex = 0;
if (this.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
currentNodeRunIndex = this.runExecutionData.resultData.runData[nodeName].length;
}
void addExecutionDataFunctions(
'input',
nodeName,
data,
this.runExecutionData,
connectionType,
this.additionalData,
nodeName,
this.runIndex,
currentNodeRunIndex,
).catch((error) => {
this.logger.warn(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
`There was a problem logging input data of node "${nodeName}": ${error.message}`,
);
});
return { index: currentNodeRunIndex };
}
addOutputData(
connectionType: NodeConnectionType,
currentNodeRunIndex: number,
data: INodeExecutionData[][] | ExecutionBaseError,
metadata?: ITaskMetadata,
): void {
const nodeName = this.node.name;
addExecutionDataFunctions(
'output',
nodeName,
data,
this.runExecutionData,
connectionType,
this.additionalData,
nodeName,
this.runIndex,
currentNodeRunIndex,
metadata,
).catch((error) => {
this.logger.warn(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
`There was a problem logging output data of node "${nodeName}": ${error.message}`,
);
});
}
getParentCallbackManager(): CallbackManager | undefined {
return this.additionalData.parentCallbackManager;
}
}

View file

@ -10,34 +10,21 @@ import type {
WorkflowExecuteMode,
ITaskDataConnections,
IExecuteData,
ContextType,
AiEvent,
ISourceData,
ITaskMetadata,
} from 'n8n-workflow';
import {
ApplicationError,
createDeferredPromise,
NodeHelpers,
WorkflowDataProxy,
} from 'n8n-workflow';
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
assertBinaryData,
continueOnFail,
getAdditionalKeys,
getBinaryDataBuffer,
getBinaryHelperFunctions,
getCredentials,
getNodeParameter,
getRequestHelperFunctions,
returnJsonArray,
} from '@/NodeExecuteFunctions';
import { NodeExecutionContext } from './node-execution-context';
import { BaseExecuteContext } from './base-execute-context';
export class ExecuteSingleContext extends NodeExecutionContext implements IExecuteSingleFunctions {
export class ExecuteSingleContext extends BaseExecuteContext implements IExecuteSingleFunctions {
readonly helpers: IExecuteSingleFunctions['helpers'];
constructor(
@ -45,15 +32,26 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
private readonly runExecutionData: IRunExecutionData,
private readonly runIndex: number,
private readonly connectionInputData: INodeExecutionData[],
private readonly inputData: ITaskDataConnections,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
private readonly itemIndex: number,
private readonly executeData: IExecuteData,
private readonly abortSignal?: AbortSignal,
executeData: IExecuteData,
abortSignal?: AbortSignal,
) {
super(workflow, node, additionalData, mode);
super(
workflow,
node,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executeData,
abortSignal,
);
this.helpers = {
createDeferredPromise,
@ -74,47 +72,8 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
};
}
getExecutionCancelSignal() {
return this.abortSignal;
}
onExecutionCancellation(handler: () => unknown) {
const fn = () => {
this.abortSignal?.removeEventListener('abort', fn);
handler();
};
this.abortSignal?.addEventListener('abort', fn);
}
setMetadata(metadata: ITaskMetadata): void {
this.executeData.metadata = {
...(this.executeData.metadata ?? {}),
...metadata,
};
}
continueOnFail() {
return continueOnFail(this.node);
}
evaluateExpression(expression: string, evaluateItemIndex: number | undefined) {
evaluateItemIndex = evaluateItemIndex ?? this.itemIndex;
return this.workflow.expression.resolveSimpleParameterValue(
`=${expression}`,
{},
this.runExecutionData,
this.runIndex,
evaluateItemIndex,
this.node.name,
this.connectionInputData,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
this.executeData,
);
}
getContext(type: ContextType) {
return NodeHelpers.getContext(this.runExecutionData, type, this.node);
evaluateExpression(expression: string, itemIndex: number = this.itemIndex) {
return super.evaluateExpression(expression, itemIndex);
}
getInputData(inputIndex = 0, inputName = 'main') {
@ -154,73 +113,14 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getNodeParameter(parameterName: string, fallbackValue?: any, options?: IGetNodeParameterOptions) {
return getNodeParameter(
this.workflow,
this.runExecutionData,
this.runIndex,
this.connectionInputData,
this.node,
parameterName,
this.itemIndex,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
this.executeData,
fallbackValue,
options,
);
return this._getNodeParameter(parameterName, this.itemIndex, fallbackValue, options);
}
// TODO: extract out in a BaseExecutionContext
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
return await getCredentials<T>(
this.workflow,
this.node,
type,
this.additionalData,
this.mode,
this.executeData,
this.runExecutionData,
this.runIndex,
this.connectionInputData,
this.itemIndex,
);
}
getExecuteData() {
return this.executeData;
return await super.getCredentials<T>(type, this.itemIndex);
}
getWorkflowDataProxy() {
return new WorkflowDataProxy(
this.workflow,
this.runExecutionData,
this.runIndex,
this.itemIndex,
this.node.name,
this.connectionInputData,
{},
this.mode,
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
this.executeData,
).getDataProxy();
}
getInputSourceData(inputIndex = 0, inputName = 'main'): ISourceData {
if (this.executeData?.source === null) {
// Should never happen as n8n sets it automatically
throw new ApplicationError('Source data is missing');
}
return this.executeData.source[inputName][inputIndex] as ISourceData;
}
logAiEvent(eventName: AiEvent, msg: string) {
return this.additionalData.logAiEvent(eventName, {
executionId: this.additionalData.executionId ?? 'unsaved-execution',
nodeName: this.node.name,
workflowName: this.workflow.name ?? 'Unnamed workflow',
nodeType: this.node.type,
workflowId: this.workflow.id ?? 'unsaved-workflow',
msg,
});
return super.getWorkflowDataProxy(this.itemIndex);
}
}

View file

@ -1,12 +1,8 @@
import type {
ICredentialDataDecryptedObject,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
IHookFunctions,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
Workflow,
WorkflowActivateMode,
WorkflowExecuteMode,
@ -17,9 +13,6 @@ import { ApplicationError } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
getAdditionalKeys,
getCredentials,
getNodeParameter,
getNodeWebhookUrl,
getRequestHelperFunctions,
getWebhookDescription,
@ -48,34 +41,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
}
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,
);
return await this._getCredentials<T>(type);
}
getNodeWebhookUrl(name: WebhookType): string | undefined {
@ -85,7 +51,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
this.node,
this.additionalData,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, null),
this.additionalKeys,
this.webhookData?.isTest,
);
}

View file

@ -1,4 +1,5 @@
// eslint-disable-next-line import/no-cycle
export { ExecuteContext } from './execute-context';
export { ExecuteSingleContext } from './execute-single-context';
export { HookContext } from './hook-context';
export { LoadOptionsContext } from './load-options-context';
@ -6,3 +7,5 @@ export { PollContext } from './poll-context';
export { SupplyDataContext } from './supply-data-context';
export { TriggerContext } from './trigger-context';
export { WebhookContext } from './webhook-context';
export { getAdditionalKeys } from './utils';

View file

@ -3,9 +3,7 @@ import type {
ICredentialDataDecryptedObject,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
ILoadOptionsFunctions,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
Workflow,
@ -13,13 +11,7 @@ import type {
import { extractValue } from '@/ExtractValue';
// eslint-disable-next-line import/no-cycle
import {
getAdditionalKeys,
getCredentials,
getNodeParameter,
getRequestHelperFunctions,
getSSHTunnelFunctions,
} from '@/NodeExecuteFunctions';
import { getRequestHelperFunctions, getSSHTunnelFunctions } from '@/NodeExecuteFunctions';
import { NodeExecutionContext } from './node-execution-context';
@ -41,7 +33,7 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
return await this._getCredentials<T>(type);
}
getCurrentNodeParameter(
@ -76,31 +68,4 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
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

@ -1,17 +1,42 @@
import { get } from 'lodash';
import type {
FunctionsBase,
ICredentialDataDecryptedObject,
ICredentialsExpressionResolveValues,
IExecuteData,
IGetNodeParameterOptions,
INode,
INodeCredentialDescription,
INodeCredentialsDetails,
INodeExecutionData,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
NodeTypeAndVersion,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { deepCopy, LoggerProxy } from 'n8n-workflow';
import {
ApplicationError,
deepCopy,
ExpressionError,
LoggerProxy,
NodeHelpers,
NodeOperationError,
} from 'n8n-workflow';
import { Container } from 'typedi';
import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/Constants';
import { extractValue } from '@/ExtractValue';
import { InstanceSettings } from '@/InstanceSettings';
import {
cleanupParameterData,
ensureType,
getAdditionalKeys,
validateValueAgainstSchema,
} from './utils';
export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCredentials'> {
protected readonly instanceSettings = Container.get(InstanceSettings);
@ -20,6 +45,10 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
protected readonly node: INode,
protected readonly additionalData: IWorkflowExecuteAdditionalData,
protected readonly mode: WorkflowExecuteMode,
protected readonly runExecutionData: IRunExecutionData | null = null,
protected readonly runIndex = 0,
protected readonly connectionInputData: INodeExecutionData[] = [],
protected readonly executeData?: IExecuteData,
) {}
get logger() {
@ -101,6 +130,255 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
return this.additionalData.credentialsHelper.getCredentialsProperties(type);
}
/** Returns the requested decrypted credentials if the node has access to them */
protected async _getCredentials<T extends object = ICredentialDataDecryptedObject>(
type: string,
executeData?: IExecuteData,
connectionInputData?: INodeExecutionData[],
itemIndex?: number,
): Promise<T> {
const { workflow, node, additionalData, mode, runExecutionData, runIndex } = this;
// Get the NodeType as it has the information if the credentials are required
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
// Hardcode for now for security reasons that only a single node can access
// all credentials
const fullAccess = [HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE].includes(node.type);
let nodeCredentialDescription: INodeCredentialDescription | undefined;
if (!fullAccess) {
if (nodeType.description.credentials === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials defined`,
{ level: 'warning' },
);
}
nodeCredentialDescription = nodeType.description.credentials.find(
(credentialTypeDescription) => credentialTypeDescription.name === type,
);
if (nodeCredentialDescription === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials of type "${type}" defined`,
{ level: 'warning' },
);
}
if (
!NodeHelpers.displayParameter(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
additionalData.currentNodeParameters || node.parameters,
nodeCredentialDescription,
node,
node.parameters,
)
) {
// Credentials should not be displayed even if they would be defined
throw new NodeOperationError(node, 'Credentials not found');
}
}
// Check if node has any credentials defined
if (!fullAccess && !node.credentials?.[type]) {
// If none are defined check if the credentials are required or not
if (nodeCredentialDescription?.required === true) {
// Credentials are required so error
if (!node.credentials) {
throw new NodeOperationError(node, 'Node does not have any credentials set', {
level: 'warning',
});
}
if (!node.credentials[type]) {
throw new NodeOperationError(
node,
`Node does not have any credentials set for "${type}"`,
{
level: 'warning',
},
);
}
} else {
// Credentials are not required
throw new NodeOperationError(node, 'Node does not require credentials');
}
}
if (fullAccess && !node.credentials?.[type]) {
// Make sure that fullAccess nodes still behave like before that if they
// request access to credentials that are currently not set it returns undefined
throw new NodeOperationError(node, 'Credentials not found');
}
let expressionResolveValues: ICredentialsExpressionResolveValues | undefined;
if (connectionInputData && runExecutionData && runIndex !== undefined) {
expressionResolveValues = {
connectionInputData,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
itemIndex: itemIndex || 0,
node,
runExecutionData,
runIndex,
workflow,
} as ICredentialsExpressionResolveValues;
}
const nodeCredentials = node.credentials
? node.credentials[type]
: ({} as INodeCredentialsDetails);
// TODO: solve using credentials via expression
// if (name.charAt(0) === '=') {
// // If the credential name is an expression resolve it
// const additionalKeys = getAdditionalKeys(additionalData, mode);
// name = workflow.expression.getParameterValue(
// name,
// runExecutionData || null,
// runIndex || 0,
// itemIndex || 0,
// node.name,
// connectionInputData || [],
// mode,
// additionalKeys,
// ) as string;
// }
const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted(
additionalData,
nodeCredentials,
type,
mode,
executeData,
false,
expressionResolveValues,
);
return decryptedDataObject as T;
}
protected get additionalKeys() {
return getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
}
/** Returns the requested resolved (all expressions replaced) node parameters. */
getNodeParameter(
parameterName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallbackValue?: any,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object {
const itemIndex = 0;
return this._getNodeParameter(parameterName, itemIndex, fallbackValue, options);
}
protected _getNodeParameter(
parameterName: string,
itemIndex: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallbackValue?: any,
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object {
const { workflow, node, mode, runExecutionData, runIndex, connectionInputData, executeData } =
this;
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const value = get(node.parameters, parameterName, fallbackValue);
if (value === undefined) {
throw new ApplicationError('Could not get parameter', { extra: { parameterName } });
}
if (options?.rawExpressions) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return value;
}
const { additionalKeys } = this;
let returnData;
try {
returnData = workflow.expression.getParameterValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
value,
runExecutionData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
additionalKeys,
executeData,
false,
{},
options?.contextNode?.name,
);
cleanupParameterData(returnData);
} catch (e) {
if (
e instanceof ExpressionError &&
node.continueOnFail &&
node.type === 'n8n-nodes-base.set'
) {
// https://linear.app/n8n/issue/PAY-684
returnData = [{ name: undefined, value: undefined }];
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (e.context) e.context.parameter = parameterName;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
e.cause = value;
throw e;
}
}
// This is outside the try/catch because it throws errors with proper messages
if (options?.extractValue) {
returnData = extractValue(returnData, parameterName, node, nodeType, itemIndex);
}
// Make sure parameter value is the type specified in the ensureType option, if needed convert it
if (options?.ensureType) {
returnData = ensureType(options.ensureType, returnData, parameterName, {
itemIndex,
runIndex,
nodeCause: node.name,
});
}
// Validate parameter value if it has a schema defined(RMC) or validateType defined
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
returnData = validateValueAgainstSchema(
node,
nodeType,
returnData,
parameterName,
runIndex,
itemIndex,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnData;
}
evaluateExpression(expression: string, itemIndex: number = 0) {
return this.workflow.expression.resolveSimpleParameterValue(
`=${expression}`,
{},
this.runExecutionData,
this.runIndex,
itemIndex,
this.node.name,
this.connectionInputData,
this.mode,
this.additionalKeys,
this.executeData,
);
}
async prepareOutputData(outputData: INodeExecutionData[]) {
return [outputData];
}

View file

@ -1,12 +1,8 @@
import type {
ICredentialDataDecryptedObject,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
IPollFunctions,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
Workflow,
WorkflowActivateMode,
WorkflowExecuteMode,
@ -15,10 +11,7 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
getAdditionalKeys,
getBinaryHelperFunctions,
getCredentials,
getNodeParameter,
getRequestHelperFunctions,
getSchedulingFunctions,
returnJsonArray,
@ -62,33 +55,6 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions
}
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,
);
return await this._getCredentials<T>(type);
}
}

View file

@ -1,11 +1,6 @@
import type {
AiEvent,
CallbackManager,
CloseFunction,
ExecuteWorkflowData,
ICredentialDataDecryptedObject,
IExecuteData,
IExecuteWorkflowInfo,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
@ -15,32 +10,20 @@ import type {
ITaskMetadata,
IWorkflowExecuteAdditionalData,
NodeConnectionType,
RelatedExecution,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
ApplicationError,
createDeferredPromise,
NodeHelpers,
WorkflowDataProxy,
} from 'n8n-workflow';
import Container from 'typedi';
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
// eslint-disable-next-line import/no-cycle
import {
assertBinaryData,
continueOnFail,
constructExecutionMetaData,
copyInputItems,
getAdditionalKeys,
getBinaryDataBuffer,
getBinaryHelperFunctions,
getCheckProcessedHelperFunctions,
getCredentials,
getFileSystemHelperFunctions,
getNodeParameter,
getRequestHelperFunctions,
getSSHTunnelFunctions,
normalizeItems,
@ -49,9 +32,9 @@ import {
addExecutionDataFunctions,
} from '@/NodeExecuteFunctions';
import { NodeExecutionContext } from './node-execution-context';
import { BaseExecuteContext } from './base-execute-context';
export class SupplyDataContext extends NodeExecutionContext implements ISupplyDataFunctions {
export class SupplyDataContext extends BaseExecuteContext implements ISupplyDataFunctions {
readonly helpers: ISupplyDataFunctions['helpers'];
readonly getNodeParameter: ISupplyDataFunctions['getNodeParameter'];
@ -61,15 +44,26 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
private readonly runExecutionData: IRunExecutionData,
private readonly runIndex: number,
private readonly connectionInputData: INodeExecutionData[],
private readonly inputData: ITaskDataConnections,
private readonly executeData: IExecuteData,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
executeData: IExecuteData,
private readonly closeFunctions: CloseFunction[],
private readonly abortSignal?: AbortSignal,
abortSignal?: AbortSignal,
) {
super(workflow, node, additionalData, mode);
super(
workflow,
node,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executeData,
abortSignal,
);
this.helpers = {
createDeferredPromise,
@ -102,116 +96,14 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
fallbackValue?: any,
options?: IGetNodeParameterOptions,
) =>
getNodeParameter(
this.workflow,
this.runExecutionData,
this.runIndex,
this.connectionInputData,
this.node,
this._getNodeParameter(
parameterName,
itemIndex,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
this.executeData,
fallbackValue,
options,
)) as ISupplyDataFunctions['getNodeParameter'];
}
getExecutionCancelSignal() {
return this.abortSignal;
}
onExecutionCancellation(handler: () => unknown) {
const fn = () => {
this.abortSignal?.removeEventListener('abort', fn);
handler();
};
this.abortSignal?.addEventListener('abort', fn);
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(
type: string,
itemIndex: number,
) {
return await getCredentials<T>(
this.workflow,
this.node,
type,
this.additionalData,
this.mode,
this.executeData,
this.runExecutionData,
this.runIndex,
this.connectionInputData,
itemIndex,
);
}
continueOnFail() {
return continueOnFail(this.node);
}
evaluateExpression(expression: string, itemIndex: number) {
return this.workflow.expression.resolveSimpleParameterValue(
`=${expression}`,
{},
this.runExecutionData,
this.runIndex,
itemIndex,
this.node.name,
this.connectionInputData,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
this.executeData,
);
}
async executeWorkflow(
workflowInfo: IExecuteWorkflowInfo,
inputData?: INodeExecutionData[],
parentCallbackManager?: CallbackManager,
options?: {
doNotWaitToFinish?: boolean;
parentExecution?: RelatedExecution;
},
): Promise<ExecuteWorkflowData> {
return await this.additionalData
.executeWorkflow(workflowInfo, this.additionalData, {
parentWorkflowId: this.workflow.id?.toString(),
inputData,
parentWorkflowSettings: this.workflow.settings,
node: this.node,
parentCallbackManager,
...options,
})
.then(async (result) => {
const data = await Container.get(BinaryDataService).duplicateBinaryData(
this.workflow.id,
this.additionalData.executionId!,
result.data,
);
return { ...result, data };
});
}
getNodeOutputs() {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
this.node.type,
this.node.typeVersion,
);
return NodeHelpers.getNodeOutputs(this.workflow, this.node, nodeType.description).map(
(output) => {
if (typeof output === 'string') {
return {
type: output,
};
}
return output;
},
);
}
async getInputConnectionData(inputName: NodeConnectionType, itemIndex: number): Promise<unknown> {
return await getInputConnectionData.call(
this,
@ -252,69 +144,11 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
return this.inputData[inputName][inputIndex];
}
getWorkflowDataProxy(itemIndex: number) {
return new WorkflowDataProxy(
this.workflow,
this.runExecutionData,
this.runIndex,
itemIndex,
this.node.name,
this.connectionInputData,
{},
this.mode,
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
this.executeData,
).getDataProxy();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessageToUI(...args: any[]): void {
if (this.mode !== 'manual') {
return;
}
try {
if (this.additionalData.sendDataToUI) {
args = args.map((arg) => {
// prevent invalid dates from being logged as null
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
if (arg.isLuxonDateTime && arg.invalidReason) return { ...arg };
// log valid dates in human readable format, as in browser
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
if (arg.isLuxonDateTime) return new Date(arg.ts).toString();
if (arg instanceof Date) return arg.toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return arg;
});
this.additionalData.sendDataToUI('sendConsoleMessage', {
source: `[Node: "${this.node.name}"]`,
messages: args,
});
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logger.warn(`There was a problem sending message to UI: ${error.message}`);
}
}
logAiEvent(eventName: AiEvent, msg: string) {
return this.additionalData.logAiEvent(eventName, {
executionId: this.additionalData.executionId ?? 'unsaved-execution',
nodeName: this.node.name,
workflowName: this.workflow.name ?? 'Unnamed workflow',
nodeType: this.node.type,
workflowId: this.workflow.id ?? 'unsaved-workflow',
msg,
});
}
addInputData(
connectionType: NodeConnectionType,
data: INodeExecutionData[][],
): { index: number } {
const nodeName = this.getNode().name;
const nodeName = this.node.name;
let currentNodeRunIndex = 0;
if (this.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
currentNodeRunIndex = this.runExecutionData.resultData.runData[nodeName].length;
@ -322,17 +156,17 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
addExecutionDataFunctions(
'input',
this.node.name,
nodeName,
data,
this.runExecutionData,
connectionType,
this.additionalData,
this.node.name,
nodeName,
this.runIndex,
currentNodeRunIndex,
).catch((error) => {
this.logger.warn(
`There was a problem logging input data of node "${this.node.name}": ${
`There was a problem logging input data of node "${nodeName}": ${
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.message
}`,
@ -348,20 +182,21 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
data: INodeExecutionData[][],
metadata?: ITaskMetadata,
): void {
const nodeName = this.node.name;
addExecutionDataFunctions(
'output',
this.node.name,
nodeName,
data,
this.runExecutionData,
connectionType,
this.additionalData,
this.node.name,
nodeName,
this.runIndex,
currentNodeRunIndex,
metadata,
).catch((error) => {
this.logger.warn(
`There was a problem logging output data of node "${this.node.name}": ${
`There was a problem logging output data of node "${nodeName}": ${
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.message
}`,

View file

@ -1,12 +1,8 @@
import type {
ICredentialDataDecryptedObject,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
IRunExecutionData,
ITriggerFunctions,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
Workflow,
WorkflowActivateMode,
WorkflowExecuteMode,
@ -15,10 +11,7 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
getAdditionalKeys,
getBinaryHelperFunctions,
getCredentials,
getNodeParameter,
getRequestHelperFunctions,
getSchedulingFunctions,
getSSHTunnelFunctions,
@ -64,33 +57,6 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc
}
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,
);
return await this._getCredentials<T>(type);
}
}

View file

@ -0,0 +1,423 @@
import { DateTime } from 'luxon';
import type {
EnsureTypeOptions,
FieldType,
IDataObject,
INode,
INodeParameters,
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
INodeType,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData,
NodeParameterValueType,
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
ExpressionError,
isResourceMapperValue,
LoggerProxy,
NodeHelpers,
validateFieldType,
} from 'n8n-workflow';
import { PLACEHOLDER_EMPTY_EXECUTION_ID } from '@/Constants';
import {
setWorkflowExecutionMetadata,
setAllWorkflowExecutionMetadata,
getWorkflowExecutionMetadata,
getAllWorkflowExecutionMetadata,
} from '@/ExecutionMetadata';
import type { ExtendedValidationResult } from '@/Interfaces';
import { getSecretsProxy } from '@/Secrets';
/**
* Clean up parameter data to make sure that only valid data gets returned
* INFO: Currently only converts Luxon Dates as we know for sure it will not be breaking
*/
export function cleanupParameterData(inputData: NodeParameterValueType): void {
if (typeof inputData !== 'object' || inputData === null) {
return;
}
if (Array.isArray(inputData)) {
inputData.forEach((value) => cleanupParameterData(value as NodeParameterValueType));
return;
}
if (typeof inputData === 'object') {
Object.keys(inputData).forEach((key) => {
const value = (inputData as INodeParameters)[key];
if (typeof value === 'object') {
if (DateTime.isDateTime(value)) {
// Is a special luxon date so convert to string
(inputData as INodeParameters)[key] = value.toString();
} else {
cleanupParameterData(value);
}
}
});
}
}
const validateResourceMapperValue = (
parameterName: string,
paramValues: { [key: string]: unknown },
node: INode,
skipRequiredCheck = false,
): ExtendedValidationResult => {
const result: ExtendedValidationResult = { valid: true, newValue: paramValues };
const paramNameParts = parameterName.split('.');
if (paramNameParts.length !== 2) {
return result;
}
const resourceMapperParamName = paramNameParts[0];
const resourceMapperField = node.parameters[resourceMapperParamName];
if (!resourceMapperField || !isResourceMapperValue(resourceMapperField)) {
return result;
}
const schema = resourceMapperField.schema;
const paramValueNames = Object.keys(paramValues);
for (let i = 0; i < paramValueNames.length; i++) {
const key = paramValueNames[i];
const resolvedValue = paramValues[key];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const schemaEntry = schema.find((s) => s.id === key);
if (
!skipRequiredCheck &&
schemaEntry?.required === true &&
schemaEntry.type !== 'boolean' &&
!resolvedValue
) {
return {
valid: false,
errorMessage: `The value "${String(key)}" is required but not set`,
fieldName: key,
};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (schemaEntry?.type) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
valueOptions: schemaEntry.options,
});
if (!validationResult.valid) {
return { ...validationResult, fieldName: key };
} else {
// If it's valid, set the casted value
paramValues[key] = validationResult.newValue;
}
}
}
return result;
};
const validateCollection = (
node: INode,
runIndex: number,
itemIndex: number,
propertyDescription: INodeProperties,
parameterPath: string[],
validationResult: ExtendedValidationResult,
): ExtendedValidationResult => {
let nestedDescriptions: INodeProperties[] | undefined;
if (propertyDescription.type === 'fixedCollection') {
nestedDescriptions = (propertyDescription.options as INodePropertyCollection[]).find(
(entry) => entry.name === parameterPath[1],
)?.values;
}
if (propertyDescription.type === 'collection') {
nestedDescriptions = propertyDescription.options as INodeProperties[];
}
if (!nestedDescriptions) {
return validationResult;
}
const validationMap: {
[key: string]: { type: FieldType; displayName: string; options?: INodePropertyOptions[] };
} = {};
for (const prop of nestedDescriptions) {
if (!prop.validateType || prop.ignoreValidationDuringExecution) continue;
validationMap[prop.name] = {
type: prop.validateType,
displayName: prop.displayName,
options:
prop.validateType === 'options' ? (prop.options as INodePropertyOptions[]) : undefined,
};
}
if (!Object.keys(validationMap).length) {
return validationResult;
}
if (validationResult.valid) {
for (const value of Array.isArray(validationResult.newValue)
? (validationResult.newValue as IDataObject[])
: [validationResult.newValue as IDataObject]) {
for (const key of Object.keys(value)) {
if (!validationMap[key]) continue;
const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, {
valueOptions: validationMap[key].options,
});
if (!fieldValidationResult.valid) {
throw new ExpressionError(
`Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`,
{
description: fieldValidationResult.errorMessage,
runIndex,
itemIndex,
nodeCause: node.name,
},
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
value[key] = fieldValidationResult.newValue;
}
}
}
return validationResult;
};
export const validateValueAgainstSchema = (
node: INode,
nodeType: INodeType,
parameterValue: string | number | boolean | object | null | undefined,
parameterName: string,
runIndex: number,
itemIndex: number,
) => {
const parameterPath = parameterName.split('.');
const propertyDescription = nodeType.description.properties.find(
(prop) =>
parameterPath[0] === prop.name && NodeHelpers.displayParameter(node.parameters, prop, node),
);
if (!propertyDescription) {
return parameterValue;
}
let validationResult: ExtendedValidationResult = { valid: true, newValue: parameterValue };
if (
parameterPath.length === 1 &&
propertyDescription.validateType &&
!propertyDescription.ignoreValidationDuringExecution
) {
validationResult = validateFieldType(
parameterName,
parameterValue,
propertyDescription.validateType,
);
} else if (
propertyDescription.type === 'resourceMapper' &&
parameterPath[1] === 'value' &&
typeof parameterValue === 'object'
) {
validationResult = validateResourceMapperValue(
parameterName,
parameterValue as { [key: string]: unknown },
node,
propertyDescription.typeOptions?.resourceMapper?.mode !== 'add',
);
} else if (['fixedCollection', 'collection'].includes(propertyDescription.type)) {
validationResult = validateCollection(
node,
runIndex,
itemIndex,
propertyDescription,
parameterPath,
validationResult,
);
}
if (!validationResult.valid) {
throw new ExpressionError(
`Invalid input for '${
validationResult.fieldName
? String(validationResult.fieldName)
: propertyDescription.displayName
}' [item ${itemIndex}]`,
{
description: validationResult.errorMessage,
runIndex,
itemIndex,
nodeCause: node.name,
},
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return validationResult.newValue;
};
export function ensureType(
toType: EnsureTypeOptions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parameterValue: any,
parameterName: string,
errorOptions?: { itemIndex?: number; runIndex?: number; nodeCause?: string },
): string | number | boolean | object {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let returnData = parameterValue;
if (returnData === null) {
throw new ExpressionError(`Parameter '${parameterName}' must not be null`, errorOptions);
}
if (returnData === undefined) {
throw new ExpressionError(
`Parameter '${parameterName}' could not be 'undefined'`,
errorOptions,
);
}
if (['object', 'array', 'json'].includes(toType)) {
if (typeof returnData !== 'object') {
// if value is not an object and is string try to parse it, else throw an error
if (typeof returnData === 'string' && returnData.length) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const parsedValue = JSON.parse(returnData);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
returnData = parsedValue;
} catch (error) {
throw new ExpressionError(`Parameter '${parameterName}' could not be parsed`, {
...errorOptions,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
description: error.message,
});
}
} else {
throw new ExpressionError(
`Parameter '${parameterName}' must be an ${toType}, but we got '${String(parameterValue)}'`,
errorOptions,
);
}
} else if (toType === 'json') {
// value is an object, make sure it is valid JSON
try {
JSON.stringify(returnData);
} catch (error) {
throw new ExpressionError(`Parameter '${parameterName}' is not valid JSON`, {
...errorOptions,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
description: error.message,
});
}
}
if (toType === 'array' && !Array.isArray(returnData)) {
// value is not an array, but has to be
throw new ExpressionError(
`Parameter '${parameterName}' must be an array, but we got object`,
errorOptions,
);
}
}
try {
if (toType === 'string') {
if (typeof returnData === 'object') {
returnData = JSON.stringify(returnData);
} else {
returnData = String(returnData);
}
}
if (toType === 'number') {
returnData = Number(returnData);
if (Number.isNaN(returnData)) {
throw new ExpressionError(
`Parameter '${parameterName}' must be a number, but we got '${parameterValue}'`,
errorOptions,
);
}
}
if (toType === 'boolean') {
returnData = Boolean(returnData);
}
} catch (error) {
if (error instanceof ExpressionError) throw error;
throw new ExpressionError(`Parameter '${parameterName}' could not be converted to ${toType}`, {
...errorOptions,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
description: error.message,
});
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnData;
}
/** Returns the additional keys for Expressions and Function-Nodes */
export function getAdditionalKeys(
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
runExecutionData: IRunExecutionData | null,
options?: { secretsEnabled?: boolean },
): IWorkflowDataProxyAdditionalKeys {
const executionId = additionalData.executionId ?? PLACEHOLDER_EMPTY_EXECUTION_ID;
const resumeUrl = `${additionalData.webhookWaitingBaseUrl}/${executionId}`;
const resumeFormUrl = `${additionalData.formWaitingBaseUrl}/${executionId}`;
return {
$execution: {
id: executionId,
mode: mode === 'manual' ? 'test' : 'production',
resumeUrl,
resumeFormUrl,
customData: runExecutionData
? {
set(key: string, value: string): void {
try {
setWorkflowExecutionMetadata(runExecutionData, key, value);
} catch (e) {
if (mode === 'manual') {
throw e;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
LoggerProxy.debug(e.message);
}
},
setAll(obj: Record<string, string>): void {
try {
setAllWorkflowExecutionMetadata(runExecutionData, obj);
} catch (e) {
if (mode === 'manual') {
throw e;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
LoggerProxy.debug(e.message);
}
},
get(key: string): string {
return getWorkflowExecutionMetadata(runExecutionData, key);
},
getAll(): Record<string, string> {
return getAllWorkflowExecutionMetadata(runExecutionData);
},
}
: undefined,
},
$vars: additionalData.variables,
$secrets: options?.secretsEnabled ? getSecretsProxy(additionalData) : undefined,
// deprecated
$executionId: executionId,
$resumeWebhookUrl: resumeUrl,
};
}

View file

@ -4,7 +4,6 @@ import type {
ICredentialDataDecryptedObject,
IDataObject,
IExecuteData,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
IRunExecutionData,
@ -13,7 +12,6 @@ import type {
IWebhookFunctions,
IWorkflowExecuteAdditionalData,
NodeConnectionType,
NodeParameterValueType,
WebhookType,
Workflow,
WorkflowExecuteMode,
@ -23,11 +21,8 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
copyBinaryFile,
getAdditionalKeys,
getBinaryHelperFunctions,
getCredentials,
getInputConnectionData,
getNodeParameter,
getNodeWebhookUrl,
getRequestHelperFunctions,
returnJsonArray,
@ -47,9 +42,28 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
mode: WorkflowExecuteMode,
private readonly webhookData: IWebhookData,
private readonly closeFunctions: CloseFunction[],
private readonly runExecutionData: IRunExecutionData | null,
runExecutionData: IRunExecutionData | null,
) {
super(workflow, node, additionalData, mode);
let connectionInputData: INodeExecutionData[] = [];
let executionData: IExecuteData | undefined;
if (runExecutionData?.executionData !== undefined) {
executionData = runExecutionData.executionData.nodeExecutionStack[0];
if (executionData !== undefined) {
connectionInputData = executionData.data.main[0]!;
}
}
super(
workflow,
node,
additionalData,
mode,
runExecutionData,
0,
connectionInputData,
executionData,
);
this.helpers = {
createDeferredPromise,
@ -71,7 +85,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
}
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
return await this._getCredentials<T>(type);
}
getBodyData() {
@ -116,7 +130,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
this.node,
this.additionalData,
this.mode,
getAdditionalKeys(this.additionalData, this.mode, null),
this.additionalKeys,
);
}
@ -144,13 +158,12 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
node: this.node,
source: null,
};
const runIndex = 0;
return await getInputConnectionData.call(
this,
this.workflow,
runExecutionData,
runIndex,
this.runIndex,
connectionInputData,
{} as ITaskDataConnections,
this.additionalData,
@ -161,73 +174,4 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
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,
);
}
}

View file

@ -2,8 +2,6 @@ import { mkdtempSync, readFileSync } from 'fs';
import type { IncomingMessage } from 'http';
import type { Agent } from 'https';
import { mock } from 'jest-mock-extended';
import toPlainObject from 'lodash/toPlainObject';
import { DateTime } from 'luxon';
import type {
IBinaryData,
IHttpRequestMethods,
@ -12,11 +10,9 @@ import type {
IRequestOptions,
ITaskDataConnections,
IWorkflowExecuteAdditionalData,
NodeParameterValue,
Workflow,
WorkflowHooks,
} from 'n8n-workflow';
import { ExpressionError } from 'n8n-workflow';
import nock from 'nock';
import { tmpdir } from 'os';
import { join } from 'path';
@ -26,9 +22,7 @@ import Container from 'typedi';
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
import { InstanceSettings } from '@/InstanceSettings';
import {
cleanupParameterData,
copyInputItems,
ensureType,
getBinaryDataBuffer,
isFilePathBlocked,
parseIncomingMessage,
@ -470,39 +464,6 @@ describe('NodeExecuteFunctions', () => {
});
});
describe('cleanupParameterData', () => {
it('should stringify Luxon dates in-place', () => {
const input = { x: 1, y: DateTime.now() as unknown as NodeParameterValue };
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('string');
});
it('should stringify plain Luxon dates in-place', () => {
const input = {
x: 1,
y: toPlainObject(DateTime.now()),
};
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('string');
});
it('should handle objects with nameless constructors', () => {
const input = { x: 1, y: { constructor: {} } as NodeParameterValue };
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('object');
});
it('should handle objects without a constructor', () => {
const input = { x: 1, y: { constructor: undefined } as unknown as NodeParameterValue };
expect(typeof input.y).toBe('object');
cleanupParameterData(input);
expect(typeof input.y).toBe('object');
});
});
describe('copyInputItems', () => {
it('should pick only selected properties', () => {
const output = copyInputItems(
@ -588,83 +549,6 @@ describe('NodeExecuteFunctions', () => {
},
);
});
describe('ensureType', () => {
it('throws error for null value', () => {
expect(() => ensureType('string', null, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' must not be null"),
);
});
it('throws error for undefined value', () => {
expect(() => ensureType('string', undefined, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' could not be 'undefined'"),
);
});
it('returns string value without modification', () => {
const value = 'hello';
const expectedValue = value;
const result = ensureType('string', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('returns number value without modification', () => {
const value = 42;
const expectedValue = value;
const result = ensureType('number', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('returns boolean value without modification', () => {
const value = true;
const expectedValue = value;
const result = ensureType('boolean', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('converts object to string if toType is string', () => {
const value = { name: 'John' };
const expectedValue = JSON.stringify(value);
const result = ensureType('string', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('converts string to number if toType is number', () => {
const value = '10';
const expectedValue = 10;
const result = ensureType('number', value, 'myParam');
expect(result).toBe(expectedValue);
});
it('throws error for invalid conversion to number', () => {
const value = 'invalid';
expect(() => ensureType('number', value, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"),
);
});
it('parses valid JSON string to object if toType is object', () => {
const value = '{"name": "Alice"}';
const expectedValue = JSON.parse(value);
const result = ensureType('object', value, 'myParam');
expect(result).toEqual(expectedValue);
});
it('throws error for invalid JSON string to object conversion', () => {
const value = 'invalid_json';
expect(() => ensureType('object', value, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' could not be parsed"),
);
});
it('throws error for non-array value if toType is array', () => {
const value = { name: 'Alice' };
expect(() => ensureType('array', value, 'myParam')).toThrowError(
new ExpressionError("Parameter 'myParam' must be an array, but we got object"),
);
});
});
});
describe('isFilePathBlocked', () => {