Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(executeWorkflowTrigger): add substitution for workflow input values #12111

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow';

import { ExecuteWorkflow } from './ExecuteWorkflow.node';
import { getWorkflowInfo } from './GenericFunctions';

jest.mock('./GenericFunctions');
jest.mock('../../../utils/utilities');

describe('ExecuteWorkflow', () => {
const executeWorkflow = new ExecuteWorkflow();
const executeFunctions = mock<IExecuteFunctions>({
getNodeParameter: jest.fn(),
getInputData: jest.fn(),
getWorkflowDataProxy: jest.fn(),
executeWorkflow: jest.fn(),
continueOnFail: jest.fn(),
setMetadata: jest.fn(),
getNode: jest.fn(),
});

beforeEach(() => {
jest.clearAllMocks();
executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]);
executeFunctions.getWorkflowDataProxy.mockReturnValue({
$workflow: { id: 'workflowId' },
$execution: { id: 'executionId' },
} as unknown as IWorkflowDataProxyData);
});

test('should execute workflow in "each" mode and wait for sub-workflow completion', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema

executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]);
executeFunctions.getWorkflowDataProxy.mockReturnValue({
$workflow: { id: 'workflowId' },
$execution: { id: 'executionId' },
} as unknown as IWorkflowDataProxyData);
(getWorkflowInfo as jest.Mock).mockResolvedValue({ id: 'subWorkflowId' });
(executeFunctions.executeWorkflow as jest.Mock).mockResolvedValue({
executionId: 'subExecutionId',
data: [[{ json: { key: 'subValue' } }]],
});

const result = await executeWorkflow.execute.call(executeFunctions);

expect(result).toEqual([
[
{
json: { key: 'value' },
index: 0,
pairedItem: { item: 0 },
metadata: {
subExecution: { workflowId: 'subWorkflowId', executionId: 'subExecutionId' },
},
},
],
]);
});

test('should execute workflow in "once" mode and not wait for sub-workflow completion', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('once') // mode
.mockReturnValueOnce(false) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema

executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]);

executeFunctions.executeWorkflow.mockResolvedValue({
executionId: 'subExecutionId',
data: [[{ json: { key: 'subValue' } }]],
});

const result = await executeWorkflow.execute.call(executeFunctions);

expect(result).toEqual([[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 } }]]);
});

test('should handle errors and continue on fail', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema

(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true);

const result = await executeWorkflow.execute.call(executeFunctions);

expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]);
});

test('should throw error if not continuing on fail', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema

(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(false);

await expect(executeWorkflow.execute.call(executeFunctions)).rejects.toThrow(
'Error executing workflow with item at index 0',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type {
ExecuteWorkflowData,
FieldValueOption,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeType,
Expand All @@ -14,9 +15,33 @@ import { loadWorkflowInputMappings } from './methods/resourceMapping';
import { generatePairedItemData } from '../../../utils/utilities';
import { getWorkflowInputData } from '../GenericFunctions';

function getWorkflowInputValues(this: IExecuteFunctions) {
igatanasov marked this conversation as resolved.
Show resolved Hide resolved
const inputData = this.getInputData();

return inputData.map((item, itemIndex) => {
const itemFieldValues = this.getNodeParameter(
'workflowInputs.value',
itemIndex,
{},
) as IDataObject;

return {
json: {
...item.json,
...itemFieldValues,
},
index: itemIndex,
pairedItem: {
item: itemIndex,
},
};
});
}

function getCurrentWorkflowInputData(this: IExecuteFunctions) {
const inputData = getWorkflowInputValues.call(this);

const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[];
const inputData = this.getInputData();

if (schema.length === 0) {
return inputData;
Expand All @@ -25,7 +50,6 @@ function getCurrentWorkflowInputData(this: IExecuteFunctions) {
.filter((x) => !x.removed)
.map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[];

// TODO: map every row to field values so we can set the static value or expression later on
return getWorkflowInputData.call(this, inputData, newParams);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { mock } from 'jest-mock-extended';
import type { FieldValueOption, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow';

import { ExecuteWorkflowTrigger } from './ExecuteWorkflowTrigger.node';
import { getFieldEntries, getWorkflowInputData } from '../GenericFunctions';

jest.mock('../GenericFunctions');

describe('ExecuteWorkflowTrigger', () => {
igatanasov marked this conversation as resolved.
Show resolved Hide resolved
const mockInputData: INodeExecutionData[] = [
{ json: { item: 0, foo: 'bar' } },
{ json: { item: 1, foo: 'quz' } },
];
const mockNode = mock<INode>({ typeVersion: 1 });
const executeFns = mock<IExecuteFunctions>({
getInputData: () => mockInputData,
getNode: () => mockNode,
getNodeParameter: jest.fn(),
});

it('should return its input data on V1', async () => {
executeFns.getNodeParameter.mockReturnValueOnce('passthrough');
const result = await new ExecuteWorkflowTrigger().execute.call(executeFns);

expect(result).toEqual([mockInputData]);
});

it('should return transformed input data based on newParams when input source is not passthrough', async () => {
executeFns.getNodeParameter.mockReturnValueOnce('usingFieldsBelow');
const mockNewParams = [
{ name: 'value1', type: 'string' },
{ name: 'value2', type: 'number' },
] as FieldValueOption[];
const getFieldEntriesMock = (getFieldEntries as jest.Mock).mockReturnValue(mockNewParams);
const getWorkflowInputDataMock = (getWorkflowInputData as jest.Mock).mockReturnValue(
mockInputData,
);

const result = await new ExecuteWorkflowTrigger().execute.call(executeFns);

expect(result).toEqual([mockInputData]);
expect(getFieldEntriesMock).toHaveBeenCalledWith(executeFns);
expect(getWorkflowInputDataMock).toHaveBeenCalledWith(mockInputData, mockNewParams);
});
});

This file was deleted.

Loading