114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import type { INode } from 'n8n-workflow';
|
||
import type { GraphConnection } from './DirectedGraph';
|
||
import { DirectedGraph } from './DirectedGraph';
|
||
|
||
function findSubgraphRecursive(
|
||
graph: DirectedGraph,
|
||
destinationNode: INode,
|
||
current: INode,
|
||
trigger: INode,
|
||
newGraph: DirectedGraph,
|
||
currentBranch: GraphConnection[],
|
||
) {
|
||
// If the current node is the chosen trigger keep this branch.
|
||
if (current === trigger) {
|
||
for (const connection of currentBranch) {
|
||
newGraph.addNodes(connection.from, connection.to);
|
||
newGraph.addConnection(connection);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
let parentConnections = graph.getDirectParents(current);
|
||
|
||
// If the current node has no parents, don’t keep this branch.
|
||
if (parentConnections.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// If the current node is the destination node again, don’t keep this branch.
|
||
const isCycleWithDestinationNode =
|
||
current === destinationNode && currentBranch.some((c) => c.to === destinationNode);
|
||
if (isCycleWithDestinationNode) {
|
||
return;
|
||
}
|
||
|
||
// If the current node was already visited, keep this branch.
|
||
const isCycleWithCurrentNode = currentBranch.some((c) => c.to === current);
|
||
if (isCycleWithCurrentNode) {
|
||
// TODO: write function that adds nodes when adding connections
|
||
for (const connection of currentBranch) {
|
||
newGraph.addNodes(connection.from, connection.to);
|
||
newGraph.addConnection(connection);
|
||
}
|
||
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) {
|
||
const incomingConnections = graph.getDirectParents(current);
|
||
const outgoingConnections = graph
|
||
.getDirectChildren(current)
|
||
// NOTE: When a node is disabled only the first output gets data
|
||
.filter((connection) => connection.outputIndex === 0);
|
||
|
||
parentConnections = [];
|
||
|
||
for (const incomingConnection of incomingConnections) {
|
||
for (const outgoingConnection of outgoingConnections) {
|
||
const newConnection = {
|
||
...incomingConnection,
|
||
to: outgoingConnection.to,
|
||
inputIndex: outgoingConnection.inputIndex,
|
||
};
|
||
|
||
parentConnections.push(newConnection);
|
||
currentBranch.pop();
|
||
currentBranch.push(newConnection);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Recurse on each parent.
|
||
for (const parentConnection of parentConnections) {
|
||
findSubgraphRecursive(graph, destinationNode, parentConnection.from, trigger, newGraph, [
|
||
...currentBranch,
|
||
parentConnection,
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Find all nodes that can lead from the trigger to the destination node,
|
||
* ignoring disabled nodes.
|
||
*
|
||
* The algorithm is:
|
||
* Start with Destination Node
|
||
*
|
||
* 1. if the current node is the chosen trigger keep this branch
|
||
* 2. if the current node has no parents, don’t keep this branch
|
||
* 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
|
||
*/
|
||
export function findSubgraph(
|
||
graph: DirectedGraph,
|
||
destinationNode: INode,
|
||
trigger: INode,
|
||
): DirectedGraph {
|
||
const newGraph = new DirectedGraph();
|
||
|
||
findSubgraphRecursive(graph, destinationNode, destinationNode, trigger, newGraph, []);
|
||
|
||
return newGraph;
|
||
}
|