mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix: Fix paired item handling of multiple inputs (#16153)
This commit is contained in:
@@ -912,106 +912,76 @@ export class WorkflowDataProxy {
|
|||||||
return outputs;
|
return outputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMultiplePairings(
|
const normalizePairedItem = (
|
||||||
pairings: IPairedItemData[],
|
paired: number | IPairedItemData | Array<number | IPairedItemData>,
|
||||||
source: ISourceData[],
|
): IPairedItemData[] => {
|
||||||
destinationNode: string,
|
const pairedItems = Array.isArray(paired) ? paired : [paired];
|
||||||
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[];
|
|
||||||
|
|
||||||
if (results.length === 1) return results[0];
|
return pairedItems.map((p) => (typeof p === 'number' ? { item: p } : p));
|
||||||
|
};
|
||||||
|
|
||||||
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 = (
|
const getPairedItem = (
|
||||||
destinationNodeName: string,
|
destinationNodeName: string,
|
||||||
incomingSourceData: ISourceData | null,
|
incomingSourceData: ISourceData | null,
|
||||||
initialPairedItem: IPairedItemData,
|
initialPairedItem: IPairedItemData,
|
||||||
usedMethodName: PairedItemMethod = PAIRED_ITEM_METHOD.$GET_PAIRED_ITEM,
|
usedMethodName: PairedItemMethod = PAIRED_ITEM_METHOD.$GET_PAIRED_ITEM,
|
||||||
): INodeExecutionData | null => {
|
nodeBeforeLast?: string,
|
||||||
|
): INodeExecutionData => {
|
||||||
// Step 1: Normalize inputs
|
// Step 1: Normalize inputs
|
||||||
const [pairedItem, sourceData] = normalizeInputs(initialPairedItem, incomingSourceData);
|
const [pairedItem, sourceData] = normalizeInputs(initialPairedItem, incomingSourceData);
|
||||||
|
|
||||||
let currentPairedItem = pairedItem;
|
if (!sourceData) {
|
||||||
let currentSource = sourceData;
|
throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast);
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Final node reached — fetch paired item
|
const taskData = getTaskData(sourceData);
|
||||||
if (!currentSource) throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast);
|
const outputData = getNodeOutput(taskData, sourceData, nodeBeforeLast);
|
||||||
|
const item = outputData[pairedItem.item];
|
||||||
|
const sourceArray = taskData?.source ?? [];
|
||||||
|
|
||||||
const finalTaskData = getTaskData(currentSource);
|
// Done: reached the destination node in the ancestry chain
|
||||||
const finalOutputData = getNodeOutput(finalTaskData, currentSource);
|
if (sourceData.previousNode === destinationNodeName) {
|
||||||
|
if (pairedItem.item >= outputData.length) {
|
||||||
|
throw createInvalidPairedItemError({ nodeName: sourceData.previousNode });
|
||||||
|
}
|
||||||
|
|
||||||
if (currentPairedItem.item >= finalOutputData.length) {
|
return item;
|
||||||
throw createInvalidPairedItemError({ nodeName: currentSource.previousNode });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalOutputData[currentPairedItem.item];
|
if (!item?.pairedItem) {
|
||||||
|
throw createMissingPairedItemError(sourceData.previousNode, usedMethodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFromAi = (
|
const handleFromAi = (
|
||||||
|
|||||||
114
packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json
vendored
Normal file
114
packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_run.json
vendored
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
203
packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_workflow.json
vendored
Normal file
203
packages/workflow/test/fixtures/WorkflowDataProxy/multiple_inputs_workflow.json
vendored
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
@@ -835,4 +835,14 @@ describe('WorkflowDataProxy', () => {
|
|||||||
expect(tools[0].aiDefinedFields).toEqual(['Start']);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user