mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
refactor(core): Extract disabled node filtering out of findSubgraph (#11941)
This commit is contained in:
@@ -470,6 +470,12 @@ export class DirectedGraph {
|
||||
return graph;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new DirectedGraph()
|
||||
.addNodes(...this.getNodes().values())
|
||||
.addConnections(...this.getConnections().values());
|
||||
}
|
||||
|
||||
private toIConnections() {
|
||||
const result: IConnections = {};
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// NOTE: Diagrams in this file have been created with https://asciiflow.com/#/
|
||||
// If you update the tests, please update the diagrams as well.
|
||||
// If you add a test, please create a new diagram.
|
||||
//
|
||||
// Map
|
||||
// 0 means the output has no run data
|
||||
// 1 means the output has run data
|
||||
// ►► denotes the node that the user wants to execute to
|
||||
// XX denotes that the node is disabled
|
||||
// PD denotes that the node has pinned data
|
||||
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
|
||||
import { createNodeData } from './helpers';
|
||||
import { DirectedGraph } from '../DirectedGraph';
|
||||
import { filterDisabledNodes } from '../filterDisabledNodes';
|
||||
|
||||
describe('filterDisabledNodes', () => {
|
||||
// XX
|
||||
// ┌───────┐ ┌────────┐ ►►
|
||||
// │ ├────────►│ │ ┌───────────┐
|
||||
// │trigger│ │disabled├─────►│destination│
|
||||
// │ ├────────►│ │ └───────────┘
|
||||
// └───────┘ └────────┘
|
||||
// turns into
|
||||
// ┌───────┐ ►►
|
||||
// │ │ ┌───────────┐
|
||||
// │trigger├─────►│destination│
|
||||
// │ │ └───────────┘
|
||||
// └───────┘
|
||||
test('filter disabled nodes', () => {
|
||||
const trigger = createNodeData({ name: 'trigger' });
|
||||
const disabled = createNodeData({ name: 'disabled', disabled: true });
|
||||
const destination = createNodeData({ name: 'destination' });
|
||||
|
||||
const graph = new DirectedGraph()
|
||||
.addNodes(trigger, disabled, destination)
|
||||
.addConnections({ from: trigger, to: disabled }, { from: disabled, to: destination });
|
||||
|
||||
const subgraph = filterDisabledNodes(graph);
|
||||
|
||||
expect(subgraph).toEqual(
|
||||
new DirectedGraph()
|
||||
.addNodes(trigger, destination)
|
||||
.addConnections({ from: trigger, to: destination }),
|
||||
);
|
||||
});
|
||||
|
||||
// XX XX
|
||||
// ┌───────┐ ┌─────┐ ┌─────┐ ┌───────────┐
|
||||
// │trigger├────►│node1├────►│node2├────►│destination│
|
||||
// └───────┘ └─────┘ └─────┘ └───────────┘
|
||||
// turns into
|
||||
// ┌───────┐ ┌───────────┐
|
||||
// │trigger├────►│destination│
|
||||
// └───────┘ └───────────┘
|
||||
test('filter multiple disabled nodes in a row', () => {
|
||||
// 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 = filterDisabledNodes(graph);
|
||||
|
||||
// ASSERT
|
||||
expect(subgraph).toEqual(
|
||||
new DirectedGraph()
|
||||
.addNodes(trigger, destination)
|
||||
.addConnections({ from: trigger, to: destination }),
|
||||
);
|
||||
});
|
||||
|
||||
describe('root nodes', () => {
|
||||
// XX
|
||||
// ┌───────┐ ┌────┐ ┌───────────┐
|
||||
// │trigger├───►root├───►destination│
|
||||
// └───────┘ └──▲─┘ └───────────┘
|
||||
// │AiLanguageModel
|
||||
// ┌┴──────┐
|
||||
// │aiModel│
|
||||
// └───────┘
|
||||
// turns into
|
||||
// ┌───────┐ ┌───────────┐
|
||||
// │trigger├────────────►destination│
|
||||
// └───────┘ └───────────┘
|
||||
test('filter disabled root nodes', () => {
|
||||
// ARRANGE
|
||||
const trigger = createNodeData({ name: 'trigger' });
|
||||
const root = createNodeData({ name: 'root', disabled: true });
|
||||
const aiModel = createNodeData({ name: 'ai_model' });
|
||||
const destination = createNodeData({ name: 'destination' });
|
||||
|
||||
const graph = new DirectedGraph()
|
||||
.addNodes(trigger, root, aiModel, destination)
|
||||
.addConnections(
|
||||
{ from: trigger, to: root },
|
||||
{ from: aiModel, type: NodeConnectionType.AiLanguageModel, to: root },
|
||||
{ from: root, to: destination },
|
||||
);
|
||||
|
||||
// ACT
|
||||
const subgraph = filterDisabledNodes(graph);
|
||||
|
||||
// ASSERT
|
||||
expect(subgraph).toEqual(
|
||||
new DirectedGraph()
|
||||
// The model is still in the graph, but orphaned. This is ok for
|
||||
// partial executions as findSubgraph will remove orphaned nodes.
|
||||
.addNodes(trigger, destination, aiModel)
|
||||
.addConnections({ from: trigger, to: destination }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -79,70 +79,6 @@ describe('findSubgraph', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// XX
|
||||
// ┌───────┐ ┌────────┐ ►►
|
||||
// │ ├────────►│ │ ┌───────────┐
|
||||
// │trigger│ │disabled├─────►│destination│
|
||||
// │ ├────────►│ │ └───────────┘
|
||||
// └───────┘ └────────┘
|
||||
// turns into
|
||||
// ┌───────┐ ►►
|
||||
// │ │ ┌───────────┐
|
||||
// │trigger├─────►│destination│
|
||||
// │ │ └───────────┘
|
||||
// └───────┘
|
||||
test('skip disabled nodes', () => {
|
||||
const trigger = createNodeData({ name: 'trigger' });
|
||||
const disabled = createNodeData({ name: 'disabled', disabled: true });
|
||||
const destination = createNodeData({ name: 'destination' });
|
||||
|
||||
const graph = new DirectedGraph()
|
||||
.addNodes(trigger, disabled, destination)
|
||||
.addConnections({ from: trigger, to: disabled }, { from: disabled, to: destination });
|
||||
|
||||
const subgraph = findSubgraph({ graph, destination, trigger });
|
||||
|
||||
expect(subgraph).toEqual(
|
||||
new DirectedGraph()
|
||||
.addNodes(trigger, destination)
|
||||
.addConnections({ from: trigger, to: destination }),
|
||||
);
|
||||
});
|
||||
|
||||
// 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│
|
||||
@@ -291,36 +227,29 @@ describe('findSubgraph', () => {
|
||||
expect(subgraph.getNodes().size).toBe(0);
|
||||
});
|
||||
|
||||
// ┌───────┐ ┌───────────┐
|
||||
// │trigger├────────────►destination│
|
||||
// └───────┘ └───────────┘
|
||||
//
|
||||
// XX
|
||||
// ┌───────┐ ┌────┐ ┌───────────┐
|
||||
// │trigger├───►root├───►destination│
|
||||
// └───────┘ └──▲─┘ └───────────┘
|
||||
// │AiLanguageModel
|
||||
// ┌┴──────┐
|
||||
// ┌───────┐
|
||||
// │aiModel│
|
||||
// └───────┘
|
||||
// turns into
|
||||
// ┌───────┐ ┌───────────┐
|
||||
// │trigger├────────────►destination│
|
||||
// └───────┘ └───────────┘
|
||||
test('skip disabled root nodes', () => {
|
||||
test('remove orphaned nodes', () => {
|
||||
// ARRANGE
|
||||
const trigger = createNodeData({ name: 'trigger' });
|
||||
const root = createNodeData({ name: 'root', disabled: true });
|
||||
const aiModel = createNodeData({ name: 'ai_model' });
|
||||
const destination = createNodeData({ name: 'destination' });
|
||||
|
||||
const graph = new DirectedGraph()
|
||||
.addNodes(trigger, root, aiModel, destination)
|
||||
.addConnections(
|
||||
{ from: trigger, to: root },
|
||||
{ from: aiModel, type: NodeConnectionType.AiLanguageModel, to: root },
|
||||
{ from: root, to: destination },
|
||||
);
|
||||
.addNodes(trigger, aiModel, destination)
|
||||
.addConnections({ from: trigger, to: destination });
|
||||
|
||||
// ACT
|
||||
const subgraph = findSubgraph({ graph, destination: root, trigger });
|
||||
const subgraph = findSubgraph({ graph, destination, trigger });
|
||||
|
||||
// ASSERT
|
||||
expect(subgraph).toEqual(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
|
||||
import type { DirectedGraph } from './DirectedGraph';
|
||||
|
||||
export function filterDisabledNodes(graph: DirectedGraph): DirectedGraph {
|
||||
const filteredGraph = graph.clone();
|
||||
|
||||
for (const node of filteredGraph.getNodes().values()) {
|
||||
if (node.disabled) {
|
||||
filteredGraph.removeNode(node, {
|
||||
reconnectConnections: true,
|
||||
skipConnectionFn: (c) => c.type !== NodeConnectionType.Main,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return filteredGraph;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ function findSubgraphRecursive(
|
||||
return;
|
||||
}
|
||||
|
||||
let parentConnections = graph.getDirectParentConnections(current);
|
||||
const parentConnections = graph.getDirectParentConnections(current);
|
||||
|
||||
// If the current node has no parents, don’t keep this branch.
|
||||
if (parentConnections.length === 0) {
|
||||
@@ -46,27 +46,6 @@ function findSubgraphRecursive(
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current node is disabled, don’t keep this node, but keep the
|
||||
// branch.
|
||||
// Take every incoming connection and connect it to every node that is
|
||||
// connected to the current node’s first output
|
||||
if (current.disabled) {
|
||||
// The last segment on the current branch is still pointing to the removed
|
||||
// node, so let's remove it.
|
||||
currentBranch.pop();
|
||||
|
||||
// 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
|
||||
// them further.
|
||||
parentConnections = graph.removeNode(current, {
|
||||
reconnectConnections: true,
|
||||
// If the node has non-Main connections we don't want to rewire those.
|
||||
// Otherwise we'd end up connecting AI utilities to nodes that don't
|
||||
// support them.
|
||||
skipConnectionFn: (c) => c.type !== NodeConnectionType.Main,
|
||||
});
|
||||
}
|
||||
|
||||
// Recurse on each parent.
|
||||
for (const parentConnection of parentConnections) {
|
||||
// Skip parents that are connected via non-Main connection types. They are
|
||||
@@ -84,8 +63,7 @@ function findSubgraphRecursive(
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all nodes that can lead from the trigger to the destination node,
|
||||
* ignoring disabled nodes.
|
||||
* Find all nodes that can lead from the trigger to the destination node.
|
||||
*
|
||||
* The algorithm is:
|
||||
* Start with Destination Node
|
||||
@@ -95,12 +73,8 @@ function findSubgraphRecursive(
|
||||
* 3. if the current node is the destination node again, don’t keep this
|
||||
* branch
|
||||
* 4. if the current node was already visited, keep this branch
|
||||
* 5. if the current node is disabled, don’t keep this node, but keep the
|
||||
* branch
|
||||
* - take every incoming connection and connect it to every node that is
|
||||
* connected to the current node’s first output
|
||||
* 6. Recurse on each parent
|
||||
* 7. Re-add all connections that don't use the `Main` connections type.
|
||||
* 5. Recurse on each parent
|
||||
* 6. Re-add all connections that don't use the `Main` connections type.
|
||||
* Theses are used by nodes called root nodes and they are not part of the
|
||||
* dataflow in the graph they are utility nodes, like the AI model used in a
|
||||
* lang chain node.
|
||||
|
||||
@@ -5,3 +5,4 @@ export { findSubgraph } from './findSubgraph';
|
||||
export { recreateNodeExecutionStack } from './recreateNodeExecutionStack';
|
||||
export { cleanRunData } from './cleanRunData';
|
||||
export { handleCycles } from './handleCycles';
|
||||
export { filterDisabledNodes } from './filterDisabledNodes';
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
cleanRunData,
|
||||
recreateNodeExecutionStack,
|
||||
handleCycles,
|
||||
filterDisabledNodes,
|
||||
} from './PartialExecutionUtils';
|
||||
|
||||
export class WorkflowExecute {
|
||||
@@ -347,7 +348,7 @@ export class WorkflowExecute {
|
||||
|
||||
// 2. Find the Subgraph
|
||||
const graph = DirectedGraph.fromWorkflow(workflow);
|
||||
const subgraph = findSubgraph({ graph, destination, trigger });
|
||||
const subgraph = findSubgraph({ graph: filterDisabledNodes(graph), destination, trigger });
|
||||
const filteredNodes = subgraph.getNodes();
|
||||
|
||||
// 3. Find the Start Nodes
|
||||
|
||||
Reference in New Issue
Block a user