From 7012d9bb9c06ff8f89834f5e2d909f8ccab0cf40 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Tue, 26 Nov 2024 12:38:06 +0100 Subject: [PATCH] feat(Execute Workflow Trigger Node): Add MVP for explicit input parameters (#11874) --- .../ExecuteWorkflowTrigger.node.ts | 130 +++++++++++++++++- .../test/ExecuteWorkflowTrigger.node.test.ts | 4 +- 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts index feb33a160d..ecb471a610 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts @@ -1,17 +1,22 @@ import { + type INodeExecutionData, NodeConnectionType, + NodeOperationError, type IExecuteFunctions, type INodeType, type INodeTypeDescription, } from 'n8n-workflow'; +const WORKFLOW_INPUTS = 'workflowInputs'; +const VALUES = 'values'; + export class ExecuteWorkflowTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Execute Workflow Trigger', name: 'executeWorkflowTrigger', icon: 'fa:sign-out-alt', group: ['trigger'], - version: 1, + version: [1, 1.1], description: 'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.', eventTriggerDescription: '', @@ -46,10 +51,131 @@ export class ExecuteWorkflowTrigger implements INodeType { ], default: 'worklfow_call', }, + { + displayName: 'Workflow Inputs', + 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, + }, + displayOptions: { + show: { '@version': [{ _cnd: { gte: 1.1 } }] }, + }, + default: {}, + options: [ + { + name: VALUES, + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: 'Name of the field', + }, + // { + // displayName: 'Type', + // name: 'type', + // type: 'options', + // description: 'The field value type', + // // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items + // options: [ + // { + // name: 'String', + // value: 'stringValue', + // }, + // { + // name: 'Number', + // value: 'numberValue', + // }, + // { + // name: 'Boolean', + // value: 'booleanValue', + // }, + // { + // name: 'Array', + // value: 'arrayValue', + // }, + // { + // name: 'Object', + // value: 'objectValue', + // }, + // ], + // default: 'stringValue', + // }, + ], + }, + ], + }, ], }; async execute(this: IExecuteFunctions) { - return [this.getInputData()]; + const inputData = this.getInputData(); + + if (this.getNode().typeVersion < 1.1) { + return [inputData]; + } else { + // Need to mask type due to bad `getNodeParameter` typing + const marker = Symbol() as unknown as object; + const hasFields = + inputData.length >= 0 && + inputData.some( + (_x, i) => this.getNodeParameter(`${WORKFLOW_INPUTS}.${VALUES}`, i, marker) !== marker, + ); + + if (!hasFields) { + return [inputData]; + } + + const items: INodeExecutionData[] = []; + + for (const [itemIndex, item] of inputData.entries()) { + // Fields listed here will explicitly overwrite original fields + const newItem: INodeExecutionData = { + json: {}, + // TODO: Ensure we handle sub-execution jumps correctly. + // metadata: { + // subExecution: { + // executionId: 'uhh', + // workflowId: 'maybe?', + // }, + // }, + pairedItem: { item: itemIndex }, + }; + try { + const newParams = this.getNodeParameter( + `${WORKFLOW_INPUTS}.${VALUES}`, + itemIndex, + [], + ) as Array<{ + name: string; + }>; + for (const { name } of newParams) { + /** TODO type check goes here */ + newItem.json[name] = name in item.json ? item.json[name] : /* TODO default */ null; + } + + // TODO Do we want to copy non-json data (e.g. binary) as well? + items.push(Object.assign({}, item, newItem)); + } catch (error) { + if (this.continueOnFail()) { + /** todo error case? */ + } else { + throw new NodeOperationError(this.getNode(), error, { + itemIndex, + }); + } + } + } + + return [items]; + } } } diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts index ad35bff192..8a4b1cc8d5 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/test/ExecuteWorkflowTrigger.node.test.ts @@ -1,5 +1,5 @@ import { mock } from 'jest-mock-extended'; -import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import type { IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow'; import { ExecuteWorkflowTrigger } from '../ExecuteWorkflowTrigger.node'; @@ -9,8 +9,10 @@ describe('ExecuteWorkflowTrigger', () => { { json: { item: 0, foo: 'bar' } }, { json: { item: 1, foo: 'quz' } }, ]; + const mockNode = { typeVersion: 1 } as INode; const executeFns = mock({ getInputData: () => mockInputData, + getNode: () => mockNode, }); const result = await new ExecuteWorkflowTrigger().execute.call(executeFns);