Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add graph and MermaidJS flowchart #192

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 136 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
![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)

- [Serverless Workflow Specification - TypeScript SDK](#serverless-workflow-specification---typescript-sdk)
- [Status](#status)
- [SDK Structure](#sdk-structure)
- [Types and Interfaces](#types-and-interfaces)
- [Classes](#classes)
- [Fluent Builders](#fluent-builders)
- [Validation Function](#validation-function)
- [Other tools](#other-tools)
- [Getting Started](#getting-started)
- [Installation](#installation)
- [Usage](#usage)
- [Create a Workflow Definition from YAML or JSON](#create-a-workflow-definition-from-yaml-or-json)
- [Create a Workflow Definition by Casting an Object](#create-a-workflow-definition-by-casting-an-object)
- [Create a Workflow Definition Using a Class Constructor](#create-a-workflow-definition-using-a-class-constructor)
- [Create a Workflow Definition Using the Builder API](#create-a-workflow-definition-using-the-builder-api)
- [Serialize a Workflow Definition to YAML or JSON](#serialize-a-workflow-definition-to-yaml-or-json)
- [Validate Workflow Definitions](#validate-workflow-definitions)
- [Generate a directed graph](#generate-a-directed-graph)
- [Generate a MermaidJS flowchart](#generate-a-mermaidjs-flowchart)
- [Building Locally](#building-locally)

# Serverless Workflow Specification - TypeScript SDK

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

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

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

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

### Other Tools
The SDK also ships tools to build directed graph and MermaidJS flowcharts from a workflow.

## Getting Started

### Installation
Expand Down Expand Up @@ -86,7 +110,7 @@ do:
set:
variable: 'my first workflow'
`;
const workflowDefinition = Classes.Workflow.deserialize(text);
const workflow = Classes.Workflow.deserialize(text);
```

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

// Simply cast an object:
const workflowDefinition = {
const workflow = {
document: {
dsl: '1.0.0',
name: 'test',
Expand All @@ -116,9 +140,9 @@ const workflowDefinition = {

// Validate it
try {
validate('Workflow', workflowDefinition);
validate('Workflow', workflow);
// Serialize it
const definitionTxt = Classes.Workflow.serialize(workflowDefinition);
const definitionTxt = Classes.Workflow.serialize(workflow);
}
catch (ex) {
// Invalid workflow definition
Expand All @@ -132,7 +156,7 @@ You can create a workflow definition by calling a constructor:
import { Classes, validate } from '@serverlessworkflow/sdk';

// Simply use the constructor
const workflowDefinition = new Classes.Workflow({
const workflow = new Classes.Workflow({
document: {
dsl: '1.0.0',
name: 'test',
Expand All @@ -149,7 +173,7 @@ const workflowDefinition = new Classes.Workflow({
},
*/],
});
workflowDefinition.do.push({
workflow.do.push({
step1: new Classes.SetTask({
set: {
variable: 'my first workflow',
Expand All @@ -159,9 +183,9 @@ workflowDefinition.do.push({

// Validate it
try {
workflowDefinition.validate();
workflow.validate();
// Serialize it
const definitionTxt = workflowDefinition.serialize();
const definitionTxt = workflow.serialize();
}
catch (ex) {
// Invalid workflow definition
Expand All @@ -174,7 +198,7 @@ You can use the fluent API to build a validated and normalized workflow definiti
```typescript
import { documentBuilder, setTaskBuilder, taskListBuilder, workflowBuilder } from '@serverlessworkflow/sdk';

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

// const workflowDefinition = <Your preferred method>;
if (workflowDefinition instanceof Classes.Workflow) {
const yaml = workflowDefinition.serialize(/*'yaml' | 'json' */);
// const workflow = <Your preferred method>;
if (workflow instanceof Classes.Workflow) {
const yaml = workflow.serialize(/*'yaml' | 'json' */);
}
else {
const json = Classes.Workflow.serialize(workflowDefinition, 'json');
const json = Classes.Workflow.serialize(workflow, 'json');
}
```
> [!NOTE]
Expand All @@ -223,20 +247,114 @@ Validation can be achieved in two ways: via the `validate` function or the insta
```typescript
import { Classes, validate } from '@serverlessworkflow/sdk';

// const workflowDefinition = <Your preferred method>;
const workflow = /* <Your preferred method> */;
try {
if (workflowDefinition instanceof Classes.Workflow) {
workflowDefinition.validate();
if (workflow instanceof Classes.Workflow) {
workflow.validate();
}
else {
validate('Workflow', workflowDefinition);
validate('Workflow', workflow);
}
}
catch (ex) {
// Workflow definition is invalid
}
```

#### Generate a directed graph
A [directed graph](https://en.wikipedia.org/wiki/Directed_graph) of a workflow can be generated using the `buildGraph` function, or alternatives:
- Workflow instance `.toGraph();`
- Static `Classes.Workflow.toGraph(workflow)`

```typescript
import { buildGraph } from '@serverlessworkflow/sdk';

const workflow = {
document: {
dsl: '1.0.0',
name: 'using-plain-object',
version: '1.0.0',
namespace: 'default',
},
do: [
{
step1: {
set: {
variable: 'my first workflow',
},
},
},
],
};
const graph = buildGraph(workflow);
// const workflow = new Classes.Workflow({...}); const graph = workflow.toGraph();
// const graph = Classes.Workflow.toGraph(workflow);
/*{
id: 'root',
type: 'root',
label: undefined,
parent: null,
nodes: [...], // length 3 - root entry node, step1 node, root exit node
edges: [...], // length 2 - entry to step1, step1 to exit
entryNode: {...}, // root entry node
exitNode: {...} // root exit node
}*/
```

#### Generate a MermaidJS flowchart
Generating a [MermaidJS](https://mermaid.js.org/) flowchart can be achieved in two ways: using the `convertToMermaidCode`, the legacy `MermaidDiagram` class, or alternatives:
- Workflow instance `.toMermaidCode();`
- Static `Classes.Workflow.toMermaidCode(workflow)`

```typescript
import { convertToMermaidCode, MermaidDiagram } from '@serverlessworkflow/sdk';

const workflow = {
document: {
dsl: '1.0.0',
name: 'using-plain-object',
version: '1.0.0',
namespace: 'default',
},
do: [
{
step1: {
set: {
variable: 'my first workflow',
},
},
},
],
};
const mermaidCode = convertToMermaidCode(workflow) /* or */;
// const mermaidCode = new MermaidDiagram(workflow).sourceCode();
// const workflow = new Classes.Workflow({...}); const mermaidCode = workflow.toMermaidCode();
// const mermaidCode = Classes.Workflow.toMermaidCode(workflow);
/*
flowchart TD
root-entry-node(( ))
root-exit-node((( )))
/do/0/step1["step1"]
/do/0/step1 --> root-exit-node
root-entry-node --> /do/0/step1


classDef hidden display: none;
*/
```

```mermaid
flowchart TD
root-entry-node(( ))
root-exit-node((( )))
/do/0/step1["step1"]
/do/0/step1 --> root-exit-node
root-entry-node --> /do/0/step1


classDef hidden display: none;
```

### Building Locally

To build the project and run tests locally, use the following commands:
Expand Down
131 changes: 131 additions & 0 deletions examples/browser/mermaid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Serveless Workflow</title>
<base href="/">
<meta content="width=device-width, initial-scale=1" name="viewport">
</head>

<body>
<p>YAML or JSON:</p>
<textarea id="input" rows="50" cols="100"></textarea>
<div id="diagram-container"></div>
<pre id="output"></pre>
<script src="../../dist/umd/index.umd.js"></script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
(async () => {
const { Classes, Specification, validate, convertToMermaidCode } = serverWorkflowSdk;
const workflowDefinition = {
"document": {
"dsl": "1.0.0",
"namespace": "examples",
"name": "accumulate-room-readings",
"version": "0.1.0"
},
"do": [
{
"consumeReading": {
"listen": {
"to": {
"all": [
{
"with": {
"source": "https://my.home.com/sensor",
"type": "my.home.sensors.temperature"
},
"correlate": {
"roomId": {
"from": ".roomid"
}
}
},
{
"with": {
"source": "https://my.home.com/sensor",
"type": "my.home.sensors.humidity"
},
"correlate": {
"roomId": {
"from": ".roomid"
}
}
}
]
}
},
"output": {
"as": ".data.reading"
}
}
},
{
"logReading": {
"for": {
"each": "reading",
"in": ".readings"
},
"do": [
{
"callOrderService": {
"call": "openapi",
"with": {
"document": {
"endpoint": "http://myorg.io/ordersservices.json"
},
"operationId": "logreading"
}
}
}
]
}
},
{
"generateReport": {
"call": "openapi",
"with": {
"document": {
"endpoint": "http://myorg.io/ordersservices.json"
},
"operationId": "produceReport"
}
}
}
],
"timeout": {
"after": {
"hours": 1
}
}
}/* as Specification.Workflow // <-- If you're using TypeScript*/;
const diagramContainerEl = document.getElementById('diagram-container');
const inputTextarea = document.getElementById('input');
const processWorkflow = async () => {
try {
const workflow = Classes.Workflow.deserialize(inputTextarea.value);
const mermaidCode = convertToMermaidCode(workflow);
document.getElementById('output').innerHTML = `--- YAML ---\n${Classes.Workflow.serialize(workflow)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflow, 'json')}\n\n--- MERMAID ---\n${mermaidCode}`;
mermaid.initialize({ startOnLoad: false });
const { svg, bindFunctions } = await mermaid.render('sw-diagram', mermaidCode);
diagramContainerEl.innerHTML = svg;
}
catch (ex) {
console.error('Invalid workflow', ex);
}
};
let debounceHandle;
inputTextarea.addEventListener('keyup', () => {
if (debounceHandle) {
clearTimeout(debounceHandle);
}
debounceHandle = setTimeout(processWorkflow, 300);
});
inputTextarea.value = JSON.stringify(workflowDefinition, null, 4);
await processWorkflow();
})();
</script>
</body>

</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@serverlessworkflow/sdk",
"version": "1.0.0",
"version": "1.0.1",
"schemaVersion": "1.0.0",
"description": "Typescript SDK for Serverless Workflow Specification",
"main": "umd/index.umd.min.js",
Expand Down
Loading