mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 05:59:42 -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-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
|
||||
import { Credentials, getAdditionalKeys } from 'n8n-core';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsExpressionResolveValues,
|
||||
|
@ -379,7 +379,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
|
||||
}
|
||||
|
||||
const additionalKeys = NodeExecuteFunctions.getAdditionalKeys(additionalData, mode, null, {
|
||||
const additionalKeys = getAdditionalKeys(additionalData, mode, null, {
|
||||
secretsEnabled: canUseSecrets,
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||
INodeType,
|
||||
INodeTypes,
|
||||
OnError,
|
||||
ContextType,
|
||||
IContextObject,
|
||||
ICredentialDataDecryptedObject,
|
||||
ISourceData,
|
||||
ITaskMetadata,
|
||||
} from 'n8n-workflow';
|
||||
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
import { describeCommonTests } from './shared-tests';
|
||||
import { ExecuteSingleContext } from '../execute-single-context';
|
||||
|
||||
describe('ExecuteSingleContext', () => {
|
||||
|
@ -45,6 +41,7 @@ describe('ExecuteSingleContext', () => {
|
|||
const expression = mock<Expression>();
|
||||
const workflow = mock<Workflow>({ expression, nodeTypes });
|
||||
const node = mock<INode>({
|
||||
name: 'Test Node',
|
||||
credentials: {
|
||||
[testCredentialType]: {
|
||||
id: 'testCredentialId',
|
||||
|
@ -58,7 +55,7 @@ describe('ExecuteSingleContext', () => {
|
|||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
|
||||
const mode: WorkflowExecuteMode = 'manual';
|
||||
const runExecutionData = mock<IRunExecutionData>();
|
||||
const connectionInputData = mock<INodeExecutionData[]>();
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
|
||||
const executeData = mock<IExecuteData>();
|
||||
const runIndex = 0;
|
||||
|
@ -84,81 +81,12 @@ describe('ExecuteSingleContext', () => {
|
|||
expression.getParameterValue.mockImplementation((value) => value);
|
||||
});
|
||||
|
||||
describe('getExecutionCancelSignal', () => {
|
||||
it('should return the abort signal', () => {
|
||||
expect(executeSingleContext.getExecutionCancelSignal()).toBe(abortSignal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('continueOnFail', () => {
|
||||
afterEach(() => {
|
||||
node.onError = undefined;
|
||||
node.continueOnFail = false;
|
||||
});
|
||||
|
||||
it('should return false for nodes by default', () => {
|
||||
expect(executeSingleContext.continueOnFail()).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true if node has continueOnFail set to true', () => {
|
||||
node.continueOnFail = true;
|
||||
expect(executeSingleContext.continueOnFail()).toEqual(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['continueRegularOutput', true],
|
||||
['continueErrorOutput', true],
|
||||
['stopWorkflow', false],
|
||||
])('if node has onError set to %s, it should return %s', (onError, expected) => {
|
||||
node.onError = onError as OnError;
|
||||
expect(executeSingleContext.continueOnFail()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateExpression', () => {
|
||||
it('should evaluate the expression correctly', () => {
|
||||
const expression = '$json.test';
|
||||
const expectedResult = 'data';
|
||||
const resolveSimpleParameterValueSpy = jest.spyOn(
|
||||
workflow.expression,
|
||||
'resolveSimpleParameterValue',
|
||||
);
|
||||
resolveSimpleParameterValueSpy.mockReturnValue(expectedResult);
|
||||
|
||||
expect(executeSingleContext.evaluateExpression(expression, itemIndex)).toEqual(
|
||||
expectedResult,
|
||||
);
|
||||
|
||||
expect(resolveSimpleParameterValueSpy).toHaveBeenCalledWith(
|
||||
`=${expression}`,
|
||||
{},
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
node.name,
|
||||
connectionInputData,
|
||||
mode,
|
||||
expect.objectContaining({}),
|
||||
executeData,
|
||||
);
|
||||
|
||||
resolveSimpleParameterValueSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContext', () => {
|
||||
it('should return the context object', () => {
|
||||
const contextType: ContextType = 'node';
|
||||
const expectedContext = mock<IContextObject>();
|
||||
const getContextSpy = jest.spyOn(NodeHelpers, 'getContext');
|
||||
getContextSpy.mockReturnValue(expectedContext);
|
||||
|
||||
expect(executeSingleContext.getContext(contextType)).toEqual(expectedContext);
|
||||
|
||||
expect(getContextSpy).toHaveBeenCalledWith(runExecutionData, contextType, node);
|
||||
|
||||
getContextSpy.mockRestore();
|
||||
});
|
||||
describeCommonTests(executeSingleContext, {
|
||||
abortSignal,
|
||||
node,
|
||||
workflow,
|
||||
executeData,
|
||||
runExecutionData,
|
||||
});
|
||||
|
||||
describe('getInputData', () => {
|
||||
|
@ -266,54 +194,4 @@ describe('ExecuteSingleContext', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputSourceData', () => {
|
||||
it('should return the input source data correctly', () => {
|
||||
const inputSourceData = mock<ISourceData>();
|
||||
executeData.source = { main: [inputSourceData] };
|
||||
|
||||
expect(executeSingleContext.getInputSourceData()).toEqual(inputSourceData);
|
||||
});
|
||||
|
||||
it('should throw an error if the source data is missing', () => {
|
||||
executeData.source = null;
|
||||
|
||||
expect(() => executeSingleContext.getInputSourceData()).toThrow(ApplicationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logAiEvent', () => {
|
||||
it('should log the AI event correctly', () => {
|
||||
const eventName = 'ai-tool-called';
|
||||
const msg = 'test message';
|
||||
|
||||
executeSingleContext.logAiEvent(eventName, msg);
|
||||
|
||||
expect(additionalData.logAiEvent).toHaveBeenCalledWith(eventName, {
|
||||
executionId: additionalData.executionId,
|
||||
nodeName: node.name,
|
||||
workflowName: workflow.name,
|
||||
nodeType: node.type,
|
||||
workflowId: workflow.id,
|
||||
msg,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMetadata', () => {
|
||||
it('sets metadata on execution data', () => {
|
||||
const metadata: ITaskMetadata = {
|
||||
subExecution: {
|
||||
workflowId: '123',
|
||||
executionId: 'xyz',
|
||||
},
|
||||
};
|
||||
|
||||
expect(executeSingleContext.getExecuteData().metadata?.subExecution).toEqual(undefined);
|
||||
executeSingleContext.setMetadata(metadata);
|
||||
expect(executeSingleContext.getExecuteData().metadata?.subExecution).toEqual(
|
||||
metadata.subExecution,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ import { NodeExecutionContext } from '../node-execution-context';
|
|||
|
||||
class TestContext extends NodeExecutionContext {}
|
||||
|
||||
describe('BaseContext', () => {
|
||||
describe('NodeExecutionContext', () => {
|
||||
const instanceSettings = mock<InstanceSettings>({ instanceId: 'abc123' });
|
||||
Container.set(InstanceSettings, instanceSettings);
|
||||
|
|
@ -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,
|
||||
INodeType,
|
||||
INodeTypes,
|
||||
OnError,
|
||||
ICredentialDataDecryptedObject,
|
||||
} from 'n8n-workflow';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
import { describeCommonTests } from './shared-tests';
|
||||
import { SupplyDataContext } from '../supply-data-context';
|
||||
|
||||
describe('SupplyDataContext', () => {
|
||||
|
@ -41,6 +41,7 @@ describe('SupplyDataContext', () => {
|
|||
const expression = mock<Expression>();
|
||||
const workflow = mock<Workflow>({ expression, nodeTypes });
|
||||
const node = mock<INode>({
|
||||
name: 'Test Node',
|
||||
credentials: {
|
||||
[testCredentialType]: {
|
||||
id: 'testCredentialId',
|
||||
|
@ -54,7 +55,7 @@ describe('SupplyDataContext', () => {
|
|||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ credentialsHelper });
|
||||
const mode: WorkflowExecuteMode = 'manual';
|
||||
const runExecutionData = mock<IRunExecutionData>();
|
||||
const connectionInputData = mock<INodeExecutionData[]>();
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
const inputData: ITaskDataConnections = { main: [[{ json: { test: 'data' } }]] };
|
||||
const executeData = mock<IExecuteData>();
|
||||
const runIndex = 0;
|
||||
|
@ -80,64 +81,12 @@ describe('SupplyDataContext', () => {
|
|||
expression.getParameterValue.mockImplementation((value) => value);
|
||||
});
|
||||
|
||||
describe('getExecutionCancelSignal', () => {
|
||||
it('should return the abort signal', () => {
|
||||
expect(supplyDataContext.getExecutionCancelSignal()).toBe(abortSignal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('continueOnFail', () => {
|
||||
afterEach(() => {
|
||||
node.onError = undefined;
|
||||
node.continueOnFail = false;
|
||||
});
|
||||
|
||||
it('should return false for nodes by default', () => {
|
||||
expect(supplyDataContext.continueOnFail()).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true if node has continueOnFail set to true', () => {
|
||||
node.continueOnFail = true;
|
||||
expect(supplyDataContext.continueOnFail()).toEqual(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['continueRegularOutput', true],
|
||||
['continueErrorOutput', true],
|
||||
['stopWorkflow', false],
|
||||
])('if node has onError set to %s, it should return %s', (onError, expected) => {
|
||||
node.onError = onError as OnError;
|
||||
expect(supplyDataContext.continueOnFail()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateExpression', () => {
|
||||
it('should evaluate the expression correctly', () => {
|
||||
const expression = '$json.test';
|
||||
const expectedResult = 'data';
|
||||
const resolveSimpleParameterValueSpy = jest.spyOn(
|
||||
workflow.expression,
|
||||
'resolveSimpleParameterValue',
|
||||
);
|
||||
resolveSimpleParameterValueSpy.mockReturnValue(expectedResult);
|
||||
|
||||
expect(supplyDataContext.evaluateExpression(expression, 0)).toEqual(expectedResult);
|
||||
|
||||
expect(resolveSimpleParameterValueSpy).toHaveBeenCalledWith(
|
||||
`=${expression}`,
|
||||
{},
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
0,
|
||||
node.name,
|
||||
connectionInputData,
|
||||
mode,
|
||||
expect.objectContaining({}),
|
||||
executeData,
|
||||
);
|
||||
|
||||
resolveSimpleParameterValueSpy.mockRestore();
|
||||
});
|
||||
describeCommonTests(supplyDataContext, {
|
||||
abortSignal,
|
||||
node,
|
||||
workflow,
|
||||
executeData,
|
||||
runExecutionData,
|
||||
});
|
||||
|
||||
describe('getInputData', () => {
|
||||
|
@ -219,22 +168,4 @@ describe('SupplyDataContext', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logAiEvent', () => {
|
||||
it('should log the AI event correctly', () => {
|
||||
const eventName = 'ai-tool-called';
|
||||
const msg = 'test message';
|
||||
|
||||
supplyDataContext.logAiEvent(eventName, msg);
|
||||
|
||||
expect(additionalData.logAiEvent).toHaveBeenCalledWith(eventName, {
|
||||
executionId: additionalData.executionId,
|
||||
nodeName: node.name,
|
||||
workflowName: workflow.name,
|
||||
nodeType: node.type,
|
||||
workflowId: workflow.id,
|
||||
msg,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,121 @@
|
|||
import type { IDataObject, INode, INodeType } from 'n8n-workflow';
|
||||
import toPlainObject from 'lodash/toPlainObject';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { IDataObject, INode, INodeType, NodeParameterValue } from 'n8n-workflow';
|
||||
import { ExpressionError } from 'n8n-workflow';
|
||||
|
||||
import { validateValueAgainstSchema } from '@/NodeExecuteFunctions';
|
||||
import { cleanupParameterData, ensureType, validateValueAgainstSchema } from '../utils';
|
||||
|
||||
describe('Validation', () => {
|
||||
describe('cleanupParameterData', () => {
|
||||
it('should stringify Luxon dates in-place', () => {
|
||||
const input = { x: 1, y: DateTime.now() as unknown as NodeParameterValue };
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('string');
|
||||
});
|
||||
|
||||
it('should stringify plain Luxon dates in-place', () => {
|
||||
const input = {
|
||||
x: 1,
|
||||
y: toPlainObject(DateTime.now()),
|
||||
};
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle objects with nameless constructors', () => {
|
||||
const input = { x: 1, y: { constructor: {} } as NodeParameterValue };
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('object');
|
||||
});
|
||||
|
||||
it('should handle objects without a constructor', () => {
|
||||
const input = { x: 1, y: { constructor: undefined } as unknown as NodeParameterValue };
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureType', () => {
|
||||
it('throws error for null value', () => {
|
||||
expect(() => ensureType('string', null, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' must not be null"),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for undefined value', () => {
|
||||
expect(() => ensureType('string', undefined, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' could not be 'undefined'"),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns string value without modification', () => {
|
||||
const value = 'hello';
|
||||
const expectedValue = value;
|
||||
const result = ensureType('string', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('returns number value without modification', () => {
|
||||
const value = 42;
|
||||
const expectedValue = value;
|
||||
const result = ensureType('number', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('returns boolean value without modification', () => {
|
||||
const value = true;
|
||||
const expectedValue = value;
|
||||
const result = ensureType('boolean', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('converts object to string if toType is string', () => {
|
||||
const value = { name: 'John' };
|
||||
const expectedValue = JSON.stringify(value);
|
||||
const result = ensureType('string', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('converts string to number if toType is number', () => {
|
||||
const value = '10';
|
||||
const expectedValue = 10;
|
||||
const result = ensureType('number', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('throws error for invalid conversion to number', () => {
|
||||
const value = 'invalid';
|
||||
expect(() => ensureType('number', value, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"),
|
||||
);
|
||||
});
|
||||
|
||||
it('parses valid JSON string to object if toType is object', () => {
|
||||
const value = '{"name": "Alice"}';
|
||||
const expectedValue = JSON.parse(value);
|
||||
const result = ensureType('object', value, 'myParam');
|
||||
expect(result).toEqual(expectedValue);
|
||||
});
|
||||
|
||||
it('throws error for invalid JSON string to object conversion', () => {
|
||||
const value = 'invalid_json';
|
||||
expect(() => ensureType('object', value, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' could not be parsed"),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for non-array value if toType is array', () => {
|
||||
const value = { name: 'Alice' };
|
||||
expect(() => ensureType('array', value, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' must be an array, but we got object"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateValueAgainstSchema', () => {
|
||||
test('should validate fixedCollection values parameter', () => {
|
||||
const nodeType = {
|
||||
description: {
|
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,
|
||||
ITaskDataConnections,
|
||||
IExecuteData,
|
||||
ContextType,
|
||||
AiEvent,
|
||||
ISourceData,
|
||||
ITaskMetadata,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ApplicationError,
|
||||
createDeferredPromise,
|
||||
NodeHelpers,
|
||||
WorkflowDataProxy,
|
||||
} from 'n8n-workflow';
|
||||
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
assertBinaryData,
|
||||
continueOnFail,
|
||||
getAdditionalKeys,
|
||||
getBinaryDataBuffer,
|
||||
getBinaryHelperFunctions,
|
||||
getCredentials,
|
||||
getNodeParameter,
|
||||
getRequestHelperFunctions,
|
||||
returnJsonArray,
|
||||
} from '@/NodeExecuteFunctions';
|
||||
|
||||
import { NodeExecutionContext } from './node-execution-context';
|
||||
import { BaseExecuteContext } from './base-execute-context';
|
||||
|
||||
export class ExecuteSingleContext extends NodeExecutionContext implements IExecuteSingleFunctions {
|
||||
export class ExecuteSingleContext extends BaseExecuteContext implements IExecuteSingleFunctions {
|
||||
readonly helpers: IExecuteSingleFunctions['helpers'];
|
||||
|
||||
constructor(
|
||||
|
@ -45,15 +32,26 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
|
|||
node: INode,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
mode: WorkflowExecuteMode,
|
||||
private readonly runExecutionData: IRunExecutionData,
|
||||
private readonly runIndex: number,
|
||||
private readonly connectionInputData: INodeExecutionData[],
|
||||
private readonly inputData: ITaskDataConnections,
|
||||
runExecutionData: IRunExecutionData,
|
||||
runIndex: number,
|
||||
connectionInputData: INodeExecutionData[],
|
||||
inputData: ITaskDataConnections,
|
||||
private readonly itemIndex: number,
|
||||
private readonly executeData: IExecuteData,
|
||||
private readonly abortSignal?: AbortSignal,
|
||||
executeData: IExecuteData,
|
||||
abortSignal?: AbortSignal,
|
||||
) {
|
||||
super(workflow, node, additionalData, mode);
|
||||
super(
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
mode,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
inputData,
|
||||
executeData,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
this.helpers = {
|
||||
createDeferredPromise,
|
||||
|
@ -74,47 +72,8 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
|
|||
};
|
||||
}
|
||||
|
||||
getExecutionCancelSignal() {
|
||||
return this.abortSignal;
|
||||
}
|
||||
|
||||
onExecutionCancellation(handler: () => unknown) {
|
||||
const fn = () => {
|
||||
this.abortSignal?.removeEventListener('abort', fn);
|
||||
handler();
|
||||
};
|
||||
this.abortSignal?.addEventListener('abort', fn);
|
||||
}
|
||||
|
||||
setMetadata(metadata: ITaskMetadata): void {
|
||||
this.executeData.metadata = {
|
||||
...(this.executeData.metadata ?? {}),
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
|
||||
continueOnFail() {
|
||||
return continueOnFail(this.node);
|
||||
}
|
||||
|
||||
evaluateExpression(expression: string, evaluateItemIndex: number | undefined) {
|
||||
evaluateItemIndex = evaluateItemIndex ?? this.itemIndex;
|
||||
return this.workflow.expression.resolveSimpleParameterValue(
|
||||
`=${expression}`,
|
||||
{},
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
evaluateItemIndex,
|
||||
this.node.name,
|
||||
this.connectionInputData,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
||||
this.executeData,
|
||||
);
|
||||
}
|
||||
|
||||
getContext(type: ContextType) {
|
||||
return NodeHelpers.getContext(this.runExecutionData, type, this.node);
|
||||
evaluateExpression(expression: string, itemIndex: number = this.itemIndex) {
|
||||
return super.evaluateExpression(expression, itemIndex);
|
||||
}
|
||||
|
||||
getInputData(inputIndex = 0, inputName = 'main') {
|
||||
|
@ -154,73 +113,14 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getNodeParameter(parameterName: string, fallbackValue?: any, options?: IGetNodeParameterOptions) {
|
||||
return getNodeParameter(
|
||||
this.workflow,
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
this.connectionInputData,
|
||||
this.node,
|
||||
parameterName,
|
||||
this.itemIndex,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
||||
this.executeData,
|
||||
fallbackValue,
|
||||
options,
|
||||
);
|
||||
return this._getNodeParameter(parameterName, this.itemIndex, fallbackValue, options);
|
||||
}
|
||||
|
||||
// TODO: extract out in a BaseExecutionContext
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await getCredentials<T>(
|
||||
this.workflow,
|
||||
this.node,
|
||||
type,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
this.executeData,
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
this.connectionInputData,
|
||||
this.itemIndex,
|
||||
);
|
||||
}
|
||||
|
||||
getExecuteData() {
|
||||
return this.executeData;
|
||||
return await super.getCredentials<T>(type, this.itemIndex);
|
||||
}
|
||||
|
||||
getWorkflowDataProxy() {
|
||||
return new WorkflowDataProxy(
|
||||
this.workflow,
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
this.itemIndex,
|
||||
this.node.name,
|
||||
this.connectionInputData,
|
||||
{},
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
||||
this.executeData,
|
||||
).getDataProxy();
|
||||
}
|
||||
|
||||
getInputSourceData(inputIndex = 0, inputName = 'main'): ISourceData {
|
||||
if (this.executeData?.source === null) {
|
||||
// Should never happen as n8n sets it automatically
|
||||
throw new ApplicationError('Source data is missing');
|
||||
}
|
||||
return this.executeData.source[inputName][inputIndex] as ISourceData;
|
||||
}
|
||||
|
||||
logAiEvent(eventName: AiEvent, msg: string) {
|
||||
return this.additionalData.logAiEvent(eventName, {
|
||||
executionId: this.additionalData.executionId ?? 'unsaved-execution',
|
||||
nodeName: this.node.name,
|
||||
workflowName: this.workflow.name ?? 'Unnamed workflow',
|
||||
nodeType: this.node.type,
|
||||
workflowId: this.workflow.id ?? 'unsaved-workflow',
|
||||
msg,
|
||||
});
|
||||
return super.getWorkflowDataProxy(this.itemIndex);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
IHookFunctions,
|
||||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
|
@ -17,9 +13,6 @@ import { ApplicationError } from 'n8n-workflow';
|
|||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
getAdditionalKeys,
|
||||
getCredentials,
|
||||
getNodeParameter,
|
||||
getNodeWebhookUrl,
|
||||
getRequestHelperFunctions,
|
||||
getWebhookDescription,
|
||||
|
@ -48,34 +41,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
|
|||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
|
||||
}
|
||||
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const runExecutionData: IRunExecutionData | null = null;
|
||||
const itemIndex = 0;
|
||||
const runIndex = 0;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
|
||||
return getNodeParameter(
|
||||
this.workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
this.node,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, runExecutionData),
|
||||
undefined,
|
||||
fallbackValue,
|
||||
options,
|
||||
);
|
||||
return await this._getCredentials<T>(type);
|
||||
}
|
||||
|
||||
getNodeWebhookUrl(name: WebhookType): string | undefined {
|
||||
|
@ -85,7 +51,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
|
|||
this.node,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, null),
|
||||
this.additionalKeys,
|
||||
this.webhookData?.isTest,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// eslint-disable-next-line import/no-cycle
|
||||
export { ExecuteContext } from './execute-context';
|
||||
export { ExecuteSingleContext } from './execute-single-context';
|
||||
export { HookContext } from './hook-context';
|
||||
export { LoadOptionsContext } from './load-options-context';
|
||||
|
@ -6,3 +7,5 @@ export { PollContext } from './poll-context';
|
|||
export { SupplyDataContext } from './supply-data-context';
|
||||
export { TriggerContext } from './trigger-context';
|
||||
export { WebhookContext } from './webhook-context';
|
||||
|
||||
export { getAdditionalKeys } from './utils';
|
||||
|
|
|
@ -3,9 +3,7 @@ import type {
|
|||
ICredentialDataDecryptedObject,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ILoadOptionsFunctions,
|
||||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
Workflow,
|
||||
|
@ -13,13 +11,7 @@ import type {
|
|||
|
||||
import { extractValue } from '@/ExtractValue';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
getAdditionalKeys,
|
||||
getCredentials,
|
||||
getNodeParameter,
|
||||
getRequestHelperFunctions,
|
||||
getSSHTunnelFunctions,
|
||||
} from '@/NodeExecuteFunctions';
|
||||
import { getRequestHelperFunctions, getSSHTunnelFunctions } from '@/NodeExecuteFunctions';
|
||||
|
||||
import { NodeExecutionContext } from './node-execution-context';
|
||||
|
||||
|
@ -41,7 +33,7 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
|
|||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
|
||||
return await this._getCredentials<T>(type);
|
||||
}
|
||||
|
||||
getCurrentNodeParameter(
|
||||
|
@ -76,31 +68,4 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
|
|||
getCurrentNodeParameters() {
|
||||
return this.additionalData.currentNodeParameters;
|
||||
}
|
||||
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const runExecutionData: IRunExecutionData | null = null;
|
||||
const itemIndex = 0;
|
||||
const runIndex = 0;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
|
||||
return getNodeParameter(
|
||||
this.workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
this.node,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, runExecutionData),
|
||||
undefined,
|
||||
fallbackValue,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,42 @@
|
|||
import { get } from 'lodash';
|
||||
import type {
|
||||
FunctionsBase,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsExpressionResolveValues,
|
||||
IExecuteData,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeCredentialDescription,
|
||||
INodeCredentialsDetails,
|
||||
INodeExecutionData,
|
||||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
NodeTypeAndVersion,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy, LoggerProxy } from 'n8n-workflow';
|
||||
import {
|
||||
ApplicationError,
|
||||
deepCopy,
|
||||
ExpressionError,
|
||||
LoggerProxy,
|
||||
NodeHelpers,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/Constants';
|
||||
import { extractValue } from '@/ExtractValue';
|
||||
import { InstanceSettings } from '@/InstanceSettings';
|
||||
|
||||
import {
|
||||
cleanupParameterData,
|
||||
ensureType,
|
||||
getAdditionalKeys,
|
||||
validateValueAgainstSchema,
|
||||
} from './utils';
|
||||
|
||||
export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCredentials'> {
|
||||
protected readonly instanceSettings = Container.get(InstanceSettings);
|
||||
|
||||
|
@ -20,6 +45,10 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
|||
protected readonly node: INode,
|
||||
protected readonly additionalData: IWorkflowExecuteAdditionalData,
|
||||
protected readonly mode: WorkflowExecuteMode,
|
||||
protected readonly runExecutionData: IRunExecutionData | null = null,
|
||||
protected readonly runIndex = 0,
|
||||
protected readonly connectionInputData: INodeExecutionData[] = [],
|
||||
protected readonly executeData?: IExecuteData,
|
||||
) {}
|
||||
|
||||
get logger() {
|
||||
|
@ -101,6 +130,255 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
|||
return this.additionalData.credentialsHelper.getCredentialsProperties(type);
|
||||
}
|
||||
|
||||
/** Returns the requested decrypted credentials if the node has access to them */
|
||||
protected async _getCredentials<T extends object = ICredentialDataDecryptedObject>(
|
||||
type: string,
|
||||
executeData?: IExecuteData,
|
||||
connectionInputData?: INodeExecutionData[],
|
||||
itemIndex?: number,
|
||||
): Promise<T> {
|
||||
const { workflow, node, additionalData, mode, runExecutionData, runIndex } = this;
|
||||
// Get the NodeType as it has the information if the credentials are required
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
// Hardcode for now for security reasons that only a single node can access
|
||||
// all credentials
|
||||
const fullAccess = [HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE].includes(node.type);
|
||||
|
||||
let nodeCredentialDescription: INodeCredentialDescription | undefined;
|
||||
if (!fullAccess) {
|
||||
if (nodeType.description.credentials === undefined) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`Node type "${node.type}" does not have any credentials defined`,
|
||||
{ level: 'warning' },
|
||||
);
|
||||
}
|
||||
|
||||
nodeCredentialDescription = nodeType.description.credentials.find(
|
||||
(credentialTypeDescription) => credentialTypeDescription.name === type,
|
||||
);
|
||||
if (nodeCredentialDescription === undefined) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`Node type "${node.type}" does not have any credentials of type "${type}" defined`,
|
||||
{ level: 'warning' },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!NodeHelpers.displayParameter(
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
additionalData.currentNodeParameters || node.parameters,
|
||||
nodeCredentialDescription,
|
||||
node,
|
||||
node.parameters,
|
||||
)
|
||||
) {
|
||||
// Credentials should not be displayed even if they would be defined
|
||||
throw new NodeOperationError(node, 'Credentials not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if node has any credentials defined
|
||||
if (!fullAccess && !node.credentials?.[type]) {
|
||||
// If none are defined check if the credentials are required or not
|
||||
|
||||
if (nodeCredentialDescription?.required === true) {
|
||||
// Credentials are required so error
|
||||
if (!node.credentials) {
|
||||
throw new NodeOperationError(node, 'Node does not have any credentials set', {
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
if (!node.credentials[type]) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`Node does not have any credentials set for "${type}"`,
|
||||
{
|
||||
level: 'warning',
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Credentials are not required
|
||||
throw new NodeOperationError(node, 'Node does not require credentials');
|
||||
}
|
||||
}
|
||||
|
||||
if (fullAccess && !node.credentials?.[type]) {
|
||||
// Make sure that fullAccess nodes still behave like before that if they
|
||||
// request access to credentials that are currently not set it returns undefined
|
||||
throw new NodeOperationError(node, 'Credentials not found');
|
||||
}
|
||||
|
||||
let expressionResolveValues: ICredentialsExpressionResolveValues | undefined;
|
||||
if (connectionInputData && runExecutionData && runIndex !== undefined) {
|
||||
expressionResolveValues = {
|
||||
connectionInputData,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
itemIndex: itemIndex || 0,
|
||||
node,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
workflow,
|
||||
} as ICredentialsExpressionResolveValues;
|
||||
}
|
||||
|
||||
const nodeCredentials = node.credentials
|
||||
? node.credentials[type]
|
||||
: ({} as INodeCredentialsDetails);
|
||||
|
||||
// TODO: solve using credentials via expression
|
||||
// if (name.charAt(0) === '=') {
|
||||
// // If the credential name is an expression resolve it
|
||||
// const additionalKeys = getAdditionalKeys(additionalData, mode);
|
||||
// name = workflow.expression.getParameterValue(
|
||||
// name,
|
||||
// runExecutionData || null,
|
||||
// runIndex || 0,
|
||||
// itemIndex || 0,
|
||||
// node.name,
|
||||
// connectionInputData || [],
|
||||
// mode,
|
||||
// additionalKeys,
|
||||
// ) as string;
|
||||
// }
|
||||
|
||||
const decryptedDataObject = await additionalData.credentialsHelper.getDecrypted(
|
||||
additionalData,
|
||||
nodeCredentials,
|
||||
type,
|
||||
mode,
|
||||
executeData,
|
||||
false,
|
||||
expressionResolveValues,
|
||||
);
|
||||
|
||||
return decryptedDataObject as T;
|
||||
}
|
||||
|
||||
protected get additionalKeys() {
|
||||
return getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
|
||||
}
|
||||
|
||||
/** Returns the requested resolved (all expressions replaced) node parameters. */
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const itemIndex = 0;
|
||||
return this._getNodeParameter(parameterName, itemIndex, fallbackValue, options);
|
||||
}
|
||||
|
||||
protected _getNodeParameter(
|
||||
parameterName: string,
|
||||
itemIndex: number,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const { workflow, node, mode, runExecutionData, runIndex, connectionInputData, executeData } =
|
||||
this;
|
||||
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const value = get(node.parameters, parameterName, fallbackValue);
|
||||
|
||||
if (value === undefined) {
|
||||
throw new ApplicationError('Could not get parameter', { extra: { parameterName } });
|
||||
}
|
||||
|
||||
if (options?.rawExpressions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return value;
|
||||
}
|
||||
|
||||
const { additionalKeys } = this;
|
||||
|
||||
let returnData;
|
||||
|
||||
try {
|
||||
returnData = workflow.expression.getParameterValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
value,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
node.name,
|
||||
connectionInputData,
|
||||
mode,
|
||||
additionalKeys,
|
||||
executeData,
|
||||
false,
|
||||
{},
|
||||
options?.contextNode?.name,
|
||||
);
|
||||
cleanupParameterData(returnData);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ExpressionError &&
|
||||
node.continueOnFail &&
|
||||
node.type === 'n8n-nodes-base.set'
|
||||
) {
|
||||
// https://linear.app/n8n/issue/PAY-684
|
||||
returnData = [{ name: undefined, value: undefined }];
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.context) e.context.parameter = parameterName;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
e.cause = value;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// This is outside the try/catch because it throws errors with proper messages
|
||||
if (options?.extractValue) {
|
||||
returnData = extractValue(returnData, parameterName, node, nodeType, itemIndex);
|
||||
}
|
||||
|
||||
// Make sure parameter value is the type specified in the ensureType option, if needed convert it
|
||||
if (options?.ensureType) {
|
||||
returnData = ensureType(options.ensureType, returnData, parameterName, {
|
||||
itemIndex,
|
||||
runIndex,
|
||||
nodeCause: node.name,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate parameter value if it has a schema defined(RMC) or validateType defined
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
returnData = validateValueAgainstSchema(
|
||||
node,
|
||||
nodeType,
|
||||
returnData,
|
||||
parameterName,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return returnData;
|
||||
}
|
||||
|
||||
evaluateExpression(expression: string, itemIndex: number = 0) {
|
||||
return this.workflow.expression.resolveSimpleParameterValue(
|
||||
`=${expression}`,
|
||||
{},
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
itemIndex,
|
||||
this.node.name,
|
||||
this.connectionInputData,
|
||||
this.mode,
|
||||
this.additionalKeys,
|
||||
this.executeData,
|
||||
);
|
||||
}
|
||||
|
||||
async prepareOutputData(outputData: INodeExecutionData[]) {
|
||||
return [outputData];
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
IPollFunctions,
|
||||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
|
@ -15,10 +11,7 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
|||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
getAdditionalKeys,
|
||||
getBinaryHelperFunctions,
|
||||
getCredentials,
|
||||
getNodeParameter,
|
||||
getRequestHelperFunctions,
|
||||
getSchedulingFunctions,
|
||||
returnJsonArray,
|
||||
|
@ -62,33 +55,6 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions
|
|||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
|
||||
}
|
||||
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const runExecutionData: IRunExecutionData | null = null;
|
||||
const itemIndex = 0;
|
||||
const runIndex = 0;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
|
||||
return getNodeParameter(
|
||||
this.workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
this.node,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, runExecutionData),
|
||||
undefined,
|
||||
fallbackValue,
|
||||
options,
|
||||
);
|
||||
return await this._getCredentials<T>(type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import type {
|
||||
AiEvent,
|
||||
CallbackManager,
|
||||
CloseFunction,
|
||||
ExecuteWorkflowData,
|
||||
ICredentialDataDecryptedObject,
|
||||
IExecuteData,
|
||||
IExecuteWorkflowInfo,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
|
@ -15,32 +10,20 @@ import type {
|
|||
ITaskMetadata,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeConnectionType,
|
||||
RelatedExecution,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ApplicationError,
|
||||
createDeferredPromise,
|
||||
NodeHelpers,
|
||||
WorkflowDataProxy,
|
||||
} from 'n8n-workflow';
|
||||
import Container from 'typedi';
|
||||
import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
||||
|
||||
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
assertBinaryData,
|
||||
continueOnFail,
|
||||
constructExecutionMetaData,
|
||||
copyInputItems,
|
||||
getAdditionalKeys,
|
||||
getBinaryDataBuffer,
|
||||
getBinaryHelperFunctions,
|
||||
getCheckProcessedHelperFunctions,
|
||||
getCredentials,
|
||||
getFileSystemHelperFunctions,
|
||||
getNodeParameter,
|
||||
getRequestHelperFunctions,
|
||||
getSSHTunnelFunctions,
|
||||
normalizeItems,
|
||||
|
@ -49,9 +32,9 @@ import {
|
|||
addExecutionDataFunctions,
|
||||
} from '@/NodeExecuteFunctions';
|
||||
|
||||
import { NodeExecutionContext } from './node-execution-context';
|
||||
import { BaseExecuteContext } from './base-execute-context';
|
||||
|
||||
export class SupplyDataContext extends NodeExecutionContext implements ISupplyDataFunctions {
|
||||
export class SupplyDataContext extends BaseExecuteContext implements ISupplyDataFunctions {
|
||||
readonly helpers: ISupplyDataFunctions['helpers'];
|
||||
|
||||
readonly getNodeParameter: ISupplyDataFunctions['getNodeParameter'];
|
||||
|
@ -61,15 +44,26 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
|||
node: INode,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
mode: WorkflowExecuteMode,
|
||||
private readonly runExecutionData: IRunExecutionData,
|
||||
private readonly runIndex: number,
|
||||
private readonly connectionInputData: INodeExecutionData[],
|
||||
private readonly inputData: ITaskDataConnections,
|
||||
private readonly executeData: IExecuteData,
|
||||
runExecutionData: IRunExecutionData,
|
||||
runIndex: number,
|
||||
connectionInputData: INodeExecutionData[],
|
||||
inputData: ITaskDataConnections,
|
||||
executeData: IExecuteData,
|
||||
private readonly closeFunctions: CloseFunction[],
|
||||
private readonly abortSignal?: AbortSignal,
|
||||
abortSignal?: AbortSignal,
|
||||
) {
|
||||
super(workflow, node, additionalData, mode);
|
||||
super(
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
mode,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
inputData,
|
||||
executeData,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
this.helpers = {
|
||||
createDeferredPromise,
|
||||
|
@ -102,116 +96,14 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
|||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
) =>
|
||||
getNodeParameter(
|
||||
this.workflow,
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
this.connectionInputData,
|
||||
this.node,
|
||||
this._getNodeParameter(
|
||||
parameterName,
|
||||
itemIndex,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
||||
this.executeData,
|
||||
fallbackValue,
|
||||
options,
|
||||
)) as ISupplyDataFunctions['getNodeParameter'];
|
||||
}
|
||||
|
||||
getExecutionCancelSignal() {
|
||||
return this.abortSignal;
|
||||
}
|
||||
|
||||
onExecutionCancellation(handler: () => unknown) {
|
||||
const fn = () => {
|
||||
this.abortSignal?.removeEventListener('abort', fn);
|
||||
handler();
|
||||
};
|
||||
this.abortSignal?.addEventListener('abort', fn);
|
||||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(
|
||||
type: string,
|
||||
itemIndex: number,
|
||||
) {
|
||||
return await getCredentials<T>(
|
||||
this.workflow,
|
||||
this.node,
|
||||
type,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
this.executeData,
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
this.connectionInputData,
|
||||
itemIndex,
|
||||
);
|
||||
}
|
||||
|
||||
continueOnFail() {
|
||||
return continueOnFail(this.node);
|
||||
}
|
||||
|
||||
evaluateExpression(expression: string, itemIndex: number) {
|
||||
return this.workflow.expression.resolveSimpleParameterValue(
|
||||
`=${expression}`,
|
||||
{},
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
itemIndex,
|
||||
this.node.name,
|
||||
this.connectionInputData,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
||||
this.executeData,
|
||||
);
|
||||
}
|
||||
|
||||
async executeWorkflow(
|
||||
workflowInfo: IExecuteWorkflowInfo,
|
||||
inputData?: INodeExecutionData[],
|
||||
parentCallbackManager?: CallbackManager,
|
||||
options?: {
|
||||
doNotWaitToFinish?: boolean;
|
||||
parentExecution?: RelatedExecution;
|
||||
},
|
||||
): Promise<ExecuteWorkflowData> {
|
||||
return await this.additionalData
|
||||
.executeWorkflow(workflowInfo, this.additionalData, {
|
||||
parentWorkflowId: this.workflow.id?.toString(),
|
||||
inputData,
|
||||
parentWorkflowSettings: this.workflow.settings,
|
||||
node: this.node,
|
||||
parentCallbackManager,
|
||||
...options,
|
||||
})
|
||||
.then(async (result) => {
|
||||
const data = await Container.get(BinaryDataService).duplicateBinaryData(
|
||||
this.workflow.id,
|
||||
this.additionalData.executionId!,
|
||||
result.data,
|
||||
);
|
||||
return { ...result, data };
|
||||
});
|
||||
}
|
||||
|
||||
getNodeOutputs() {
|
||||
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
|
||||
this.node.type,
|
||||
this.node.typeVersion,
|
||||
);
|
||||
return NodeHelpers.getNodeOutputs(this.workflow, this.node, nodeType.description).map(
|
||||
(output) => {
|
||||
if (typeof output === 'string') {
|
||||
return {
|
||||
type: output,
|
||||
};
|
||||
}
|
||||
return output;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getInputConnectionData(inputName: NodeConnectionType, itemIndex: number): Promise<unknown> {
|
||||
return await getInputConnectionData.call(
|
||||
this,
|
||||
|
@ -252,69 +144,11 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
|||
return this.inputData[inputName][inputIndex];
|
||||
}
|
||||
|
||||
getWorkflowDataProxy(itemIndex: number) {
|
||||
return new WorkflowDataProxy(
|
||||
this.workflow,
|
||||
this.runExecutionData,
|
||||
this.runIndex,
|
||||
itemIndex,
|
||||
this.node.name,
|
||||
this.connectionInputData,
|
||||
{},
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData),
|
||||
this.executeData,
|
||||
).getDataProxy();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
sendMessageToUI(...args: any[]): void {
|
||||
if (this.mode !== 'manual') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.additionalData.sendDataToUI) {
|
||||
args = args.map((arg) => {
|
||||
// prevent invalid dates from being logged as null
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
|
||||
if (arg.isLuxonDateTime && arg.invalidReason) return { ...arg };
|
||||
|
||||
// log valid dates in human readable format, as in browser
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
||||
if (arg.isLuxonDateTime) return new Date(arg.ts).toString();
|
||||
if (arg instanceof Date) return arg.toString();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return arg;
|
||||
});
|
||||
|
||||
this.additionalData.sendDataToUI('sendConsoleMessage', {
|
||||
source: `[Node: "${this.node.name}"]`,
|
||||
messages: args,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
this.logger.warn(`There was a problem sending message to UI: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
logAiEvent(eventName: AiEvent, msg: string) {
|
||||
return this.additionalData.logAiEvent(eventName, {
|
||||
executionId: this.additionalData.executionId ?? 'unsaved-execution',
|
||||
nodeName: this.node.name,
|
||||
workflowName: this.workflow.name ?? 'Unnamed workflow',
|
||||
nodeType: this.node.type,
|
||||
workflowId: this.workflow.id ?? 'unsaved-workflow',
|
||||
msg,
|
||||
});
|
||||
}
|
||||
|
||||
addInputData(
|
||||
connectionType: NodeConnectionType,
|
||||
data: INodeExecutionData[][],
|
||||
): { index: number } {
|
||||
const nodeName = this.getNode().name;
|
||||
const nodeName = this.node.name;
|
||||
let currentNodeRunIndex = 0;
|
||||
if (this.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
||||
currentNodeRunIndex = this.runExecutionData.resultData.runData[nodeName].length;
|
||||
|
@ -322,17 +156,17 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
|||
|
||||
addExecutionDataFunctions(
|
||||
'input',
|
||||
this.node.name,
|
||||
nodeName,
|
||||
data,
|
||||
this.runExecutionData,
|
||||
connectionType,
|
||||
this.additionalData,
|
||||
this.node.name,
|
||||
nodeName,
|
||||
this.runIndex,
|
||||
currentNodeRunIndex,
|
||||
).catch((error) => {
|
||||
this.logger.warn(
|
||||
`There was a problem logging input data of node "${this.node.name}": ${
|
||||
`There was a problem logging input data of node "${nodeName}": ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
error.message
|
||||
}`,
|
||||
|
@ -348,20 +182,21 @@ export class SupplyDataContext extends NodeExecutionContext implements ISupplyDa
|
|||
data: INodeExecutionData[][],
|
||||
metadata?: ITaskMetadata,
|
||||
): void {
|
||||
const nodeName = this.node.name;
|
||||
addExecutionDataFunctions(
|
||||
'output',
|
||||
this.node.name,
|
||||
nodeName,
|
||||
data,
|
||||
this.runExecutionData,
|
||||
connectionType,
|
||||
this.additionalData,
|
||||
this.node.name,
|
||||
nodeName,
|
||||
this.runIndex,
|
||||
currentNodeRunIndex,
|
||||
metadata,
|
||||
).catch((error) => {
|
||||
this.logger.warn(
|
||||
`There was a problem logging output data of node "${this.node.name}": ${
|
||||
`There was a problem logging output data of node "${nodeName}": ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
error.message
|
||||
}`,
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
IRunExecutionData,
|
||||
ITriggerFunctions,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
|
@ -15,10 +11,7 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
|||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
getAdditionalKeys,
|
||||
getBinaryHelperFunctions,
|
||||
getCredentials,
|
||||
getNodeParameter,
|
||||
getRequestHelperFunctions,
|
||||
getSchedulingFunctions,
|
||||
getSSHTunnelFunctions,
|
||||
|
@ -64,33 +57,6 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc
|
|||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
|
||||
}
|
||||
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const runExecutionData: IRunExecutionData | null = null;
|
||||
const itemIndex = 0;
|
||||
const runIndex = 0;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
|
||||
return getNodeParameter(
|
||||
this.workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
this.node,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, runExecutionData),
|
||||
undefined,
|
||||
fallbackValue,
|
||||
options,
|
||||
);
|
||||
return await this._getCredentials<T>(type);
|
||||
}
|
||||
}
|
||||
|
|
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,
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
IRunExecutionData,
|
||||
|
@ -13,7 +12,6 @@ import type {
|
|||
IWebhookFunctions,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeConnectionType,
|
||||
NodeParameterValueType,
|
||||
WebhookType,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
|
@ -23,11 +21,8 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow';
|
|||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
copyBinaryFile,
|
||||
getAdditionalKeys,
|
||||
getBinaryHelperFunctions,
|
||||
getCredentials,
|
||||
getInputConnectionData,
|
||||
getNodeParameter,
|
||||
getNodeWebhookUrl,
|
||||
getRequestHelperFunctions,
|
||||
returnJsonArray,
|
||||
|
@ -47,9 +42,28 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
|||
mode: WorkflowExecuteMode,
|
||||
private readonly webhookData: IWebhookData,
|
||||
private readonly closeFunctions: CloseFunction[],
|
||||
private readonly runExecutionData: IRunExecutionData | null,
|
||||
runExecutionData: IRunExecutionData | null,
|
||||
) {
|
||||
super(workflow, node, additionalData, mode);
|
||||
let connectionInputData: INodeExecutionData[] = [];
|
||||
let executionData: IExecuteData | undefined;
|
||||
|
||||
if (runExecutionData?.executionData !== undefined) {
|
||||
executionData = runExecutionData.executionData.nodeExecutionStack[0];
|
||||
if (executionData !== undefined) {
|
||||
connectionInputData = executionData.data.main[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
super(
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
mode,
|
||||
runExecutionData,
|
||||
0,
|
||||
connectionInputData,
|
||||
executionData,
|
||||
);
|
||||
|
||||
this.helpers = {
|
||||
createDeferredPromise,
|
||||
|
@ -71,7 +85,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
|||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await getCredentials<T>(this.workflow, this.node, type, this.additionalData, this.mode);
|
||||
return await this._getCredentials<T>(type);
|
||||
}
|
||||
|
||||
getBodyData() {
|
||||
|
@ -116,7 +130,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
|||
this.node,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
getAdditionalKeys(this.additionalData, this.mode, null),
|
||||
this.additionalKeys,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -144,13 +158,12 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
|||
node: this.node,
|
||||
source: null,
|
||||
};
|
||||
const runIndex = 0;
|
||||
|
||||
return await getInputConnectionData.call(
|
||||
this,
|
||||
this.workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
this.runIndex,
|
||||
connectionInputData,
|
||||
{} as ITaskDataConnections,
|
||||
this.additionalData,
|
||||
|
@ -161,73 +174,4 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
|||
itemIndex,
|
||||
);
|
||||
}
|
||||
|
||||
evaluateExpression(expression: string, evaluateItemIndex?: number) {
|
||||
const itemIndex = evaluateItemIndex ?? 0;
|
||||
const runIndex = 0;
|
||||
|
||||
let connectionInputData: INodeExecutionData[] = [];
|
||||
let executionData: IExecuteData | undefined;
|
||||
|
||||
if (this.runExecutionData?.executionData !== undefined) {
|
||||
executionData = this.runExecutionData.executionData.nodeExecutionStack[0];
|
||||
|
||||
if (executionData !== undefined) {
|
||||
connectionInputData = executionData.data.main[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
const additionalKeys = getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
|
||||
|
||||
return this.workflow.expression.resolveSimpleParameterValue(
|
||||
`=${expression}`,
|
||||
{},
|
||||
this.runExecutionData,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
this.node.name,
|
||||
connectionInputData,
|
||||
this.mode,
|
||||
additionalKeys,
|
||||
executionData,
|
||||
);
|
||||
}
|
||||
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object {
|
||||
const itemIndex = 0;
|
||||
const runIndex = 0;
|
||||
|
||||
let connectionInputData: INodeExecutionData[] = [];
|
||||
let executionData: IExecuteData | undefined;
|
||||
|
||||
if (this.runExecutionData?.executionData !== undefined) {
|
||||
executionData = this.runExecutionData.executionData.nodeExecutionStack[0];
|
||||
|
||||
if (executionData !== undefined) {
|
||||
connectionInputData = executionData.data.main[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
const additionalKeys = getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
|
||||
|
||||
return getNodeParameter(
|
||||
this.workflow,
|
||||
this.runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
this.node,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
this.mode,
|
||||
additionalKeys,
|
||||
executionData,
|
||||
fallbackValue,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import { mkdtempSync, readFileSync } from 'fs';
|
|||
import type { IncomingMessage } from 'http';
|
||||
import type { Agent } from 'https';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import toPlainObject from 'lodash/toPlainObject';
|
||||
import { DateTime } from 'luxon';
|
||||
import type {
|
||||
IBinaryData,
|
||||
IHttpRequestMethods,
|
||||
|
@ -12,11 +10,9 @@ import type {
|
|||
IRequestOptions,
|
||||
ITaskDataConnections,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValue,
|
||||
Workflow,
|
||||
WorkflowHooks,
|
||||
} from 'n8n-workflow';
|
||||
import { ExpressionError } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
@ -26,9 +22,7 @@ import Container from 'typedi';
|
|||
import { BinaryDataService } from '@/BinaryData/BinaryData.service';
|
||||
import { InstanceSettings } from '@/InstanceSettings';
|
||||
import {
|
||||
cleanupParameterData,
|
||||
copyInputItems,
|
||||
ensureType,
|
||||
getBinaryDataBuffer,
|
||||
isFilePathBlocked,
|
||||
parseIncomingMessage,
|
||||
|
@ -470,39 +464,6 @@ describe('NodeExecuteFunctions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cleanupParameterData', () => {
|
||||
it('should stringify Luxon dates in-place', () => {
|
||||
const input = { x: 1, y: DateTime.now() as unknown as NodeParameterValue };
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('string');
|
||||
});
|
||||
|
||||
it('should stringify plain Luxon dates in-place', () => {
|
||||
const input = {
|
||||
x: 1,
|
||||
y: toPlainObject(DateTime.now()),
|
||||
};
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle objects with nameless constructors', () => {
|
||||
const input = { x: 1, y: { constructor: {} } as NodeParameterValue };
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('object');
|
||||
});
|
||||
|
||||
it('should handle objects without a constructor', () => {
|
||||
const input = { x: 1, y: { constructor: undefined } as unknown as NodeParameterValue };
|
||||
expect(typeof input.y).toBe('object');
|
||||
cleanupParameterData(input);
|
||||
expect(typeof input.y).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyInputItems', () => {
|
||||
it('should pick only selected properties', () => {
|
||||
const output = copyInputItems(
|
||||
|
@ -588,83 +549,6 @@ describe('NodeExecuteFunctions', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('ensureType', () => {
|
||||
it('throws error for null value', () => {
|
||||
expect(() => ensureType('string', null, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' must not be null"),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for undefined value', () => {
|
||||
expect(() => ensureType('string', undefined, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' could not be 'undefined'"),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns string value without modification', () => {
|
||||
const value = 'hello';
|
||||
const expectedValue = value;
|
||||
const result = ensureType('string', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('returns number value without modification', () => {
|
||||
const value = 42;
|
||||
const expectedValue = value;
|
||||
const result = ensureType('number', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('returns boolean value without modification', () => {
|
||||
const value = true;
|
||||
const expectedValue = value;
|
||||
const result = ensureType('boolean', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('converts object to string if toType is string', () => {
|
||||
const value = { name: 'John' };
|
||||
const expectedValue = JSON.stringify(value);
|
||||
const result = ensureType('string', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('converts string to number if toType is number', () => {
|
||||
const value = '10';
|
||||
const expectedValue = 10;
|
||||
const result = ensureType('number', value, 'myParam');
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
|
||||
it('throws error for invalid conversion to number', () => {
|
||||
const value = 'invalid';
|
||||
expect(() => ensureType('number', value, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"),
|
||||
);
|
||||
});
|
||||
|
||||
it('parses valid JSON string to object if toType is object', () => {
|
||||
const value = '{"name": "Alice"}';
|
||||
const expectedValue = JSON.parse(value);
|
||||
const result = ensureType('object', value, 'myParam');
|
||||
expect(result).toEqual(expectedValue);
|
||||
});
|
||||
|
||||
it('throws error for invalid JSON string to object conversion', () => {
|
||||
const value = 'invalid_json';
|
||||
expect(() => ensureType('object', value, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' could not be parsed"),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for non-array value if toType is array', () => {
|
||||
const value = { name: 'Alice' };
|
||||
expect(() => ensureType('array', value, 'myParam')).toThrowError(
|
||||
new ExpressionError("Parameter 'myParam' must be an array, but we got object"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFilePathBlocked', () => {
|
||||
|
|
Loading…
Reference in a new issue