mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(core): Fix new partial execution flow not working with workflows with multiple disabled nodes in a row (no-changelog) (#11024)
This commit is contained in:
parent
3191912168
commit
5903592a23
|
@ -67,6 +67,63 @@ export class DirectedGraph {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a node from the graph.
|
||||||
|
*
|
||||||
|
* By default it will also remove all connections that use that node and
|
||||||
|
* return nothing.
|
||||||
|
*
|
||||||
|
* If you pass `{ reconnectConnections: true }` it will rewire all
|
||||||
|
* connections making sure all parent nodes are connected to all child nodes
|
||||||
|
* and return the new connections.
|
||||||
|
*/
|
||||||
|
removeNode(node: INode, options?: { reconnectConnections: true }): GraphConnection[];
|
||||||
|
removeNode(node: INode, options?: { reconnectConnections: false }): undefined;
|
||||||
|
removeNode(node: INode, { reconnectConnections = false } = {}): undefined | GraphConnection[] {
|
||||||
|
if (reconnectConnections) {
|
||||||
|
const incomingConnections = this.getDirectParents(node);
|
||||||
|
const outgoingConnections = this.getDirectChildren(node);
|
||||||
|
|
||||||
|
const newConnections: GraphConnection[] = [];
|
||||||
|
|
||||||
|
for (const incomingConnection of incomingConnections) {
|
||||||
|
for (const outgoingConnection of outgoingConnections) {
|
||||||
|
const newConnection = {
|
||||||
|
...incomingConnection,
|
||||||
|
to: outgoingConnection.to,
|
||||||
|
inputIndex: outgoingConnection.inputIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
newConnections.push(newConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, connection] of this.connections.entries()) {
|
||||||
|
if (connection.to === node || connection.from === node) {
|
||||||
|
this.connections.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const newConnection of newConnections) {
|
||||||
|
this.connections.set(this.makeKey(newConnection), newConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes.delete(node.name);
|
||||||
|
|
||||||
|
return newConnections;
|
||||||
|
} else {
|
||||||
|
for (const [key, connection] of this.connections.entries()) {
|
||||||
|
if (connection.to === node || connection.from === node) {
|
||||||
|
this.connections.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodes.delete(node.name);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addConnection(connectionInput: {
|
addConnection(connectionInput: {
|
||||||
from: INode;
|
from: INode;
|
||||||
to: INode;
|
to: INode;
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
// XX denotes that the node is disabled
|
// XX denotes that the node is disabled
|
||||||
// PD denotes that the node has pinned data
|
// PD denotes that the node has pinned data
|
||||||
|
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
import { createNodeData, defaultWorkflowParameter } from './helpers';
|
import { createNodeData, defaultWorkflowParameter } from './helpers';
|
||||||
import { DirectedGraph } from '../DirectedGraph';
|
import { DirectedGraph } from '../DirectedGraph';
|
||||||
|
|
||||||
|
@ -86,4 +88,202 @@ describe('DirectedGraph', () => {
|
||||||
expect(children).toEqual(new Set([node1, node2, node3]));
|
expect(children).toEqual(new Set([node1, node2, node3]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeNode', () => {
|
||||||
|
// XX
|
||||||
|
// ┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
// │node0├───►│node1├──►│node2│
|
||||||
|
// └─────┘ └─────┘ └─────┘
|
||||||
|
// turns into
|
||||||
|
// ┌─────┐ ┌─────┐
|
||||||
|
// │node0│ │node2│
|
||||||
|
// └─────┘ └─────┘
|
||||||
|
test('remove node and all connections', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const node0 = createNodeData({ name: 'node0' });
|
||||||
|
const node1 = createNodeData({ name: 'node1' });
|
||||||
|
const node2 = createNodeData({ name: 'node2' });
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
.addNodes(node0, node1, node2)
|
||||||
|
.addConnections({ from: node0, to: node1 }, { from: node0, to: node2 });
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
graph.removeNode(node1);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(graph).toEqual(
|
||||||
|
new DirectedGraph().addNodes(node0, node2).addConnections({ from: node0, to: node2 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// XX
|
||||||
|
// ┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
// │node0├───►│node1├──►│node2│
|
||||||
|
// └─────┘ └─────┘ └─────┘
|
||||||
|
// turns into
|
||||||
|
// ┌─────┐ ┌─────┐
|
||||||
|
// │node0├──►│node2│
|
||||||
|
// └─────┘ └─────┘
|
||||||
|
test('remove node, but reconnect connections', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const node0 = createNodeData({ name: 'node0' });
|
||||||
|
const node1 = createNodeData({ name: 'node1' });
|
||||||
|
const node2 = createNodeData({ name: 'node2' });
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
.addNodes(node0, node1, node2)
|
||||||
|
.addConnections({ from: node0, to: node1 }, { from: node1, to: node2 });
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const newConnections = graph.removeNode(node1, { reconnectConnections: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(newConnections).toHaveLength(1);
|
||||||
|
expect(newConnections[0]).toEqual({
|
||||||
|
from: node0,
|
||||||
|
outputIndex: 0,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
inputIndex: 0,
|
||||||
|
to: node2,
|
||||||
|
});
|
||||||
|
expect(graph).toEqual(
|
||||||
|
new DirectedGraph().addNodes(node0, node2).addConnections({ from: node0, to: node2 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// XX
|
||||||
|
// ┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
// │ │o o│ │o o│ │
|
||||||
|
// │ │o─┐ o│ │o o│ │
|
||||||
|
// │node0│o └►o│node1│o o│node2│
|
||||||
|
// │ │o o│ │o─┐ o│ │
|
||||||
|
// │ │o o│ │o └►o│ │
|
||||||
|
// └─────┘ └─────┘ └─────┘
|
||||||
|
// turns into
|
||||||
|
// ┌─────┐ ┌─────┐
|
||||||
|
// │ │o o│ │
|
||||||
|
// │ │o───────┐ o│ │
|
||||||
|
// │node0│o │ o│node2│
|
||||||
|
// │ │o │ o│ │
|
||||||
|
// │ │o └──────►o│ │
|
||||||
|
// └─────┘ └─────┘
|
||||||
|
test('remove node, reconnect connections and retaining the input indexes', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const node0 = createNodeData({ name: 'node0' });
|
||||||
|
const node1 = createNodeData({ name: 'node1' });
|
||||||
|
const node2 = createNodeData({ name: 'node2' });
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
.addNodes(node0, node1, node2)
|
||||||
|
.addConnections(
|
||||||
|
{ from: node0, outputIndex: 1, inputIndex: 2, to: node1 },
|
||||||
|
{ from: node1, outputIndex: 3, inputIndex: 4, to: node2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const newConnections = graph.removeNode(node1, { reconnectConnections: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(newConnections).toHaveLength(1);
|
||||||
|
expect(newConnections[0]).toEqual({
|
||||||
|
from: node0,
|
||||||
|
outputIndex: 1,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
inputIndex: 4,
|
||||||
|
to: node2,
|
||||||
|
});
|
||||||
|
expect(graph).toEqual(
|
||||||
|
new DirectedGraph()
|
||||||
|
.addNodes(node0, node2)
|
||||||
|
.addConnections({ from: node0, outputIndex: 1, inputIndex: 4, to: node2 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// XX
|
||||||
|
// ┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
// │ │o o│ │o │ │
|
||||||
|
// │ │o─┐ o│ │o │ │
|
||||||
|
// │node0│ └►o│node1│o ┌►o│node2│
|
||||||
|
// │ │ │ │o─┘ │ │
|
||||||
|
// │ │ │ │ │ │
|
||||||
|
// └─────┘ └─────┘ └─────┘
|
||||||
|
// turns into
|
||||||
|
// ┌─────┐ ┌─────┐
|
||||||
|
// │ │o │ │
|
||||||
|
// │ │o───────┐ │ │
|
||||||
|
// │node0│ └──────►o│node2│
|
||||||
|
// │ │ │ │
|
||||||
|
// │ │ │ │
|
||||||
|
// └─────┘ └─────┘
|
||||||
|
test('remove node, reconnect connections and retaining the input indexes, even if the child has less inputs than the than the removed node had', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const node0 = createNodeData({ name: 'node0' });
|
||||||
|
const node1 = createNodeData({ name: 'node1' });
|
||||||
|
const node2 = createNodeData({ name: 'node2' });
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
.addNodes(node0, node1, node2)
|
||||||
|
.addConnections(
|
||||||
|
{ from: node0, outputIndex: 1, inputIndex: 2, to: node1 },
|
||||||
|
{ from: node1, outputIndex: 3, inputIndex: 0, to: node2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const newConnections = graph.removeNode(node1, { reconnectConnections: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const expectedGraph = new DirectedGraph()
|
||||||
|
.addNodes(node0, node2)
|
||||||
|
.addConnections({ from: node0, outputIndex: 1, inputIndex: 0, to: node2 });
|
||||||
|
expect(newConnections).toHaveLength(1);
|
||||||
|
expect(newConnections).toEqual(expectedGraph.getConnections());
|
||||||
|
expect(graph).toEqual(expectedGraph);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ┌─────┐ ┌──────┐
|
||||||
|
// │left0├─┐ XX ┌►│right0│
|
||||||
|
// └─────┘ │ ┌──────┐ │ └──────┘
|
||||||
|
// ├─►│center├──┤
|
||||||
|
// ┌─────┐ │ └──────┘ │ ┌──────┐
|
||||||
|
// │left1├─┘ └►│right1│
|
||||||
|
// └─────┘ └──────┘
|
||||||
|
// turns into
|
||||||
|
//
|
||||||
|
// ┌─────┐ ┌──────┐
|
||||||
|
// │left0├─┐ ┌─►│right0│
|
||||||
|
// └─────┘ │ │ └──────┘
|
||||||
|
// ├───────────┤
|
||||||
|
// ┌─────┐ │ │ ┌──────┐
|
||||||
|
// │left1├─┘ └─►│right1│
|
||||||
|
// └─────┘ └──────┘
|
||||||
|
test('remove node, reconnect connections and multiplexes them', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const left0 = createNodeData({ name: 'left0' });
|
||||||
|
const left1 = createNodeData({ name: 'left1' });
|
||||||
|
const center = createNodeData({ name: 'center' });
|
||||||
|
const right0 = createNodeData({ name: 'right0' });
|
||||||
|
const right1 = createNodeData({ name: 'right1' });
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
.addNodes(left0, left1, center, right0, right1)
|
||||||
|
.addConnections(
|
||||||
|
{ from: left0, to: center },
|
||||||
|
{ from: left1, to: center },
|
||||||
|
{ from: center, to: right0 },
|
||||||
|
{ from: center, to: right1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const newConnections = graph.removeNode(center, { reconnectConnections: true });
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const expectedGraph = new DirectedGraph()
|
||||||
|
.addNodes(left0, left1, right0, right1)
|
||||||
|
.addConnections(
|
||||||
|
{ from: left0, to: right0 },
|
||||||
|
{ from: left0, to: right1 },
|
||||||
|
{ from: left1, to: right0 },
|
||||||
|
{ from: left1, to: right1 },
|
||||||
|
);
|
||||||
|
expect(newConnections).toHaveLength(4);
|
||||||
|
expect(newConnections).toEqual(expectedGraph.getConnections());
|
||||||
|
expect(graph).toEqual(expectedGraph);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { createNodeData } from './helpers';
|
||||||
import { DirectedGraph } from '../DirectedGraph';
|
import { DirectedGraph } from '../DirectedGraph';
|
||||||
import { findSubgraph } from '../findSubgraph';
|
import { findSubgraph } from '../findSubgraph';
|
||||||
|
|
||||||
describe('findSubgraph2', () => {
|
describe('findSubgraph', () => {
|
||||||
// ►►
|
// ►►
|
||||||
// ┌───────┐ ┌───────────┐
|
// ┌───────┐ ┌───────────┐
|
||||||
// │trigger├────►│destination│
|
// │trigger├────►│destination│
|
||||||
|
@ -83,6 +83,12 @@ describe('findSubgraph2', () => {
|
||||||
// │trigger│ │disabled├─────►│destination│
|
// │trigger│ │disabled├─────►│destination│
|
||||||
// │ ├────────►│ │ └───────────┘
|
// │ ├────────►│ │ └───────────┘
|
||||||
// └───────┘ └────────┘
|
// └───────┘ └────────┘
|
||||||
|
// turns into
|
||||||
|
// ┌───────┐ ►►
|
||||||
|
// │ │ ┌───────────┐
|
||||||
|
// │trigger├─────►│destination│
|
||||||
|
// │ │ └───────────┘
|
||||||
|
// └───────┘
|
||||||
test('skip disabled nodes', () => {
|
test('skip disabled nodes', () => {
|
||||||
const trigger = createNodeData({ name: 'trigger' });
|
const trigger = createNodeData({ name: 'trigger' });
|
||||||
const disabled = createNodeData({ name: 'disabled', disabled: true });
|
const disabled = createNodeData({ name: 'disabled', disabled: true });
|
||||||
|
@ -101,6 +107,40 @@ describe('findSubgraph2', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// XX XX
|
||||||
|
// ┌───────┐ ┌─────┐ ┌─────┐ ┌───────────┐
|
||||||
|
// │trigger├────►│node1├────►│node2├────►│destination│
|
||||||
|
// └───────┘ └─────┘ └─────┘ └───────────┘
|
||||||
|
// turns into
|
||||||
|
// ┌───────┐ ┌───────────┐
|
||||||
|
// │trigger├────►│destination│
|
||||||
|
// └───────┘ └───────────┘
|
||||||
|
test('skip multiple disabled nodes', () => {
|
||||||
|
// ARRANGE
|
||||||
|
const trigger = createNodeData({ name: 'trigger' });
|
||||||
|
const disabledNode1 = createNodeData({ name: 'disabledNode1', disabled: true });
|
||||||
|
const disabledNode2 = createNodeData({ name: 'disabledNode2', disabled: true });
|
||||||
|
const destination = createNodeData({ name: 'destination' });
|
||||||
|
|
||||||
|
const graph = new DirectedGraph()
|
||||||
|
.addNodes(trigger, disabledNode1, disabledNode2, destination)
|
||||||
|
.addConnections(
|
||||||
|
{ from: trigger, to: disabledNode1 },
|
||||||
|
{ from: disabledNode1, to: disabledNode2 },
|
||||||
|
{ from: disabledNode2, to: destination },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const subgraph = findSubgraph(graph, destination, trigger);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(subgraph).toEqual(
|
||||||
|
new DirectedGraph()
|
||||||
|
.addNodes(trigger, destination)
|
||||||
|
.addConnections({ from: trigger, to: destination }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ►►
|
// ►►
|
||||||
// ┌───────┐ ┌─────┐ ┌─────┐
|
// ┌───────┐ ┌─────┐ ┌─────┐
|
||||||
// │Trigger├───┬──►│Node1├───┬─►│Node2│
|
// │Trigger├───┬──►│Node1├───┬─►│Node2│
|
||||||
|
|
|
@ -51,27 +51,14 @@ function findSubgraphRecursive(
|
||||||
// Take every incoming connection and connect it to every node that is
|
// Take every incoming connection and connect it to every node that is
|
||||||
// connected to the current node’s first output
|
// connected to the current node’s first output
|
||||||
if (current.disabled) {
|
if (current.disabled) {
|
||||||
const incomingConnections = graph.getDirectParents(current);
|
// The last segment on the current branch is still pointing to the removed
|
||||||
const outgoingConnections = graph
|
// node, so let's remove it.
|
||||||
.getDirectChildren(current)
|
currentBranch.pop();
|
||||||
// NOTE: When a node is disabled only the first output gets data
|
|
||||||
.filter((connection) => connection.outputIndex === 0);
|
|
||||||
|
|
||||||
parentConnections = [];
|
// The node is replaced by a set of new connections, connecting the parents
|
||||||
|
// and children of it directly. In the recursive call below we'll follow
|
||||||
for (const incomingConnection of incomingConnections) {
|
// them further.
|
||||||
for (const outgoingConnection of outgoingConnections) {
|
parentConnections = graph.removeNode(current, { reconnectConnections: true });
|
||||||
const newConnection = {
|
|
||||||
...incomingConnection,
|
|
||||||
to: outgoingConnection.to,
|
|
||||||
inputIndex: outgoingConnection.inputIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
parentConnections.push(newConnection);
|
|
||||||
currentBranch.pop();
|
|
||||||
currentBranch.push(newConnection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse on each parent.
|
// Recurse on each parent.
|
||||||
|
|
|
@ -44,12 +44,12 @@ export function recreateNodeExecutionStack(
|
||||||
// Validate invariants.
|
// Validate invariants.
|
||||||
|
|
||||||
// The graph needs to be free of disabled nodes. If it's not it hasn't been
|
// The graph needs to be free of disabled nodes. If it's not it hasn't been
|
||||||
// passed through findSubgraph2.
|
// passed through findSubgraph.
|
||||||
for (const node of graph.getNodes().values()) {
|
for (const node of graph.getNodes().values()) {
|
||||||
a.notEqual(
|
a.notEqual(
|
||||||
node.disabled,
|
node.disabled,
|
||||||
true,
|
true,
|
||||||
`Graph contains disabled nodes. This is not supported. Make sure to pass the graph through "findSubgraph2" before calling "recreateNodeExecutionStack". The node in question is "${node.name}"`,
|
`Graph contains disabled nodes. This is not supported. Make sure to pass the graph through "findSubgraph" before calling "recreateNodeExecutionStack". The node in question is "${node.name}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue