first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.executeWorkflow",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "The Execute Workflow node can be used when you want your workflow to treat another workflow as a step in your flow. It allows you to modularize your workflows and have a single source of truth for series of actions you perform often. ",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["n8n", "call", "sub", "workflow", "sub-workflow", "subworkflow"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Helpers", "Flow"]
|
||||
}
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, IWorkflowDataProxyData, INode } 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({}) // workflowInputs.value
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(true); // waitForSubWorkflow
|
||||
|
||||
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: 'subValue' },
|
||||
pairedItem: { item: 0 },
|
||||
metadata: {
|
||||
subExecution: { workflowId: 'subWorkflowId', executionId: 'subExecutionId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
// Verify shouldResume is set correctly
|
||||
expect(executeFunctions.executeWorkflow).toHaveBeenCalledWith(
|
||||
{ id: 'subWorkflowId' },
|
||||
[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 }, binary: undefined }],
|
||||
undefined,
|
||||
{
|
||||
parentExecution: {
|
||||
executionId: 'executionId',
|
||||
workflowId: 'workflowId',
|
||||
shouldResume: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('should execute workflow in "once" mode and not wait for sub-workflow completion', async () => {
|
||||
executeFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('database') // source
|
||||
.mockReturnValueOnce('once') // mode
|
||||
.mockReturnValueOnce({}) // workflowInputs.value
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(false); // waitForSubWorkflow
|
||||
|
||||
executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]);
|
||||
(getWorkflowInfo as jest.Mock).mockResolvedValue({ id: 'subWorkflowId' });
|
||||
|
||||
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 }, binary: undefined }],
|
||||
]);
|
||||
|
||||
// Verify shouldResume is set to false
|
||||
expect(executeFunctions.executeWorkflow).toHaveBeenCalledWith(
|
||||
{ id: 'subWorkflowId' },
|
||||
[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 }, binary: undefined }],
|
||||
undefined,
|
||||
{
|
||||
doNotWaitToFinish: true,
|
||||
parentExecution: {
|
||||
executionId: 'executionId',
|
||||
workflowId: 'workflowId',
|
||||
shouldResume: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle errors and continue on fail, no items, < 1.3 version', async () => {
|
||||
executeFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('database') // source
|
||||
.mockReturnValueOnce('each') // mode
|
||||
.mockReturnValueOnce({}) // workflowInputs.value
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(true); // waitForSubWorkflow
|
||||
|
||||
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode);
|
||||
|
||||
(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 handle errors and continue on fail, multiple items, < 1.3 version', async () => {
|
||||
executeFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('database') // source
|
||||
.mockReturnValueOnce('each') // mode
|
||||
.mockReturnValueOnce({}) // workflowInputs.value (item 0)
|
||||
.mockReturnValueOnce({}) // workflowInputs.value (item 1)
|
||||
.mockReturnValueOnce({}) // workflowInputs.value (item 2)
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(true) // waitForSubWorkflow (item 0)
|
||||
.mockReturnValueOnce(true) // waitForSubWorkflow (item 1)
|
||||
.mockReturnValueOnce(true); // waitForSubWorkflow (item 2)
|
||||
|
||||
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode);
|
||||
executeFunctions.getInputData.mockReturnValueOnce([
|
||||
{ json: { key: '1' } },
|
||||
{ json: { key: '2' } },
|
||||
{ json: { key: '3' } },
|
||||
]);
|
||||
|
||||
(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 }, metadata: undefined }],
|
||||
[{ json: { error: 'Test error' }, pairedItem: { item: 1 }, metadata: undefined }],
|
||||
[{ json: { error: 'Test error' }, pairedItem: { item: 2 }, metadata: undefined }],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle errors and continue on fail, no items, >= 1.3 version', async () => {
|
||||
executeFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('database') // source
|
||||
.mockReturnValueOnce('each') // mode
|
||||
.mockReturnValueOnce({}) // workflowInputs.value
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(true); // waitForSubWorkflow
|
||||
|
||||
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode);
|
||||
|
||||
(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 handle errors and continue on fail, multiple items, >= 1.3 version', async () => {
|
||||
executeFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('database') // source
|
||||
.mockReturnValueOnce('each') // mode
|
||||
.mockReturnValueOnce({}) // workflowInputs.value (item 0)
|
||||
.mockReturnValueOnce({}) // workflowInputs.value (item 1)
|
||||
.mockReturnValueOnce({}) // workflowInputs.value (item 2)
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(true) // waitForSubWorkflow (item 0)
|
||||
.mockReturnValueOnce(true) // waitForSubWorkflow (item 1)
|
||||
.mockReturnValueOnce(true); // waitForSubWorkflow (item 2)
|
||||
|
||||
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode);
|
||||
executeFunctions.getInputData.mockReturnValueOnce([
|
||||
{ json: { key: '1' } },
|
||||
{ json: { key: '2' } },
|
||||
{ json: { key: '3' } },
|
||||
]);
|
||||
|
||||
(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 }, metadata: undefined },
|
||||
{ json: { error: 'Test error' }, pairedItem: { item: 1 }, metadata: undefined },
|
||||
{ json: { error: 'Test error' }, pairedItem: { item: 2 }, metadata: undefined },
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should throw error if not continuing on fail', async () => {
|
||||
executeFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('database') // source
|
||||
.mockReturnValueOnce('each') // mode
|
||||
.mockReturnValueOnce({}) // workflowInputs.value
|
||||
.mockReturnValueOnce([]) // workflowInputs.schema
|
||||
.mockReturnValueOnce(true); // waitForSubWorkflow
|
||||
|
||||
(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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,493 @@
|
||||
import { NodeConnectionTypes, NodeOperationError, parseErrorMetadata } from 'n8n-workflow';
|
||||
import type {
|
||||
ExecuteWorkflowData,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { findPairedItemThroughWorkflowData } from './../../../utils/workflow-backtracking';
|
||||
import { getWorkflowInfo } from './GenericFunctions';
|
||||
import { localResourceMapping } from './methods';
|
||||
import { generatePairedItemData } from '../../../utils/utilities';
|
||||
import { getCurrentWorkflowInputData } from '../../../utils/workflowInputsResourceMapping/GenericFunctions';
|
||||
|
||||
export class ExecuteWorkflow implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Execute Sub-workflow',
|
||||
name: 'executeWorkflow',
|
||||
icon: 'fa:sign-in-alt',
|
||||
iconColor: 'orange-red',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
|
||||
description: 'Execute another workflow',
|
||||
defaults: {
|
||||
name: 'Execute Workflow',
|
||||
color: '#ff6d5a',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'hidden',
|
||||
noDataExpression: true,
|
||||
default: 'call_workflow',
|
||||
options: [
|
||||
{
|
||||
name: 'Execute a Sub-Workflow',
|
||||
value: 'call_workflow',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'This node is out of date. Please upgrade by removing it and adding a new one',
|
||||
name: 'outdatedVersionWarning',
|
||||
type: 'notice',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Source',
|
||||
name: 'source',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Database',
|
||||
value: 'database',
|
||||
description: 'Load the workflow from the database by ID',
|
||||
},
|
||||
{
|
||||
name: 'Local File',
|
||||
value: 'localFile',
|
||||
description: 'Load the workflow from a locally saved file',
|
||||
},
|
||||
{
|
||||
name: 'Parameter',
|
||||
value: 'parameter',
|
||||
description: 'Load the workflow from a parameter',
|
||||
},
|
||||
{
|
||||
name: 'URL',
|
||||
value: 'url',
|
||||
description: 'Load the workflow from an URL',
|
||||
},
|
||||
],
|
||||
default: 'database',
|
||||
description: 'Where to get the workflow to execute from',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Source',
|
||||
name: 'source',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Database',
|
||||
value: 'database',
|
||||
description: 'Load the workflow from the database by ID',
|
||||
},
|
||||
{
|
||||
name: 'Define Below',
|
||||
value: 'parameter',
|
||||
description: 'Pass the JSON code of a workflow',
|
||||
},
|
||||
],
|
||||
default: 'database',
|
||||
description: 'Where to get the workflow to execute from',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.2 } }] } },
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// source:database
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Workflow ID',
|
||||
name: 'workflowId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
hint: 'Can be found in the URL of the workflow',
|
||||
description:
|
||||
"Note on using an expression here: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow',
|
||||
name: 'workflowId',
|
||||
type: 'workflowSelector',
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
// ----------------------------------
|
||||
// source:localFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Workflow Path',
|
||||
name: 'workflowPath',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['localFile'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: '/data/workflow.json',
|
||||
required: true,
|
||||
description: 'The path to local JSON workflow file to execute',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// source:parameter
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Workflow JSON',
|
||||
name: 'workflowJson',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['parameter'],
|
||||
},
|
||||
},
|
||||
default: '\n\n\n',
|
||||
required: true,
|
||||
description: 'The workflow JSON code to execute',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// source:url
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Workflow URL',
|
||||
name: 'workflowUrl',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['url'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'https://example.com/workflow.json',
|
||||
required: true,
|
||||
description: 'The URL from which to load the workflow from',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Any data you pass into this node will be output by the Execute Workflow Trigger. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/" target="_blank">More info</a>',
|
||||
name: 'executeWorkflowNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow Inputs',
|
||||
name: 'workflowInputs',
|
||||
type: 'resourceMapper',
|
||||
noDataExpression: true,
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['workflowId.value'],
|
||||
resourceMapper: {
|
||||
localResourceMapperMethod: 'loadSubWorkflowInputs',
|
||||
valuesLabel: 'Workflow Inputs',
|
||||
mode: 'map',
|
||||
fieldWords: {
|
||||
singular: 'input',
|
||||
plural: 'inputs',
|
||||
},
|
||||
addAllFields: true,
|
||||
multiKeyMatch: false,
|
||||
supportAutoMap: false,
|
||||
showTypeConversionOptions: true,
|
||||
},
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
hide: {
|
||||
workflowId: [''],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Run once with all items',
|
||||
value: 'once',
|
||||
description: 'Pass all items into a single execution of the sub-workflow',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Run once for each item',
|
||||
value: 'each',
|
||||
description: 'Call the sub-workflow individually for each item',
|
||||
},
|
||||
],
|
||||
default: 'once',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
placeholder: 'Add option',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Wait For Sub-Workflow Completion',
|
||||
name: 'waitForSubWorkflow',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether the main workflow should wait for the sub-workflow to complete its execution before proceeding',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hints: [
|
||||
{
|
||||
type: 'info',
|
||||
message:
|
||||
"Note on using an expression for workflow ID: Since this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
|
||||
displayCondition:
|
||||
'={{ $rawParameter.workflowId.startsWith("=") && $parameter.mode === "once" && $nodeVersion >= 1.2 }}',
|
||||
whenToDisplay: 'always',
|
||||
location: 'outputPane',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
localResourceMapping,
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const source = this.getNodeParameter('source', 0) as string;
|
||||
const mode = this.getNodeParameter('mode', 0, false) as string;
|
||||
const items = getCurrentWorkflowInputData.call(this);
|
||||
|
||||
const workflowProxy = this.getWorkflowDataProxy(0);
|
||||
const currentWorkflowId = workflowProxy.$workflow.id as string;
|
||||
|
||||
if (mode === 'each') {
|
||||
const returnData: INodeExecutionData[][] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const waitForSubWorkflow = this.getNodeParameter(
|
||||
'options.waitForSubWorkflow',
|
||||
i,
|
||||
true,
|
||||
) as boolean;
|
||||
const workflowInfo = await getWorkflowInfo.call(this, source, i);
|
||||
|
||||
if (waitForSubWorkflow) {
|
||||
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
|
||||
workflowInfo,
|
||||
[items[i]],
|
||||
undefined,
|
||||
{
|
||||
parentExecution: {
|
||||
executionId: workflowProxy.$execution.id,
|
||||
workflowId: workflowProxy.$workflow.id,
|
||||
shouldResume: waitForSubWorkflow,
|
||||
},
|
||||
executionMode: this.getMode(),
|
||||
},
|
||||
);
|
||||
const workflowResult = executionResult.data as INodeExecutionData[][];
|
||||
|
||||
for (const [outputIndex, outputData] of workflowResult.entries()) {
|
||||
for (const item of outputData) {
|
||||
item.pairedItem = { item: i };
|
||||
item.metadata = {
|
||||
subExecution: {
|
||||
executionId: executionResult.executionId,
|
||||
workflowId: workflowInfo.id ?? currentWorkflowId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (returnData[outputIndex] === undefined) {
|
||||
returnData[outputIndex] = [];
|
||||
}
|
||||
|
||||
returnData[outputIndex].push(...outputData);
|
||||
}
|
||||
} else {
|
||||
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
|
||||
workflowInfo,
|
||||
[items[i]],
|
||||
undefined,
|
||||
{
|
||||
doNotWaitToFinish: true,
|
||||
parentExecution: {
|
||||
executionId: workflowProxy.$execution.id,
|
||||
workflowId: workflowProxy.$workflow.id,
|
||||
shouldResume: waitForSubWorkflow,
|
||||
},
|
||||
executionMode: this.getMode(),
|
||||
},
|
||||
);
|
||||
|
||||
if (returnData.length === 0) {
|
||||
returnData.push([]);
|
||||
}
|
||||
|
||||
returnData[0].push({
|
||||
...items[i],
|
||||
metadata: {
|
||||
subExecution: {
|
||||
workflowId: workflowInfo.id ?? currentWorkflowId,
|
||||
executionId: executionResult.executionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
// In versions < 1.3 using the "Continue (using error output)" mode
|
||||
// the node would return items in extra "error branches" instead of
|
||||
// returning an array of items on the error output. These branches weren't really shown correctly on the UI.
|
||||
// In the fixed >= 1.3 versions the errors are now all output into the single error output as an array of error items.
|
||||
const outputIndex = nodeVersion >= 1.3 ? 0 : i;
|
||||
|
||||
returnData[outputIndex] ??= [];
|
||||
const metadata = parseErrorMetadata(error);
|
||||
returnData[outputIndex].push({
|
||||
json: { error: error.message },
|
||||
pairedItem: { item: i },
|
||||
metadata,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw new NodeOperationError(this.getNode(), error, {
|
||||
message: `Error executing workflow with item at index ${i}`,
|
||||
description: error.message,
|
||||
itemIndex: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setMetadata({
|
||||
subExecutionsCount: items.length,
|
||||
});
|
||||
|
||||
return returnData;
|
||||
} else {
|
||||
try {
|
||||
const waitForSubWorkflow = this.getNodeParameter(
|
||||
'options.waitForSubWorkflow',
|
||||
0,
|
||||
true,
|
||||
) as boolean;
|
||||
const workflowInfo = await getWorkflowInfo.call(this, source);
|
||||
|
||||
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
|
||||
workflowInfo,
|
||||
items,
|
||||
undefined,
|
||||
{
|
||||
doNotWaitToFinish: !waitForSubWorkflow,
|
||||
parentExecution: {
|
||||
executionId: workflowProxy.$execution.id,
|
||||
workflowId: workflowProxy.$workflow.id,
|
||||
shouldResume: waitForSubWorkflow,
|
||||
},
|
||||
executionMode: this.getMode(),
|
||||
},
|
||||
);
|
||||
|
||||
this.setMetadata({
|
||||
subExecution: {
|
||||
executionId: executionResult.executionId,
|
||||
workflowId: workflowInfo.id ?? (workflowProxy.$workflow.id as string),
|
||||
},
|
||||
subExecutionsCount: 1,
|
||||
});
|
||||
|
||||
if (!waitForSubWorkflow) {
|
||||
return [items];
|
||||
}
|
||||
|
||||
const workflowRunData = await this.getExecutionDataById(executionResult.executionId);
|
||||
|
||||
const workflowResult = executionResult.data as INodeExecutionData[][];
|
||||
|
||||
const fallbackPairedItemData = generatePairedItemData(items.length);
|
||||
|
||||
for (const output of workflowResult) {
|
||||
const sameLength = output.length === items.length;
|
||||
|
||||
for (const [itemIndex, item] of output.entries()) {
|
||||
if (item.pairedItem) {
|
||||
// If the item already has a paired item, we need to follow these to the start of the child workflow
|
||||
if (workflowRunData !== undefined) {
|
||||
const pairedItem = findPairedItemThroughWorkflowData(
|
||||
workflowRunData,
|
||||
item,
|
||||
itemIndex,
|
||||
);
|
||||
if (pairedItem !== undefined) {
|
||||
item.pairedItem = pairedItem;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sameLength) {
|
||||
item.pairedItem = { item: itemIndex };
|
||||
} else {
|
||||
item.pairedItem = fallbackPairedItemData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return workflowResult;
|
||||
} catch (error) {
|
||||
const pairedItem = generatePairedItemData(items.length);
|
||||
if (this.continueOnFail()) {
|
||||
const metadata = parseErrorMetadata(error);
|
||||
return [
|
||||
[
|
||||
{
|
||||
json: { error: error.message },
|
||||
metadata,
|
||||
pairedItem,
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { mockDeep, type DeepMockProxy } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import { getWorkflowInfo } from './GenericFunctions';
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
readFile: jest.fn().mockResolvedValue('sensitive data'),
|
||||
}));
|
||||
|
||||
describe('ExecuteWorkflow node - GenericFunctions', () => {
|
||||
let executeFunctionsMock: DeepMockProxy<ILoadOptionsFunctions | IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
executeFunctionsMock = mockDeep<ILoadOptionsFunctions | IExecuteFunctions>();
|
||||
});
|
||||
|
||||
describe('getWorkflowInfo', () => {
|
||||
it('should throw an error without the file content when source is localFile and the file is not json', async () => {
|
||||
executeFunctionsMock.getNode.mockReturnValue({
|
||||
typeVersion: 1,
|
||||
} as INode);
|
||||
executeFunctionsMock.getNodeParameter.mockReturnValue('path/to/file');
|
||||
|
||||
await expect(getWorkflowInfo.call(executeFunctionsMock, 'localFile', 0)).rejects.toThrow(
|
||||
'The file content is not valid JSON',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { readFile as fsReadFile } from 'fs/promises';
|
||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
ILoadOptionsFunctions,
|
||||
INodeParameterResourceLocator,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function getWorkflowInfo(
|
||||
this: ILoadOptionsFunctions | IExecuteFunctions,
|
||||
source: string,
|
||||
itemIndex = 0,
|
||||
) {
|
||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
if (source === 'database') {
|
||||
// Read workflow from database
|
||||
if (nodeVersion === 1) {
|
||||
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
||||
} else {
|
||||
const { value } = this.getNodeParameter(
|
||||
'workflowId',
|
||||
itemIndex,
|
||||
{},
|
||||
) as INodeParameterResourceLocator;
|
||||
workflowInfo.id = value as string;
|
||||
}
|
||||
} else if (source === 'localFile') {
|
||||
// Read workflow from filesystem
|
||||
const workflowPath = this.getNodeParameter('workflowPath', itemIndex) as string;
|
||||
|
||||
let workflowJson;
|
||||
try {
|
||||
workflowJson = await fsReadFile(workflowPath, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The file "${workflowPath}" could not be found, [item ${itemIndex}]`,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
workflowInfo.code = jsonParse(workflowJson, {
|
||||
errorMessage: 'The file content is not valid JSON', // pass a custom error message to not expose the file contents
|
||||
});
|
||||
} else if (source === 'parameter') {
|
||||
// Read workflow from parameter
|
||||
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
||||
workflowInfo.code = jsonParse(workflowJson);
|
||||
} else if (source === 'url') {
|
||||
// Read workflow from url
|
||||
const workflowUrl = this.getNodeParameter('workflowUrl', itemIndex) as string;
|
||||
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
accept: 'application/json,text/*;q=0.99',
|
||||
},
|
||||
method: 'GET',
|
||||
uri: workflowUrl,
|
||||
json: true,
|
||||
gzip: true,
|
||||
} satisfies IRequestOptions;
|
||||
|
||||
const response = await this.helpers.request(requestOptions);
|
||||
workflowInfo.code = response;
|
||||
}
|
||||
|
||||
return workflowInfo;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as localResourceMapping from './localResourceMapping';
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow';
|
||||
|
||||
import { loadWorkflowInputMappings } from '@utils/workflowInputsResourceMapping/GenericFunctions';
|
||||
|
||||
export async function loadSubWorkflowInputs(
|
||||
this: ILocalLoadOptionsFunctions,
|
||||
): Promise<ResourceMapperFields> {
|
||||
const { fields, dataMode, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
|
||||
let emptyFieldsNotice: string | undefined;
|
||||
if (fields.length === 0) {
|
||||
const { triggerId, workflowId } = subworkflowInfo ?? {};
|
||||
const path = (workflowId ?? '') + (triggerId ? `/${triggerId.slice(0, 6)}` : '');
|
||||
const subworkflowLink = workflowId
|
||||
? `<a href="/workflow/${path}" target="_blank">sub-workflow’s trigger</a>`
|
||||
: 'sub-workflow’s trigger';
|
||||
|
||||
switch (dataMode) {
|
||||
case 'passthrough':
|
||||
emptyFieldsNotice = `This sub-workflow will consume all input data passed to it. You can define specific expected input in the ${subworkflowLink}.`;
|
||||
break;
|
||||
default:
|
||||
emptyFieldsNotice = `The sub-workflow isn't set up to accept any inputs. Change this in the ${subworkflowLink}.`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { fields, emptyFieldsNotice };
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflowtrigger/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
},
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Helpers"]
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { FieldValueOption, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import { ExecuteWorkflowTrigger } from './ExecuteWorkflowTrigger.node';
|
||||
import { WORKFLOW_INPUTS } from '../../../utils/workflowInputsResourceMapping/constants';
|
||||
import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions';
|
||||
|
||||
jest.mock('../../../utils/workflowInputsResourceMapping/GenericFunctions', () => ({
|
||||
getFieldEntries: jest.fn(),
|
||||
getWorkflowInputData: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ExecuteWorkflowTrigger', () => {
|
||||
const mockInputData: INodeExecutionData[] = [
|
||||
{ json: { item: 0, foo: 'bar' }, index: 0 },
|
||||
{ json: { item: 1, foo: 'quz' }, index: 1 },
|
||||
];
|
||||
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 or V1.1 passthrough', async () => {
|
||||
// User selection in V1.1, or fallback return value in V1 with dropdown not displayed
|
||||
executeFns.getNodeParameter.mockReturnValueOnce('passthrough');
|
||||
const result = await new ExecuteWorkflowTrigger().execute.call(executeFns);
|
||||
|
||||
expect(result).toEqual([mockInputData]);
|
||||
});
|
||||
|
||||
it('should filter out parent input in `Using Fields below` mode', async () => {
|
||||
executeFns.getNodeParameter.mockReturnValueOnce(WORKFLOW_INPUTS);
|
||||
const mockNewParams: {
|
||||
fields: FieldValueOption[];
|
||||
noFieldsMessage?: string;
|
||||
} = {
|
||||
fields: [
|
||||
{ name: 'value1', type: 'string' },
|
||||
{ name: 'value2', type: 'number' },
|
||||
{ name: 'foo', type: 'string' },
|
||||
],
|
||||
};
|
||||
const getFieldEntriesMock = (getFieldEntries as jest.Mock).mockReturnValue(mockNewParams);
|
||||
|
||||
const result = await new ExecuteWorkflowTrigger().execute.call(executeFns);
|
||||
const expected = [
|
||||
[
|
||||
{ index: 0, json: { value1: null, value2: null, foo: mockInputData[0].json.foo } },
|
||||
{ index: 1, json: { value1: null, value2: null, foo: mockInputData[1].json.foo } },
|
||||
],
|
||||
];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
expect(getFieldEntriesMock).toHaveBeenCalledWith(executeFns);
|
||||
});
|
||||
});
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import {
|
||||
type INodeExecutionData,
|
||||
NodeConnectionTypes,
|
||||
type IExecuteFunctions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ITriggerFunctions,
|
||||
type ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
INPUT_SOURCE,
|
||||
WORKFLOW_INPUTS,
|
||||
JSON_EXAMPLE,
|
||||
VALUES,
|
||||
TYPE_OPTIONS,
|
||||
PASSTHROUGH,
|
||||
FALLBACK_DEFAULT_VALUE,
|
||||
} from '../../../utils/workflowInputsResourceMapping/constants';
|
||||
import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions';
|
||||
|
||||
export class ExecuteWorkflowTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Execute Workflow Trigger',
|
||||
name: 'executeWorkflowTrigger',
|
||||
icon: 'fa:sign-out-alt',
|
||||
group: ['trigger'],
|
||||
version: [1, 1.1],
|
||||
description:
|
||||
'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.',
|
||||
eventTriggerDescription: '',
|
||||
maxNodes: 1,
|
||||
defaults: {
|
||||
name: 'When Executed by Another Workflow',
|
||||
color: '#ff6d5a',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
hints: [
|
||||
{
|
||||
message:
|
||||
"This workflow isn't set to accept any input data. Fill out the workflow input schema or change the workflow to accept any data passed to it.",
|
||||
// This condition checks if we have no input fields, which gets a bit awkward:
|
||||
// For WORKFLOW_INPUTS: keys() only contains `VALUES` if at least one value is provided
|
||||
// For JSON_EXAMPLE: We remove all whitespace and check if we're left with an empty object. Note that we already error if the example is not valid JSON
|
||||
displayCondition:
|
||||
`={{$parameter['${INPUT_SOURCE}'] === '${WORKFLOW_INPUTS}' && !$parameter['${WORKFLOW_INPUTS}'].keys().length ` +
|
||||
`|| $parameter['${INPUT_SOURCE}'] === '${JSON_EXAMPLE}' && $parameter['${JSON_EXAMPLE}'].toString().replaceAll(' ', '').replaceAll('\\n', '') === '{}' }}`,
|
||||
whenToDisplay: 'always',
|
||||
location: 'ndv',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'hidden',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Workflow Call',
|
||||
value: 'worklfow_call',
|
||||
description: 'When executed by another workflow using Execute Workflow Trigger',
|
||||
action: 'When executed by Another Workflow',
|
||||
},
|
||||
],
|
||||
default: 'worklfow_call',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
"When an ‘execute workflow’ node calls this workflow, the execution starts here. Any data passed into the 'execute workflow' node will be output by this node.",
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { eq: 1 } }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'This node is out of date. Please upgrade by removing it and adding a new one',
|
||||
name: 'outdatedVersionWarning',
|
||||
type: 'notice',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { eq: 1 } }] } },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
displayName: 'Input data mode',
|
||||
name: INPUT_SOURCE,
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Define using fields below',
|
||||
value: WORKFLOW_INPUTS,
|
||||
description: 'Provide input fields via UI',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Define using JSON example',
|
||||
value: JSON_EXAMPLE,
|
||||
description: 'Generate a schema from an example JSON object',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Accept all data',
|
||||
value: PASSTHROUGH,
|
||||
description: 'Use all incoming data from the parent workflow',
|
||||
},
|
||||
],
|
||||
default: WORKFLOW_INPUTS,
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Provide an example object to infer fields and their types.<br>To allow any type for a given field, set the value to null.',
|
||||
name: `${JSON_EXAMPLE}_notice`,
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Example',
|
||||
name: JSON_EXAMPLE,
|
||||
type: 'json',
|
||||
default: JSON.stringify(
|
||||
{
|
||||
aField: 'a string',
|
||||
aNumber: 123,
|
||||
thisFieldAcceptsAnyType: null,
|
||||
anArray: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow Input Schema',
|
||||
name: WORKFLOW_INPUTS,
|
||||
placeholder: 'Add field',
|
||||
type: 'fixedCollection',
|
||||
description:
|
||||
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
minRequiredFields: 1,
|
||||
},
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [WORKFLOW_INPUTS] },
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: VALUES,
|
||||
displayName: 'Values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. fieldName',
|
||||
description:
|
||||
'A unique name for this workflow input, used to reference it from another workflows',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
description:
|
||||
"Expected data type for this input value. Determines how this field's values are stored, validated, and displayed.",
|
||||
options: TYPE_OPTIONS,
|
||||
required: true,
|
||||
default: 'string',
|
||||
noDataExpression: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||
// ExecuteWorkflowTrigger is triggered by the ExecuteWorkflow node
|
||||
// No setup or teardown is required, as the triggering is handled externally
|
||||
return {};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const inputData = this.getInputData();
|
||||
const inputSource = this.getNodeParameter(INPUT_SOURCE, 0, PASSTHROUGH) as string;
|
||||
|
||||
// Note on the data we receive from ExecuteWorkflow caller:
|
||||
//
|
||||
// The ExecuteWorkflow node typechecks all fields explicitly provided by the user here via the resourceMapper
|
||||
// and removes all fields that are in the schema, but `removed` in the resourceMapper.
|
||||
//
|
||||
// In passthrough and legacy node versions, inputData will line up since the resourceMapper is empty,
|
||||
// in which case all input is passed through.
|
||||
// In other cases we will already have matching types and fields provided by the resource mapper,
|
||||
// so we just need to be permissive on this end,
|
||||
// while ensuring we provide default values for fields in our schema, which are removed in the resourceMapper.
|
||||
|
||||
if (inputSource === PASSTHROUGH) {
|
||||
return [inputData];
|
||||
} else {
|
||||
const newParams = getFieldEntries(this);
|
||||
const newKeys = new Set(newParams.fields.map((x) => x.name));
|
||||
const itemsInSchema: INodeExecutionData[] = inputData.map(({ json, binary }, index) => ({
|
||||
json: {
|
||||
...Object.fromEntries(newParams.fields.map((x) => [x.name, FALLBACK_DEFAULT_VALUE])),
|
||||
// Need to trim to the expected schema to support legacy Execute Workflow callers passing through all their data
|
||||
// which we do not want to expose past this node.
|
||||
...pickBy(json, (_value, key) => newKeys.has(key)),
|
||||
},
|
||||
index,
|
||||
binary,
|
||||
}));
|
||||
|
||||
return [itemsInSchema];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user