diff --git a/packages/workflow/src/workflow-data-proxy.ts b/packages/workflow/src/workflow-data-proxy.ts index 1cdf5d9669..da5a648a76 100644 --- a/packages/workflow/src/workflow-data-proxy.ts +++ b/packages/workflow/src/workflow-data-proxy.ts @@ -912,76 +912,106 @@ export class WorkflowDataProxy { return outputs; } - const normalizePairedItem = ( - paired: number | IPairedItemData | Array, - ): IPairedItemData[] => { - const pairedItems = Array.isArray(paired) ? paired : [paired]; + 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[]; - return pairedItems.map((p) => (typeof p === 'number' ? { item: p } : p)); - }; + if (results.length === 1) return results[0]; + const allSame = results.every((r) => r === results[0]); + if (allSame) return results[0]; + + 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, - nodeBeforeLast?: string, - ): INodeExecutionData => { + ): INodeExecutionData | null => { // Step 1: Normalize inputs const [pairedItem, sourceData] = normalizeInputs(initialPairedItem, incomingSourceData); - if (!sourceData) { - throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast); - } + let currentPairedItem = pairedItem; + let currentSource = sourceData; + let nodeBeforeLast: string | undefined; - const taskData = getTaskData(sourceData); - const outputData = getNodeOutput(taskData, sourceData, nodeBeforeLast); - const item = outputData[pairedItem.item]; - const sourceArray = taskData?.source ?? []; + // Step 2: Traverse ancestry to find correct node + while (currentSource && currentSource.previousNode !== destinationNodeName) { + const taskData = getTaskData(currentSource); - // Done: reached the destination node in the ancestry chain - if (sourceData.previousNode === destinationNodeName) { - if (pairedItem.item >= outputData.length) { - throw createInvalidPairedItemError({ nodeName: sourceData.previousNode }); + 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); } - return item; + // 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 (!item?.pairedItem) { - throw createMissingPairedItemError(sourceData.previousNode, usedMethodName); + // Step 3: Final node reached — fetch paired item + if (!currentSource) throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast); + + const finalTaskData = getTaskData(currentSource); + const finalOutputData = getNodeOutput(finalTaskData, currentSource); + + if (currentPairedItem.item >= finalOutputData.length) { + throw createInvalidPairedItemError({ nodeName: currentSource.previousNode }); } - // Normalize paired item to always be IPairedItemData[] - const nextPairedItems = normalizePairedItem(item.pairedItem); - - // 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; + return finalOutputData[currentPairedItem.item]; }; const handleFromAi = ( diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json b/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json deleted file mode 100644 index a1638bee70..0000000000 --- a/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "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 deleted file mode 100644 index 8b595a9e7b..0000000000 --- a/packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_workflow.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "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 ba1e8f2524..e08e59d24d 100644 --- a/packages/workflow/test/workflow-data-proxy.test.ts +++ b/packages/workflow/test/workflow-data-proxy.test.ts @@ -835,14 +835,4 @@ 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); - }); - }); });