mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
fix(core): Execute multi-input nodes re-fired through a loop-back edge in legacy order (#37561)
Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c21f4b47c
commit
903fe572af
@@ -2248,6 +2248,62 @@ describe('WorkflowExecute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareConnectionInputData', () => {
|
||||
// Legacy (v0) order with the first input dead-padded: data delivered
|
||||
// through a loop-back edge is used, data from upstream keeps the drop.
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'uuid-1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
{
|
||||
parameters: {},
|
||||
id: 'uuid-2',
|
||||
name: 'Loop',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
typeVersion: 2.1,
|
||||
position: [200, 0],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Start: { main: [[{ node: 'Loop', type: NodeConnectionTypes.Main, index: 0 }]] },
|
||||
Loop: { main: [[{ node: 'Loop', type: NodeConnectionTypes.Main, index: 1 }]] },
|
||||
},
|
||||
active: false,
|
||||
nodeTypes: Helpers.NodeTypes(),
|
||||
settings: { executionOrder: 'v0' },
|
||||
});
|
||||
const node = workflow.getNode('Loop')!;
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const workflowExecute = new WorkflowExecute(mock<IWorkflowExecuteAdditionalData>(), 'manual');
|
||||
const loopBackItems = [{ json: { attempt: 1 } }];
|
||||
|
||||
const prepare = (previousNode: string) =>
|
||||
// @ts-expect-error private method
|
||||
workflowExecute.prepareConnectionInputData(
|
||||
workflow,
|
||||
nodeType,
|
||||
undefined,
|
||||
node,
|
||||
{ main: [[], loopBackItems] },
|
||||
{ main: [null, { previousNode }] },
|
||||
);
|
||||
|
||||
test('should use data a node sent to a later input of itself', () => {
|
||||
expect(prepare('Loop')).toEqual(loopBackItems);
|
||||
});
|
||||
|
||||
test('should skip the run when the data came from an upstream node', () => {
|
||||
expect(prepare('Start')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('incomingConnectionIsEmpty', () => {
|
||||
let workflowExecute: WorkflowExecute;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
IRunData,
|
||||
ITaskData,
|
||||
ITaskDataConnections,
|
||||
ITaskDataConnectionsSource,
|
||||
ITaskMetadata,
|
||||
NodeOperationError,
|
||||
Workflow,
|
||||
@@ -967,7 +968,9 @@ export class WorkflowExecute {
|
||||
workflow: Workflow,
|
||||
nodeType: INodeType,
|
||||
customOperation: ReturnType<WorkflowExecute['getCustomOperation']>,
|
||||
node: INode,
|
||||
inputData: ITaskDataConnections,
|
||||
source: ITaskDataConnectionsSource | null,
|
||||
): INodeExecutionData[] | null {
|
||||
if (
|
||||
nodeType.execute ||
|
||||
@@ -991,6 +994,26 @@ export class WorkflowExecute {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (!connectionInputData?.length && source?.main?.[0] === null) {
|
||||
// The first input is empty and can no longer receive data. If another
|
||||
// input got its data from a node further down the workflow (a loop
|
||||
// back to this node), use that data so the loop keeps running. Data
|
||||
// from anywhere else keeps the old behavior: skip this node run.
|
||||
const descendants = workflow.getChildNodes(node.name);
|
||||
for (let inputIndex = 0; inputIndex < inputData.main.length; inputIndex++) {
|
||||
const mainData = inputData.main[inputIndex];
|
||||
const previousNode = source.main[inputIndex]?.previousNode;
|
||||
if (
|
||||
mainData?.length &&
|
||||
previousNode !== undefined &&
|
||||
// getChildNodes never lists the node itself, so a direct
|
||||
// self-edge counts as a loop back too
|
||||
(previousNode === node.name || descendants.includes(previousNode))
|
||||
) {
|
||||
connectionInputData = mainData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connectionInputData || connectionInputData.length === 0) {
|
||||
@@ -1369,7 +1392,9 @@ export class WorkflowExecute {
|
||||
workflow,
|
||||
nodeType,
|
||||
customOperation,
|
||||
node,
|
||||
inputData,
|
||||
executionData.source,
|
||||
);
|
||||
|
||||
if (connectionInputData === null) {
|
||||
|
||||
@@ -2824,6 +2824,139 @@ export const legacyWorkflowExecuteTests: WorkflowTestData[] = [
|
||||
nodeData: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'should keep executing when a loop-back edge lands on a later input of a multi-input node',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: 'b8b0b1a0-0000-4000-8000-000000000001',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
{
|
||||
parameters: {},
|
||||
id: 'b8b0b1a0-0000-4000-8000-000000000002',
|
||||
name: 'Merge',
|
||||
type: 'n8n-nodes-base.merge',
|
||||
typeVersion: 2.1,
|
||||
position: [200, 0],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
conditions: {
|
||||
number: [
|
||||
{
|
||||
value1: '={{ $json.attempt || 0 }}',
|
||||
operation: 'larger',
|
||||
value2: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
id: 'b8b0b1a0-0000-4000-8000-000000000003',
|
||||
name: 'IF',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 1,
|
||||
position: [400, 0],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
values: {
|
||||
number: [
|
||||
{
|
||||
name: 'attempt',
|
||||
value: '={{ ($input.first().json.attempt || 0) + 1 }}',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {},
|
||||
},
|
||||
id: 'b8b0b1a0-0000-4000-8000-000000000004',
|
||||
name: 'Retry',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 2,
|
||||
position: [400, 200],
|
||||
},
|
||||
{
|
||||
parameters: {},
|
||||
id: 'b8b0b1a0-0000-4000-8000-000000000005',
|
||||
name: 'Done',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
position: [600, 0],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Start: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Merge',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Merge: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'IF',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
IF: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Done',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
node: 'Retry',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Retry: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Merge',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeExecutionOrder: ['Start', 'Merge', 'IF', 'Retry', 'Merge', 'IF', 'Done'],
|
||||
nodeData: {
|
||||
Start: [[{}]],
|
||||
Merge: [[{}], [{ attempt: 1 }]],
|
||||
IF: [[], [{ attempt: 1 }]],
|
||||
Retry: [[{ attempt: 1 }]],
|
||||
Done: [[{ attempt: 1 }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const v1WorkflowExecuteTests: WorkflowTestData[] = [
|
||||
|
||||
Reference in New Issue
Block a user