@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.
@@ -1,277 +0,0 @@
1
- /**
2
- * Workflow Visualizer
3
- *
4
- * Generates visual representations of workflows.
5
- */
6
- /**
7
- * Create workflow visualizer
8
- */
9
- export function createVisualizer(config = {}) {
10
- const { width = 1200, height = 800, nodeWidth = 180, nodeHeight = 60, hSpacing = 100, vSpacing = 80, autoLayout: enableAutoLayout = true, } = config;
11
- /**
12
- * Generate visual layout
13
- */
14
- function layout(workflow) {
15
- let nodePositions;
16
- if (enableAutoLayout) {
17
- nodePositions = autoLayoutNodes(workflow.nodes, workflow.connections);
18
- }
19
- else {
20
- // Use existing positions or default
21
- nodePositions = {};
22
- workflow.nodes.forEach(node => {
23
- nodePositions[node.id] = node.position || { x: 0, y: 0 };
24
- });
25
- }
26
- // Create visual nodes
27
- const visualNodes = workflow.nodes.map(node => {
28
- const pos = nodePositions[node.id];
29
- return {
30
- id: node.id,
31
- x: pos.x,
32
- y: pos.y,
33
- width: nodeWidth,
34
- height: nodeHeight,
35
- label: node.name || node.type,
36
- color: getNodeColor(node.type),
37
- icon: getNodeIcon(node.type),
38
- };
39
- });
40
- // Create visual connections
41
- const visualConnections = workflow.connections.map(conn => {
42
- const sourcePos = nodePositions[conn.source];
43
- const targetPos = nodePositions[conn.target];
44
- const source = {
45
- x: sourcePos.x + nodeWidth / 2,
46
- y: sourcePos.y + nodeHeight,
47
- };
48
- const target = {
49
- x: targetPos.x + nodeWidth / 2,
50
- y: targetPos.y,
51
- };
52
- return {
53
- id: conn.id,
54
- source,
55
- target,
56
- path: generatePath(source, target),
57
- };
58
- });
59
- // Calculate bounds
60
- const bounds = calculateBounds(visualNodes);
61
- return {
62
- nodes: visualNodes,
63
- connections: visualConnections,
64
- bounds,
65
- };
66
- }
67
- /**
68
- * Auto-layout nodes using layered graph layout
69
- */
70
- function autoLayout(nodes, connections) {
71
- return autoLayoutNodes(nodes, connections);
72
- }
73
- /**
74
- * Auto-layout nodes
75
- */
76
- function autoLayoutNodes(nodes, connections) {
77
- const positions = {};
78
- // Build adjacency list
79
- const incoming = new Map();
80
- const outgoing = new Map();
81
- nodes.forEach(node => {
82
- incoming.set(node.id, []);
83
- outgoing.set(node.id, []);
84
- });
85
- connections.forEach(conn => {
86
- outgoing.get(conn.source)?.push(conn.target);
87
- incoming.get(conn.target)?.push(conn.source);
88
- });
89
- // Topological sort with layering
90
- const layers = [];
91
- const visited = new Set();
92
- const inDegree = new Map();
93
- nodes.forEach(node => {
94
- inDegree.set(node.id, incoming.get(node.id)?.length || 0);
95
- });
96
- while (visited.size < nodes.length) {
97
- const layer = [];
98
- nodes.forEach(node => {
99
- if (!visited.has(node.id) && (inDegree.get(node.id) || 0) === 0) {
100
- layer.push(node.id);
101
- }
102
- });
103
- if (layer.length === 0) {
104
- // Cycle detected, add remaining nodes
105
- nodes.forEach(node => {
106
- if (!visited.has(node.id)) {
107
- layer.push(node.id);
108
- }
109
- });
110
- }
111
- layers.push(layer);
112
- layer.forEach(nodeId => {
113
- visited.add(nodeId);
114
- outgoing.get(nodeId)?.forEach(targetId => {
115
- const degree = inDegree.get(targetId) || 0;
116
- inDegree.set(targetId, degree - 1);
117
- });
118
- });
119
- }
120
- // Position nodes in layers
121
- layers.forEach((layer, layerIndex) => {
122
- const layerWidth = layer.length * (nodeWidth + hSpacing) - hSpacing;
123
- const startX = (width - layerWidth) / 2;
124
- layer.forEach((nodeId, index) => {
125
- positions[nodeId] = {
126
- x: startX + index * (nodeWidth + hSpacing),
127
- y: 100 + layerIndex * (nodeHeight + vSpacing),
128
- };
129
- });
130
- });
131
- return positions;
132
- }
133
- /**
134
- * Generate SVG path between two points
135
- */
136
- function generatePath(source, target) {
137
- const midY = (source.y + target.y) / 2;
138
- return `M ${source.x} ${source.y} C ${source.x} ${midY}, ${target.x} ${midY}, ${target.x} ${target.y}`;
139
- }
140
- /**
141
- * Calculate bounds
142
- */
143
- function calculateBounds(nodes) {
144
- if (nodes.length === 0) {
145
- return { minX: 0, minY: 0, maxX: width, maxY: height, width, height };
146
- }
147
- const xs = nodes.map(n => n.x);
148
- const ys = nodes.map(n => n.y);
149
- const minX = Math.min(...xs) - 50;
150
- const minY = Math.min(...ys) - 50;
151
- const maxX = Math.max(...xs) + nodeWidth + 50;
152
- const maxY = Math.max(...ys) + nodeHeight + 50;
153
- return {
154
- minX,
155
- minY,
156
- maxX,
157
- maxY,
158
- width: maxX - minX,
159
- height: maxY - minY,
160
- };
161
- }
162
- /**
163
- * Get node color by type
164
- */
165
- function getNodeColor(type) {
166
- const colors = {
167
- 'trigger:manual': '#3b82f6',
168
- 'trigger:schedule': '#3b82f6',
169
- 'trigger:webhook': '#3b82f6',
170
- 'transform:map': '#10b981',
171
- 'transform:filter': '#10b981',
172
- 'condition:if': '#f59e0b',
173
- 'loop:for-each': '#8b5cf6',
174
- 'delay:wait': '#6b7280',
175
- 'http:request': '#ec4899',
176
- 'output:result': '#ef4444',
177
- 'output:log': '#6b7280',
178
- };
179
- return colors[type] || '#6b7280';
180
- }
181
- /**
182
- * Get node icon by type
183
- */
184
- function getNodeIcon(type) {
185
- const icons = {
186
- 'trigger:manual': 'play',
187
- 'trigger:schedule': 'clock',
188
- 'trigger:webhook': 'webhook',
189
- 'transform:map': 'transform',
190
- 'transform:filter': 'filter',
191
- 'condition:if': 'git-branch',
192
- 'loop:for-each': 'repeat',
193
- 'delay:wait': 'timer',
194
- 'http:request': 'globe',
195
- 'output:result': 'check-circle',
196
- 'output:log': 'file-text',
197
- };
198
- return icons[type] || 'box';
199
- }
200
- /**
201
- * Export to SVG
202
- */
203
- function toSVG(workflow) {
204
- const { nodes, connections, bounds } = layout(workflow);
205
- const svgWidth = bounds.width;
206
- const svgHeight = bounds.height;
207
- let svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="${bounds.minX} ${bounds.minY} ${svgWidth} ${svgHeight}" xmlns="http://www.w3.org/2000/svg">\n`;
208
- // Definitions
209
- svg += ` <defs>\n`;
210
- svg += ` <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">\n`;
211
- svg += ` <polygon points="0 0, 10 3.5, 0 7" fill="#6b7280" />\n`;
212
- svg += ` </marker>\n`;
213
- svg += ` </defs>\n`;
214
- // Connections
215
- connections.forEach(conn => {
216
- svg += ` <path d="${conn.path}" fill="none" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)" />\n`;
217
- });
218
- // Nodes
219
- nodes.forEach(node => {
220
- const rx = 8;
221
- svg += ` <g transform="translate(${node.x}, ${node.y})">\n`;
222
- svg += ` <rect width="${node.width}" height="${node.height}" rx="${rx}" fill="${node.color}" stroke="none" />\n`;
223
- 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`;
224
- svg += ` </g>\n`;
225
- });
226
- svg += `</svg>`;
227
- return svg;
228
- }
229
- /**
230
- * Export to Mermaid diagram
231
- */
232
- function toMermaid(workflow) {
233
- let mermaid = 'graph TD\n';
234
- // Node definitions
235
- workflow.nodes.forEach(node => {
236
- const label = node.name || node.type;
237
- mermaid += ` ${node.id}["${label}"]\n`;
238
- });
239
- // Connections
240
- workflow.connections.forEach(conn => {
241
- mermaid += ` ${conn.source} --> ${conn.target}\n`;
242
- });
243
- return mermaid;
244
- }
245
- /**
246
- * Export to DOT (Graphviz)
247
- */
248
- function toDOT(workflow) {
249
- let dot = 'digraph Workflow {\n';
250
- dot += ' rankdir=TB;\n';
251
- dot += ' node [shape=box, style=filled, fontname="sans-serif"];\n';
252
- // Node definitions
253
- workflow.nodes.forEach(node => {
254
- const label = node.name || node.type;
255
- const color = getNodeColor(node.type);
256
- dot += ` "${node.id}" [label="${label}", fillcolor="${color}", fontcolor=white];\n`;
257
- });
258
- // Connections
259
- workflow.connections.forEach(conn => {
260
- dot += ` "${conn.source}" -> "${conn.target}";\n`;
261
- });
262
- dot += '}';
263
- return dot;
264
- }
265
- return {
266
- layout,
267
- autoLayout,
268
- toSVG,
269
- toMermaid,
270
- toDOT,
271
- };
272
- }
273
- /**
274
- * Global visualizer instance
275
- */
276
- export const globalVisualizer = createVisualizer();
277
- //# sourceMappingURL=index.js.map
@@ -1,110 +0,0 @@
1
- /**
2
- * Workflow Visualizer Tests
3
- */
4
-
5
- import { describe, it, expect } from 'vitest';
6
- import { createVisualizer } from '../visualizer';
7
- import type { Workflow } from '../types';
8
-
9
- describe('WorkflowVisualizer', () => {
10
- const sampleWorkflow: Workflow = {
11
- id: 'test-workflow',
12
- name: 'Test Workflow',
13
- version: '1.0.0',
14
- nodes: [
15
- { id: 'trigger', type: 'trigger:manual', name: 'Start', position: { x: 100, y: 100 } },
16
- { id: 'http', type: 'http:request', name: 'HTTP Request', position: { x: 100, y: 200 } },
17
- { id: 'condition', type: 'condition:if', name: 'Check', position: { x: 100, y: 300 } },
18
- { id: 'output1', type: 'output:result', name: 'Success', position: { x: 50, y: 400 } },
19
- { id: 'output2', type: 'output:result', name: 'Error', position: { x: 150, y: 400 } },
20
- ],
21
- connections: [
22
- { id: 'c1', source: 'trigger', target: 'http' },
23
- { id: 'c2', source: 'http', target: 'condition' },
24
- { id: 'c3', source: 'condition', target: 'output1' },
25
- { id: 'c4', source: 'condition', target: 'output2' },
26
- ],
27
- };
28
-
29
- describe('layout', () => {
30
- it('should generate visual layout', () => {
31
- const visualizer = createVisualizer({ autoLayout: false });
32
- const layout = visualizer.layout(sampleWorkflow);
33
-
34
- expect(layout.nodes).toHaveLength(5);
35
- expect(layout.connections).toHaveLength(4);
36
- expect(layout.bounds).toBeDefined();
37
- });
38
-
39
- it('should position nodes correctly', () => {
40
- const visualizer = createVisualizer({ autoLayout: false });
41
- const layout = visualizer.layout(sampleWorkflow);
42
-
43
- const triggerNode = layout.nodes.find(n => n.id === 'trigger');
44
- expect(triggerNode?.x).toBe(100);
45
- expect(triggerNode?.y).toBe(100);
46
- });
47
- });
48
-
49
- describe('auto layout', () => {
50
- it('should auto-layout nodes in layers', () => {
51
- const visualizer = createVisualizer({ autoLayout: true });
52
- const positions = visualizer.autoLayout(sampleWorkflow.nodes, sampleWorkflow.connections);
53
-
54
- expect(Object.keys(positions)).toHaveLength(5);
55
-
56
- // Check that all nodes have positions
57
- sampleWorkflow.nodes.forEach(node => {
58
- expect(positions[node.id]).toBeDefined();
59
- expect(positions[node.id].x).toBeGreaterThanOrEqual(0);
60
- expect(positions[node.id].y).toBeGreaterThanOrEqual(0);
61
- });
62
- });
63
- });
64
-
65
- describe('export formats', () => {
66
- it('should export to SVG', () => {
67
- const visualizer = createVisualizer();
68
- const svg = visualizer.toSVG(sampleWorkflow);
69
-
70
- expect(svg).toContain('<svg');
71
- expect(svg).toContain('</svg>');
72
- expect(svg).toContain('Start');
73
- expect(svg).toContain('HTTP Request');
74
- });
75
-
76
- it('should export to Mermaid', () => {
77
- const visualizer = createVisualizer();
78
- const mermaid = visualizer.toMermaid(sampleWorkflow);
79
-
80
- expect(mermaid).toContain('graph TD');
81
- expect(mermaid).toContain('trigger');
82
- expect(mermaid).toContain('http');
83
- expect(mermaid).toContain('-->');
84
- });
85
-
86
- it('should export to DOT', () => {
87
- const visualizer = createVisualizer();
88
- const dot = visualizer.toDOT(sampleWorkflow);
89
-
90
- expect(dot).toContain('digraph Workflow');
91
- expect(dot).toContain('trigger');
92
- expect(dot).toContain('->');
93
- });
94
- });
95
-
96
- describe('node colors', () => {
97
- it('should assign colors by node type', () => {
98
- const visualizer = createVisualizer();
99
- const layout = visualizer.layout(sampleWorkflow);
100
-
101
- const triggerNode = layout.nodes.find(n => n.id === 'trigger');
102
- const httpNode = layout.nodes.find(n => n.id === 'http');
103
- const conditionNode = layout.nodes.find(n => n.id === 'condition');
104
-
105
- expect(triggerNode?.color).toBe('#3b82f6'); // Blue for triggers
106
- expect(httpNode?.color).toBe('#ec4899'); // Pink for HTTP
107
- expect(conditionNode?.color).toBe('#f59e0b'); // Orange for conditions
108
- });
109
- });
110
- });
@@ -1,239 +0,0 @@
1
- /**
2
- * Workflow Engine Tests
3
- */
4
-
5
- import { describe, it, expect, beforeEach } from 'vitest';
6
- import { createWorkflowEngine } from '../engine/workflow-engine';
7
- import type { Workflow } from '../types';
8
-
9
- describe('WorkflowEngine', () => {
10
- let engine: ReturnType<typeof createWorkflowEngine>;
11
-
12
- beforeEach(() => {
13
- engine = createWorkflowEngine();
14
- });
15
-
16
- describe('workflow management', () => {
17
- it('should register and retrieve workflow', () => {
18
- const workflow: Workflow = {
19
- id: 'test-workflow',
20
- name: 'Test Workflow',
21
- version: '1.0.0',
22
- nodes: [],
23
- connections: [],
24
- };
25
-
26
- engine.registerWorkflow(workflow);
27
- const retrieved = engine.getWorkflow('test-workflow');
28
-
29
- expect(retrieved).toEqual(workflow);
30
- });
31
-
32
- it('should list all workflows', () => {
33
- const workflow1: Workflow = {
34
- id: 'workflow-1',
35
- name: 'Workflow 1',
36
- version: '1.0.0',
37
- nodes: [],
38
- connections: [],
39
- };
40
-
41
- const workflow2: Workflow = {
42
- id: 'workflow-2',
43
- name: 'Workflow 2',
44
- version: '1.0.0',
45
- nodes: [],
46
- connections: [],
47
- };
48
-
49
- engine.registerWorkflow(workflow1);
50
- engine.registerWorkflow(workflow2);
51
-
52
- const workflows = engine.listWorkflows();
53
- expect(workflows).toHaveLength(2);
54
- });
55
-
56
- it('should delete workflow', () => {
57
- const workflow: Workflow = {
58
- id: 'test-workflow',
59
- name: 'Test Workflow',
60
- version: '1.0.0',
61
- nodes: [],
62
- connections: [],
63
- };
64
-
65
- engine.registerWorkflow(workflow);
66
- expect(engine.getWorkflow('test-workflow')).toBeDefined();
67
-
68
- engine.deleteWorkflow('test-workflow');
69
- expect(engine.getWorkflow('test-workflow')).toBeUndefined();
70
- });
71
- });
72
-
73
- describe('workflow execution', () => {
74
- it('should execute simple workflow', async () => {
75
- const workflow: Workflow = {
76
- id: 'simple-workflow',
77
- name: 'Simple Workflow',
78
- version: '1.0.0',
79
- nodes: [
80
- {
81
- id: 'trigger',
82
- type: 'trigger:manual',
83
- name: 'Start',
84
- },
85
- {
86
- id: 'output',
87
- type: 'output:result',
88
- name: 'End',
89
- },
90
- ],
91
- connections: [
92
- { id: 'c1', source: 'trigger', target: 'output' },
93
- ],
94
- };
95
-
96
- engine.registerWorkflow(workflow);
97
- const execution = await engine.execute('simple-workflow');
98
-
99
- expect(execution.status).toBe('completed');
100
- expect(execution.workflowId).toBe('simple-workflow');
101
- });
102
-
103
- it('should throw error for non-existent workflow', async () => {
104
- await expect(engine.execute('non-existent')).rejects.toThrow('Workflow not found');
105
- });
106
-
107
- it('should track execution state', async () => {
108
- const workflow: Workflow = {
109
- id: 'tracking-workflow',
110
- name: 'Tracking Workflow',
111
- version: '1.0.0',
112
- nodes: [
113
- { id: 'trigger', type: 'trigger:manual', name: 'Start' },
114
- { id: 'log', type: 'output:log', name: 'Log' },
115
- { id: 'output', type: 'output:result', name: 'End' },
116
- ],
117
- connections: [
118
- { id: 'c1', source: 'trigger', target: 'log' },
119
- { id: 'c2', source: 'log', target: 'output' },
120
- ],
121
- };
122
-
123
- engine.registerWorkflow(workflow);
124
- const execution = await engine.execute('tracking-workflow');
125
-
126
- expect(execution.context?.state.completedNodes).toContain('trigger');
127
- expect(execution.context?.state.completedNodes).toContain('log');
128
- expect(execution.context?.state.completedNodes).toContain('output');
129
- expect(execution.context?.state.executionPath).toHaveLength(3);
130
- });
131
- });
132
-
133
- describe('node types', () => {
134
- it('should get built-in node types', () => {
135
- const triggerType = engine.getNodeType('trigger:manual');
136
- expect(triggerType).toBeDefined();
137
- expect(triggerType?.displayName).toBe('Manual Trigger');
138
- });
139
-
140
- it('should register custom node type', () => {
141
- engine.registerNodeType({
142
- type: 'custom:action',
143
- category: 'custom',
144
- displayName: 'Custom Action',
145
- executor: async () => ({ result: 'custom' }),
146
- });
147
-
148
- const customType = engine.getNodeType('custom:action');
149
- expect(customType).toBeDefined();
150
- expect(customType?.displayName).toBe('Custom Action');
151
- });
152
- });
153
-
154
- describe('execution control', () => {
155
- it('should get execution by id', async () => {
156
- const workflow: Workflow = {
157
- id: 'test-workflow',
158
- name: 'Test Workflow',
159
- version: '1.0.0',
160
- nodes: [
161
- { id: 'trigger', type: 'trigger:manual', name: 'Start' },
162
- { id: 'output', type: 'output:result', name: 'End' },
163
- ],
164
- connections: [{ id: 'c1', source: 'trigger', target: 'output' }],
165
- };
166
-
167
- engine.registerWorkflow(workflow);
168
- const execution = await engine.execute('test-workflow');
169
-
170
- const retrieved = engine.getExecution(execution.id);
171
- expect(retrieved).toEqual(execution);
172
- });
173
- });
174
- });
175
-
176
- describe('WorkflowEngine with conditions', () => {
177
- it('should execute conditional workflow', async () => {
178
- const engine = createWorkflowEngine();
179
-
180
- const workflow: Workflow = {
181
- id: 'conditional-workflow',
182
- name: 'Conditional Workflow',
183
- version: '1.0.0',
184
- nodes: [
185
- { id: 'trigger', type: 'trigger:manual', name: 'Start' },
186
- {
187
- id: 'condition',
188
- type: 'condition:if',
189
- name: 'Check Value',
190
- config: { condition: 'data.value > 10' },
191
- },
192
- { id: 'high', type: 'output:result', name: 'High Value' },
193
- { id: 'low', type: 'output:result', name: 'Low Value' },
194
- ],
195
- connections: [
196
- { id: 'c1', source: 'trigger', target: 'condition' },
197
- { id: 'c2', source: 'condition', target: 'high', condition: 'result.true' },
198
- { id: 'c3', source: 'condition', target: 'low', condition: 'result.false' },
199
- ],
200
- };
201
-
202
- engine.registerWorkflow(workflow);
203
- const execution = await engine.execute('conditional-workflow', { value: 15 });
204
-
205
- expect(execution.status).toBe('completed');
206
- });
207
- });
208
-
209
- describe('WorkflowEngine with transforms', () => {
210
- it('should execute transform node', async () => {
211
- const engine = createWorkflowEngine();
212
-
213
- const workflow: Workflow = {
214
- id: 'transform-workflow',
215
- name: 'Transform Workflow',
216
- version: '1.0.0',
217
- nodes: [
218
- { id: 'trigger', type: 'trigger:manual', name: 'Start' },
219
- {
220
- id: 'transform',
221
- type: 'transform:map',
222
- name: 'Double Value',
223
- config: { expression: 'data.value * 2' },
224
- inputs: [{ target: 'data', source: '${value}' }],
225
- },
226
- { id: 'output', type: 'output:result', name: 'Result' },
227
- ],
228
- connections: [
229
- { id: 'c1', source: 'trigger', target: 'transform' },
230
- { id: 'c2', source: 'transform', target: 'output' },
231
- ],
232
- };
233
-
234
- engine.registerWorkflow(workflow);
235
- const execution = await engine.execute('transform-workflow', { value: 5 });
236
-
237
- expect(execution.status).toBe('completed');
238
- });
239
- });