@allternit/workflow-engine 0.1.0 → 0.1.1
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/README.md +1 -1
- package/dist/engine/workflow-engine.d.ts.map +1 -1
- package/dist/engine/workflow-engine.js +32 -6
- package/dist/engine/workflow-engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +4 -16
- package/src/engine/workflow-engine.ts +35 -6
- package/src/index.ts +1 -1
- package/src/types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/engine/workflow-engine.d.ts.bak +0 -39
- package/dist/engine/workflow-engine.js.bak +0 -532
- package/dist/index.d.ts.bak +0 -38
- package/dist/index.js.bak +0 -41
- package/dist/scheduler/index.d.ts.bak +0 -53
- package/dist/scheduler/index.js.bak +0 -135
- package/dist/types.d.ts.bak +0 -402
- package/dist/types.js.bak +0 -7
- package/dist/visualizer/index.d.ts.bak +0 -81
- package/dist/visualizer/index.js.bak +0 -277
- package/src/__tests__/visualizer.test.ts.bak +0 -110
- package/src/__tests__/workflow-engine.test.ts.bak +0 -239
- package/src/engine/workflow-engine.test.ts.bak +0 -852
- package/src/engine/workflow-engine.ts.bak +0 -651
- package/src/index.ts.bak +0 -89
- package/src/scheduler/index.ts.bak +0 -204
- package/src/scheduler/scheduler.test.ts.bak +0 -521
- package/src/types.ts.bak +0 -449
- package/src/visualizer/index.ts.bak +0 -409
- package/src/visualizer/visualizer.test.ts.bak +0 -844
- package/vitest.config.ts.bak +0 -9
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Visualizer
|
|
3
|
-
*
|
|
4
|
-
* Generates visual representations of workflows.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Workflow, WorkflowNode, Connection, Position } from '../types';
|
|
8
|
-
|
|
9
|
-
export interface VisualizerConfig {
|
|
10
|
-
/** Canvas width */
|
|
11
|
-
width?: number;
|
|
12
|
-
/** Canvas height */
|
|
13
|
-
height?: number;
|
|
14
|
-
/** Node width */
|
|
15
|
-
nodeWidth?: number;
|
|
16
|
-
/** Node height */
|
|
17
|
-
nodeHeight?: number;
|
|
18
|
-
/** Horizontal spacing */
|
|
19
|
-
hSpacing?: number;
|
|
20
|
-
/** Vertical spacing */
|
|
21
|
-
vSpacing?: number;
|
|
22
|
-
/** Enable auto-layout */
|
|
23
|
-
autoLayout?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface VisualLayout {
|
|
27
|
-
nodes: VisualNode[];
|
|
28
|
-
connections: VisualConnection[];
|
|
29
|
-
bounds: Bounds;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface VisualNode {
|
|
33
|
-
id: string;
|
|
34
|
-
x: number;
|
|
35
|
-
y: number;
|
|
36
|
-
width: number;
|
|
37
|
-
height: number;
|
|
38
|
-
color?: string;
|
|
39
|
-
icon?: string;
|
|
40
|
-
label: string;
|
|
41
|
-
status?: 'pending' | 'running' | 'completed' | 'failed';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface VisualConnection {
|
|
45
|
-
id: string;
|
|
46
|
-
source: { x: number; y: number };
|
|
47
|
-
target: { x: number; y: number };
|
|
48
|
-
path: string;
|
|
49
|
-
color?: string;
|
|
50
|
-
animated?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface Bounds {
|
|
54
|
-
minX: number;
|
|
55
|
-
minY: number;
|
|
56
|
-
maxX: number;
|
|
57
|
-
maxY: number;
|
|
58
|
-
width: number;
|
|
59
|
-
height: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface WorkflowVisualizer {
|
|
63
|
-
/** Generate visual layout */
|
|
64
|
-
layout(workflow: Workflow): VisualLayout;
|
|
65
|
-
/** Auto-layout nodes */
|
|
66
|
-
autoLayout(nodes: WorkflowNode[], connections: Connection[]): Record<string, Position>;
|
|
67
|
-
/** Export to SVG */
|
|
68
|
-
toSVG(workflow: Workflow): string;
|
|
69
|
-
/** Export to Mermaid diagram */
|
|
70
|
-
toMermaid(workflow: Workflow): string;
|
|
71
|
-
/** Export to DOT (Graphviz) */
|
|
72
|
-
toDOT(workflow: Workflow): string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Create workflow visualizer
|
|
77
|
-
*/
|
|
78
|
-
export function createVisualizer(config: VisualizerConfig = {}): WorkflowVisualizer {
|
|
79
|
-
const {
|
|
80
|
-
width = 1200,
|
|
81
|
-
height = 800,
|
|
82
|
-
nodeWidth = 180,
|
|
83
|
-
nodeHeight = 60,
|
|
84
|
-
hSpacing = 100,
|
|
85
|
-
vSpacing = 80,
|
|
86
|
-
autoLayout: enableAutoLayout = true,
|
|
87
|
-
} = config;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Generate visual layout
|
|
91
|
-
*/
|
|
92
|
-
function layout(workflow: Workflow): VisualLayout {
|
|
93
|
-
let nodePositions: Record<string, Position>;
|
|
94
|
-
|
|
95
|
-
if (enableAutoLayout) {
|
|
96
|
-
nodePositions = autoLayoutNodes(workflow.nodes, workflow.connections);
|
|
97
|
-
} else {
|
|
98
|
-
// Use existing positions or default
|
|
99
|
-
nodePositions = {};
|
|
100
|
-
workflow.nodes.forEach(node => {
|
|
101
|
-
nodePositions[node.id] = node.position || { x: 0, y: 0 };
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Create visual nodes
|
|
106
|
-
const visualNodes: VisualNode[] = workflow.nodes.map(node => {
|
|
107
|
-
const pos = nodePositions[node.id];
|
|
108
|
-
return {
|
|
109
|
-
id: node.id,
|
|
110
|
-
x: pos.x,
|
|
111
|
-
y: pos.y,
|
|
112
|
-
width: nodeWidth,
|
|
113
|
-
height: nodeHeight,
|
|
114
|
-
label: node.name || node.type,
|
|
115
|
-
color: getNodeColor(node.type),
|
|
116
|
-
icon: getNodeIcon(node.type),
|
|
117
|
-
};
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Create visual connections
|
|
121
|
-
const visualConnections: VisualConnection[] = workflow.connections.map(conn => {
|
|
122
|
-
const sourcePos = nodePositions[conn.source];
|
|
123
|
-
const targetPos = nodePositions[conn.target];
|
|
124
|
-
|
|
125
|
-
const source = {
|
|
126
|
-
x: sourcePos.x + nodeWidth / 2,
|
|
127
|
-
y: sourcePos.y + nodeHeight,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const target = {
|
|
131
|
-
x: targetPos.x + nodeWidth / 2,
|
|
132
|
-
y: targetPos.y,
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
id: conn.id,
|
|
137
|
-
source,
|
|
138
|
-
target,
|
|
139
|
-
path: generatePath(source, target),
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Calculate bounds
|
|
144
|
-
const bounds = calculateBounds(visualNodes);
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
nodes: visualNodes,
|
|
148
|
-
connections: visualConnections,
|
|
149
|
-
bounds,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Auto-layout nodes using layered graph layout
|
|
155
|
-
*/
|
|
156
|
-
function autoLayout(
|
|
157
|
-
nodes: WorkflowNode[],
|
|
158
|
-
connections: Connection[]
|
|
159
|
-
): Record<string, Position> {
|
|
160
|
-
return autoLayoutNodes(nodes, connections);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Auto-layout nodes
|
|
165
|
-
*/
|
|
166
|
-
function autoLayoutNodes(
|
|
167
|
-
nodes: WorkflowNode[],
|
|
168
|
-
connections: Connection[]
|
|
169
|
-
): Record<string, Position> {
|
|
170
|
-
const positions: Record<string, Position> = {};
|
|
171
|
-
|
|
172
|
-
// Build adjacency list
|
|
173
|
-
const incoming = new Map<string, string[]>();
|
|
174
|
-
const outgoing = new Map<string, string[]>();
|
|
175
|
-
|
|
176
|
-
nodes.forEach(node => {
|
|
177
|
-
incoming.set(node.id, []);
|
|
178
|
-
outgoing.set(node.id, []);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
connections.forEach(conn => {
|
|
182
|
-
outgoing.get(conn.source)?.push(conn.target);
|
|
183
|
-
incoming.get(conn.target)?.push(conn.source);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Topological sort with layering
|
|
187
|
-
const layers: string[][] = [];
|
|
188
|
-
const visited = new Set<string>();
|
|
189
|
-
const inDegree = new Map<string, number>();
|
|
190
|
-
|
|
191
|
-
nodes.forEach(node => {
|
|
192
|
-
inDegree.set(node.id, incoming.get(node.id)?.length || 0);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
while (visited.size < nodes.length) {
|
|
196
|
-
const layer: string[] = [];
|
|
197
|
-
|
|
198
|
-
nodes.forEach(node => {
|
|
199
|
-
if (!visited.has(node.id) && (inDegree.get(node.id) || 0) === 0) {
|
|
200
|
-
layer.push(node.id);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
if (layer.length === 0) {
|
|
205
|
-
// Cycle detected, add remaining nodes
|
|
206
|
-
nodes.forEach(node => {
|
|
207
|
-
if (!visited.has(node.id)) {
|
|
208
|
-
layer.push(node.id);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
layers.push(layer);
|
|
214
|
-
|
|
215
|
-
layer.forEach(nodeId => {
|
|
216
|
-
visited.add(nodeId);
|
|
217
|
-
outgoing.get(nodeId)?.forEach(targetId => {
|
|
218
|
-
const degree = inDegree.get(targetId) || 0;
|
|
219
|
-
inDegree.set(targetId, degree - 1);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Position nodes in layers
|
|
225
|
-
layers.forEach((layer, layerIndex) => {
|
|
226
|
-
const layerWidth = layer.length * (nodeWidth + hSpacing) - hSpacing;
|
|
227
|
-
const startX = (width - layerWidth) / 2;
|
|
228
|
-
|
|
229
|
-
layer.forEach((nodeId, index) => {
|
|
230
|
-
positions[nodeId] = {
|
|
231
|
-
x: startX + index * (nodeWidth + hSpacing),
|
|
232
|
-
y: 100 + layerIndex * (nodeHeight + vSpacing),
|
|
233
|
-
};
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return positions;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Generate SVG path between two points
|
|
242
|
-
*/
|
|
243
|
-
function generatePath(
|
|
244
|
-
source: { x: number; y: number },
|
|
245
|
-
target: { x: number; y: number }
|
|
246
|
-
): string {
|
|
247
|
-
const midY = (source.y + target.y) / 2;
|
|
248
|
-
return `M ${source.x} ${source.y} C ${source.x} ${midY}, ${target.x} ${midY}, ${target.x} ${target.y}`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Calculate bounds
|
|
253
|
-
*/
|
|
254
|
-
function calculateBounds(nodes: VisualNode[]): Bounds {
|
|
255
|
-
if (nodes.length === 0) {
|
|
256
|
-
return { minX: 0, minY: 0, maxX: width, maxY: height, width, height };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const xs = nodes.map(n => n.x);
|
|
260
|
-
const ys = nodes.map(n => n.y);
|
|
261
|
-
|
|
262
|
-
const minX = Math.min(...xs) - 50;
|
|
263
|
-
const minY = Math.min(...ys) - 50;
|
|
264
|
-
const maxX = Math.max(...xs) + nodeWidth + 50;
|
|
265
|
-
const maxY = Math.max(...ys) + nodeHeight + 50;
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
minX,
|
|
269
|
-
minY,
|
|
270
|
-
maxX,
|
|
271
|
-
maxY,
|
|
272
|
-
width: maxX - minX,
|
|
273
|
-
height: maxY - minY,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Get node color by type
|
|
279
|
-
*/
|
|
280
|
-
function getNodeColor(type: string): string {
|
|
281
|
-
const colors: Record<string, string> = {
|
|
282
|
-
'trigger:manual': '#3b82f6',
|
|
283
|
-
'trigger:schedule': '#3b82f6',
|
|
284
|
-
'trigger:webhook': '#3b82f6',
|
|
285
|
-
'transform:map': '#10b981',
|
|
286
|
-
'transform:filter': '#10b981',
|
|
287
|
-
'condition:if': '#f59e0b',
|
|
288
|
-
'loop:for-each': '#8b5cf6',
|
|
289
|
-
'delay:wait': '#6b7280',
|
|
290
|
-
'http:request': '#ec4899',
|
|
291
|
-
'output:result': '#ef4444',
|
|
292
|
-
'output:log': '#6b7280',
|
|
293
|
-
};
|
|
294
|
-
return colors[type] || '#6b7280';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Get node icon by type
|
|
299
|
-
*/
|
|
300
|
-
function getNodeIcon(type: string): string {
|
|
301
|
-
const icons: Record<string, string> = {
|
|
302
|
-
'trigger:manual': 'play',
|
|
303
|
-
'trigger:schedule': 'clock',
|
|
304
|
-
'trigger:webhook': 'webhook',
|
|
305
|
-
'transform:map': 'transform',
|
|
306
|
-
'transform:filter': 'filter',
|
|
307
|
-
'condition:if': 'git-branch',
|
|
308
|
-
'loop:for-each': 'repeat',
|
|
309
|
-
'delay:wait': 'timer',
|
|
310
|
-
'http:request': 'globe',
|
|
311
|
-
'output:result': 'check-circle',
|
|
312
|
-
'output:log': 'file-text',
|
|
313
|
-
};
|
|
314
|
-
return icons[type] || 'box';
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Export to SVG
|
|
319
|
-
*/
|
|
320
|
-
function toSVG(workflow: Workflow): string {
|
|
321
|
-
const { nodes, connections, bounds } = layout(workflow);
|
|
322
|
-
|
|
323
|
-
const svgWidth = bounds.width;
|
|
324
|
-
const svgHeight = bounds.height;
|
|
325
|
-
|
|
326
|
-
let svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="${bounds.minX} ${bounds.minY} ${svgWidth} ${svgHeight}" xmlns="http://www.w3.org/2000/svg">\n`;
|
|
327
|
-
|
|
328
|
-
// Definitions
|
|
329
|
-
svg += ` <defs>\n`;
|
|
330
|
-
svg += ` <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">\n`;
|
|
331
|
-
svg += ` <polygon points="0 0, 10 3.5, 0 7" fill="#6b7280" />\n`;
|
|
332
|
-
svg += ` </marker>\n`;
|
|
333
|
-
svg += ` </defs>\n`;
|
|
334
|
-
|
|
335
|
-
// Connections
|
|
336
|
-
connections.forEach(conn => {
|
|
337
|
-
svg += ` <path d="${conn.path}" fill="none" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)" />\n`;
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Nodes
|
|
341
|
-
nodes.forEach(node => {
|
|
342
|
-
const rx = 8;
|
|
343
|
-
svg += ` <g transform="translate(${node.x}, ${node.y})">\n`;
|
|
344
|
-
svg += ` <rect width="${node.width}" height="${node.height}" rx="${rx}" fill="${node.color}" stroke="none" />\n`;
|
|
345
|
-
svg += ` <text x="${node.width / 2}" y="${node.height / 2}" text-anchor="middle" dominant-baseline="middle" fill="white" font-family="sans-serif" font-size="14">${node.label}</text>\n`;
|
|
346
|
-
svg += ` </g>\n`;
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
svg += `</svg>`;
|
|
350
|
-
return svg;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Export to Mermaid diagram
|
|
355
|
-
*/
|
|
356
|
-
function toMermaid(workflow: Workflow): string {
|
|
357
|
-
let mermaid = 'graph TD\n';
|
|
358
|
-
|
|
359
|
-
// Node definitions
|
|
360
|
-
workflow.nodes.forEach(node => {
|
|
361
|
-
const label = node.name || node.type;
|
|
362
|
-
mermaid += ` ${node.id}["${label}"]\n`;
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// Connections
|
|
366
|
-
workflow.connections.forEach(conn => {
|
|
367
|
-
mermaid += ` ${conn.source} --> ${conn.target}\n`;
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
return mermaid;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Export to DOT (Graphviz)
|
|
375
|
-
*/
|
|
376
|
-
function toDOT(workflow: Workflow): string {
|
|
377
|
-
let dot = 'digraph Workflow {\n';
|
|
378
|
-
dot += ' rankdir=TB;\n';
|
|
379
|
-
dot += ' node [shape=box, style=filled, fontname="sans-serif"];\n';
|
|
380
|
-
|
|
381
|
-
// Node definitions
|
|
382
|
-
workflow.nodes.forEach(node => {
|
|
383
|
-
const label = node.name || node.type;
|
|
384
|
-
const color = getNodeColor(node.type);
|
|
385
|
-
dot += ` "${node.id}" [label="${label}", fillcolor="${color}", fontcolor=white];\n`;
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// Connections
|
|
389
|
-
workflow.connections.forEach(conn => {
|
|
390
|
-
dot += ` "${conn.source}" -> "${conn.target}";\n`;
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
dot += '}';
|
|
394
|
-
return dot;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
layout,
|
|
399
|
-
autoLayout,
|
|
400
|
-
toSVG,
|
|
401
|
-
toMermaid,
|
|
402
|
-
toDOT,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Global visualizer instance
|
|
408
|
-
*/
|
|
409
|
-
export const globalVisualizer = createVisualizer();
|