Skip to content

Commit 2f279b8

Browse files
committed
fix: flatten now removes parent node in favor of parentId
Signed-off-by: Jean-Baptiste Bianchi <jb.bianchi@neuroglia.io>
1 parent 93e2e1e commit 2f279b8

File tree

3 files changed

+78
-47
lines changed

3 files changed

+78
-47
lines changed

src/lib/graph-builder.ts

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ const catchReference = '/catch';
2727
const branchReference = '/fork/branches';
2828
const tryReference = '/try';
2929

30-
/**
31-
* Represents a generic within a graph.
32-
* This serves as a base type for nodes, edges, and graphs.
33-
*/
34-
export type GraphElement = {
35-
/** A unique identifier for this graph element. */
36-
id: string;
37-
/** An optional label to provide additional context or naming. */
38-
label?: string;
39-
};
40-
4130
/**
4231
* Enumeration of possible node types in a graph.
4332
*/
@@ -63,6 +52,17 @@ export enum GraphNodeType {
6352
Wait = 'wait',
6453
}
6554

55+
/**
56+
* Represents a generic within a graph.
57+
* This serves as a base type for nodes, edges, and graphs.
58+
*/
59+
export type GraphElement = {
60+
/** A unique identifier for this graph element. */
61+
id: string;
62+
/** An optional label to provide additional context or naming. */
63+
label?: string;
64+
};
65+
6666
/**
6767
* Represents a node within the graph.
6868
*/
@@ -75,6 +75,14 @@ export type GraphNode = GraphElement & {
7575
task?: Task;
7676
};
7777

78+
/**
79+
* Represents a flattened node within the graph.
80+
*/
81+
export type FlatGraphNode = Omit<GraphNode, 'parent'> & {
82+
/** The id of parent graph, if any. */
83+
parentId?: string;
84+
};
85+
7886
/**
7987
* Represents a directed edge connecting two nodes in the graph.
8088
*/
@@ -90,7 +98,21 @@ export type GraphEdge = GraphElement & {
9098
*/
9199
export type Graph = GraphNode & {
92100
/** A collection of nodes that belong to this graph. */
93-
nodes: GraphNode[];
101+
nodes: Array<Graph | GraphNode>;
102+
/** A collection of edges that define relationships between nodes. */
103+
edges: GraphEdge[];
104+
/** The entry node of the graph, if any. */
105+
entryNode?: GraphNode;
106+
/** The exit node of the graph, if any. */
107+
exitNode?: GraphNode;
108+
};
109+
110+
/**
111+
* Represents a flattened graph.
112+
*/
113+
export type FlatGraph = FlatGraphNode & {
114+
/** A collection of nodes that belong to this graph. */
115+
nodes: FlatGraphNode[];
94116
/** A collection of edges that define relationships between nodes. */
95117
edges: GraphEdge[];
96118
/** The entry node of the graph, if any. */
@@ -226,6 +248,7 @@ function getNextTask(
226248
let index: number = 0;
227249
if (transition && transition != FlowDirective.Continue) {
228250
index = Array.from(tasksList.keys()).indexOf(transition);
251+
if (index === -1) throw new Error(`Unable to find task to transition to '${transition}' from '${taskName}'`);
229252
} else if (currentTask) {
230253
index = Array.from(tasksList.values()).indexOf(currentTask) + 1;
231254
if (index >= tasksList.size) {
@@ -363,7 +386,6 @@ function buildGenericTaskNode(task: Task, type: GraphNodeType, context: TaskCont
363386
*/
364387
function buildCallTaskNode(task: CallTask, context: TaskContext): GraphNode {
365388
const node = buildGenericTaskNode(task, GraphNodeType.Call, context);
366-
// TODO: add some details about the task?
367389
return node;
368390
}
369391

@@ -396,7 +418,6 @@ function buildDoTaskNode(task: DoTask, context: TaskContext): Graph {
396418
*/
397419
function buildEmitTaskNode(task: EmitTask, context: TaskContext): GraphNode {
398420
const node = buildGenericTaskNode(task, GraphNodeType.Emit, context);
399-
// TODO: add some details about the task?
400421
return node;
401422
}
402423

@@ -459,7 +480,6 @@ function buildForkTaskNode(task: ForkTask, context: TaskContext): Graph {
459480
*/
460481
function buildListenTaskNode(task: ListenTask, context: TaskContext): GraphNode {
461482
const node = buildGenericTaskNode(task, GraphNodeType.Listen, context);
462-
// TODO: add some details about the task?
463483
return node;
464484
}
465485

@@ -471,7 +491,6 @@ function buildListenTaskNode(task: ListenTask, context: TaskContext): GraphNode
471491
*/
472492
function buildRaiseTaskNode(task: RaiseTask, context: TaskContext): GraphNode {
473493
const node = buildGenericTaskNode(task, GraphNodeType.Raise, context);
474-
// TODO: add some details about the task?
475494
return node;
476495
}
477496

@@ -483,7 +502,6 @@ function buildRaiseTaskNode(task: RaiseTask, context: TaskContext): GraphNode {
483502
*/
484503
function buildRunTaskNode(task: RunTask, context: TaskContext): GraphNode {
485504
const node = buildGenericTaskNode(task, GraphNodeType.Run, context);
486-
// TODO: add some details about the task?
487505
return node;
488506
}
489507

@@ -495,7 +513,6 @@ function buildRunTaskNode(task: RunTask, context: TaskContext): GraphNode {
495513
*/
496514
function buildSetTaskNode(task: SetTask, context: TaskContext): GraphNode {
497515
const node = buildGenericTaskNode(task, GraphNodeType.Set, context);
498-
// TODO: add some details about the task?
499516
return node;
500517
}
501518

@@ -602,7 +619,6 @@ function buildTryCatchTaskNode(task: TryTask, context: TaskContext): Graph {
602619
*/
603620
function buildWaitTaskNode(task: WaitTask, context: TaskContext): GraphNode {
604621
const node = buildGenericTaskNode(task, GraphNodeType.Wait, context);
605-
// TODO: add some details about the task?
606622
return node;
607623
}
608624

@@ -692,13 +708,13 @@ export const flattenEdges = (graph: Graph): GraphEdge[] => [
692708
* @param graph The graph/node to flatten the nodes of
693709
* @returns All the nodes and subnodes declared in the graph
694710
*/
695-
export const flattenNodes = (node: Graph | GraphNode): Array<Graph | GraphNode> => [
711+
export const flattenNodes = (node: Graph | GraphNode): FlatGraphNode[] => [
696712
{
697-
...node,
698-
entryNode: undefined,
699-
exitNode: undefined,
700-
nodes: undefined,
701-
edges: undefined,
713+
id: node.id,
714+
label: node.label,
715+
type: node.type,
716+
task: node.task,
717+
parentId: node.parent?.id,
702718
},
703719
...((node as Graph).nodes || []).flatMap(flattenNodes),
704720
];
@@ -708,12 +724,12 @@ export const flattenNodes = (node: Graph | GraphNode): Array<Graph | GraphNode>
708724
* @param graph The target graph
709725
* @returns The modified graph
710726
*/
711-
export function flattenGraph(graph: Graph): Graph {
727+
export function reshapeGraph(graph: Graph): FlatGraph {
712728
const edges = remapEdges(flattenEdges(graph));
713729
const nodes = graph.nodes
714730
.flatMap((node) => flattenNodes(node))
715731
.filter((node) => node.type !== GraphNodeType.Entry && node.type !== GraphNodeType.Exit);
716-
const newGraph: Graph = {
732+
const newGraph: FlatGraph = {
717733
...graph,
718734
nodes,
719735
edges,
@@ -725,10 +741,15 @@ export function flattenGraph(graph: Graph): Graph {
725741
* Constructs a graph representation based on the given workflow.
726742
*
727743
* @param workflow The workflow to be converted into a graph structure.
728-
* @param simplifyGraph A boolean indicating whether the graph nodes & edges should be flattened and free of internal entry/exit nodes.
744+
* @param flattenGraph A boolean indicating whether the graph nodes & edges should be flattened.
745+
* @param removePorts A boolean indicationg whether the port nodes should be removed.
729746
* @returns A graph representation of the workflow.
730747
*/
731-
export function buildGraph(workflow: Workflow, simplifyGraph: boolean = false): Graph {
748+
export function buildGraph(
749+
workflow: Workflow,
750+
flattenGraph: boolean = false,
751+
removePorts: boolean = false,
752+
): Graph | FlatGraph {
732753
const graph = initGraph(GraphNodeType.Root);
733754
if (!graph.entryNode) throw new Error('The root graph should have an entry node.');
734755
buildTransitions(graph.entryNode, {
@@ -738,6 +759,17 @@ export function buildGraph(workflow: Workflow, simplifyGraph: boolean = false):
738759
taskReference: doReference,
739760
knownEdges: [],
740761
});
741-
if (!simplifyGraph) return graph;
742-
return flattenGraph(graph);
762+
if (removePorts && !flattenGraph) throw new Error(`Ports can only be removed if the graph has been flattened.`);
763+
if (!flattenGraph) return graph;
764+
const flatGraph = {
765+
...graph,
766+
edges: flattenEdges(graph),
767+
nodes: graph.nodes.flatMap(flattenNodes),
768+
};
769+
if (!removePorts) return flatGraph;
770+
return {
771+
...flatGraph,
772+
edges: remapEdges(flatGraph.edges),
773+
nodes: flatGraph.nodes.filter((node) => node.type !== GraphNodeType.Entry && node.type !== GraphNodeType.Exit),
774+
};
743775
}

src/lib/mermaid-converter.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Workflow } from './generated/definitions/specification';
2-
import { buildGraph, Graph, GraphEdge, GraphNode, GraphNodeType } from './graph-builder';
2+
import { buildGraph, FlatGraph, FlatGraphNode, GraphEdge, GraphNodeType } from './graph-builder';
33

44
/**
55
* Adds indentation to each line of the provided code
@@ -14,18 +14,17 @@ const indent = (code: string) =>
1414

1515
/**
1616
* Converts a graph to Mermaid code
17-
* @param graph The graph to convert
1817
* @param root The root graph
18+
* @param subgraphNode The graph to convert
1919
* @returns The converted graph
2020
*/
21-
function convertGraphToCode(graph: Graph, root: Graph): string {
22-
const isRoot: boolean = graph === root;
23-
const nodes = isRoot ? graph.nodes : root.nodes.filter((node) => node.parent?.id === graph.id);
24-
const edges = isRoot ? graph.edges : [];
25-
const code = `${isRoot ? 'flowchart TD' : `subgraph ${graph.id} ["${graph.label || graph.id}"]`}
26-
${indent(nodes.map((node) => convertNodeToCode(node, root)).join('\n'))}
21+
function convertGraphToCode(root: FlatGraph, subgraphNode?: FlatGraphNode): string {
22+
const nodes = !subgraphNode ? root.nodes : root.nodes.filter((n) => n.parentId === subgraphNode.id);
23+
const edges = !subgraphNode ? root.edges : [];
24+
const code = `${!subgraphNode ? 'flowchart TD' : `subgraph ${subgraphNode.id} ["${subgraphNode.label || subgraphNode.id}"]`}
25+
${indent(nodes.map((node) => convertNodeToCode(root, node)).join('\n'))}
2726
${indent(edges.map((edge) => convertEdgeToCode(edge)).join('\n'))}
28-
${isRoot ? '' : 'end'}`;
27+
${!subgraphNode ? '' : 'end'}`;
2928
return code;
3029
}
3130

@@ -35,10 +34,10 @@ ${isRoot ? '' : 'end'}`;
3534
* @param graph The root graph
3635
* @returns The converted node
3736
*/
38-
function convertNodeToCode(node: GraphNode | Graph, root: Graph): string {
37+
function convertNodeToCode(root: FlatGraph, node: FlatGraphNode): string {
3938
let code = '';
40-
if (root.nodes.filter((n) => n.parent?.id === node.id).length) {
41-
code = convertGraphToCode(node as Graph, root);
39+
if (root.nodes.filter((n) => n.parentId === node.id).length) {
40+
code = convertGraphToCode(root, node);
4241
} else {
4342
code = node.id;
4443
switch (node.type) {
@@ -78,9 +77,9 @@ function convertEdgeToCode(edge: GraphEdge): string {
7877
* @returns The Mermaid diagram
7978
*/
8079
export function convertToMermaidCode(workflow: Workflow): string {
81-
const graph = buildGraph(workflow, true);
80+
const graph = buildGraph(workflow, true, true);
8281
return (
83-
convertGraphToCode(graph, graph) +
82+
convertGraphToCode(graph) +
8483
`
8584
8685
classDef hidden width: 1px, height: 1px;` // should be "classDef hidden display: none;" but it can induce a Mermaid bug - https://github.com/mermaid-js/mermaid/issues/6452

tests/graph/simplified-graph.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ do:
1313
- initialize:
1414
set:
1515
foo: bar`);
16-
const graph = buildGraph(workflow, true);
16+
const graph = buildGraph(workflow, true, true);
1717
expect(graph).toBeDefined();
1818
expect(graph.nodes.length).toBe(3); // start --> initialize --> end
1919
expect(graph.edges.length).toBe(2);
@@ -42,7 +42,7 @@ do:
4242
type: com.fake.petclinic.pets.checkup.completed.v2
4343
output:
4444
as: '.pets + [{ "id": $pet.id }]'`);
45-
const graph = buildGraph(workflow, true);
45+
const graph = buildGraph(workflow, true, true);
4646
expect(graph).toBeDefined();
4747
expect(graph.nodes.length).toBe(4); // start --[--> waitForCheckup --]--> end
4848
expect(graph.edges.length).toBe(2);

0 commit comments

Comments
 (0)