Skip to content

Commit dbdae13

Browse files
authoredMar 27, 2025··
Merge pull request #192 from neuroglia-io/feat-mermaid-diagram
feat: add graph and MermaidJS flowchart
2 parents 06e5a4a + d059bf0 commit dbdae13

File tree

10 files changed

+1361
-21
lines changed

10 files changed

+1361
-21
lines changed
 

‎README.md

+136-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
![Node CI](https://github.com/serverlessworkflow/sdk-typescript/workflows/Node%20CI/badge.svg) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/serverlessworkflow/sdk-typescript)
22

3+
- [Serverless Workflow Specification - TypeScript SDK](#serverless-workflow-specification---typescript-sdk)
4+
- [Status](#status)
5+
- [SDK Structure](#sdk-structure)
6+
- [Types and Interfaces](#types-and-interfaces)
7+
- [Classes](#classes)
8+
- [Fluent Builders](#fluent-builders)
9+
- [Validation Function](#validation-function)
10+
- [Other tools](#other-tools)
11+
- [Getting Started](#getting-started)
12+
- [Installation](#installation)
13+
- [Usage](#usage)
14+
- [Create a Workflow Definition from YAML or JSON](#create-a-workflow-definition-from-yaml-or-json)
15+
- [Create a Workflow Definition by Casting an Object](#create-a-workflow-definition-by-casting-an-object)
16+
- [Create a Workflow Definition Using a Class Constructor](#create-a-workflow-definition-using-a-class-constructor)
17+
- [Create a Workflow Definition Using the Builder API](#create-a-workflow-definition-using-the-builder-api)
18+
- [Serialize a Workflow Definition to YAML or JSON](#serialize-a-workflow-definition-to-yaml-or-json)
19+
- [Validate Workflow Definitions](#validate-workflow-definitions)
20+
- [Generate a directed graph](#generate-a-directed-graph)
21+
- [Generate a MermaidJS flowchart](#generate-a-mermaidjs-flowchart)
22+
- [Building Locally](#building-locally)
23+
324
# Serverless Workflow Specification - TypeScript SDK
425

526
This SDK provides a TypeScript API for working with the [Serverless Workflow Specification](https://github.com/serverlessworkflow/specification).
@@ -14,7 +35,7 @@ The npm [`@serverlessworkflow/sdk`](https://www.npmjs.com/package/@serverlesswor
1435

1536
| Latest Releases | Conformance to Spec Version |
1637
| :---: | :---: |
17-
| [v1.0.0.\*](https://github.com/serverlessworkflow/sdk-typescript/releases/) | [v1.0.0](https://github.com/serverlessworkflow/specification) |
38+
| [v1.0.\*](https://github.com/serverlessworkflow/sdk-typescript/releases/) | [v1.0.0](https://github.com/serverlessworkflow/specification) |
1839

1940
> [!WARNING]
2041
> Previous versions of the SDK were published with a typo in the scope:
@@ -56,6 +77,9 @@ The SDK includes a validation function to check if objects conform to the expect
5677

5778
The `validate` function is directly exported and can be used as `validate('Workflow', workflowObject)`.
5879

80+
### Other Tools
81+
The SDK also ships tools to build directed graph and MermaidJS flowcharts from a workflow.
82+
5983
## Getting Started
6084

6185
### Installation
@@ -86,7 +110,7 @@ do:
86110
set:
87111
variable: 'my first workflow'
88112
`;
89-
const workflowDefinition = Classes.Workflow.deserialize(text);
113+
const workflow = Classes.Workflow.deserialize(text);
90114
```
91115

92116
#### Create a Workflow Definition by Casting an Object
@@ -96,7 +120,7 @@ You can type-cast an object to match the structure of a workflow definition:
96120
import { Classes, Specification, validate } from '@serverlessworkflow/sdk';
97121

98122
// Simply cast an object:
99-
const workflowDefinition = {
123+
const workflow = {
100124
document: {
101125
dsl: '1.0.0',
102126
name: 'test',
@@ -116,9 +140,9 @@ const workflowDefinition = {
116140

117141
// Validate it
118142
try {
119-
validate('Workflow', workflowDefinition);
143+
validate('Workflow', workflow);
120144
// Serialize it
121-
const definitionTxt = Classes.Workflow.serialize(workflowDefinition);
145+
const definitionTxt = Classes.Workflow.serialize(workflow);
122146
}
123147
catch (ex) {
124148
// Invalid workflow definition
@@ -132,7 +156,7 @@ You can create a workflow definition by calling a constructor:
132156
import { Classes, validate } from '@serverlessworkflow/sdk';
133157

134158
// Simply use the constructor
135-
const workflowDefinition = new Classes.Workflow({
159+
const workflow = new Classes.Workflow({
136160
document: {
137161
dsl: '1.0.0',
138162
name: 'test',
@@ -149,7 +173,7 @@ const workflowDefinition = new Classes.Workflow({
149173
},
150174
*/],
151175
});
152-
workflowDefinition.do.push({
176+
workflow.do.push({
153177
step1: new Classes.SetTask({
154178
set: {
155179
variable: 'my first workflow',
@@ -159,9 +183,9 @@ workflowDefinition.do.push({
159183

160184
// Validate it
161185
try {
162-
workflowDefinition.validate();
186+
workflow.validate();
163187
// Serialize it
164-
const definitionTxt = workflowDefinition.serialize();
188+
const definitionTxt = workflow.serialize();
165189
}
166190
catch (ex) {
167191
// Invalid workflow definition
@@ -174,7 +198,7 @@ You can use the fluent API to build a validated and normalized workflow definiti
174198
```typescript
175199
import { documentBuilder, setTaskBuilder, taskListBuilder, workflowBuilder } from '@serverlessworkflow/sdk';
176200

177-
const workflowDefinition = workflowBuilder(/*workflowDefinitionObject*/)
201+
const workflow = workflowBuilder(/*workflowObject*/)
178202
.document(
179203
documentBuilder()
180204
.dsl('1.0.0')
@@ -206,12 +230,12 @@ You can serialize a workflow definition either by using its `serialize` method i
206230
```typescript
207231
import { Classes } from '@serverlessworkflow/sdk';
208232

209-
// const workflowDefinition = <Your preferred method>;
210-
if (workflowDefinition instanceof Classes.Workflow) {
211-
const yaml = workflowDefinition.serialize(/*'yaml' | 'json' */);
233+
// const workflow = <Your preferred method>;
234+
if (workflow instanceof Classes.Workflow) {
235+
const yaml = workflow.serialize(/*'yaml' | 'json' */);
212236
}
213237
else {
214-
const json = Classes.Workflow.serialize(workflowDefinition, 'json');
238+
const json = Classes.Workflow.serialize(workflow, 'json');
215239
}
216240
```
217241
> [!NOTE]
@@ -223,20 +247,114 @@ Validation can be achieved in two ways: via the `validate` function or the insta
223247
```typescript
224248
import { Classes, validate } from '@serverlessworkflow/sdk';
225249

226-
// const workflowDefinition = <Your preferred method>;
250+
const workflow = /* <Your preferred method> */;
227251
try {
228-
if (workflowDefinition instanceof Classes.Workflow) {
229-
workflowDefinition.validate();
252+
if (workflow instanceof Classes.Workflow) {
253+
workflow.validate();
230254
}
231255
else {
232-
validate('Workflow', workflowDefinition);
256+
validate('Workflow', workflow);
233257
}
234258
}
235259
catch (ex) {
236260
// Workflow definition is invalid
237261
}
238262
```
239263

264+
#### Generate a directed graph
265+
A [directed graph](https://en.wikipedia.org/wiki/Directed_graph) of a workflow can be generated using the `buildGraph` function, or alternatives:
266+
- Workflow instance `.toGraph();`
267+
- Static `Classes.Workflow.toGraph(workflow)`
268+
269+
```typescript
270+
import { buildGraph } from '@serverlessworkflow/sdk';
271+
272+
const workflow = {
273+
document: {
274+
dsl: '1.0.0',
275+
name: 'using-plain-object',
276+
version: '1.0.0',
277+
namespace: 'default',
278+
},
279+
do: [
280+
{
281+
step1: {
282+
set: {
283+
variable: 'my first workflow',
284+
},
285+
},
286+
},
287+
],
288+
};
289+
const graph = buildGraph(workflow);
290+
// const workflow = new Classes.Workflow({...}); const graph = workflow.toGraph();
291+
// const graph = Classes.Workflow.toGraph(workflow);
292+
/*{
293+
id: 'root',
294+
type: 'root',
295+
label: undefined,
296+
parent: null,
297+
nodes: [...], // length 3 - root entry node, step1 node, root exit node
298+
edges: [...], // length 2 - entry to step1, step1 to exit
299+
entryNode: {...}, // root entry node
300+
exitNode: {...} // root exit node
301+
}*/
302+
```
303+
304+
#### Generate a MermaidJS flowchart
305+
Generating a [MermaidJS](https://mermaid.js.org/) flowchart can be achieved in two ways: using the `convertToMermaidCode`, the legacy `MermaidDiagram` class, or alternatives:
306+
- Workflow instance `.toMermaidCode();`
307+
- Static `Classes.Workflow.toMermaidCode(workflow)`
308+
309+
```typescript
310+
import { convertToMermaidCode, MermaidDiagram } from '@serverlessworkflow/sdk';
311+
312+
const workflow = {
313+
document: {
314+
dsl: '1.0.0',
315+
name: 'using-plain-object',
316+
version: '1.0.0',
317+
namespace: 'default',
318+
},
319+
do: [
320+
{
321+
step1: {
322+
set: {
323+
variable: 'my first workflow',
324+
},
325+
},
326+
},
327+
],
328+
};
329+
const mermaidCode = convertToMermaidCode(workflow) /* or */;
330+
// const mermaidCode = new MermaidDiagram(workflow).sourceCode();
331+
// const workflow = new Classes.Workflow({...}); const mermaidCode = workflow.toMermaidCode();
332+
// const mermaidCode = Classes.Workflow.toMermaidCode(workflow);
333+
/*
334+
flowchart TD
335+
root-entry-node(( ))
336+
root-exit-node((( )))
337+
/do/0/step1["step1"]
338+
/do/0/step1 --> root-exit-node
339+
root-entry-node --> /do/0/step1
340+
341+
342+
classDef hidden display: none;
343+
*/
344+
```
345+
346+
```mermaid
347+
flowchart TD
348+
root-entry-node(( ))
349+
root-exit-node((( )))
350+
/do/0/step1["step1"]
351+
/do/0/step1 --> root-exit-node
352+
root-entry-node --> /do/0/step1
353+
354+
355+
classDef hidden display: none;
356+
```
357+
240358
### Building Locally
241359

242360
To build the project and run tests locally, use the following commands:

‎examples/browser/mermaid.html

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Serveless Workflow</title>
7+
<base href="/">
8+
<meta content="width=device-width, initial-scale=1" name="viewport">
9+
</head>
10+
11+
<body>
12+
<p>YAML or JSON:</p>
13+
<textarea id="input" rows="50" cols="100"></textarea>
14+
<div id="diagram-container"></div>
15+
<pre id="output"></pre>
16+
<script src="../../dist/umd/index.umd.js"></script>
17+
<script type="module">
18+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
19+
(async () => {
20+
const { Classes, Specification, validate, convertToMermaidCode } = serverWorkflowSdk;
21+
const workflowDefinition = {
22+
"document": {
23+
"dsl": "1.0.0",
24+
"namespace": "examples",
25+
"name": "accumulate-room-readings",
26+
"version": "0.1.0"
27+
},
28+
"do": [
29+
{
30+
"consumeReading": {
31+
"listen": {
32+
"to": {
33+
"all": [
34+
{
35+
"with": {
36+
"source": "https://my.home.com/sensor",
37+
"type": "my.home.sensors.temperature"
38+
},
39+
"correlate": {
40+
"roomId": {
41+
"from": ".roomid"
42+
}
43+
}
44+
},
45+
{
46+
"with": {
47+
"source": "https://my.home.com/sensor",
48+
"type": "my.home.sensors.humidity"
49+
},
50+
"correlate": {
51+
"roomId": {
52+
"from": ".roomid"
53+
}
54+
}
55+
}
56+
]
57+
}
58+
},
59+
"output": {
60+
"as": ".data.reading"
61+
}
62+
}
63+
},
64+
{
65+
"logReading": {
66+
"for": {
67+
"each": "reading",
68+
"in": ".readings"
69+
},
70+
"do": [
71+
{
72+
"callOrderService": {
73+
"call": "openapi",
74+
"with": {
75+
"document": {
76+
"endpoint": "http://myorg.io/ordersservices.json"
77+
},
78+
"operationId": "logreading"
79+
}
80+
}
81+
}
82+
]
83+
}
84+
},
85+
{
86+
"generateReport": {
87+
"call": "openapi",
88+
"with": {
89+
"document": {
90+
"endpoint": "http://myorg.io/ordersservices.json"
91+
},
92+
"operationId": "produceReport"
93+
}
94+
}
95+
}
96+
],
97+
"timeout": {
98+
"after": {
99+
"hours": 1
100+
}
101+
}
102+
}/* as Specification.Workflow // <-- If you're using TypeScript*/;
103+
const diagramContainerEl = document.getElementById('diagram-container');
104+
const inputTextarea = document.getElementById('input');
105+
const processWorkflow = async () => {
106+
try {
107+
const workflow = Classes.Workflow.deserialize(inputTextarea.value);
108+
const mermaidCode = convertToMermaidCode(workflow);
109+
document.getElementById('output').innerHTML = `--- YAML ---\n${Classes.Workflow.serialize(workflow)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflow, 'json')}\n\n--- MERMAID ---\n${mermaidCode}`;
110+
mermaid.initialize({ startOnLoad: false });
111+
const { svg, bindFunctions } = await mermaid.render('sw-diagram', mermaidCode);
112+
diagramContainerEl.innerHTML = svg;
113+
}
114+
catch (ex) {
115+
console.error('Invalid workflow', ex);
116+
}
117+
};
118+
let debounceHandle;
119+
inputTextarea.addEventListener('keyup', () => {
120+
if (debounceHandle) {
121+
clearTimeout(debounceHandle);
122+
}
123+
debounceHandle = setTimeout(processWorkflow, 300);
124+
});
125+
inputTextarea.value = JSON.stringify(workflowDefinition, null, 4);
126+
await processWorkflow();
127+
})();
128+
</script>
129+
</body>
130+
131+
</html>

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@serverlessworkflow/sdk",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"schemaVersion": "1.0.0",
55
"description": "Typescript SDK for Serverless Workflow Specification",
66
"main": "umd/index.umd.min.js",

‎src/lib/generated/classes/workflow.ts

+40
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { getLifecycleHooks } from '../../lifecycle-hooks';
3333
import { validate } from '../../validation';
3434
import { isObject } from '../../utils';
3535
import * as yaml from 'js-yaml';
36+
import { buildGraph, Graph } from '../../graph-builder';
37+
import { convertToMermaidCode } from '../../mermaid-converter';
3638

3739
/**
3840
* Represents the intersection between the Workflow class and type
@@ -112,6 +114,14 @@ export class Workflow extends ObjectHydrator<Specification.Workflow> {
112114
return yaml.dump(normalized);
113115
}
114116

117+
static toGraph(model: Partial<WorkflowIntersection>): Graph {
118+
return buildGraph(model as unknown as WorkflowIntersection);
119+
}
120+
121+
static toMermaidCode(model: Partial<WorkflowIntersection>): string {
122+
return convertToMermaidCode(model as unknown as WorkflowIntersection);
123+
}
124+
115125
/**
116126
* Serializes the workflow to YAML or JSON
117127
* @param format The format, 'yaml' or 'json', default is 'yaml'
@@ -121,6 +131,22 @@ export class Workflow extends ObjectHydrator<Specification.Workflow> {
121131
serialize(format: 'yaml' | 'json' = 'yaml', normalize: boolean = true): string {
122132
return Workflow.serialize(this as unknown as WorkflowIntersection, format, normalize);
123133
}
134+
135+
/**
136+
* Creates a directed graph representation of the workflow
137+
* @returns A directed graph of the workflow
138+
*/
139+
toGraph(): Graph {
140+
return Workflow.toGraph(this as unknown as WorkflowIntersection);
141+
}
142+
143+
/**
144+
* Generates the MermaidJS code corresponding to the workflow
145+
* @returns The MermaidJS code
146+
*/
147+
toMermaidCode(): string {
148+
return Workflow.toMermaidCode(this as unknown as WorkflowIntersection);
149+
}
124150
}
125151

126152
export const _Workflow = Workflow as WorkflowConstructor & {
@@ -139,4 +165,18 @@ export const _Workflow = Workflow as WorkflowConstructor & {
139165
* @returns A string representation of the workflow
140166
*/
141167
serialize(workflow: Partial<WorkflowIntersection>, format?: 'yaml' | 'json', normalize?: boolean): string;
168+
169+
/**
170+
* Creates a directed graph representation of the provided workflow
171+
* @param workflow The workflow to convert
172+
* @returns A directed graph of the provided workflow
173+
*/
174+
toGraph(workflow: Partial<WorkflowIntersection>): Graph;
175+
176+
/**
177+
* Generates the MermaidJS code corresponding to the provided workflow
178+
* @param workflow The workflow to convert
179+
* @returns The MermaidJS code
180+
*/
181+
toMermaidCode(workflow: Partial<WorkflowIntersection>): string;
142182
};

‎src/lib/graph-builder.ts

+584
Large diffs are not rendered by default.

‎src/lib/mermaid-converter.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Workflow } from './generated/definitions/specification';
2+
import { buildGraph, Graph, GraphEdge, GraphNode, GraphNodeType } from './graph-builder';
3+
4+
/**
5+
* Adds indentation to each line of the provided code
6+
* @param code The code to indent
7+
* @returns The indented code
8+
*/
9+
const indent = (code: string) =>
10+
code
11+
.split('\n')
12+
.map((line) => ` ${line}`)
13+
.join('\n');
14+
15+
/**
16+
* Converts a graph to Mermaid code
17+
* @param graph The graph to convert
18+
* @returns The converted graph
19+
*/
20+
function convertGraphToCode(graph: Graph): string {
21+
const isRoot: boolean = graph.id === 'root';
22+
const code = `${isRoot ? 'flowchart TD' : `subgraph ${graph.id} ["${graph.label || graph.id}"]`}
23+
${indent(graph.nodes.map((node) => convertNodeToCode(node)).join('\n'))}
24+
${indent(graph.edges.map((edge) => convertEdgeToCode(edge)).join('\n'))}
25+
${isRoot ? '' : 'end'}`;
26+
return code;
27+
}
28+
29+
/**
30+
* Converts a node to Mermaid code
31+
* @param node The node to convert
32+
* @returns The converted node
33+
*/
34+
function convertNodeToCode(node: GraphNode | Graph): string {
35+
let code = '';
36+
if ((node as Graph).nodes?.length) {
37+
code = convertGraphToCode(node as Graph);
38+
} else {
39+
code = node.id;
40+
switch (node.type) {
41+
case GraphNodeType.Entry:
42+
case GraphNodeType.Exit:
43+
code += ':::hidden';
44+
break;
45+
case GraphNodeType.Start:
46+
code += '(( ))'; // alt '@{ shape: circle, label: " "}';
47+
break;
48+
case GraphNodeType.End:
49+
code += '((( )))'; // alt '@{ shape: dbl-circ, label: " "}';
50+
break;
51+
default:
52+
code += `["${node.label}"]`; // alt `@{ label: "${node.label}" }`
53+
}
54+
}
55+
return code;
56+
}
57+
58+
/**
59+
* Converts an edge to Mermaid code
60+
* @param edge The edge to convert
61+
* @returns The converted edge
62+
*/
63+
function convertEdgeToCode(edge: GraphEdge): string {
64+
const ignoreEndArrow =
65+
!edge.destinationId.startsWith('root') &&
66+
(edge.destinationId.endsWith('-entry-node') || edge.destinationId.endsWith('-exit-node'));
67+
const code = `${edge.sourceId} ${edge.label ? `--"${edge.label}"` : ''}--${ignoreEndArrow ? '-' : '>'} ${edge.destinationId}`;
68+
return code;
69+
}
70+
71+
/**
72+
* Converts the provided workflow to Mermaid code
73+
* @param workflow The workflow to convert
74+
* @returns The Mermaid diagram
75+
*/
76+
export function convertToMermaidCode(workflow: Workflow): string {
77+
const graph = buildGraph(workflow);
78+
return (
79+
convertGraphToCode(graph) +
80+
`
81+
82+
classDef hidden display: none;`
83+
);
84+
}
85+
86+
/**
87+
* Represents a Mermaid diagram generator for a given workflow.
88+
* This class takes a workflow definition and converts it into a Mermaid.js-compatible diagram.
89+
*/
90+
export class MermaidDiagram {
91+
constructor(private workflow: Workflow) {}
92+
93+
/**
94+
* Generates the Mermaid code representation of the workflow.
95+
* @returns The Mermaid diagram source code as a string.
96+
*/
97+
sourceCode(): string {
98+
return convertToMermaidCode(this.workflow);
99+
}
100+
}

‎src/serverless-workflow-sdk.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './lib/generated/builders';
22
export * from './lib/generated/classes';
33
export * from './lib/generated/definitions';
44
export * from './lib/validation';
5+
export * from './lib/graph-builder';
6+
export * from './lib/mermaid-converter';

‎tests/graph/graph.spec.ts

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Specification } from '../../src';
2+
import { Classes } from '../../src/lib/generated/classes';
3+
import { buildGraph, Graph } from '../../src/lib/graph-builder';
4+
5+
describe('Workflow to Graph', () => {
6+
it('should build a graph for a workflow with a Set task, using the buildGraph function', () => {
7+
const workflow = Classes.Workflow.deserialize(`
8+
document:
9+
dsl: '1.0.0'
10+
namespace: test
11+
name: set
12+
version: '0.1.0'
13+
do:
14+
- initialize:
15+
set:
16+
foo: bar`);
17+
const graph = buildGraph(workflow);
18+
expect(graph).toBeDefined();
19+
expect(graph.nodes.length).toBe(3); // start --> initialize --> end
20+
expect(graph.edges.length).toBe(2);
21+
});
22+
23+
it('should build a graph for a workflow with a Set task, using the instance method', () => {
24+
const workflow = Classes.Workflow.deserialize(`
25+
document:
26+
dsl: '1.0.0'
27+
namespace: test
28+
name: set
29+
version: '0.1.0'
30+
do:
31+
- initialize:
32+
set:
33+
foo: bar`);
34+
const graph = workflow.toGraph();
35+
expect(graph).toBeDefined();
36+
expect(graph.nodes.length).toBe(3); // start --> initialize --> end
37+
expect(graph.edges.length).toBe(2);
38+
});
39+
40+
it('should build a graph for a workflow with a Set task, using the static method', () => {
41+
const workflow = {
42+
document: {
43+
dsl: '1.0.0',
44+
name: 'set',
45+
version: '1.0.0',
46+
namespace: 'test',
47+
},
48+
do: [
49+
{
50+
initialize: {
51+
set: {
52+
foo: 'bar',
53+
},
54+
},
55+
},
56+
],
57+
} as Specification.Workflow;
58+
const graph = Classes.Workflow.toGraph(workflow);
59+
expect(graph).toBeDefined();
60+
expect(graph.nodes.length).toBe(3); // start --> initialize --> end
61+
expect(graph.edges.length).toBe(2);
62+
});
63+
64+
it('should build a graph for a workflow with multiple Set tasks', () => {
65+
const workflow = Classes.Workflow.deserialize(`
66+
document:
67+
dsl: '1.0.0'
68+
namespace: test
69+
name: set
70+
version: '0.1.0'
71+
do:
72+
- step1:
73+
set:
74+
foo: bar
75+
- step2:
76+
set:
77+
foo2: bar
78+
- step3:
79+
set:
80+
foo3: bar`);
81+
const graph = buildGraph(workflow);
82+
expect(graph).toBeDefined();
83+
expect(graph.nodes.length).toBe(5); // start --> step1 --> step2 --> step3 --> end
84+
expect(graph.edges.length).toBe(4);
85+
});
86+
87+
it('should build a graph for a workflow with a task containing an If clause, producing an alternative edge', () => {
88+
const workflow = Classes.Workflow.deserialize(`
89+
document:
90+
dsl: '1.0.0'
91+
namespace: test
92+
name: set
93+
version: '0.1.0'
94+
do:
95+
- initialize:
96+
if: \${ input.data == true }
97+
set:
98+
foo: bar`);
99+
const graph = buildGraph(workflow);
100+
expect(graph).toBeDefined();
101+
expect(graph.nodes.length).toBe(3); // start --> initialize --> end
102+
expect(graph.edges.length).toBe(3); // ----------------->
103+
expect(graph.edges.filter((e) => e.label === '${ input.data == true }').length).toBe(1);
104+
});
105+
106+
it('should build a graph for a workflow with a For task, producing a subgraph', () => {
107+
const workflow = Classes.Workflow.deserialize(`
108+
document:
109+
dsl: '1.0.0'
110+
namespace: test
111+
name: for-example
112+
version: '0.1.0'
113+
do:
114+
- checkup:
115+
for:
116+
each: pet
117+
in: .pets
118+
at: index
119+
while: .vet != null
120+
do:
121+
- waitForCheckup:
122+
listen:
123+
to:
124+
one:
125+
with:
126+
type: com.fake.petclinic.pets.checkup.completed.v2
127+
output:
128+
as: '.pets + [{ "id": $pet.id }]'`);
129+
const graph = buildGraph(workflow);
130+
const forSubgraph = graph.nodes.find((node) => node.label === 'checkup') as Graph;
131+
expect(graph).toBeDefined();
132+
expect(graph.nodes.length).toBe(3); // start --> checkup --> end
133+
expect(graph.edges.length).toBe(2);
134+
135+
expect(forSubgraph).toBeDefined();
136+
expect(forSubgraph.nodes.length).toBe(3); // entry --> waitForCheckup --> exit
137+
expect(forSubgraph.edges.length).toBe(2);
138+
});
139+
});

‎tests/mermaid/mermaid.spec.ts

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { Classes } from '../../src/lib/generated/classes';
2+
import { Specification } from '../../src/lib/generated/definitions';
3+
import { convertToMermaidCode, MermaidDiagram } from '../../src/lib/mermaid-converter';
4+
5+
describe('Workflow to MermaidJS Flowchart', () => {
6+
it('should build a Mermaid diagram for a workflow with a Set task, using the convertToMermaidCode function', () => {
7+
const workflow = Classes.Workflow.deserialize(`
8+
document:
9+
dsl: '1.0.0'
10+
namespace: test
11+
name: set
12+
version: '0.1.0'
13+
do:
14+
- initialize:
15+
set:
16+
foo: bar`);
17+
const mermaidCode = convertToMermaidCode(workflow).trim();
18+
expect(mermaidCode).toBe(
19+
`flowchart TD
20+
root-entry-node(( ))
21+
root-exit-node((( )))
22+
/do/0/initialize["initialize"]
23+
/do/0/initialize --> root-exit-node
24+
root-entry-node --> /do/0/initialize
25+
26+
27+
classDef hidden display: none;`.trim(),
28+
);
29+
});
30+
31+
it('should build a Mermaid diagram for a workflow with a Set task, using the instance method', () => {
32+
const workflow = Classes.Workflow.deserialize(`
33+
document:
34+
dsl: '1.0.0'
35+
namespace: test
36+
name: set
37+
version: '0.1.0'
38+
do:
39+
- initialize:
40+
set:
41+
foo: bar`);
42+
const mermaidCode = workflow.toMermaidCode().trim();
43+
expect(mermaidCode).toBe(
44+
`flowchart TD
45+
root-entry-node(( ))
46+
root-exit-node((( )))
47+
/do/0/initialize["initialize"]
48+
/do/0/initialize --> root-exit-node
49+
root-entry-node --> /do/0/initialize
50+
51+
52+
classDef hidden display: none;`.trim(),
53+
);
54+
});
55+
56+
it('should build a Mermaid diagram for a workflow with a Set task, using the static method', () => {
57+
const workflow = {
58+
document: {
59+
dsl: '1.0.0',
60+
name: 'set',
61+
version: '1.0.0',
62+
namespace: 'test',
63+
},
64+
do: [
65+
{
66+
initialize: {
67+
set: {
68+
foo: 'bar',
69+
},
70+
},
71+
},
72+
],
73+
} as Specification.Workflow;
74+
const mermaidCode = Classes.Workflow.toMermaidCode(workflow).trim();
75+
expect(mermaidCode).toBe(
76+
`flowchart TD
77+
root-entry-node(( ))
78+
root-exit-node((( )))
79+
/do/0/initialize["initialize"]
80+
/do/0/initialize --> root-exit-node
81+
root-entry-node --> /do/0/initialize
82+
83+
84+
classDef hidden display: none;`.trim(),
85+
);
86+
});
87+
88+
it('should build a Mermaid diagram for a workflow with a Set task, using the legacy MermaidDiagram class', () => {
89+
const workflow = Classes.Workflow.deserialize(`
90+
document:
91+
dsl: '1.0.0'
92+
namespace: test
93+
name: set
94+
version: '0.1.0'
95+
do:
96+
- initialize:
97+
set:
98+
foo: bar`);
99+
const mermaidCode = new MermaidDiagram(workflow).sourceCode().trim();
100+
expect(mermaidCode).toBe(
101+
`flowchart TD
102+
root-entry-node(( ))
103+
root-exit-node((( )))
104+
/do/0/initialize["initialize"]
105+
/do/0/initialize --> root-exit-node
106+
root-entry-node --> /do/0/initialize
107+
108+
109+
classDef hidden display: none;`.trim(),
110+
);
111+
});
112+
113+
it('should build a Mermaid diagram with an alternative, labelled, edge', () => {
114+
const workflow = Classes.Workflow.deserialize(`
115+
document:
116+
dsl: '1.0.0'
117+
namespace: test
118+
name: set
119+
version: '0.1.0'
120+
do:
121+
- initialize:
122+
if: \${ input.data == true }
123+
set:
124+
foo: bar`);
125+
const mermaidCode = convertToMermaidCode(workflow).trim();
126+
expect(mermaidCode).toBe(
127+
`flowchart TD
128+
root-entry-node(( ))
129+
root-exit-node((( )))
130+
/do/0/initialize["initialize"]
131+
/do/0/initialize --> root-exit-node
132+
root-entry-node --"\${ input.data == true }"--> /do/0/initialize
133+
root-entry-node --> root-exit-node
134+
135+
136+
classDef hidden display: none;`.trim(),
137+
);
138+
});
139+
140+
it('should build a Mermaid diagram for a workflow with a For task', () => {
141+
const workflow = Classes.Workflow.deserialize(`
142+
document:
143+
dsl: '1.0.0'
144+
namespace: test
145+
name: for-example
146+
version: '0.1.0'
147+
do:
148+
- checkup:
149+
for:
150+
each: pet
151+
in: .pets
152+
at: index
153+
while: .vet != null
154+
do:
155+
- waitForCheckup:
156+
listen:
157+
to:
158+
one:
159+
with:
160+
type: com.fake.petclinic.pets.checkup.completed.v2
161+
output:
162+
as: '.pets + [{ "id": $pet.id }]'`);
163+
const mermaidCode = convertToMermaidCode(workflow).trim();
164+
expect(mermaidCode).toBe(
165+
`flowchart TD
166+
root-entry-node(( ))
167+
root-exit-node((( )))
168+
subgraph /do/0/checkup ["checkup"]
169+
/do/0/checkup-entry-node:::hidden
170+
/do/0/checkup-exit-node:::hidden
171+
/do/0/checkup/for/do/0/waitForCheckup["waitForCheckup"]
172+
/do/0/checkup/for/do/0/waitForCheckup --- /do/0/checkup-exit-node
173+
/do/0/checkup-entry-node --> /do/0/checkup/for/do/0/waitForCheckup
174+
end
175+
/do/0/checkup-exit-node --> root-exit-node
176+
root-entry-node --- /do/0/checkup-entry-node
177+
178+
179+
classDef hidden display: none;`.trim(),
180+
);
181+
});
182+
});

‎tools/4_generate-classes.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ import { Specification } from '../definitions';
4646
import { getLifecycleHooks } from '../../lifecycle-hooks';
4747
import { validate } from '../../validation';
4848
${hydrationResult.code ? `import { isObject } from '../../utils';` : ''}
49-
${name === 'Workflow' ? `import * as yaml from 'js-yaml';` : ''}
49+
${
50+
name === 'Workflow'
51+
? `import * as yaml from 'js-yaml';
52+
import { buildGraph, Graph } from '../../graph-builder';
53+
import { convertToMermaidCode } from '../../mermaid-converter';`
54+
: ''
55+
}
5056
5157
/**
5258
* Represents the intersection between the ${name} class and type
@@ -125,6 +131,14 @@ export class ${name} extends ${baseClass ? '_' + baseClass : `ObjectHydrator<Spe
125131
}
126132
return yaml.dump(normalized);
127133
}
134+
135+
static toGraph(model: Partial<WorkflowIntersection>): Graph {
136+
return buildGraph(model as unknown as WorkflowIntersection);
137+
}
138+
139+
static toMermaidCode(model: Partial<WorkflowIntersection>): string {
140+
return convertToMermaidCode(model as unknown as WorkflowIntersection);
141+
}
128142
129143
/**
130144
* Serializes the workflow to YAML or JSON
@@ -134,6 +148,22 @@ export class ${name} extends ${baseClass ? '_' + baseClass : `ObjectHydrator<Spe
134148
*/
135149
serialize(format: 'yaml' | 'json' = 'yaml', normalize: boolean = true): string {
136150
return Workflow.serialize(this as unknown as WorkflowIntersection, format, normalize);
151+
}
152+
153+
/**
154+
* Creates a directed graph representation of the workflow
155+
* @returns A directed graph of the workflow
156+
*/
157+
toGraph(): Graph {
158+
return Workflow.toGraph(this as unknown as WorkflowIntersection);
159+
}
160+
161+
/**
162+
* Generates the MermaidJS code corresponding to the workflow
163+
* @returns The MermaidJS code
164+
*/
165+
toMermaidCode(): string {
166+
return Workflow.toMermaidCode(this as unknown as WorkflowIntersection);
137167
}`
138168
: ''
139169
}
@@ -156,7 +186,21 @@ export const _${name} = ${name} as ${name}Constructor${
156186
* @param normalize If the workflow should be normalized before serialization, default true
157187
* @returns A string representation of the workflow
158188
*/
159-
serialize(workflow: Partial<WorkflowIntersection>, format?: 'yaml' | 'json', normalize?: boolean): string
189+
serialize(workflow: Partial<WorkflowIntersection>, format?: 'yaml' | 'json', normalize?: boolean): string;
190+
191+
/**
192+
* Creates a directed graph representation of the provided workflow
193+
* @param workflow The workflow to convert
194+
* @returns A directed graph of the provided workflow
195+
*/
196+
toGraph(workflow: Partial<WorkflowIntersection>): Graph;
197+
198+
/**
199+
* Generates the MermaidJS code corresponding to the provided workflow
200+
* @param workflow The workflow to convert
201+
* @returns The MermaidJS code
202+
*/
203+
toMermaidCode(workflow: Partial<WorkflowIntersection>): string;
160204
}`
161205
: ''
162206
};`;

0 commit comments

Comments
 (0)
Please sign in to comment.