diff --git a/packages/workflow/src/workflow-data-proxy.ts b/packages/workflow/src/workflow-data-proxy.ts index da5a648a76..5e1b130bff 100644 --- a/packages/workflow/src/workflow-data-proxy.ts +++ b/packages/workflow/src/workflow-data-proxy.ts @@ -912,106 +912,80 @@ export class WorkflowDataProxy { return outputs; } - function resolveMultiplePairings( - pairings: IPairedItemData[], - source: ISourceData[], - destinationNode: string, - method: PairedItemMethod, - itemIndex: number, - ): INodeExecutionData { - const results = pairings - .map((pairing) => { - try { - const input = pairing.input || 0; - if (input >= source.length) { - // Could not resolve pairedItem as the defined node input does not exist on source.previousNode. - return null; - } - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return getPairedItem(destinationNode, source[input], pairing, method); - } catch { - return null; - } - }) - .filter(Boolean) as INodeExecutionData[]; + const normalizePairedItem = ( + paired: number | IPairedItemData | Array | null | undefined, + ): IPairedItemData[] => { + if (paired === null || paired === undefined) { + return []; + } - if (results.length === 1) return results[0]; + const pairedItems = Array.isArray(paired) ? paired : [paired]; - const allSame = results.every((r) => r === results[0]); - if (allSame) return results[0]; + return pairedItems.map((p) => (typeof p === 'number' ? { item: p } : p)); + }; - throw createPairedItemMultipleItemsFound(destinationNode, itemIndex); - } - - /** - * Attempts to find the execution data for a specific paired item - * by traversing the node execution ancestry chain. - */ const getPairedItem = ( destinationNodeName: string, incomingSourceData: ISourceData | null, initialPairedItem: IPairedItemData, usedMethodName: PairedItemMethod = PAIRED_ITEM_METHOD.$GET_PAIRED_ITEM, - ): INodeExecutionData | null => { - // Step 1: Normalize inputs + nodeBeforeLast?: string, + ): INodeExecutionData => { + // Normalize inputs const [pairedItem, sourceData] = normalizeInputs(initialPairedItem, incomingSourceData); - let currentPairedItem = pairedItem; - let currentSource = sourceData; - let nodeBeforeLast: string | undefined; - - // Step 2: Traverse ancestry to find correct node - while (currentSource && currentSource.previousNode !== destinationNodeName) { - const taskData = getTaskData(currentSource); - - const outputData = getNodeOutput(taskData, currentSource, nodeBeforeLast); - const sourceArray = taskData?.source.filter((s): s is ISourceData => s !== null) ?? []; - - const item = outputData[currentPairedItem.item]; - if (item?.pairedItem === undefined) { - throw createMissingPairedItemError(currentSource.previousNode, usedMethodName); - } - - // Multiple pairings? Recurse over all - if (Array.isArray(item.pairedItem)) { - return resolveMultiplePairings( - item.pairedItem, - sourceArray, - destinationNodeName, - usedMethodName, - currentPairedItem.item, - ); - } - - // Follow single paired item - currentPairedItem = - typeof item.pairedItem === 'number' ? { item: item.pairedItem } : item.pairedItem; - - const inputIndex = currentPairedItem.input || 0; - if (inputIndex >= sourceArray.length) { - if (sourceArray.length === 0) throw createNoConnectionError(destinationNodeName); - throw createBranchNotFoundError( - currentSource.previousNode, - currentPairedItem.item, - nodeBeforeLast, - ); - } - - nodeBeforeLast = currentSource.previousNode; - currentSource = currentPairedItem.sourceOverwrite || sourceArray[inputIndex]; + if (!sourceData) { + throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast); } - // Step 3: Final node reached — fetch paired item - if (!currentSource) throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast); + const taskData = getTaskData(sourceData); + const outputData = getNodeOutput(taskData, sourceData, nodeBeforeLast); + const item = outputData[pairedItem.item]; + const sourceArray = taskData?.source ?? []; - const finalTaskData = getTaskData(currentSource); - const finalOutputData = getNodeOutput(finalTaskData, currentSource); + // Done: reached the destination node in the ancestry chain + if (sourceData.previousNode === destinationNodeName) { + if (pairedItem.item >= outputData.length) { + throw createInvalidPairedItemError({ nodeName: sourceData.previousNode }); + } - if (currentPairedItem.item >= finalOutputData.length) { - throw createInvalidPairedItemError({ nodeName: currentSource.previousNode }); + return item; } - return finalOutputData[currentPairedItem.item]; + // Normalize paired item to always be IPairedItemData[] + const nextPairedItems = normalizePairedItem(item.pairedItem); + + if (nextPairedItems.length === 0) { + throw createMissingPairedItemError(sourceData.previousNode, usedMethodName); + } + + // Recursively traverse ancestry to find the destination node + paired item + const results = nextPairedItems.flatMap((nextPairedItem) => { + const inputIndex = nextPairedItem.input ?? 0; + + if (inputIndex >= sourceArray.length) return []; + + const nextSource = nextPairedItem.sourceOverwrite ?? sourceArray[inputIndex]; + return getPairedItem( + destinationNodeName, + nextSource, + { ...nextPairedItem, input: inputIndex }, + usedMethodName, + sourceData.previousNode, + ); + }); + + if (results.length === 0) { + if (sourceArray.length === 0) throw createNoConnectionError(destinationNodeName); + throw createBranchNotFoundError(sourceData.previousNode, pairedItem.item, nodeBeforeLast); + } + + const [first, ...rest] = results; + if (rest.some((r) => r !== first)) { + throw createPairedItemMultipleItemsFound(destinationNodeName, pairedItem.item); + } + + return first; }; const handleFromAi = ( diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json b/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json new file mode 100644 index 0000000000..a1638bee70 --- /dev/null +++ b/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json @@ -0,0 +1,114 @@ +{ + "data": { + "startData": {}, + "resultData": { + "runData": { + "Manual Trigger": [ + { + "startTime": 1749486952181, + "executionIndex": 0, + "source": [], + "hints": [], + "executionTime": 2, + "executionStatus": "success", + "data": { "main": [[{ "json": {}, "pairedItem": { "item": 0 } }]] } + } + ], + "Set main variable": [ + { + "startTime": 1749486952183, + "executionIndex": 1, + "source": [{ "previousNode": "Manual Trigger" }], + "hints": [], + "executionTime": 2, + "executionStatus": "success", + "data": { "main": [[{ "json": { "main_variable": 2 }, "pairedItem": { "item": 0 } }]] } + } + ], + "Set variable_1": [ + { + "startTime": 1749486952185, + "executionIndex": 2, + "source": [{ "previousNode": "Set main variable" }], + "hints": [], + "executionTime": 0, + "executionStatus": "success", + "data": { + "main": [[{ "json": { "variable_1": "1234" }, "pairedItem": { "item": 0 } }]] + } + } + ], + "Set variable_2": [ + { + "startTime": 1749486952186, + "executionIndex": 3, + "source": [{ "previousNode": "Set main variable" }], + "hints": [], + "executionTime": 0, + "executionStatus": "success", + "data": { + "main": [[{ "json": { "variable_2": "2345" }, "pairedItem": { "item": 0 } }]] + } + } + ], + "Set variable_3": [ + { + "startTime": 1749486952187, + "executionIndex": 4, + "source": [{ "previousNode": "Set main variable" }], + "hints": [], + "executionTime": 0, + "executionStatus": "success", + "data": { + "main": [[{ "json": { "variable_3": "3456" }, "pairedItem": { "item": 0 } }]] + } + } + ], + "Merge": [ + { + "startTime": 1749486952197, + "executionIndex": 5, + "source": [null, null, { "previousNode": "Set variable_3" }], + "hints": [], + "executionTime": 12, + "executionStatus": "success", + "data": { + "main": [ + [{ "json": { "variable_3": "3456" }, "pairedItem": { "item": 0, "input": 2 } }] + ] + } + } + ], + "Output": [ + { + "startTime": 1749486952210, + "executionIndex": 6, + "source": [{ "previousNode": "Merge" }], + "hints": [], + "executionTime": 4, + "executionStatus": "success", + "data": { + "main": [ + [ + { + "json": { "final_variable_2": "3456", "main": "2" }, + "pairedItem": { "item": 0 } + } + ] + ] + } + } + ] + }, + "pinData": {}, + "lastNodeExecuted": "Output" + }, + "executionData": { + "contextData": {}, + "nodeExecutionStack": [], + "metadata": {}, + "waitingExecution": {}, + "waitingExecutionSource": {} + } + } +} diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_workflow.json b/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_workflow.json new file mode 100644 index 0000000000..8b595a9e7b --- /dev/null +++ b/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_workflow.json @@ -0,0 +1,203 @@ +{ + "name": "Paired item", + "nodes": [ + { + "parameters": { + "numberInputs": 3 + }, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.1, + "position": [560, 300], + "id": "2b68168b-1494-4c4b-b416-b4fb6bb0afd8", + "name": "Merge", + "alwaysOutputData": true + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "63830a30-a4cc-4a66-9d01-8f0a058d4d43", + "name": "variable_1", + "value": "1234", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [200, 100], + "id": "a1d151cc-8f44-43c6-962f-baecb879d33c", + "name": "Set variable_1" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "63830a30-a4cc-4a66-9d01-8f0a058d4d43", + "name": "variable_2", + "value": "2345", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [200, 300], + "id": "404ff876-b524-4873-8dc0-36664639907a", + "name": "Set variable_2" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "63830a30-a4cc-4a66-9d01-8f0a058d4d43", + "name": "variable_3", + "value": "3456", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [200, 500], + "id": "d403e979-7651-4acf-8e08-1d342b7abe7f", + "name": "Set variable_3" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "89862e7d-0c44-4d6a-897e-a249c06f6346", + "name": "final_variable_2", + "value": "={{ $('Set variable_3').item.json.variable_3 }}", + "type": "string" + }, + { + "id": "060e841c-c236-41ae-9396-e23566825f47", + "name": "main", + "value": "={{ $('Set main variable').item.json.main_variable }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [840, 300], + "id": "16514f79-e309-465c-b571-7d81e268d7f0", + "name": "Output" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-240, 300], + "id": "3a8a9543-567c-443c-8742-fa0a8d9fb2e7", + "name": "Manual Trigger" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "6bb9d060-adee-429f-884d-5009ab1a1811", + "name": "main_variable", + "value": 2, + "type": "number" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [-20, 300], + "id": "b6ec9c0f-de04-45d3-a402-3969251a6914", + "name": "Set main variable" + } + ], + "pinData": {}, + "connections": { + "Merge": { + "main": [ + [ + { + "node": "Output", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set variable_1": { + "main": [[]] + }, + "Set variable_2": { + "main": [[]] + }, + "Set variable_3": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 2 + } + ] + ] + }, + "Manual Trigger": { + "main": [ + [ + { + "node": "Set main variable", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set main variable": { + "main": [ + [ + { + "node": "Set variable_2", + "type": "main", + "index": 0 + }, + { + "node": "Set variable_1", + "type": "main", + "index": 0 + }, + { + "node": "Set variable_3", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "5678abcc-b267-4b5b-ba1b-5f7bb1b085ec", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "TakJu1jBtMGTFXEA", + "tags": [] +} diff --git a/packages/workflow/test/workflow-data-proxy.test.ts b/packages/workflow/test/workflow-data-proxy.test.ts index e08e59d24d..ba1e8f2524 100644 --- a/packages/workflow/test/workflow-data-proxy.test.ts +++ b/packages/workflow/test/workflow-data-proxy.test.ts @@ -835,4 +835,14 @@ describe('WorkflowDataProxy', () => { expect(tools[0].aiDefinedFields).toEqual(['Start']); }); }); + + describe('multiple inputs', () => { + const fixture = loadFixture('multiple_inputs'); + + it('should correctly resolve expressions with multiple inputs (using paired item)', () => { + const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Output'); + expect(proxy.$('Set variable_3').item.json.variable_3).toEqual('3456'); + expect(proxy.$('Set main variable').item.json.main_variable).toEqual(2); + }); + }); });