Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Core package agnostic from the rendering library and its types.

## Modules

`workflowSdk.ts` and `graph.ts` are the only places in the diagram editor that import from the SDK directly, keeping the rest of the editor decoupled from SDK implementation details.

### workflowSdk.ts

Abstraction layer over the `@serverlessworkflow/sdk`. This is the only place in the diagram editor that imports from the SDK directly keeping the rest of the editor decoupled from SDK implementation details.
Abstraction layer over the `@serverlessworkflow/sdk`.

### graph.ts

Add custom types to the original sdk `Graph` type.
32 changes: 32 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/autoLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ExtendedGraph, Position, Size } from "./graph";

export function applyAutoLayout(graph: ExtendedGraph): ExtendedGraph {
// TODO: This is just a temporary implementation until the actual auto-layout engine is integrated
const nodeSize: Size = { height: 50, width: 70 };
let position: Position = { x: 0, y: 0 };

// TODO: Containment is not supported for now.
graph.nodes.forEach((node) => {
node.size = { ...nodeSize };
node.position = { ...position };
position.y = position.y + 100;
});

return graph;
}
101 changes: 101 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Graph, GraphEdge, GraphNode } from "@serverlessworkflow/sdk";

// Override / add multiple properties of a type in a generic way
export type Override<T, NewProps> = Omit<T, keyof NewProps> & NewProps;

// Supported edge types
export enum GraphEdgeType {
Default = "default",
Error = "error",
Condition = "condition",
}

export type Point = {
x: number;
y: number;
};

export type Position = Point;

export type Size = {
height: number;
width: number;
};

export type WayPoints = Point[];

// Add extra properties to GraphNode
export type ExtendedGraphNode = Override<
GraphNode,
{
position?: Position;
size?: Size;
}
>;

// Add extra properties to GraphEdge
export type ExtendedGraphEdge = GraphEdge & {
type?: GraphEdgeType;
wayPoints?: WayPoints;
};

export type ExtendedGraph = Override<
Graph,
{
parent?: ExtendedGraph | null;
nodes: ExtendedGraphNode[];
edges: ExtendedGraphEdge[];
entryNode: ExtendedGraphNode;
exitNode: ExtendedGraphNode;
}
>;

export function solveEdgeTypes(graph: ExtendedGraph): ExtendedGraph {
if (!graph.edges || !graph.nodes) {
return graph;
}

for (let i = 0; i < graph.nodes.length; i++) {
const graphNode = graph.nodes[i]! as ExtendedGraph;

// look into n levels
if (graphNode.edges || graphNode.nodes) {
solveEdgeTypes(graphNode);
}

for (let j = 0; j < graph.edges.length; j++) {
const graphEdge = graph.edges[j]!;

if (graphNode.id === graphEdge.sourceId) {
switch (graphNode.type) {
case "raise":
graphEdge.type = GraphEdgeType.Error;
break;
case "switch":
graphEdge.type = GraphEdgeType.Condition;
break;
default:
graphEdge.type = GraphEdgeType.Default;
}
}
}
}

return graph;
}
2 changes: 2 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
*/

export * from "./workflowSdk";
export * from "./graph";
export * from "./autoLayout";
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
*/

import yaml from "js-yaml";
import { Classes, Specification, validate } from "@serverlessworkflow/sdk";
import * as sdk from "@serverlessworkflow/sdk";
import { ExtendedGraph, solveEdgeTypes } from "./graph";

export type WorkflowParseResult = {
model: Specification.Workflow | null;
model: sdk.Specification.Workflow | null;
errors: Error[];
};

export function validateWorkflow(model: Specification.Workflow): Error[] {
export function validateWorkflow(model: sdk.Specification.Workflow): Error[] {
try {
validate("Workflow", model);
sdk.validate("Workflow", model);
return [];
} catch (err) {
// TODO: Parse individual validation errors from the SDK into separate Error objects when we are ready to render them.
Expand All @@ -33,10 +34,10 @@ export function validateWorkflow(model: Specification.Workflow): Error[] {
}

export function parseWorkflow(text: string): WorkflowParseResult {
let raw: Partial<Specification.Workflow>;
let raw: Partial<sdk.Specification.Workflow>;

try {
raw = yaml.load(text, { schema: yaml.DEFAULT_SCHEMA }) as Partial<Specification.Workflow>;
raw = yaml.load(text, { schema: yaml.DEFAULT_SCHEMA }) as Partial<sdk.Specification.Workflow>;
} catch (err) {
return {
model: null,
Expand All @@ -48,8 +49,12 @@ export function parseWorkflow(text: string): WorkflowParseResult {
return { model: null, errors: [new Error("Not a valid workflow object")] };
}

const model = new Classes.Workflow(raw) as Specification.Workflow;
const model = new sdk.Classes.Workflow(raw) as sdk.Specification.Workflow;
const errors = validateWorkflow(model);

return { model, errors };
}

export function buildGraph(model: sdk.Specification.Workflow): ExtendedGraph {
return solveEdgeTypes(sdk.buildGraph(model));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering should we wrap this in a try/catch in case sdk buildGraph throws and maybe worth splitting into 2 steps first call sdk.buildGraph(model) and get result and then pass to solveEdgeTypes? wdyt?

Copy link
Copy Markdown
Contributor Author

@handreyrc handreyrc Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lornakelly,

The buildGraph is an extension of the original function in the SDK. A try/catch would just catch the exception and throw the same exception to the caller to be handled. I think we should leave the exception handling to the callers. Same as using the original method from the skd.
WDYT?

Copy link
Copy Markdown
Contributor

@lornakelly lornakelly Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, thanks for clarifying, will just need to make sure we handle it by callers

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`applyAutoLayout > apply auto-layout calculated layout to graph elements 1`] = `
{
"edges": [
{
"destinationId": "root-exit-node",
"id": "/do/4/step5-root-exit-node",
"label": "",
"sourceId": "/do/4/step5",
"type": "default",
},
{
"destinationId": "/do/4/step5",
"id": "/do/3/step4-/do/4/step5",
"label": "",
"sourceId": "/do/3/step4",
"type": "default",
},
{
"destinationId": "/do/3/step4",
"id": "/do/2/step3-/do/3/step4",
"label": "",
"sourceId": "/do/2/step3",
"type": "default",
},
{
"destinationId": "/do/2/step3",
"id": "/do/1/step2-/do/2/step3",
"label": "",
"sourceId": "/do/1/step2",
"type": "default",
},
{
"destinationId": "/do/1/step2",
"id": "/do/0/step1-/do/1/step2",
"label": "",
"sourceId": "/do/0/step1",
"type": "default",
},
{
"destinationId": "/do/0/step1",
"id": "root-entry-node-/do/0/step1",
"label": "",
"sourceId": "root-entry-node",
"type": "default",
},
],
"entryNode": {
"id": "root-entry-node",
"position": {
"x": 0,
"y": 0,
},
"size": {
"height": 50,
"width": 70,
},
"type": "start",
},
"exitNode": {
"id": "root-exit-node",
"position": {
"x": 0,
"y": 100,
},
"size": {
"height": 50,
"width": 70,
},
"type": "end",
},
"id": "root",
"label": undefined,
"nodes": [
{
"id": "root-entry-node",
"position": {
"x": 0,
"y": 0,
},
"size": {
"height": 50,
"width": 70,
},
"type": "start",
},
{
"id": "root-exit-node",
"position": {
"x": 0,
"y": 100,
},
"size": {
"height": 50,
"width": 70,
},
"type": "end",
},
{
"id": "/do/0/step1",
"label": "step1",
"position": {
"x": 0,
"y": 200,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/1/step2",
"label": "step2",
"position": {
"x": 0,
"y": 300,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/2/step3",
"label": "step3",
"position": {
"x": 0,
"y": 400,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/3/step4",
"label": "step4",
"position": {
"x": 0,
"y": 500,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/4/step5",
"label": "step5",
"position": {
"x": 0,
"y": 600,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
],
"parent": undefined,
"type": "root",
}
`;
Loading
Loading