otomato-sdk 2.0.15 → 2.0.16

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.15';
1
+ export const SDK_VERSION = '2.0.16';
2
2
  export function compareVersions(v1, v2) {
3
3
  // Split the version strings into parts
4
4
  const v1Parts = v1.split('.').map(Number);
@@ -11,6 +11,7 @@ import { Node } from './Node.js';
11
11
  import { Edge } from './Edge.js';
12
12
  import { apiServices } from '../services/ApiService.js';
13
13
  import { Note } from './Note.js';
14
+ import { positionWorkflowNodesAvoidOverlap } from '../utils/WorkflowNodePositioner.js';
14
15
  export class Workflow {
15
16
  constructor(name = '', nodes = [], edges = []) {
16
17
  this.id = null;
@@ -22,18 +23,101 @@ export class Workflow {
22
23
  this.nodes = nodes;
23
24
  this.edges = edges;
24
25
  this.state = 'inactive';
26
+ positionWorkflowNodesAvoidOverlap(this);
25
27
  }
26
28
  setName(name) {
27
29
  this.name = name;
28
30
  }
29
31
  addNode(node) {
30
32
  this.nodes.push(node);
33
+ positionWorkflowNodesAvoidOverlap(this);
31
34
  }
32
35
  addNodes(nodes) {
33
36
  this.nodes.push(...nodes);
37
+ positionWorkflowNodesAvoidOverlap(this);
38
+ }
39
+ deleteNode(nodeToDelete) {
40
+ // Remove the node from the nodes array
41
+ const nodeIndex = this.nodes.findIndex(node => node === nodeToDelete);
42
+ if (nodeIndex === -1) {
43
+ throw new Error(`Node not found in the workflow.`);
44
+ }
45
+ this.nodes.splice(nodeIndex, 1);
46
+ // Collect incoming and outgoing edges
47
+ const incomingEdges = this.edges.filter(edge => edge.target === nodeToDelete);
48
+ const outgoingEdges = this.edges.filter(edge => edge.source === nodeToDelete);
49
+ // Create new edges to replace the deleted node's connections
50
+ const newEdges = [];
51
+ incomingEdges.forEach(inEdge => {
52
+ outgoingEdges.forEach(outEdge => {
53
+ newEdges.push(new Edge({ source: inEdge.source, target: outEdge.target }));
54
+ });
55
+ });
56
+ // Update the edges array: remove edges involving the deleted node and add the new ones
57
+ this.edges = this.edges.filter(edge => edge.source !== nodeToDelete && edge.target !== nodeToDelete);
58
+ this.edges.push(...newEdges);
59
+ // Recalculate positions
60
+ positionWorkflowNodesAvoidOverlap(this);
61
+ }
62
+ insertNode(nodeToInsert, nodeBefore, nodeAfter) {
63
+ // Ensure nodeBefore exists in the workflow
64
+ if (!this.nodes.includes(nodeBefore)) {
65
+ throw new Error('The nodeBefore must exist in the workflow.');
66
+ }
67
+ // If nodeAfter is not provided, insert the new node as a child of nodeBefore
68
+ if (!nodeAfter) {
69
+ // Add the new node to the workflow
70
+ this.addNode(nodeToInsert);
71
+ // Add a new edge between nodeBefore and nodeToInsert
72
+ const newEdge = new Edge({ source: nodeBefore, target: nodeToInsert });
73
+ this.addEdge(newEdge);
74
+ // Recalculate positions
75
+ positionWorkflowNodesAvoidOverlap(this);
76
+ return;
77
+ }
78
+ // If nodeAfter is provided, ensure both nodes exist in the workflow
79
+ if (!this.nodes.includes(nodeAfter)) {
80
+ throw new Error('The nodeAfter must exist in the workflow.');
81
+ }
82
+ // Check if an edge exists between nodeBefore and nodeAfter
83
+ const edgeBetween = this.edges.find(edge => edge.source === nodeBefore && edge.target === nodeAfter);
84
+ if (!edgeBetween) {
85
+ throw new Error('No edge exists between nodeBefore and nodeAfter.');
86
+ }
87
+ // Add the new node to the workflow
88
+ this.addNode(nodeToInsert);
89
+ // Remove the existing edge between nodeBefore and nodeAfter
90
+ this.edges = this.edges.filter(edge => edge !== edgeBetween);
91
+ // Add new edges
92
+ const newEdge1 = new Edge({ source: nodeBefore, target: nodeToInsert });
93
+ const newEdge2 = new Edge({ source: nodeToInsert, target: nodeAfter });
94
+ this.addEdges([newEdge1, newEdge2]);
95
+ // Recalculate positions
96
+ positionWorkflowNodesAvoidOverlap(this);
97
+ }
98
+ swapNode(oldNode, newNode) {
99
+ // Find the index of the node to replace
100
+ const nodeIndex = this.nodes.findIndex(node => node === oldNode);
101
+ if (nodeIndex === -1) {
102
+ throw new Error(`Node to swap not found in the workflow.`);
103
+ }
104
+ // Replace the old node with the new node in the nodes array
105
+ this.nodes[nodeIndex] = newNode;
106
+ // Update edges to point to the new node
107
+ this.edges.forEach(edge => {
108
+ if (edge.source === oldNode) {
109
+ edge.source = newNode;
110
+ }
111
+ if (edge.target === oldNode) {
112
+ edge.target = newNode;
113
+ }
114
+ });
115
+ // Recalculate positions
116
+ positionWorkflowNodesAvoidOverlap(this);
34
117
  }
35
118
  addEdge(edge) {
36
119
  this.edges.push(edge);
120
+ positionWorkflowNodesAvoidOverlap(this);
37
121
  }
38
122
  updateEdge(edgeId, newEdge) {
39
123
  const edgeToUpdate = this.edges.find(e => e.id === edgeId);
@@ -44,9 +128,11 @@ export class Workflow {
44
128
  else {
45
129
  throw new Error(`Edge with id ${edgeId} not found`);
46
130
  }
131
+ positionWorkflowNodesAvoidOverlap(this);
47
132
  }
48
133
  addEdges(edges) {
49
134
  this.edges.push(...edges);
135
+ positionWorkflowNodesAvoidOverlap(this);
50
136
  }
51
137
  getState() {
52
138
  return this.state;
@@ -90,7 +176,7 @@ export class Workflow {
90
176
  executionId: this.executionId,
91
177
  nodes: this.nodes.map(node => node.toJSON()),
92
178
  edges: this.edges.map(edge => edge.toJSON()),
93
- notes: this.getNotes(), // Include notes
179
+ notes: this.getNotes(),
94
180
  };
95
181
  }
96
182
  create() {
@@ -175,6 +261,7 @@ export class Workflow {
175
261
  this.nodes = yield Promise.all(response.nodes.map((nodeData) => __awaiter(this, void 0, void 0, function* () { return yield Node.fromJSON(nodeData); })));
176
262
  this.edges = response.edges.map((edgeData) => Edge.fromJSON(edgeData, this.nodes));
177
263
  this.notes = response.notes.map((noteData) => Note.fromJSON(noteData));
264
+ positionWorkflowNodesAvoidOverlap(this);
178
265
  return this;
179
266
  }
180
267
  catch (error) {
@@ -0,0 +1,202 @@
1
+ // workflowNodePositioner.ts
2
+ /**
3
+ * Positions nodes in a BFS manner, assuming exactly one root node.
4
+ * - The root is placed at (400, 120) unless it already has a position.
5
+ * - If a parent has exactly 1 child, that child is placed directly below the parent
6
+ * (same x, with an offset in y).
7
+ * - If a parent has multiple children, they are horizontally spread around the parent's X.
8
+ * - Existing positions are not overwritten.
9
+ */
10
+ /*export function autoPositionNodes(workflow: Workflow): void {
11
+ // 1. Build adjacency & incoming edge count
12
+ const adjacency = new Map<string, Node[]>();
13
+ const incomingCount = new Map<string, number>();
14
+
15
+ // Constants you can tweak or increase if you see overlap or crossing edges
16
+ const ROOT_X = 400;
17
+ const ROOT_Y = 120;
18
+ const MIN_SPACING_X = 500; // Horizontal spacing between siblings
19
+ const MIN_SPACING_Y = 120; // Vertical spacing from parent to child
20
+
21
+ // Initialize adjacency and incoming counts
22
+ workflow.nodes.forEach((node) => {
23
+ adjacency.set(node.getRef(), []);
24
+ incomingCount.set(node.getRef(), 0);
25
+ });
26
+
27
+ // Fill adjacency list (source -> array of targets) and count incoming edges
28
+ workflow.edges.forEach((edge: Edge) => {
29
+ const sourceRef = edge.source.getRef();
30
+ const targetRef = edge.target.getRef();
31
+ adjacency.get(sourceRef)?.push(edge.target);
32
+ incomingCount.set(targetRef, (incomingCount.get(targetRef) ?? 0) + 1);
33
+ });
34
+
35
+ // 2. Find all root nodes (no incoming edges). If multiple, just pick the first.
36
+ const rootNodes = workflow.nodes.filter(
37
+ (n) => (incomingCount.get(n.getRef()) || 0) === 0
38
+ );
39
+ if (rootNodes.length === 0) {
40
+ // No root found; nothing to do
41
+ return;
42
+ }
43
+
44
+ // We'll assume the first root is "the" root for this layout
45
+ const root = rootNodes[0];
46
+ if (!hasPosition(root)) {
47
+ root.setPosition(ROOT_X, ROOT_Y);
48
+ }
49
+
50
+ // 3. BFS from that single root
51
+ const queue = [root];
52
+ while (queue.length > 0) {
53
+ const parent = queue.shift()!;
54
+ const parentRef = parent.getRef();
55
+
56
+ // Identify the children
57
+ const children = adjacency.get(parentRef) || [];
58
+
59
+ // Among those children, find any that do NOT already have positions
60
+ const unpositionedKids = children.filter((c) => !hasPosition(c));
61
+
62
+ if (unpositionedKids.length > 0) {
63
+ const parentX = parent.position?.x ?? 0;
64
+ const parentY = parent.position?.y ?? 0;
65
+
66
+ if (unpositionedKids.length === 1) {
67
+ // Single child: place it straight below the parent
68
+ const onlyChild = unpositionedKids[0];
69
+ onlyChild.setPosition(parentX, parentY + MIN_SPACING_Y);
70
+ } else {
71
+ // Multiple children: spread them horizontally around the parent's X
72
+ const count = unpositionedKids.length;
73
+ unpositionedKids.forEach((child, i) => {
74
+ const offset = i - (count - 1) / 2;
75
+ const childX = parentX + offset * MIN_SPACING_X;
76
+ const childY = parentY + MIN_SPACING_Y;
77
+ child.setPosition(childX, childY);
78
+ });
79
+ }
80
+ }
81
+
82
+ // Decrement incoming edges for each child. Once a child hits 0, enqueue it
83
+ children.forEach((c) => {
84
+ const cRef = c.getRef();
85
+ const oldCount = incomingCount.get(cRef) ?? 0;
86
+ const newCount = oldCount - 1;
87
+ incomingCount.set(cRef, newCount);
88
+ if (newCount === 0) {
89
+ queue.push(c);
90
+ }
91
+ });
92
+ }
93
+ }*/
94
+ export const xSpacing = 400;
95
+ export const ySpacing = 120;
96
+ export const ROOT_X = 400;
97
+ export const ROOT_Y = 120;
98
+ export function positionWorkflowNodes(workflow) {
99
+ try {
100
+ // Step 1: Find the starting nodes using identityStartingNodes function
101
+ const startingNodes = identityStartingNodes(workflow);
102
+ // Step 2: Place the starting nodes
103
+ let xPosition = ROOT_X;
104
+ startingNodes.forEach((startNode) => {
105
+ startNode.setPosition(xPosition, ROOT_Y);
106
+ xPosition += xSpacing;
107
+ });
108
+ // Step 3: Place all other nodes relative to their parents
109
+ const nodesToPosition = workflow.nodes.filter((node) => !startingNodes.includes(node));
110
+ nodesToPosition.forEach((node) => positionNode(node, workflow.edges, xSpacing, ySpacing, workflow));
111
+ }
112
+ catch (e) {
113
+ console.error(e);
114
+ }
115
+ }
116
+ export function positionNode(node, edges, xSpacing, ySpacing, workflow) {
117
+ // Get children of the node
118
+ const parents = getParents(node, edges);
119
+ // todo: what if we have multiple parents?
120
+ const children = getChildren(parents[0], edges);
121
+ const childrenCountOfParent = children.length;
122
+ 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;
123
+ 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; }));
124
+ // Compute position based on parent children count
125
+ if (childrenCountOfParent === 1) {
126
+ node.setPosition(parentX, parentY + ySpacing);
127
+ }
128
+ else {
129
+ const index = children.indexOf(node); // Get the position of this node among its siblings
130
+ const totalChildren = children.length;
131
+ // Compute the x position for this node
132
+ const offset = index - (totalChildren - 1) / 2; // Center the children around the parent
133
+ node.setPosition(parentX + offset * xSpacing, parentY + ySpacing);
134
+ }
135
+ }
136
+ export function positionWorkflowNodesAvoidOverlap(workflow) {
137
+ const levels = new Map();
138
+ // Helper: Add node to its level
139
+ function addToLevel(node) {
140
+ const level = Math.round(node.position.y / ySpacing);
141
+ if (!levels.has(level)) {
142
+ levels.set(level, []);
143
+ }
144
+ levels.get(level).push(node);
145
+ }
146
+ // Step 1: Position nodes using the existing logic
147
+ positionWorkflowNodes(workflow);
148
+ // Step 2: Populate levels
149
+ workflow.nodes.forEach((node) => {
150
+ if (node.position) {
151
+ addToLevel(node);
152
+ }
153
+ });
154
+ // Step 3: Resolve overlaps for each level
155
+ levels.forEach((nodes, level) => {
156
+ // Sort nodes by X position
157
+ nodes.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); });
158
+ // Adjust overlapping nodes
159
+ for (let i = 1; i < nodes.length; i++) {
160
+ const prevNode = nodes[i - 1];
161
+ const currentNode = nodes[i];
162
+ if (currentNode.position.x - prevNode.position.x < xSpacing) {
163
+ const shift = xSpacing - (currentNode.position.x - prevNode.position.x);
164
+ moveNodeAndChildren(currentNode, shift, workflow.edges);
165
+ }
166
+ }
167
+ });
168
+ }
169
+ function moveNodeAndChildren(node, shift, edges) {
170
+ // Move the node
171
+ node.setPosition(node.position.x + shift, node.position.y);
172
+ // Propagate to children
173
+ edges
174
+ .filter((edge) => edge.source === node)
175
+ .forEach((edge) => {
176
+ moveNodeAndChildren(edge.target, shift, edges);
177
+ });
178
+ }
179
+ export function identifyLeafNodes(workflow) {
180
+ const nonLeafNodes = new Set(workflow.edges.map(edge => edge.source.getRef()));
181
+ return workflow.nodes.filter(node => !nonLeafNodes.has(node.getRef()));
182
+ }
183
+ /**
184
+ * Identifies starting nodes (nodes with no incoming edges).
185
+ * A starting node is defined as one that is not a target of any edge.
186
+ *
187
+ * @param workflow The workflow to analyze.
188
+ * @returns An array of nodes that have no incoming edges.
189
+ */
190
+ export function identityStartingNodes(workflow) {
191
+ const childRefs = new Set(workflow.edges.map((edge) => edge.target.getRef()));
192
+ return workflow.nodes.filter((node) => !childRefs.has(node.getRef()));
193
+ }
194
+ export function getChildren(node, edges) {
195
+ return edges.filter(edge => edge.source === node).map(edge => edge.target);
196
+ }
197
+ export function getParents(node, edges) {
198
+ return edges.filter(edge => edge.target === node).map(edge => edge.source);
199
+ }
200
+ export function getEdges(node, edges) {
201
+ return edges.filter(edge => edge.source === node || edge.target === node);
202
+ }
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "2.0.15";
1
+ export declare const SDK_VERSION = "2.0.16";
2
2
  export declare function compareVersions(v1: string, v2: string): number;
@@ -17,6 +17,9 @@ export declare class Workflow {
17
17
  setName(name: string): void;
18
18
  addNode(node: Node): void;
19
19
  addNodes(nodes: Node[]): void;
20
+ deleteNode(nodeToDelete: Node): void;
21
+ insertNode(nodeToInsert: Node, nodeBefore: Node, nodeAfter?: Node): void;
22
+ swapNode(oldNode: Node, newNode: Node): void;
20
23
  addEdge(edge: Edge): void;
21
24
  updateEdge(edgeId: string, newEdge: Edge): void;
22
25
  addEdges(edges: Edge[]): void;
@@ -0,0 +1,30 @@
1
+ import { Workflow } from '../models/Workflow.js';
2
+ import { Node } from '../models/Node.js';
3
+ import { Edge } from '../models/Edge.js';
4
+ /**
5
+ * Positions nodes in a BFS manner, assuming exactly one root node.
6
+ * - The root is placed at (400, 120) unless it already has a position.
7
+ * - If a parent has exactly 1 child, that child is placed directly below the parent
8
+ * (same x, with an offset in y).
9
+ * - If a parent has multiple children, they are horizontally spread around the parent's X.
10
+ * - Existing positions are not overwritten.
11
+ */
12
+ export declare const xSpacing = 400;
13
+ export declare const ySpacing = 120;
14
+ export declare const ROOT_X = 400;
15
+ export declare const ROOT_Y = 120;
16
+ export declare function positionWorkflowNodes(workflow: Workflow): void;
17
+ export declare function positionNode(node: Node, edges: Edge[], xSpacing: number, ySpacing: number, workflow: Workflow): void;
18
+ export declare function positionWorkflowNodesAvoidOverlap(workflow: Workflow): void;
19
+ export declare function identifyLeafNodes(workflow: Workflow): Node[];
20
+ /**
21
+ * Identifies starting nodes (nodes with no incoming edges).
22
+ * A starting node is defined as one that is not a target of any edge.
23
+ *
24
+ * @param workflow The workflow to analyze.
25
+ * @returns An array of nodes that have no incoming edges.
26
+ */
27
+ export declare function identityStartingNodes(workflow: Workflow): Node[];
28
+ export declare function getChildren(node: Node, edges: Edge[]): Node[];
29
+ export declare function getParents(node: Node, edges: Edge[]): Node[];
30
+ export declare function getEdges(node: Node, edges: Edge[]): Edge[];
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "otomato-sdk",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
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",