@flowuent-org/diagramming-core 1.1.5 → 1.1.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -95,6 +95,7 @@ export * from './lib/utils/object';
95
95
  export * from './lib/utils/nodeutils';
96
96
  export * from './lib/utils/utilities';
97
97
  export * from './lib/utils/vhToPixels';
98
+ export * from './lib/utils/nodeOrderByEdges';
98
99
  export * from './lib/assets/markers/markers.param';
99
100
  export * from './lib/assets/markers/markers.type';
100
101
  export * from './lib/organisms/Card/card.params';
@@ -0,0 +1,437 @@
1
+ import { Node, Edge } from '@xyflow/react';
2
+
3
+ /**
4
+ * Options for ordering nodes by edges
5
+ */
6
+ export interface NodeOrderOptions {
7
+ /** Whether to include orphan nodes (nodes with no connections) at the end */
8
+ includeOrphanNodes?: boolean;
9
+ /** Strategy for handling branches: 'breadth-first' or 'depth-first' */
10
+ traversalStrategy?: 'breadth-first' | 'depth-first';
11
+ }
12
+
13
+ /**
14
+ * Result of topological sort with additional metadata
15
+ */
16
+ export interface NodeOrderResult<T extends Node = Node> {
17
+ /** Ordered nodes following the arrow direction */
18
+ orderedNodes: T[];
19
+ /** Nodes that have no incoming edges (start nodes) */
20
+ startNodes: T[];
21
+ /** Nodes that have no outgoing edges (end nodes) */
22
+ endNodes: T[];
23
+ /** Nodes with no connections (orphan nodes) */
24
+ orphanNodes: T[];
25
+ /** Whether the graph has cycles */
26
+ hasCycles: boolean;
27
+ /** Execution levels - nodes grouped by their depth in the flow */
28
+ levels: T[][];
29
+ }
30
+
31
+ /**
32
+ * Orders nodes based on edge connections (arrow direction from source to target).
33
+ * Uses topological sort to determine the correct execution order.
34
+ *
35
+ * @param nodes - Array of nodes to order
36
+ * @param edges - Array of edges connecting the nodes
37
+ * @param options - Optional configuration for ordering
38
+ * @returns NodeOrderResult with ordered nodes and metadata
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import { orderNodesByEdges } from '@diagrams/utils/nodeOrderByEdges';
43
+ *
44
+ * const { orderedNodes, startNodes, endNodes } = orderNodesByEdges(nodes, edges);
45
+ *
46
+ * // Execute nodes in order
47
+ * for (const node of orderedNodes) {
48
+ * await executeNode(node);
49
+ * }
50
+ * ```
51
+ */
52
+ export function orderNodesByEdges<T extends Node = Node>(
53
+ nodes: T[],
54
+ edges: Edge[],
55
+ options: NodeOrderOptions = {}
56
+ ): NodeOrderResult<T> {
57
+ const { includeOrphanNodes = true, traversalStrategy = 'breadth-first' } = options;
58
+
59
+ // Build adjacency list and in-degree map
60
+ const adjacencyList = new Map<string, string[]>();
61
+ const inDegree = new Map<string, number>();
62
+ const nodeMap = new Map<string, T>();
63
+
64
+ // Initialize maps
65
+ nodes.forEach((node) => {
66
+ nodeMap.set(node.id, node);
67
+ adjacencyList.set(node.id, []);
68
+ inDegree.set(node.id, 0);
69
+ });
70
+
71
+ // Build graph from edges
72
+ edges.forEach((edge) => {
73
+ const sourceId = edge.source;
74
+ const targetId = edge.target;
75
+
76
+ // Only process edges where both source and target exist
77
+ if (nodeMap.has(sourceId) && nodeMap.has(targetId)) {
78
+ adjacencyList.get(sourceId)!.push(targetId);
79
+ inDegree.set(targetId, (inDegree.get(targetId) || 0) + 1);
80
+ }
81
+ });
82
+
83
+ // Find start nodes (no incoming edges)
84
+ const startNodes: T[] = [];
85
+ const orphanNodes: T[] = [];
86
+
87
+ nodes.forEach((node) => {
88
+ const hasIncoming = inDegree.get(node.id)! > 0;
89
+ const hasOutgoing = (adjacencyList.get(node.id)?.length || 0) > 0;
90
+
91
+ if (!hasIncoming && !hasOutgoing) {
92
+ orphanNodes.push(node);
93
+ } else if (!hasIncoming) {
94
+ startNodes.push(node);
95
+ }
96
+ });
97
+
98
+ // Find end nodes (no outgoing edges)
99
+ const endNodes: T[] = nodes.filter((node) => {
100
+ const hasOutgoing = (adjacencyList.get(node.id)?.length || 0) > 0;
101
+ const hasIncoming = inDegree.get(node.id)! > 0;
102
+ return !hasOutgoing && hasIncoming;
103
+ });
104
+
105
+ // Perform topological sort (Kahn's algorithm with BFS or DFS)
106
+ const orderedNodes: T[] = [];
107
+ const levels: T[][] = [];
108
+ const visited = new Set<string>();
109
+
110
+ if (traversalStrategy === 'breadth-first') {
111
+ // BFS-based topological sort
112
+ const queue: string[] = startNodes.map((n) => n.id);
113
+ const tempInDegree = new Map(inDegree);
114
+
115
+ while (queue.length > 0) {
116
+ const currentLevel: T[] = [];
117
+ const levelSize = queue.length;
118
+
119
+ for (let i = 0; i < levelSize; i++) {
120
+ const nodeId = queue.shift()!;
121
+
122
+ if (visited.has(nodeId)) continue;
123
+ visited.add(nodeId);
124
+
125
+ const node = nodeMap.get(nodeId);
126
+ if (node) {
127
+ orderedNodes.push(node);
128
+ currentLevel.push(node);
129
+ }
130
+
131
+ // Process neighbors
132
+ const neighbors = adjacencyList.get(nodeId) || [];
133
+ neighbors.forEach((neighborId) => {
134
+ tempInDegree.set(neighborId, (tempInDegree.get(neighborId) || 1) - 1);
135
+ if (tempInDegree.get(neighborId) === 0 && !visited.has(neighborId)) {
136
+ queue.push(neighborId);
137
+ }
138
+ });
139
+ }
140
+
141
+ if (currentLevel.length > 0) {
142
+ levels.push(currentLevel);
143
+ }
144
+ }
145
+ } else {
146
+ // DFS-based topological sort
147
+ const stack: string[] = [];
148
+ const tempVisited = new Set<string>();
149
+
150
+ const dfs = (nodeId: string) => {
151
+ if (tempVisited.has(nodeId)) return;
152
+ tempVisited.add(nodeId);
153
+
154
+ const neighbors = adjacencyList.get(nodeId) || [];
155
+ neighbors.forEach((neighborId) => {
156
+ if (!tempVisited.has(neighborId)) {
157
+ dfs(neighborId);
158
+ }
159
+ });
160
+
161
+ stack.push(nodeId);
162
+ };
163
+
164
+ startNodes.forEach((node) => dfs(node.id));
165
+
166
+ // Reverse to get correct order
167
+ while (stack.length > 0) {
168
+ const nodeId = stack.pop()!;
169
+ visited.add(nodeId);
170
+ const node = nodeMap.get(nodeId);
171
+ if (node) {
172
+ orderedNodes.push(node);
173
+ }
174
+ }
175
+
176
+ // Build levels for DFS (approximate - based on longest path from start)
177
+ const levelMap = new Map<string, number>();
178
+ orderedNodes.forEach((node, index) => {
179
+ const neighbors = adjacencyList.get(node.id) || [];
180
+ let maxParentLevel = -1;
181
+
182
+ edges.forEach((edge) => {
183
+ if (edge.target === node.id && levelMap.has(edge.source)) {
184
+ maxParentLevel = Math.max(maxParentLevel, levelMap.get(edge.source)!);
185
+ }
186
+ });
187
+
188
+ levelMap.set(node.id, maxParentLevel + 1);
189
+ });
190
+
191
+ // Group by levels
192
+ const levelGroups = new Map<number, T[]>();
193
+ orderedNodes.forEach((node) => {
194
+ const level = levelMap.get(node.id) || 0;
195
+ if (!levelGroups.has(level)) {
196
+ levelGroups.set(level, []);
197
+ }
198
+ levelGroups.get(level)!.push(node);
199
+ });
200
+
201
+ // Convert to array
202
+ const sortedLevels = Array.from(levelGroups.keys()).sort((a, b) => a - b);
203
+ sortedLevels.forEach((level) => {
204
+ levels.push(levelGroups.get(level)!);
205
+ });
206
+ }
207
+
208
+ // Check for cycles (nodes not visited = cycle exists)
209
+ const hasCycles = visited.size < (nodes.length - orphanNodes.length);
210
+
211
+ // Add orphan nodes at the end if requested
212
+ if (includeOrphanNodes) {
213
+ orderedNodes.push(...orphanNodes);
214
+ }
215
+
216
+ return {
217
+ orderedNodes,
218
+ startNodes,
219
+ endNodes,
220
+ orphanNodes,
221
+ hasCycles,
222
+ levels,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Gets the next nodes that follow a given node based on edge connections
228
+ *
229
+ * @param nodeId - ID of the current node
230
+ * @param edges - Array of edges
231
+ * @param nodes - Array of nodes
232
+ * @returns Array of next nodes (targets of edges from this node)
233
+ */
234
+ export function getNextNodes<T extends Node = Node>(
235
+ nodeId: string,
236
+ edges: Edge[],
237
+ nodes: T[]
238
+ ): T[] {
239
+ const outgoingEdges = edges.filter((edge) => edge.source === nodeId);
240
+ const nextNodeIds = outgoingEdges.map((edge) => edge.target);
241
+ return nodes.filter((node) => nextNodeIds.includes(node.id));
242
+ }
243
+
244
+ /**
245
+ * Gets the previous nodes that lead to a given node based on edge connections
246
+ *
247
+ * @param nodeId - ID of the current node
248
+ * @param edges - Array of edges
249
+ * @param nodes - Array of nodes
250
+ * @returns Array of previous nodes (sources of edges to this node)
251
+ */
252
+ export function getPreviousNodes<T extends Node = Node>(
253
+ nodeId: string,
254
+ edges: Edge[],
255
+ nodes: T[]
256
+ ): T[] {
257
+ const incomingEdges = edges.filter((edge) => edge.target === nodeId);
258
+ const prevNodeIds = incomingEdges.map((edge) => edge.source);
259
+ return nodes.filter((node) => prevNodeIds.includes(node.id));
260
+ }
261
+
262
+ /**
263
+ * Finds all paths from start nodes to end nodes
264
+ *
265
+ * @param nodes - Array of nodes
266
+ * @param edges - Array of edges
267
+ * @returns Array of paths, where each path is an array of node IDs
268
+ */
269
+ export function findAllPaths<T extends Node = Node>(
270
+ nodes: T[],
271
+ edges: Edge[]
272
+ ): string[][] {
273
+ const { startNodes, endNodes } = orderNodesByEdges(nodes, edges);
274
+ const paths: string[][] = [];
275
+
276
+ const adjacencyList = new Map<string, string[]>();
277
+ nodes.forEach((node) => adjacencyList.set(node.id, []));
278
+ edges.forEach((edge) => {
279
+ if (adjacencyList.has(edge.source)) {
280
+ adjacencyList.get(edge.source)!.push(edge.target);
281
+ }
282
+ });
283
+
284
+ const dfs = (currentId: string, currentPath: string[], visited: Set<string>) => {
285
+ currentPath.push(currentId);
286
+ visited.add(currentId);
287
+
288
+ const neighbors = adjacencyList.get(currentId) || [];
289
+
290
+ if (neighbors.length === 0 || endNodes.some((n) => n.id === currentId)) {
291
+ // Reached an end node or leaf
292
+ paths.push([...currentPath]);
293
+ } else {
294
+ neighbors.forEach((neighborId) => {
295
+ if (!visited.has(neighborId)) {
296
+ dfs(neighborId, currentPath, new Set(visited));
297
+ }
298
+ });
299
+ }
300
+
301
+ currentPath.pop();
302
+ };
303
+
304
+ startNodes.forEach((startNode) => {
305
+ dfs(startNode.id, [], new Set());
306
+ });
307
+
308
+ return paths;
309
+ }
310
+
311
+ /**
312
+ * Gets the execution order of nodes for a workflow, handling branches
313
+ *
314
+ * @param nodes - Array of nodes
315
+ * @param edges - Array of edges
316
+ * @returns Object with execution sequence and branch information
317
+ */
318
+ export function getWorkflowExecutionOrder<T extends Node = Node>(
319
+ nodes: T[],
320
+ edges: Edge[]
321
+ ): {
322
+ sequence: T[];
323
+ branches: { branchPoint: T; branches: T[][] }[];
324
+ } {
325
+ const { orderedNodes, startNodes } = orderNodesByEdges(nodes, edges);
326
+
327
+ const adjacencyList = new Map<string, string[]>();
328
+ nodes.forEach((node) => adjacencyList.set(node.id, []));
329
+ edges.forEach((edge) => {
330
+ if (adjacencyList.has(edge.source)) {
331
+ adjacencyList.get(edge.source)!.push(edge.target);
332
+ }
333
+ });
334
+
335
+ const nodeMap = new Map<string, T>();
336
+ nodes.forEach((node) => nodeMap.set(node.id, node));
337
+
338
+ // Find branch points (nodes with multiple outgoing edges)
339
+ const branches: { branchPoint: T; branches: T[][] }[] = [];
340
+
341
+ orderedNodes.forEach((node) => {
342
+ const outgoing = adjacencyList.get(node.id) || [];
343
+ if (outgoing.length > 1) {
344
+ const branchPaths = outgoing.map((targetId) => {
345
+ const path: T[] = [];
346
+ let currentId = targetId;
347
+ const visited = new Set<string>();
348
+
349
+ while (currentId && !visited.has(currentId)) {
350
+ visited.add(currentId);
351
+ const currentNode = nodeMap.get(currentId);
352
+ if (currentNode) {
353
+ path.push(currentNode);
354
+ }
355
+ const nextNodes = adjacencyList.get(currentId) || [];
356
+ currentId = nextNodes[0]; // Follow first path for simplicity
357
+ }
358
+
359
+ return path;
360
+ });
361
+
362
+ branches.push({
363
+ branchPoint: node,
364
+ branches: branchPaths,
365
+ });
366
+ }
367
+ });
368
+
369
+ return {
370
+ sequence: orderedNodes,
371
+ branches,
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Validates the node graph for execution
377
+ *
378
+ * @param nodes - Array of nodes
379
+ * @param edges - Array of edges
380
+ * @returns Validation result with any errors
381
+ */
382
+ export function validateNodeGraph<T extends Node = Node>(
383
+ nodes: T[],
384
+ edges: Edge[]
385
+ ): {
386
+ isValid: boolean;
387
+ errors: string[];
388
+ warnings: string[];
389
+ } {
390
+ const errors: string[] = [];
391
+ const warnings: string[] = [];
392
+
393
+ const { startNodes, endNodes, orphanNodes, hasCycles } = orderNodesByEdges(nodes, edges);
394
+
395
+ // Check for start nodes
396
+ if (startNodes.length === 0) {
397
+ errors.push('No start node found (no nodes without incoming edges)');
398
+ }
399
+
400
+ // Check for multiple start nodes
401
+ if (startNodes.length > 1) {
402
+ warnings.push(`Multiple start nodes found: ${startNodes.map((n) => n.id).join(', ')}`);
403
+ }
404
+
405
+ // Check for end nodes
406
+ if (endNodes.length === 0 && nodes.length > 0) {
407
+ warnings.push('No end node found (no nodes without outgoing edges)');
408
+ }
409
+
410
+ // Check for orphan nodes
411
+ if (orphanNodes.length > 0) {
412
+ warnings.push(`Found ${orphanNodes.length} orphan node(s) with no connections: ${orphanNodes.map((n) => n.id).join(', ')}`);
413
+ }
414
+
415
+ // Check for cycles
416
+ if (hasCycles) {
417
+ errors.push('Graph contains cycles - execution order may be undefined');
418
+ }
419
+
420
+ // Check for disconnected edges
421
+ const nodeIds = new Set(nodes.map((n) => n.id));
422
+ edges.forEach((edge) => {
423
+ if (!nodeIds.has(edge.source)) {
424
+ errors.push(`Edge ${edge.id} has invalid source: ${edge.source}`);
425
+ }
426
+ if (!nodeIds.has(edge.target)) {
427
+ errors.push(`Edge ${edge.id} has invalid target: ${edge.target}`);
428
+ }
429
+ });
430
+
431
+ return {
432
+ isValid: errors.length === 0,
433
+ errors,
434
+ warnings,
435
+ };
436
+ }
437
+