otomato-sdk 2.0.35 → 2.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- export const SDK_VERSION = '2.0.35';
1
+ export const SDK_VERSION = '2.0.36';
2
2
  export function compareVersions(v1, v2) {
3
3
  // Split the version strings into parts
4
4
  const v1Parts = v1.split('.').map(Number);
@@ -10,8 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { Node } from './Node.js';
11
11
  import { Edge } from './Edge.js';
12
12
  import { apiServices } from '../services/ApiService.js';
13
+ import { Action } from './Action.js';
13
14
  import { Note } from './Note.js';
14
- import { getEndNodePositions, positionWorkflowNodesAvoidOverlap } from '../utils/WorkflowNodePositioner.js';
15
+ import { getEndNodePositions, positionWorkflowNodes } from '../utils/WorkflowNodePositioner.js';
16
+ import { ACTIONS } from '../constants/Blocks.js';
15
17
  export class Workflow {
16
18
  constructor(name = '', nodes = [], edges = []) {
17
19
  this.id = null;
@@ -23,18 +25,18 @@ export class Workflow {
23
25
  this.nodes = nodes;
24
26
  this.edges = edges;
25
27
  this.state = 'inactive';
26
- positionWorkflowNodesAvoidOverlap(this);
28
+ positionWorkflowNodes(this);
27
29
  }
28
30
  setName(name) {
29
31
  this.name = name;
30
32
  }
31
33
  addNode(node) {
32
34
  this.nodes.push(node);
33
- positionWorkflowNodesAvoidOverlap(this);
35
+ positionWorkflowNodes(this);
34
36
  }
35
37
  addNodes(nodes) {
36
38
  this.nodes.push(...nodes);
37
- positionWorkflowNodesAvoidOverlap(this);
39
+ positionWorkflowNodes(this);
38
40
  }
39
41
  deleteNode(nodeToDelete) {
40
42
  // Remove the node from the nodes array
@@ -57,68 +59,152 @@ export class Workflow {
57
59
  this.edges = this.edges.filter(edge => edge.source !== nodeToDelete && edge.target !== nodeToDelete);
58
60
  this.edges.push(...newEdges);
59
61
  // Recalculate positions
60
- positionWorkflowNodesAvoidOverlap(this);
62
+ positionWorkflowNodes(this);
61
63
  }
62
64
  /**
63
- * Inserts a new node into the workflow:
64
- * - If both nodeBefore and nodeAfter are provided, we expect an existing edge
65
- * from nodeBefore -> nodeAfter, which gets replaced by two edges:
66
- * nodeBefore -> nodeToInsert and nodeToInsert -> nodeAfter.
65
+ * Inserts a new node into the workflow, potentially splitting an existing edge.
67
66
  *
68
- * - If nodeAfter is NOT provided, we create a single edge from nodeBefore
69
- * to nodeToInsert. The user can optionally provide `edgeLabel` and `edgeValue`
70
- * in that scenario only.
67
+ * - If `nodeAfter` is specified, we expect an edge nodeBefore->nodeAfter to exist.
68
+ * That edge is removed, and replaced by two new edges:
69
+ * nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
70
+ * nodeToInsert->nodeAfter (with `edgeLabelAfter`, `edgeValueAfter`)
71
71
  *
72
- * @param nodeToInsert The node you want to insert
73
- * @param nodeBefore The existing node in the workflow that precedes `nodeToInsert`
74
- * @param nodeAfter (Optional) The existing node that should follow `nodeToInsert`
75
- * @param edgeLabel (Optional) Label for the edge, only allowed if `nodeAfter` is not provided
76
- * @param edgeValue (Optional) Value for the edge, only allowed if `nodeAfter` is not provided
72
+ * - If `nodeAfter` is not specified, we create only one edge:
73
+ * nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
74
+ *
75
+ * @param nodeToInsert The node to insert
76
+ * @param nodeBefore The existing node that precedes `nodeToInsert`
77
+ * @param nodeAfter (Optional) The existing node that should follow `nodeToInsert`
78
+ * @param edgeLabelBefore (Optional) Label for the edge nodeBefore->nodeToInsert
79
+ * @param edgeValueBefore (Optional) Value for the edge nodeBefore->nodeToInsert
80
+ * @param edgeLabelAfter (Optional) Label for the edge nodeToInsert->nodeAfter
81
+ * @param edgeValueAfter (Optional) Value for the edge nodeToInsert->nodeAfter
77
82
  */
78
- insertNode(nodeToInsert, nodeBefore, nodeAfter, edgeLabel = null, edgeValue = null) {
83
+ insertNode(nodeToInsert, nodeBefore, nodeAfter, edgeLabelBefore = null, edgeValueBefore = null, edgeLabelAfter = null, edgeValueAfter = null) {
79
84
  // Ensure nodeBefore exists in the workflow
80
85
  if (!this.nodes.includes(nodeBefore)) {
81
86
  throw new Error('The nodeBefore must exist in the workflow.');
82
87
  }
83
- // If nodeAfter is provided, disallow edgeLabel/edgeValue
84
- if (nodeAfter && (edgeLabel !== null || edgeValue !== null)) {
85
- throw new Error('Cannot provide edgeLabel/edgeValue if nodeAfter is specified.');
86
- }
87
- // If nodeAfter is not provided, insert the new node as a child of nodeBefore
88
+ // CASE A: If no nodeAfter => we create a single edge from nodeBefore to nodeToInsert
88
89
  if (!nodeAfter) {
89
90
  // Add the new node to the workflow
90
91
  this.addNode(nodeToInsert);
91
- // Add a new edge between nodeBefore and nodeToInsert (with optional label/value)
92
+ // Create the edge nodeBefore->nodeToInsert
92
93
  const newEdge = new Edge({
93
94
  source: nodeBefore,
94
95
  target: nodeToInsert,
95
- label: edgeLabel !== null && edgeLabel !== void 0 ? edgeLabel : undefined,
96
- value: edgeValue !== null && edgeValue !== void 0 ? edgeValue : undefined
96
+ label: edgeLabelBefore !== null && edgeLabelBefore !== void 0 ? edgeLabelBefore : undefined,
97
+ value: edgeValueBefore !== null && edgeValueBefore !== void 0 ? edgeValueBefore : undefined
97
98
  });
98
99
  this.addEdge(newEdge);
99
100
  // Recalculate positions
100
- positionWorkflowNodesAvoidOverlap(this);
101
+ positionWorkflowNodes(this);
101
102
  return;
102
103
  }
103
- // If nodeAfter is provided, ensure both nodes exist in the workflow
104
+ // CASE B: nodeAfter is provided => we expect an edge nodeBefore->nodeAfter to exist
104
105
  if (!this.nodes.includes(nodeAfter)) {
105
106
  throw new Error('The nodeAfter must exist in the workflow.');
106
107
  }
107
- // Check if an edge exists between nodeBefore and nodeAfter
108
- const edgeBetween = this.edges.find(edge => edge.source === nodeBefore && edge.target === nodeAfter);
108
+ // Find the existing edge nodeBefore->nodeAfter
109
+ const edgeBetween = this.edges.find((edge) => edge.source === nodeBefore && edge.target === nodeAfter);
109
110
  if (!edgeBetween) {
110
111
  throw new Error('No edge exists between nodeBefore and nodeAfter.');
111
112
  }
112
113
  // Add the new node to the workflow
113
114
  this.addNode(nodeToInsert);
114
115
  // Remove the existing edge between nodeBefore and nodeAfter
115
- this.edges = this.edges.filter(edge => edge !== edgeBetween);
116
- // Add new edges
117
- const newEdge1 = new Edge({ source: nodeBefore, target: nodeToInsert });
118
- const newEdge2 = new Edge({ source: nodeToInsert, target: nodeAfter });
116
+ this.edges = this.edges.filter((edge) => edge !== edgeBetween);
117
+ // Create the two new edges:
118
+ // 1) nodeBefore->nodeToInsert
119
+ const newEdge1 = new Edge({
120
+ source: nodeBefore,
121
+ target: nodeToInsert,
122
+ label: edgeLabelBefore !== null && edgeLabelBefore !== void 0 ? edgeLabelBefore : undefined,
123
+ value: edgeValueBefore !== null && edgeValueBefore !== void 0 ? edgeValueBefore : undefined
124
+ });
125
+ // 2) nodeToInsert->nodeAfter
126
+ const newEdge2 = new Edge({
127
+ source: nodeToInsert,
128
+ target: nodeAfter,
129
+ label: edgeLabelAfter !== null && edgeLabelAfter !== void 0 ? edgeLabelAfter : undefined,
130
+ value: edgeValueAfter !== null && edgeValueAfter !== void 0 ? edgeValueAfter : undefined
131
+ });
119
132
  this.addEdges([newEdge1, newEdge2]);
120
133
  // Recalculate positions
121
- positionWorkflowNodesAvoidOverlap(this);
134
+ positionWorkflowNodes(this);
135
+ }
136
+ insertCondition(nodeToInsert, nodeBefore, nodeAfter, addElseCase, addEmptyNodes = true) {
137
+ if (nodeAfter) {
138
+ this.insertNode(nodeToInsert, nodeBefore, nodeAfter, null, null, 'true', 'true');
139
+ }
140
+ // Otherwise, create both "true" and "false" paths.
141
+ // 1) "true" path: nodeBefore -> nodeToInsert -> nodeAfter
142
+ if (!nodeAfter) {
143
+ this.insertNode(nodeToInsert, nodeBefore, undefined);
144
+ if (addEmptyNodes) {
145
+ const emptyNode1 = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
146
+ this.insertNode(emptyNode1, nodeToInsert, undefined, 'true', 'true');
147
+ }
148
+ }
149
+ // 2) "false" path: nodeBefore -> nodeToInsert,
150
+ // but here we pass no nodeAfter (so nodeAfter=null),
151
+ // and label/value are "false".
152
+ if (addElseCase && addEmptyNodes) {
153
+ const emptyNode2 = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
154
+ this.insertNode(emptyNode2, nodeToInsert, undefined, 'false', 'false');
155
+ }
156
+ }
157
+ /**
158
+ * Inserts a "split" node, creating `numberOfBranches` parallel branches.
159
+ *
160
+ * If `nodeAfter` is given, we call `insertNode(...)` to splice the split node
161
+ * between `nodeBefore` and `nodeAfter`. That yields two edges:
162
+ * nodeBefore->nodeToInsert and nodeToInsert->nodeAfter
163
+ * The first branch is effectively "nodeAfter."
164
+ * Then we add additional empty blocks for the remaining branches.
165
+ *
166
+ * If `nodeAfter` is NOT given, we just insertNode(split, nodeBefore),
167
+ * which yields an edge: nodeBefore->split.
168
+ * Then we create `numberOfBranches` empty blocks off the split node.
169
+ *
170
+ * @param nodeToInsert The split node to insert (e.g. type="Split").
171
+ * @param nodeBefore The node after which the split is inserted.
172
+ * @param nodeAfter (Optional) If we’re splitting in the middle of a flow, the node that was originally after `nodeBefore`.
173
+ * @param numberOfBranches The total number of branches to create from `nodeToInsert`.
174
+ */
175
+ insertSplit(nodeToInsert, nodeBefore, nodeAfter, numberOfBranches) {
176
+ // Basic validation
177
+ if (!this.nodes.includes(nodeBefore)) {
178
+ throw new Error('nodeBefore must exist in the workflow.');
179
+ }
180
+ if (numberOfBranches < 2) {
181
+ throw new Error('numberOfBranches must be at least 2.');
182
+ }
183
+ // Step 1: Insert the "split" node via insertNode
184
+ // - If nodeAfter is defined, it removes nodeBefore->nodeAfter
185
+ // and creates nodeBefore->nodeToInsert & nodeToInsert->nodeAfter.
186
+ // - If nodeAfter is undefined, we simply get nodeBefore->nodeToInsert.
187
+ this.insertNode(nodeToInsert, nodeBefore, nodeAfter);
188
+ // Step 2: Create the parallel branches
189
+ if (nodeAfter) {
190
+ // The first branch is already nodeToInsert->nodeAfter
191
+ // So we only need to create the remaining (numberOfBranches - 1) branches
192
+ const remaining = numberOfBranches - 1;
193
+ for (let i = 0; i < remaining; i++) {
194
+ const emptyBlock = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
195
+ // Insert the emptyBlock after nodeToInsert
196
+ // => this creates a new edge nodeToInsert->emptyBlock
197
+ this.insertNode(emptyBlock, nodeToInsert);
198
+ }
199
+ }
200
+ else {
201
+ // nodeAfter is undefined => all branches must be created
202
+ // Each branch is nodeToInsert->(new empty block)
203
+ for (let i = 0; i < numberOfBranches; i++) {
204
+ const emptyBlock = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
205
+ this.insertNode(emptyBlock, nodeToInsert);
206
+ }
207
+ }
122
208
  }
123
209
  swapNode(oldNode, newNode) {
124
210
  // Find the index of the node to replace
@@ -138,11 +224,11 @@ export class Workflow {
138
224
  }
139
225
  });
140
226
  // Recalculate positions
141
- positionWorkflowNodesAvoidOverlap(this);
227
+ positionWorkflowNodes(this);
142
228
  }
143
229
  addEdge(edge) {
144
230
  this.edges.push(edge);
145
- positionWorkflowNodesAvoidOverlap(this);
231
+ positionWorkflowNodes(this);
146
232
  }
147
233
  updateEdge(edgeId, newEdge) {
148
234
  const edgeToUpdate = this.edges.find(e => e.id === edgeId);
@@ -153,11 +239,11 @@ export class Workflow {
153
239
  else {
154
240
  throw new Error(`Edge with id ${edgeId} not found`);
155
241
  }
156
- positionWorkflowNodesAvoidOverlap(this);
242
+ positionWorkflowNodes(this);
157
243
  }
158
244
  addEdges(edges) {
159
245
  this.edges.push(...edges);
160
- positionWorkflowNodesAvoidOverlap(this);
246
+ positionWorkflowNodes(this);
161
247
  }
162
248
  getState() {
163
249
  return this.state;
@@ -286,7 +372,7 @@ export class Workflow {
286
372
  this.nodes = yield Promise.all(response.nodes.map((nodeData) => __awaiter(this, void 0, void 0, function* () { return yield Node.fromJSON(nodeData); })));
287
373
  this.edges = response.edges.map((edgeData) => Edge.fromJSON(edgeData, this.nodes));
288
374
  this.notes = response.notes.map((noteData) => Note.fromJSON(noteData));
289
- positionWorkflowNodesAvoidOverlap(this);
375
+ positionWorkflowNodes(this);
290
376
  return this;
291
377
  }
292
378
  catch (error) {
@@ -1,176 +1,85 @@
1
- // workflowNodePositioner.ts
2
- export const xSpacing = 500;
3
- export const ySpacing = 120;
4
- export const ROOT_X = 400;
1
+ // Note: Using 'dagre' for layered DAG layout
2
+ import dagre from 'dagre';
3
+ // or: import * as dagre from 'dagre';
4
+ export const xSpacing = 700; // used by dagre as node separation
5
+ export const ySpacing = 75;
6
+ export const ROOT_X = 400; // we’ll keep references, but Dagre decides actual positions
5
7
  export const ROOT_Y = 120;
8
+ /**
9
+ * Dagre-based layout for the “top-down” pass. We’re effectively ignoring the old
10
+ * manual code. Instead, we:
11
+ * 1) Build a dagre graph
12
+ * 2) Add all nodes & edges
13
+ * 3) dagre.layout(g)
14
+ * 4) Extract positions & write them back to workflow.nodes
15
+ */
6
16
  export function positionWorkflowNodes(workflow) {
7
- try {
8
- // Step 1: Find the starting nodes using identityStartingNodes function
9
- const startingNodes = identityStartingNodes(workflow);
10
- // Step 2: Place the starting nodes
11
- let xPosition = ROOT_X;
12
- startingNodes.forEach((startNode) => {
13
- startNode.setPosition(xPosition, ROOT_Y);
14
- xPosition += xSpacing;
15
- });
16
- // Step 3: Place all other nodes relative to their parents
17
- const nodesToPosition = workflow.nodes.filter((node) => !startingNodes.includes(node));
18
- nodesToPosition.forEach((node) => positionNode(node, workflow.edges, xSpacing, ySpacing, workflow));
19
- }
20
- catch (e) {
21
- console.error(e);
22
- }
23
- }
24
- export function positionNode(node, edges, xSpacing, ySpacing, workflow) {
25
- const parents = getParents(node, edges);
26
- // todo: what if we have multiple parents?
27
- const children = getChildren(parents[0], edges);
28
- const sortedChildren = children.sort((a, b) => {
29
- var _a, _b;
30
- const edgeA = edges.find(edge => edge.source === parents[0] && edge.target === a);
31
- const edgeB = edges.find(edge => edge.source === parents[0] && edge.target === b);
32
- const labelA = (_a = edgeA === null || edgeA === void 0 ? void 0 : edgeA.label) !== null && _a !== void 0 ? _a : "";
33
- const labelB = (_b = edgeB === null || edgeB === void 0 ? void 0 : edgeB.label) !== null && _b !== void 0 ? _b : "";
34
- if (labelA === "true" && labelB !== "true")
35
- return -1;
36
- if (labelB === "true" && labelA !== "true")
37
- return 1;
38
- if (labelA === "false" && labelB !== "false")
39
- return 1;
40
- if (labelB === "false" && labelA !== "false")
41
- return -1;
42
- return 0;
17
+ // 1) Create a new directed graph
18
+ // doc: https://github.com/dagrejs/dagre
19
+ const g = new dagre.graphlib.Graph({ multigraph: false, compound: false });
20
+ g.setGraph({
21
+ // Some layout options:
22
+ rankdir: 'TB', // "top-bottom" layering
23
+ nodesep: xSpacing * 0.5, // horizontal spacing
24
+ ranksep: ySpacing, // vertical spacing between levels
25
+ marginx: 20, // how much margin to leave around the left/right
26
+ marginy: 20, // how much margin to leave around the top/bottom
43
27
  });
44
- const childrenCountOfParent = sortedChildren.length;
45
- const parentX = parents.reduce((sum, parent) => { var _a, _b; return sum + ((_b = (_a = parent.position) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : ROOT_X); }, 0) / parents.length;
46
- const parentY = Math.max(...parents.map(parent => { var _a, _b; return (_b = (_a = parent.position) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : ROOT_Y; }));
47
- if (childrenCountOfParent === 1) {
48
- node.setPosition(parentX, parentY + ySpacing);
49
- }
50
- else {
51
- const index = sortedChildren.indexOf(node); // Get the position of this node among its siblings
52
- const totalChildren = sortedChildren.length;
53
- // Compute the x position for this node
54
- const offset = index - (totalChildren - 1) / 2; // Center the children around the parent
55
- node.setPosition(parentX + offset * xSpacing, parentY + ySpacing);
56
- }
57
- }
58
- export function positionWorkflowNodesAvoidOverlap(workflow) {
59
- const levels = new Map();
60
- function addToLevel(node) {
61
- const level = Math.round(node.position.y / ySpacing);
62
- if (!levels.has(level)) {
63
- levels.set(level, []);
64
- }
65
- levels.get(level).push(node);
66
- }
67
- // 1) Lay out nodes using existing logic
68
- positionWorkflowNodes(workflow);
69
- // 2) Fill the `levels` map
28
+ g.setDefaultEdgeLabel(() => ({}));
29
+ // 2) Add nodes to the graph
30
+ // Dagre requires each node have an id and some approximate width/height
70
31
  workflow.nodes.forEach((node) => {
71
- if (node.position) {
72
- addToLevel(node);
73
- }
74
- });
75
- // 3) Resolve horizontal overlaps and enforce parent grouping
76
- levels.forEach((nodes, level) => {
77
- // Group nodes by their parent
78
- const parentGroups = new Map();
79
- nodes.forEach((node) => {
80
- const parents = getParents(node, workflow.edges);
81
- if (parents.length > 0) {
82
- const parent = parents[0]; // Assuming single parent for simplicity
83
- if (!parentGroups.has(parent)) {
84
- parentGroups.set(parent, []);
85
- }
86
- parentGroups.get(parent).push(node);
87
- }
88
- });
89
- // Flatten the groups back into a sorted array
90
- const groupedNodes = [];
91
- parentGroups.forEach((group) => {
92
- groupedNodes.push(...group);
32
+ const nodeId = node.getRef();
33
+ // For a typical text-based node, approximate width & height
34
+ g.setNode(nodeId, {
35
+ label: nodeId,
36
+ width: 100,
37
+ height: 50
93
38
  });
94
- // Sort nodes by x within each group to detect overlaps
95
- groupedNodes.sort((a, b) => { var _a, _b; return ((_a = a.position.x) !== null && _a !== void 0 ? _a : 0) - ((_b = b.position.x) !== null && _b !== void 0 ? _b : 0); });
96
- // Shift nodes within groups to resolve overlaps
97
- for (let i = 1; i < groupedNodes.length; i++) {
98
- const prev = groupedNodes[i - 1];
99
- const current = groupedNodes[i];
100
- if (current.position.x - prev.position.x < xSpacing) {
101
- const shift = xSpacing - (current.position.x - prev.position.x);
102
- moveNodeAndChildren(current, shift, workflow.edges);
103
- }
104
- }
105
39
  });
106
- // 4) **Center each parent over its children** (the existing step)
107
- centerParentXPositions(workflow);
108
- }
109
- /**
110
- * Repositions each parent node so that its X is the average of the children’s X.
111
- * We do a simple bottom-up pass: start with all leaves, then move upward to parents.
112
- */
113
- function centerParentXPositions(workflow) {
114
- var _a, _b;
115
- // Identify the “leaf” nodes
116
- const queue = identifyLeafNodes(workflow);
117
- while (queue.length > 0) {
118
- const child = queue.shift();
119
- const parents = getParents(child, workflow.edges);
120
- for (const parent of parents) {
121
- const children = getChildren(parent, workflow.edges);
122
- if (children.length) {
123
- // Average x of all children
124
- const sumX = children.reduce((acc, c) => { var _a, _b; return acc + ((_b = (_a = c.position) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : 0); }, 0);
125
- const avgX = sumX / children.length;
126
- // Move parent to that average, keep same y
127
- parent.setPosition(avgX, (_b = (_a = parent.position) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : 0);
128
- }
129
- // Add this parent to the queue so we can recurse upward
130
- if (!queue.includes(parent)) {
131
- queue.push(parent);
132
- }
133
- }
134
- }
135
- }
136
- function moveNodeAndChildren(node, shift, edges) {
137
- // Move the node
138
- node.setPosition(node.position.x + shift, node.position.y);
139
- // Propagate to children
140
- edges
141
- .filter((edge) => edge.source === node)
142
- .forEach((edge) => {
143
- moveNodeAndChildren(edge.target, shift, edges);
40
+ // 3) Add edges
41
+ // Dagre identifies edges by (sourceId, targetId). We don’t need labels for layout,
42
+ // but we can store them if we want. If you rely on “true/false” ordering, we can tweak
43
+ // e.g. use edge label as a tie-break. (See advanced docs.)
44
+ workflow.edges.forEach((edge) => {
45
+ var _a;
46
+ const fromId = edge.source.getRef();
47
+ const toId = edge.target.getRef();
48
+ g.setEdge(fromId, toId, { label: (_a = edge.label) !== null && _a !== void 0 ? _a : '' });
49
+ });
50
+ // 4) Run Dagre layout
51
+ dagre.layout(g);
52
+ // 5) Extract positions from Dagre graph and store them back to your Node objects
53
+ g.nodes().forEach((nodeId) => {
54
+ const dagreNode = g.node(nodeId); // { x, y, width, height, ... }
55
+ const nodeObj = workflow.nodes.find((n) => n.getRef() === nodeId);
56
+ if (!nodeObj || !dagreNode)
57
+ return;
58
+ // Store x,y in your node’s position
59
+ nodeObj.setPosition(dagreNode.x, dagreNode.y);
144
60
  });
145
- }
146
- export function identifyLeafNodes(workflow) {
147
- const nonLeafNodes = new Set(workflow.edges.map(edge => edge.source.getRef()));
148
- return workflow.nodes.filter(node => !nonLeafNodes.has(node.getRef()));
149
61
  }
150
62
  /**
151
- * Identifies starting nodes (nodes with no incoming edges).
152
- * A starting node is defined as one that is not a target of any edge.
153
- *
154
- * @param workflow The workflow to analyze.
155
- * @returns An array of nodes that have no incoming edges.
63
+ * We keep these helper functions the same for external usage (if your code or tests need them),
64
+ * but we’re not actively using them in the new Dagre-based layout.
156
65
  */
157
- export function identityStartingNodes(workflow) {
158
- const childRefs = new Set(workflow.edges.map((edge) => edge.target.getRef()));
159
- return workflow.nodes.filter((node) => !childRefs.has(node.getRef()));
160
- }
161
66
  export function getChildren(node, edges) {
162
- return edges.filter(edge => edge.source === node).map(edge => edge.target);
67
+ return edges
68
+ .filter((edge) => edge.source === node)
69
+ .map((edge) => edge.target);
163
70
  }
164
71
  export function getParents(node, edges) {
165
- return edges.filter(edge => edge.target === node).map(edge => edge.source);
72
+ return edges
73
+ .filter((edge) => edge.target === node)
74
+ .map((edge) => edge.source);
166
75
  }
167
76
  export function getEdges(node, edges) {
168
- return edges.filter(edge => edge.source === node || edge.target === node);
77
+ return edges.filter((edge) => edge.source === node || edge.target === node);
169
78
  }
170
79
  export function getEndNodePositions(workflow) {
171
80
  return workflow.nodes
172
- .filter(node => getChildren(node, workflow.edges).length === 0) // node with no children
173
- .map(node => {
81
+ .filter((node) => getChildren(node, workflow.edges).length === 0)
82
+ .map((node) => {
174
83
  var _a, _b, _c, _d;
175
84
  return ({
176
85
  x: (_b = (_a = node.position) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : 0,
@@ -178,3 +87,16 @@ export function getEndNodePositions(workflow) {
178
87
  });
179
88
  });
180
89
  }
90
+ /**
91
+ * In Dagre, we no longer do an explicit “identify leaf nodes” or “center parents by average,”
92
+ * because the layout library handles that.
93
+ * But here’s your old method if something else depends on it.
94
+ */
95
+ export function identifyLeafNodes(workflow) {
96
+ const sources = new Set(workflow.edges.map((edge) => edge.source.getRef()));
97
+ return workflow.nodes.filter((node) => !sources.has(node.getRef()));
98
+ }
99
+ export function identityStartingNodes(workflow) {
100
+ const childRefs = new Set(workflow.edges.map((edge) => edge.target.getRef()));
101
+ return workflow.nodes.filter((node) => !childRefs.has(node.getRef()));
102
+ }
@@ -1,4 +1,4 @@
1
1
  /*************************************
2
- * 4. Main Workflow Builder
2
+ * 6. Main Workflow Builder
3
3
  *************************************/
4
4
  export declare function aggregatorWorkflow(): Promise<void>;
@@ -0,0 +1,4 @@
1
+ /*************************************
2
+ * 5. Main Workflow Builder (No manual edges or pushes)
3
+ *************************************/
4
+ export declare function lendingAggregatorWorkflow(): Promise<void>;
@@ -0,0 +1,4 @@
1
+ /*************************************
2
+ * 6. Main Workflow Builder
3
+ *************************************/
4
+ export declare function aggregatorWorkflow(): Promise<void>;
@@ -1 +1,7 @@
1
- export {};
1
+ /**
2
+ * Example workflow:
3
+ * Trigger -> Delay -> Condition
4
+ * ├─(true)─> Split(3)
5
+ * └─(false)-> Delay
6
+ */
7
+ export declare function conditionSplitDemo(): Promise<void>;
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.0.35";
1
+ export declare const SDK_VERSION = "2.0.36";
2
2
  export declare function compareVersions(v1: string, v2: string): number;
@@ -19,22 +19,45 @@ export declare class Workflow {
19
19
  addNodes(nodes: Node[]): void;
20
20
  deleteNode(nodeToDelete: Node): void;
21
21
  /**
22
- * Inserts a new node into the workflow:
23
- * - If both nodeBefore and nodeAfter are provided, we expect an existing edge
24
- * from nodeBefore -> nodeAfter, which gets replaced by two edges:
25
- * nodeBefore -> nodeToInsert and nodeToInsert -> nodeAfter.
22
+ * Inserts a new node into the workflow, potentially splitting an existing edge.
26
23
  *
27
- * - If nodeAfter is NOT provided, we create a single edge from nodeBefore
28
- * to nodeToInsert. The user can optionally provide `edgeLabel` and `edgeValue`
29
- * in that scenario only.
24
+ * - If `nodeAfter` is specified, we expect an edge nodeBefore->nodeAfter to exist.
25
+ * That edge is removed, and replaced by two new edges:
26
+ * nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
27
+ * nodeToInsert->nodeAfter (with `edgeLabelAfter`, `edgeValueAfter`)
30
28
  *
31
- * @param nodeToInsert The node you want to insert
32
- * @param nodeBefore The existing node in the workflow that precedes `nodeToInsert`
33
- * @param nodeAfter (Optional) The existing node that should follow `nodeToInsert`
34
- * @param edgeLabel (Optional) Label for the edge, only allowed if `nodeAfter` is not provided
35
- * @param edgeValue (Optional) Value for the edge, only allowed if `nodeAfter` is not provided
29
+ * - If `nodeAfter` is not specified, we create only one edge:
30
+ * nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
31
+ *
32
+ * @param nodeToInsert The node to insert
33
+ * @param nodeBefore The existing node that precedes `nodeToInsert`
34
+ * @param nodeAfter (Optional) The existing node that should follow `nodeToInsert`
35
+ * @param edgeLabelBefore (Optional) Label for the edge nodeBefore->nodeToInsert
36
+ * @param edgeValueBefore (Optional) Value for the edge nodeBefore->nodeToInsert
37
+ * @param edgeLabelAfter (Optional) Label for the edge nodeToInsert->nodeAfter
38
+ * @param edgeValueAfter (Optional) Value for the edge nodeToInsert->nodeAfter
39
+ */
40
+ insertNode(nodeToInsert: Node, nodeBefore: Node, nodeAfter?: Node, edgeLabelBefore?: string | null, edgeValueBefore?: any | null, edgeLabelAfter?: string | null, edgeValueAfter?: any | null): void;
41
+ insertCondition(nodeToInsert: Node, nodeBefore: Node, nodeAfter?: Node, addElseCase?: boolean, addEmptyNodes?: boolean): void;
42
+ /**
43
+ * Inserts a "split" node, creating `numberOfBranches` parallel branches.
44
+ *
45
+ * If `nodeAfter` is given, we call `insertNode(...)` to splice the split node
46
+ * between `nodeBefore` and `nodeAfter`. That yields two edges:
47
+ * nodeBefore->nodeToInsert and nodeToInsert->nodeAfter
48
+ * The first branch is effectively "nodeAfter."
49
+ * Then we add additional empty blocks for the remaining branches.
50
+ *
51
+ * If `nodeAfter` is NOT given, we just insertNode(split, nodeBefore),
52
+ * which yields an edge: nodeBefore->split.
53
+ * Then we create `numberOfBranches` empty blocks off the split node.
54
+ *
55
+ * @param nodeToInsert The split node to insert (e.g. type="Split").
56
+ * @param nodeBefore The node after which the split is inserted.
57
+ * @param nodeAfter (Optional) If we’re splitting in the middle of a flow, the node that was originally after `nodeBefore`.
58
+ * @param numberOfBranches The total number of branches to create from `nodeToInsert`.
36
59
  */
37
- insertNode(nodeToInsert: Node, nodeBefore: Node, nodeAfter?: Node, edgeLabel?: string | null, edgeValue?: any | null): void;
60
+ insertSplit(nodeToInsert: Node, nodeBefore: Node, nodeAfter: Node | undefined, numberOfBranches: number): void;
38
61
  swapNode(oldNode: Node, newNode: Node): void;
39
62
  addEdge(edge: Edge): void;
40
63
  updateEdge(edgeId: string, newEdge: Edge): void;
@@ -1,22 +1,23 @@
1
1
  import { Workflow } from '../models/Workflow.js';
2
2
  import { Node } from '../models/Node.js';
3
3
  import { Edge } from '../models/Edge.js';
4
- export declare const xSpacing = 500;
5
- export declare const ySpacing = 120;
4
+ export declare const xSpacing = 700;
5
+ export declare const ySpacing = 75;
6
6
  export declare const ROOT_X = 400;
7
7
  export declare const ROOT_Y = 120;
8
+ /**
9
+ * Dagre-based layout for the “top-down” pass. We’re effectively ignoring the old
10
+ * manual code. Instead, we:
11
+ * 1) Build a dagre graph
12
+ * 2) Add all nodes & edges
13
+ * 3) dagre.layout(g)
14
+ * 4) Extract positions & write them back to workflow.nodes
15
+ */
8
16
  export declare function positionWorkflowNodes(workflow: Workflow): void;
9
- export declare function positionNode(node: Node, edges: Edge[], xSpacing: number, ySpacing: number, workflow: Workflow): void;
10
- export declare function positionWorkflowNodesAvoidOverlap(workflow: Workflow): void;
11
- export declare function identifyLeafNodes(workflow: Workflow): Node[];
12
17
  /**
13
- * Identifies starting nodes (nodes with no incoming edges).
14
- * A starting node is defined as one that is not a target of any edge.
15
- *
16
- * @param workflow The workflow to analyze.
17
- * @returns An array of nodes that have no incoming edges.
18
+ * We keep these helper functions the same for external usage (if your code or tests need them),
19
+ * but we’re not actively using them in the new Dagre-based layout.
18
20
  */
19
- export declare function identityStartingNodes(workflow: Workflow): Node[];
20
21
  export declare function getChildren(node: Node, edges: Edge[]): Node[];
21
22
  export declare function getParents(node: Node, edges: Edge[]): Node[];
22
23
  export declare function getEdges(node: Node, edges: Edge[]): Edge[];
@@ -24,3 +25,10 @@ export declare function getEndNodePositions(workflow: Workflow): {
24
25
  x: number;
25
26
  y: number;
26
27
  }[];
28
+ /**
29
+ * In Dagre, we no longer do an explicit “identify leaf nodes” or “center parents by average,”
30
+ * because the layout library handles that.
31
+ * But here’s your old method if something else depends on it.
32
+ */
33
+ export declare function identifyLeafNodes(workflow: Workflow): Node[];
34
+ export declare function identityStartingNodes(workflow: Workflow): Node[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "otomato-sdk",
3
- "version": "2.0.35",
3
+ "version": "2.0.37",
4
4
  "description": "An SDK for building and managing automations on Otomato",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/types/src/index.d.ts",
@@ -31,6 +31,7 @@
31
31
  "devDependencies": {
32
32
  "@types/axios": "^0.14.0",
33
33
  "@types/chai": "^4.3.16",
34
+ "@types/dagre": "^0.7.52",
34
35
  "@types/jsonwebtoken": "^9.0.6",
35
36
  "@types/mocha": "^10.0.6",
36
37
  "@types/mustache": "^4.2.5",
@@ -43,6 +44,7 @@
43
44
  },
44
45
  "dependencies": {
45
46
  "axios": "^1.7.2",
47
+ "dagre": "^0.8.5",
46
48
  "ethers": "^6.13.1",
47
49
  "jsonwebtoken": "^9.0.2",
48
50
  "mustache": "^4.2.0"