mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-31 15:37:26 -08:00
refactor(core): Extract ExecuteContext out of NodeExecutionFunctions (no-changelog) (#11853)
This commit is contained in:
parent
3aa72f613f
commit
75e2b6fd9e
|
@ -4,7 +4,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
|
import { Credentials, getAdditionalKeys } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
ICredentialsExpressionResolveValues,
|
ICredentialsExpressionResolveValues,
|
||||||
|
@ -379,7 +379,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
|
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalKeys = NodeExecuteFunctions.getAdditionalKeys(additionalData, mode, null, {
|
const additionalKeys = getAdditionalKeys(additionalData, mode, null, {
|
||||||
secretsEnabled: canUseSecrets,
|
secretsEnabled: canUseSecrets,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,15 +12,11 @@ import type {
|
||||||
Expression,
|
Expression,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
OnError,
|
|
||||||
ContextType,
|
|
||||||
IContextObject,
|
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
ISourceData,
|
|
||||||
ITaskMetadata,
|
|
||||||
} from 'n8n-workflow';
|
} 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';
|
import { ExecuteSingleContext } from '../execute-single-context';
|
||||||
|
|
||||||
describe('ExecuteSingleContext', () => {
|
describe('ExecuteSingleContext', () => {
|
||||||
|
@ -45,6 +41,7 @@ describe('ExecuteSingleContext', () => {
|
||||||
const expression = mock<Expression>();
|
const expression = mock<Expression>();
|
||||||
const workflow = mock<Workflow>({ expression, nodeTypes });
|
const workflow = mock<Workflow>({ expression, nodeTypes });
|
||||||
const node = mock<INode>({
|
const node = mock<INode>({
|
||||||
|
name: 'Test Node',
|
||||||
credentials: {
|
credentials: {
|
||||||
[testCredentialType]: {
|
[testCredentialType]: {
|
||||||
id: 'testCredentialId',
|
id: 'testCredentialId',
|
||||||
|
@ -58,7 +55,7 @@ describe('ExecuteSingleContext', () => {
|
||||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
|
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
|
||||||
const mode: WorkflowExecuteMode = 'manual';
|
const mode: WorkflowExecuteMode = 'manual';
|
||||||
const runExecutionData = mock<IRunExecutionData>();
|
const runExecutionData = mock<IRunExecutionData>();
|
||||||
const connectionInputData = mock<INodeExecutionData[]>();
|
const connectionInputData: INodeExecutionData[] = [];
|
||||||
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
|
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
|
||||||
const executeData = mock<IExecuteData>();
|
const executeData = mock<IExecuteData>();
|
||||||
const runIndex = 0;
|
const runIndex = 0;
|
||||||
|
@ -84,81 +81,12 @@ describe('ExecuteSingleContext', () => {
|
||||||
expression.getParameterValue.mockImplementation((value) => value);
|
expression.getParameterValue.mockImplementation((value) => value);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getExecutionCancelSignal', () => {
|
describeCommonTests(executeSingleContext, {
|
||||||
it('should return the abort signal', () => {
|
abortSignal,
|
||||||
expect(executeSingleContext.getExecutionCancelSignal()).toBe(abortSignal);
|
node,
|
||||||
});
|
workflow,
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
executeData,
|
||||||
);
|
runExecutionData,
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getInputData', () => {
|
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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { NodeExecutionContext } from '../node-execution-context';
|
||||||
|
|
||||||
class TestContext extends NodeExecutionContext {}
|
class TestContext extends NodeExecutionContext {}
|
||||||
|
|
||||||
describe('BaseContext', () => {
|
describe('NodeExecutionContext', () => {
|
||||||
const instanceSettings = mock<InstanceSettings>({ instanceId: 'abc123' });
|
const instanceSettings = mock<InstanceSettings>({ instanceId: 'abc123' });
|
||||||
Container.set(InstanceSettings, instanceSettings);
|
Container.set(InstanceSettings, instanceSettings);
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -12,11 +12,11 @@ import type {
|
||||||
Expression,
|
Expression,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
OnError,
|
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ApplicationError } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { describeCommonTests } from './shared-tests';
|
||||||
import { SupplyDataContext } from '../supply-data-context';
|
import { SupplyDataContext } from '../supply-data-context';
|
||||||
|
|
||||||
describe('SupplyDataContext', () => {
|
describe('SupplyDataContext', () => {
|
||||||
|
@ -41,6 +41,7 @@ describe('SupplyDataContext', () => {
|
||||||
const expression = mock<Expression>();
|
const expression = mock<Expression>();
|
||||||
const workflow = mock<Workflow>({ expression, nodeTypes });
|
const workflow = mock<Workflow>({ expression, nodeTypes });
|
||||||
const node = mock<INode>({
|
const node = mock<INode>({
|
||||||
|
name: 'Test Node',
|
||||||
credentials: {
|
credentials: {
|
||||||
[testCredentialType]: {
|
[testCredentialType]: {
|
||||||
id: 'testCredentialId',
|
id: 'testCredentialId',
|
||||||
|
@ -54,7 +55,7 @@ describe('SupplyDataContext', () => {
|
||||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
|
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
|
||||||
const mode: WorkflowExecuteMode = 'manual';
|
const mode: WorkflowExecuteMode = 'manual';
|
||||||
const runExecutionData = mock<IRunExecutionData>();
|
const runExecutionData = mock<IRunExecutionData>();
|
||||||
const connectionInputData = mock<INodeExecutionData[]>();
|
const connectionInputData: INodeExecutionData[] = [];
|
||||||
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
|
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
|
||||||
const executeData = mock<IExecuteData>();
|
const executeData = mock<IExecuteData>();
|
||||||
const runIndex = 0;
|
const runIndex = 0;
|
||||||
|
@ -80,64 +81,12 @@ describe('SupplyDataContext', () => {
|
||||||
expression.getParameterValue.mockImplementation((value) => value);
|
expression.getParameterValue.mockImplementation((value) => value);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getExecutionCancelSignal', () => {
|
describeCommonTests(supplyDataContext, {
|
||||||
it('should return the abort signal', () => {
|
abortSignal,
|
||||||
expect(supplyDataContext.getExecutionCancelSignal()).toBe(abortSignal);
|
node,
|
||||||
});
|
workflow,
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
executeData,
|
||||||
);
|
runExecutionData,
|
||||||
|
|
||||||
resolveSimpleParameterValueSpy.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getInputData', () => {
|
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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', () => {
|
test('should validate fixedCollection values parameter', () => {
|
||||||
const nodeType = {
|
const nodeType = {
|
||||||
description: {
|
description: {
|
213
packages/core/src/node-execution-context/base-execute-context.ts
Normal file
213
packages/core/src/node-execution-context/base-execute-context.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
263
packages/core/src/node-execution-context/execute-context.ts
Normal file
263
packages/core/src/node-execution-context/execute-context.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,34 +10,21 @@ import type {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
ITaskDataConnections,
|
ITaskDataConnections,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
ContextType,
|
|
||||||
AiEvent,
|
|
||||||
ISourceData,
|
|
||||||
ITaskMetadata,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import {
|
|
||||||
ApplicationError,
|
|
||||||
createDeferredPromise,
|
|
||||||
NodeHelpers,
|
|
||||||
WorkflowDataProxy,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
assertBinaryData,
|
assertBinaryData,
|
||||||
continueOnFail,
|
|
||||||
getAdditionalKeys,
|
|
||||||
getBinaryDataBuffer,
|
getBinaryDataBuffer,
|
||||||
getBinaryHelperFunctions,
|
getBinaryHelperFunctions,
|
||||||
getCredentials,
|
|
||||||
getNodeParameter,
|
|
||||||
getRequestHelperFunctions,
|
getRequestHelperFunctions,
|
||||||
returnJsonArray,
|
returnJsonArray,
|
||||||
} from '@/NodeExecuteFunctions';
|
} 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'];
|
readonly helpers: IExecuteSingleFunctions['helpers'];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -45,15 +32,26 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
|
||||||
node: INode,
|
node: INode,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
private readonly runExecutionData: IRunExecutionData,
|
runExecutionData: IRunExecutionData,
|
||||||
private readonly runIndex: number,
|
runIndex: number,
|
||||||
private readonly connectionInputData: INodeExecutionData[],
|
connectionInputData: INodeExecutionData[],
|
||||||
private readonly inputData: ITaskDataConnections,
|
inputData: ITaskDataConnections,
|
||||||
private readonly itemIndex: number,
|
private readonly itemIndex: number,
|
||||||
private readonly executeData: IExecuteData,
|
executeData: IExecuteData,
|
||||||
private readonly abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
) {
|
) {
|
||||||
super(workflow, node, additionalData, mode);
|
super(
|
||||||
|
workflow,
|
||||||
|
node,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
runExecutionData,
|
||||||
|
runIndex,
|
||||||
|
connectionInputData,
|
||||||
|
inputData,
|
||||||
|
executeData,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
this.helpers = {
|
this.helpers = {
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
|
@ -74,47 +72,8 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getExecutionCancelSignal() {
|
evaluateExpression(expression: string, itemIndex: number = this.itemIndex) {
|
||||||
return this.abortSignal;
|
return super.evaluateExpression(expression, itemIndex);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputData(inputIndex = 0, inputName = 'main') {
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getNodeParameter(parameterName: string, fallbackValue?: any, options?: IGetNodeParameterOptions) {
|
getNodeParameter(parameterName: string, fallbackValue?: any, options?: IGetNodeParameterOptions) {
|
||||||
return getNodeParameter(
|
return this._getNodeParameter(parameterName, this.itemIndex, fallbackValue, options);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: extract out in a BaseExecutionContext
|
|
||||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||||
return await getCredentials<T>(
|
return await super.getCredentials<T>(type, this.itemIndex);
|
||||||
this.workflow,
|
|
||||||
this.node,
|
|
||||||
type,
|
|
||||||
this.additionalData,
|
|
||||||
this.mode,
|
|
||||||
this.executeData,
|
|
||||||
this.runExecutionData,
|
|
||||||
this.runIndex,
|
|
||||||
this.connectionInputData,
|
|
||||||
this.itemIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getExecuteData() {
|
|
||||||
return this.executeData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getWorkflowDataProxy() {
|
getWorkflowDataProxy() {
|
||||||
return new WorkflowDataProxy(
|
return super.getWorkflowDataProxy(this.itemIndex);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
IGetNodeParameterOptions,
|
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
|
||||||
IHookFunctions,
|
IHookFunctions,
|
||||||
IRunExecutionData,
|
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValueType,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -17,9 +13,6 @@ import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
getAdditionalKeys,
|
|
||||||
getCredentials,
|
|
||||||
getNodeParameter,
|
|
||||||
getNodeWebhookUrl,
|
getNodeWebhookUrl,
|
||||||
getRequestHelperFunctions,
|
getRequestHelperFunctions,
|
||||||
getWebhookDescription,
|
getWebhookDescription,
|
||||||
|
@ -48,34 +41,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
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);
|
||||||
}
|
|
||||||
|
|
||||||
getNodeParameter(
|
|
||||||
parameterName: string,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
fallbackValue?: any,
|
|
||||||
options?: IGetNodeParameterOptions,
|
|
||||||
): NodeParameterValueType | object {
|
|
||||||
const runExecutionData: IRunExecutionData | null = null;
|
|
||||||
const itemIndex = 0;
|
|
||||||
const runIndex = 0;
|
|
||||||
const connectionInputData: INodeExecutionData[] = [];
|
|
||||||
|
|
||||||
return getNodeParameter(
|
|
||||||
this.workflow,
|
|
||||||
runExecutionData,
|
|
||||||
runIndex,
|
|
||||||
connectionInputData,
|
|
||||||
this.node,
|
|
||||||
parameterName,
|
|
||||||
itemIndex,
|
|
||||||
this.mode,
|
|
||||||
getAdditionalKeys(this.additionalData, this.mode, runExecutionData),
|
|
||||||
undefined,
|
|
||||||
fallbackValue,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeWebhookUrl(name: WebhookType): string | undefined {
|
getNodeWebhookUrl(name: WebhookType): string | undefined {
|
||||||
|
@ -85,7 +51,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
|
||||||
this.node,
|
this.node,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
this.mode,
|
this.mode,
|
||||||
getAdditionalKeys(this.additionalData, this.mode, null),
|
this.additionalKeys,
|
||||||
this.webhookData?.isTest,
|
this.webhookData?.isTest,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
export { ExecuteContext } from './execute-context';
|
||||||
export { ExecuteSingleContext } from './execute-single-context';
|
export { ExecuteSingleContext } from './execute-single-context';
|
||||||
export { HookContext } from './hook-context';
|
export { HookContext } from './hook-context';
|
||||||
export { LoadOptionsContext } from './load-options-context';
|
export { LoadOptionsContext } from './load-options-context';
|
||||||
|
@ -6,3 +7,5 @@ export { PollContext } from './poll-context';
|
||||||
export { SupplyDataContext } from './supply-data-context';
|
export { SupplyDataContext } from './supply-data-context';
|
||||||
export { TriggerContext } from './trigger-context';
|
export { TriggerContext } from './trigger-context';
|
||||||
export { WebhookContext } from './webhook-context';
|
export { WebhookContext } from './webhook-context';
|
||||||
|
|
||||||
|
export { getAdditionalKeys } from './utils';
|
||||||
|
|
|
@ -3,9 +3,7 @@ import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
IGetNodeParameterOptions,
|
IGetNodeParameterOptions,
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
IRunExecutionData,
|
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
Workflow,
|
Workflow,
|
||||||
|
@ -13,13 +11,7 @@ import type {
|
||||||
|
|
||||||
import { extractValue } from '@/ExtractValue';
|
import { extractValue } from '@/ExtractValue';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import { getRequestHelperFunctions, getSSHTunnelFunctions } from '@/NodeExecuteFunctions';
|
||||||
getAdditionalKeys,
|
|
||||||
getCredentials,
|
|
||||||
getNodeParameter,
|
|
||||||
getRequestHelperFunctions,
|
|
||||||
getSSHTunnelFunctions,
|
|
||||||
} from '@/NodeExecuteFunctions';
|
|
||||||
|
|
||||||
import { NodeExecutionContext } from './node-execution-context';
|
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) {
|
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(
|
getCurrentNodeParameter(
|
||||||
|
@ -76,31 +68,4 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
|
||||||
getCurrentNodeParameters() {
|
getCurrentNodeParameters() {
|
||||||
return this.additionalData.currentNodeParameters;
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,42 @@
|
||||||
|
import { get } from 'lodash';
|
||||||
import type {
|
import type {
|
||||||
FunctionsBase,
|
FunctionsBase,
|
||||||
|
ICredentialDataDecryptedObject,
|
||||||
|
ICredentialsExpressionResolveValues,
|
||||||
|
IExecuteData,
|
||||||
|
IGetNodeParameterOptions,
|
||||||
INode,
|
INode,
|
||||||
|
INodeCredentialDescription,
|
||||||
|
INodeCredentialsDetails,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
IRunExecutionData,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
|
NodeParameterValueType,
|
||||||
NodeTypeAndVersion,
|
NodeTypeAndVersion,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { deepCopy, LoggerProxy } from 'n8n-workflow';
|
import {
|
||||||
|
ApplicationError,
|
||||||
|
deepCopy,
|
||||||
|
ExpressionError,
|
||||||
|
LoggerProxy,
|
||||||
|
NodeHelpers,
|
||||||
|
NodeOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { Container } from 'typedi';
|
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 { InstanceSettings } from '@/InstanceSettings';
|
||||||
|
|
||||||
|
import {
|
||||||
|
cleanupParameterData,
|
||||||
|
ensureType,
|
||||||
|
getAdditionalKeys,
|
||||||
|
validateValueAgainstSchema,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCredentials'> {
|
export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCredentials'> {
|
||||||
protected readonly instanceSettings = Container.get(InstanceSettings);
|
protected readonly instanceSettings = Container.get(InstanceSettings);
|
||||||
|
|
||||||
|
@ -20,6 +45,10 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
||||||
protected readonly node: INode,
|
protected readonly node: INode,
|
||||||
protected readonly additionalData: IWorkflowExecuteAdditionalData,
|
protected readonly additionalData: IWorkflowExecuteAdditionalData,
|
||||||
protected readonly mode: WorkflowExecuteMode,
|
protected readonly mode: WorkflowExecuteMode,
|
||||||
|
protected readonly runExecutionData: IRunExecutionData | null = null,
|
||||||
|
protected readonly runIndex = 0,
|
||||||
|
protected readonly connectionInputData: INodeExecutionData[] = [],
|
||||||
|
protected readonly executeData?: IExecuteData,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get logger() {
|
get logger() {
|
||||||
|
@ -101,6 +130,255 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
||||||
return this.additionalData.credentialsHelper.getCredentialsProperties(type);
|
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[]) {
|
async prepareOutputData(outputData: INodeExecutionData[]) {
|
||||||
return [outputData];
|
return [outputData];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
IGetNodeParameterOptions,
|
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
|
||||||
IPollFunctions,
|
IPollFunctions,
|
||||||
IRunExecutionData,
|
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValueType,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -15,10 +11,7 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
getAdditionalKeys,
|
|
||||||
getBinaryHelperFunctions,
|
getBinaryHelperFunctions,
|
||||||
getCredentials,
|
|
||||||
getNodeParameter,
|
|
||||||
getRequestHelperFunctions,
|
getRequestHelperFunctions,
|
||||||
getSchedulingFunctions,
|
getSchedulingFunctions,
|
||||||
returnJsonArray,
|
returnJsonArray,
|
||||||
|
@ -62,33 +55,6 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
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);
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
AiEvent,
|
|
||||||
CallbackManager,
|
|
||||||
CloseFunction,
|
CloseFunction,
|
||||||
ExecuteWorkflowData,
|
|
||||||
ICredentialDataDecryptedObject,
|
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IExecuteWorkflowInfo,
|
|
||||||
IGetNodeParameterOptions,
|
IGetNodeParameterOptions,
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
@ -15,32 +10,20 @@ import type {
|
||||||
ITaskMetadata,
|
ITaskMetadata,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
RelatedExecution,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||||
ApplicationError,
|
|
||||||
createDeferredPromise,
|
|
||||||
NodeHelpers,
|
|
||||||
WorkflowDataProxy,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import Container from 'typedi';
|
|
||||||
|
|
||||||
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
assertBinaryData,
|
assertBinaryData,
|
||||||
continueOnFail,
|
|
||||||
constructExecutionMetaData,
|
constructExecutionMetaData,
|
||||||
copyInputItems,
|
copyInputItems,
|
||||||
getAdditionalKeys,
|
|
||||||
getBinaryDataBuffer,
|
getBinaryDataBuffer,
|
||||||
getBinaryHelperFunctions,
|
getBinaryHelperFunctions,
|
||||||
getCheckProcessedHelperFunctions,
|
getCheckProcessedHelperFunctions,
|
||||||
getCredentials,
|
|
||||||
getFileSystemHelperFunctions,
|
getFileSystemHelperFunctions,
|
||||||
getNodeParameter,
|
|
||||||
getRequestHelperFunctions,
|
getRequestHelperFunctions,
|
||||||
getSSHTunnelFunctions,
|
getSSHTunnelFunctions,
|
||||||
normalizeItems,
|
normalizeItems,
|
||||||
|
@ -49,9 +32,9 @@ import {
|
||||||
addExecutionDataFunctions,
|
addExecutionDataFunctions,
|
||||||
} from '@/NodeExecuteFunctions';
|
} 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 helpers: ISupplyDataFunctions['helpers'];
|
||||||
|
|
||||||
readonly getNodeParameter: ISupplyDataFunctions['getNodeParameter'];
|
readonly getNodeParameter: ISupplyDataFunctions['getNodeParameter'];
|
||||||
|
@ -61,15 +44,26 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
||||||
node: INode,
|
node: INode,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
private readonly runExecutionData: IRunExecutionData,
|
runExecutionData: IRunExecutionData,
|
||||||
private readonly runIndex: number,
|
runIndex: number,
|
||||||
private readonly connectionInputData: INodeExecutionData[],
|
connectionInputData: INodeExecutionData[],
|
||||||
private readonly inputData: ITaskDataConnections,
|
inputData: ITaskDataConnections,
|
||||||
private readonly executeData: IExecuteData,
|
executeData: IExecuteData,
|
||||||
private readonly closeFunctions: CloseFunction[],
|
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 = {
|
this.helpers = {
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
|
@ -102,116 +96,14 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
||||||
fallbackValue?: any,
|
fallbackValue?: any,
|
||||||
options?: IGetNodeParameterOptions,
|
options?: IGetNodeParameterOptions,
|
||||||
) =>
|
) =>
|
||||||
getNodeParameter(
|
this._getNodeParameter(
|
||||||
this.workflow,
|
|
||||||
this.runExecutionData,
|
|
||||||
this.runIndex,
|
|
||||||
this.connectionInputData,
|
|
||||||
this.node,
|
|
||||||
parameterName,
|
parameterName,
|
||||||
itemIndex,
|
itemIndex,
|
||||||
this.mode,
|
|
||||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
|
||||||
this.executeData,
|
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
)) as ISupplyDataFunctions['getNodeParameter'];
|
)) 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> {
|
async getInputConnectionData(inputName: NodeConnectionType, itemIndex: number): Promise<unknown> {
|
||||||
return await getInputConnectionData.call(
|
return await getInputConnectionData.call(
|
||||||
this,
|
this,
|
||||||
|
@ -252,69 +144,11 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
||||||
return this.inputData[inputName][inputIndex];
|
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(
|
addInputData(
|
||||||
connectionType: NodeConnectionType,
|
connectionType: NodeConnectionType,
|
||||||
data: INodeExecutionData[][],
|
data: INodeExecutionData[][],
|
||||||
): { index: number } {
|
): { index: number } {
|
||||||
const nodeName = this.getNode().name;
|
const nodeName = this.node.name;
|
||||||
let currentNodeRunIndex = 0;
|
let currentNodeRunIndex = 0;
|
||||||
if (this.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
if (this.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
||||||
currentNodeRunIndex = this.runExecutionData.resultData.runData[nodeName].length;
|
currentNodeRunIndex = this.runExecutionData.resultData.runData[nodeName].length;
|
||||||
|
@ -322,17 +156,17 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
||||||
|
|
||||||
addExecutionDataFunctions(
|
addExecutionDataFunctions(
|
||||||
'input',
|
'input',
|
||||||
this.node.name,
|
nodeName,
|
||||||
data,
|
data,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
connectionType,
|
connectionType,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
this.node.name,
|
nodeName,
|
||||||
this.runIndex,
|
this.runIndex,
|
||||||
currentNodeRunIndex,
|
currentNodeRunIndex,
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
this.logger.warn(
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
error.message
|
error.message
|
||||||
}`,
|
}`,
|
||||||
|
@ -348,20 +182,21 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
||||||
data: INodeExecutionData[][],
|
data: INodeExecutionData[][],
|
||||||
metadata?: ITaskMetadata,
|
metadata?: ITaskMetadata,
|
||||||
): void {
|
): void {
|
||||||
|
const nodeName = this.node.name;
|
||||||
addExecutionDataFunctions(
|
addExecutionDataFunctions(
|
||||||
'output',
|
'output',
|
||||||
this.node.name,
|
nodeName,
|
||||||
data,
|
data,
|
||||||
this.runExecutionData,
|
this.runExecutionData,
|
||||||
connectionType,
|
connectionType,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
this.node.name,
|
nodeName,
|
||||||
this.runIndex,
|
this.runIndex,
|
||||||
currentNodeRunIndex,
|
currentNodeRunIndex,
|
||||||
metadata,
|
metadata,
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
this.logger.warn(
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
error.message
|
error.message
|
||||||
}`,
|
}`,
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
IGetNodeParameterOptions,
|
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
|
||||||
IRunExecutionData,
|
|
||||||
ITriggerFunctions,
|
ITriggerFunctions,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValueType,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -15,10 +11,7 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
getAdditionalKeys,
|
|
||||||
getBinaryHelperFunctions,
|
getBinaryHelperFunctions,
|
||||||
getCredentials,
|
|
||||||
getNodeParameter,
|
|
||||||
getRequestHelperFunctions,
|
getRequestHelperFunctions,
|
||||||
getSchedulingFunctions,
|
getSchedulingFunctions,
|
||||||
getSSHTunnelFunctions,
|
getSSHTunnelFunctions,
|
||||||
|
@ -64,33 +57,6 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
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);
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
423
packages/core/src/node-execution-context/utils.ts
Normal file
423
packages/core/src/node-execution-context/utils.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IGetNodeParameterOptions,
|
|
||||||
INode,
|
INode,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
|
@ -13,7 +12,6 @@ import type {
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
NodeParameterValueType,
|
|
||||||
WebhookType,
|
WebhookType,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -23,11 +21,8 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
copyBinaryFile,
|
copyBinaryFile,
|
||||||
getAdditionalKeys,
|
|
||||||
getBinaryHelperFunctions,
|
getBinaryHelperFunctions,
|
||||||
getCredentials,
|
|
||||||
getInputConnectionData,
|
getInputConnectionData,
|
||||||
getNodeParameter,
|
|
||||||
getNodeWebhookUrl,
|
getNodeWebhookUrl,
|
||||||
getRequestHelperFunctions,
|
getRequestHelperFunctions,
|
||||||
returnJsonArray,
|
returnJsonArray,
|
||||||
|
@ -47,9 +42,28 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
private readonly webhookData: IWebhookData,
|
private readonly webhookData: IWebhookData,
|
||||||
private readonly closeFunctions: CloseFunction[],
|
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 = {
|
this.helpers = {
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
|
@ -71,7 +85,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
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() {
|
getBodyData() {
|
||||||
|
@ -116,7 +130,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
||||||
this.node,
|
this.node,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
this.mode,
|
this.mode,
|
||||||
getAdditionalKeys(this.additionalData, this.mode, null),
|
this.additionalKeys,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,13 +158,12 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
||||||
node: this.node,
|
node: this.node,
|
||||||
source: null,
|
source: null,
|
||||||
};
|
};
|
||||||
const runIndex = 0;
|
|
||||||
|
|
||||||
return await getInputConnectionData.call(
|
return await getInputConnectionData.call(
|
||||||
this,
|
this,
|
||||||
this.workflow,
|
this.workflow,
|
||||||
runExecutionData,
|
runExecutionData,
|
||||||
runIndex,
|
this.runIndex,
|
||||||
connectionInputData,
|
connectionInputData,
|
||||||
{} as ITaskDataConnections,
|
{} as ITaskDataConnections,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
|
@ -161,73 +174,4 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
||||||
itemIndex,
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { mkdtempSync, readFileSync } from 'fs';
|
||||||
import type { IncomingMessage } from 'http';
|
import type { IncomingMessage } from 'http';
|
||||||
import type { Agent } from 'https';
|
import type { Agent } from 'https';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import toPlainObject from 'lodash/toPlainObject';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import type {
|
import type {
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
IHttpRequestMethods,
|
IHttpRequestMethods,
|
||||||
|
@ -12,11 +10,9 @@ import type {
|
||||||
IRequestOptions,
|
IRequestOptions,
|
||||||
ITaskDataConnections,
|
ITaskDataConnections,
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValue,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ExpressionError } from 'n8n-workflow';
|
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
@ -26,9 +22,7 @@ import Container from 'typedi';
|
||||||
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
|
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
|
||||||
import { InstanceSettings } from '@/InstanceSettings';
|
import { InstanceSettings } from '@/InstanceSettings';
|
||||||
import {
|
import {
|
||||||
cleanupParameterData,
|
|
||||||
copyInputItems,
|
copyInputItems,
|
||||||
ensureType,
|
|
||||||
getBinaryDataBuffer,
|
getBinaryDataBuffer,
|
||||||
isFilePathBlocked,
|
isFilePathBlocked,
|
||||||
parseIncomingMessage,
|
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', () => {
|
describe('copyInputItems', () => {
|
||||||
it('should pick only selected properties', () => {
|
it('should pick only selected properties', () => {
|
||||||
const output = copyInputItems(
|
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', () => {
|
describe('isFilePathBlocked', () => {
|
||||||
|
|
Loading…
Reference in a new issue