@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,651 +0,0 @@
1
- /**
2
- * Workflow Engine
3
- *
4
- * Core engine for executing workflows with DAG support.
5
- */
6
-
7
- import { v4 as uuidv4 } from 'uuid';
8
- import type {
9
- Workflow,
10
- WorkflowExecution,
11
- WorkflowNode,
12
- Connection,
13
- ExecutionContext,
14
- ExecutionState,
15
- NodeExecution,
16
- ExecutionStatus,
17
- ExecutionError,
18
- WorkflowEngineConfig,
19
- NodeType,
20
- } from '../types';
21
-
22
- export interface WorkflowEngine {
23
- /** Register a workflow */
24
- registerWorkflow(workflow: Workflow): void;
25
- /** Get workflow by ID */
26
- getWorkflow(id: string): Workflow | undefined;
27
- /** List all workflows */
28
- listWorkflows(): Workflow[];
29
- /** Delete workflow */
30
- deleteWorkflow(id: string): boolean;
31
- /** Execute workflow */
32
- execute(workflowId: string, inputs?: Record<string, unknown>): Promise<WorkflowExecution>;
33
- /** Get execution status */
34
- getExecution(executionId: string): WorkflowExecution | undefined;
35
- /** Cancel execution */
36
- cancelExecution(executionId: string): boolean;
37
- /** Pause execution */
38
- pauseExecution(executionId: string): boolean;
39
- /** Resume execution */
40
- resumeExecution(executionId: string): boolean;
41
- /** Register node type */
42
- registerNodeType(nodeType: NodeType): void;
43
- /** Get node type */
44
- getNodeType(type: string): NodeType | undefined;
45
- }
46
-
47
- /**
48
- * Create workflow engine
49
- */
50
- export function createWorkflowEngine(config: WorkflowEngineConfig = {}): WorkflowEngine {
51
- const workflows = new Map<string, Workflow>();
52
- const executions = new Map<string, WorkflowExecution>();
53
- const nodeTypes = new Map<string, NodeType>();
54
- const activeExecutions = new Set<string>();
55
-
56
- const {
57
- maxConcurrentExecutions = 10,
58
- defaultTimeout = 300000, // 5 minutes
59
- enableHistory = true,
60
- hooks = {},
61
- } = config;
62
-
63
- /**
64
- * Register built-in node types
65
- */
66
- function registerBuiltInNodeTypes(): void {
67
- // Input nodes
68
- registerNodeType({
69
- type: 'trigger:manual',
70
- category: 'input',
71
- displayName: 'Manual Trigger',
72
- description: 'Triggered manually or via API',
73
- outputs: [{ name: 'data', type: 'any' }],
74
- executor: async (_, __, inputs) => inputs,
75
- });
76
-
77
- registerNodeType({
78
- type: 'trigger:schedule',
79
- category: 'input',
80
- displayName: 'Schedule Trigger',
81
- description: 'Triggered on a schedule',
82
- outputs: [{ name: 'timestamp', type: 'string' }],
83
- executor: async (_, __, inputs) => inputs,
84
- });
85
-
86
- registerNodeType({
87
- type: 'trigger:webhook',
88
- category: 'input',
89
- displayName: 'Webhook Trigger',
90
- description: 'Triggered by HTTP webhook',
91
- outputs: [{ name: 'body', type: 'any' }, { name: 'headers', type: 'object' }],
92
- executor: async (_, __, inputs) => inputs,
93
- });
94
-
95
- // Transform nodes
96
- registerNodeType({
97
- type: 'transform:map',
98
- category: 'transform',
99
- displayName: 'Map',
100
- description: 'Transform data using expression',
101
- inputs: [{ name: 'data', type: 'any', required: true }],
102
- outputs: [{ name: 'result', type: 'any' }],
103
- configSchema: [
104
- { name: 'expression', type: 'string', required: true, description: 'JavaScript expression' },
105
- ],
106
- executor: async (node, context, inputs) => {
107
- const expression = node.config?.expression as string;
108
- if (!expression) throw new Error('Expression required');
109
-
110
- // Simple expression evaluation
111
- const data = inputs.data;
112
- const result = new Function('data', 'context', `return ${expression}`)(data, context);
113
- return { result };
114
- },
115
- });
116
-
117
- registerNodeType({
118
- type: 'transform:filter',
119
- category: 'transform',
120
- displayName: 'Filter',
121
- description: 'Filter array items',
122
- inputs: [{ name: 'array', type: 'array', required: true }],
123
- outputs: [{ name: 'filtered', type: 'array' }],
124
- configSchema: [
125
- { name: 'condition', type: 'string', required: true, description: 'Filter condition (item, index) => boolean' },
126
- ],
127
- executor: async (node, _, inputs) => {
128
- const array = inputs.array as unknown[];
129
- const condition = node.config?.condition as string;
130
-
131
- if (!condition) return { filtered: array };
132
-
133
- const filterFn = new Function('item', 'index', `return ${condition}`);
134
- const filtered = array.filter((item, index) => filterFn(item, index));
135
- return { filtered };
136
- },
137
- });
138
-
139
- // Condition nodes
140
- registerNodeType({
141
- type: 'condition:if',
142
- category: 'condition',
143
- displayName: 'If Condition',
144
- description: 'Branch based on condition',
145
- inputs: [{ name: 'data', type: 'any' }],
146
- outputs: [
147
- { name: 'true', type: 'any' },
148
- { name: 'false', type: 'any' },
149
- ],
150
- configSchema: [
151
- { name: 'condition', type: 'string', required: true, description: 'Boolean expression' },
152
- ],
153
- executor: async (node, context, inputs) => {
154
- const condition = node.config?.condition as string;
155
- if (!condition) throw new Error('Condition required');
156
-
157
- const result = new Function('data', 'context', `return ${condition}`)(inputs.data, context);
158
- return result ? { true: inputs.data } : { false: inputs.data };
159
- },
160
- });
161
-
162
- // Loop nodes
163
- registerNodeType({
164
- type: 'loop:for-each',
165
- category: 'loop',
166
- displayName: 'For Each',
167
- description: 'Iterate over array items',
168
- inputs: [{ name: 'array', type: 'array', required: true }],
169
- outputs: [{ name: 'results', type: 'array' }],
170
- configSchema: [
171
- { name: 'parallel', type: 'boolean', default: false, description: 'Execute in parallel' },
172
- { name: 'maxConcurrency', type: 'number', default: 5, description: 'Max parallel executions' },
173
- ],
174
- executor: async (node, context, inputs) => {
175
- // For-each logic handled by scheduler
176
- return { items: inputs.array };
177
- },
178
- });
179
-
180
- // Delay nodes
181
- registerNodeType({
182
- type: 'delay:wait',
183
- category: 'delay',
184
- displayName: 'Wait',
185
- description: 'Pause execution',
186
- inputs: [{ name: 'data', type: 'any' }],
187
- outputs: [{ name: 'data', type: 'any' }],
188
- configSchema: [
189
- { name: 'delay', type: 'number', required: true, description: 'Delay in milliseconds' },
190
- ],
191
- executor: async (node, _, inputs) => {
192
- const delay = node.config?.delay as number || 1000;
193
- await new Promise(resolve => setTimeout(resolve, delay));
194
- return { data: inputs.data };
195
- },
196
- });
197
-
198
- // HTTP nodes
199
- registerNodeType({
200
- type: 'http:request',
201
- category: 'http',
202
- displayName: 'HTTP Request',
203
- description: 'Make HTTP request',
204
- inputs: [
205
- { name: 'url', type: 'string', required: true },
206
- { name: 'body', type: 'any' },
207
- ],
208
- outputs: [
209
- { name: 'response', type: 'object' },
210
- { name: 'status', type: 'number' },
211
- ],
212
- configSchema: [
213
- { name: 'method', type: 'string', default: 'GET', description: 'HTTP method' },
214
- { name: 'headers', type: 'object', description: 'Request headers' },
215
- { name: 'timeout', type: 'number', default: 30000, description: 'Timeout in ms' },
216
- ],
217
- executor: async (node, _, inputs) => {
218
- const method = (node.config?.method as string) || 'GET';
219
- const url = inputs.url as string;
220
- const headers = node.config?.headers as Record<string, string> || {};
221
- const timeout = (node.config?.timeout as number) || 30000;
222
-
223
- const controller = new AbortController();
224
- const timeoutId = setTimeout(() => controller.abort(), timeout);
225
-
226
- try {
227
- const response = await fetch(url, {
228
- method,
229
- headers,
230
- body: inputs.body ? JSON.stringify(inputs.body) : undefined,
231
- signal: controller.signal,
232
- });
233
-
234
- clearTimeout(timeoutId);
235
-
236
- const data = await response.json().catch(() => null);
237
- const responseHeaders: Record<string, string> = {};
238
- response.headers.forEach((value, key) => {
239
- responseHeaders[key] = value;
240
- });
241
- return {
242
- response: data,
243
- status: response.status,
244
- headers: responseHeaders,
245
- };
246
- } catch (error) {
247
- clearTimeout(timeoutId);
248
- throw error;
249
- }
250
- },
251
- });
252
-
253
- // Output nodes
254
- registerNodeType({
255
- type: 'output:result',
256
- category: 'output',
257
- displayName: 'Set Result',
258
- description: 'Set workflow output',
259
- inputs: [{ name: 'data', type: 'any' }],
260
- executor: async (_, __, inputs) => inputs,
261
- });
262
-
263
- registerNodeType({
264
- type: 'output:log',
265
- category: 'output',
266
- displayName: 'Log',
267
- description: 'Log message to console',
268
- inputs: [{ name: 'message', type: 'any' }],
269
- configSchema: [
270
- { name: 'level', type: 'string', default: 'info', description: 'Log level' },
271
- ],
272
- executor: async (node, _, inputs) => {
273
- const level = (node.config?.level as string) || 'info';
274
- console[level as 'log' | 'info' | 'warn' | 'error']?.('Workflow log:', inputs.message);
275
- return { message: inputs.message };
276
- },
277
- });
278
- }
279
-
280
- /**
281
- * Register workflow
282
- */
283
- function registerWorkflow(workflow: Workflow): void {
284
- workflows.set(workflow.id, workflow);
285
- }
286
-
287
- /**
288
- * Get workflow
289
- */
290
- function getWorkflow(id: string): Workflow | undefined {
291
- return workflows.get(id);
292
- }
293
-
294
- /**
295
- * List workflows
296
- */
297
- function listWorkflows(): Workflow[] {
298
- return Array.from(workflows.values());
299
- }
300
-
301
- /**
302
- * Delete workflow
303
- */
304
- function deleteWorkflow(id: string): boolean {
305
- return workflows.delete(id);
306
- }
307
-
308
- /**
309
- * Execute workflow
310
- */
311
- async function execute(
312
- workflowId: string,
313
- inputs: Record<string, unknown> = {}
314
- ): Promise<WorkflowExecution> {
315
- const workflow = workflows.get(workflowId);
316
- if (!workflow) {
317
- throw new Error(`Workflow not found: ${workflowId}`);
318
- }
319
-
320
- // Check concurrent execution limit
321
- if (activeExecutions.size >= maxConcurrentExecutions) {
322
- throw new Error('Max concurrent executions reached');
323
- }
324
-
325
- const executionId = uuidv4();
326
-
327
- // Build DAG
328
- const dag = buildDAG(workflow);
329
-
330
- // Initialize execution
331
- const execution: WorkflowExecution = {
332
- id: executionId,
333
- workflowId,
334
- status: 'pending',
335
- inputs,
336
- context: {
337
- variables: { ...inputs },
338
- state: {
339
- activeNodes: [],
340
- completedNodes: [],
341
- failedNodes: [],
342
- nodeResults: {},
343
- executionPath: [],
344
- },
345
- },
346
- nodeExecutions: [],
347
- startedAt: new Date().toISOString(),
348
- };
349
-
350
- executions.set(executionId, execution);
351
- activeExecutions.add(executionId);
352
-
353
- // Trigger before execute hook
354
- await hooks.beforeExecute?.(execution);
355
-
356
- // Start execution
357
- execution.status = 'running';
358
-
359
- try {
360
- // Execute starting nodes (no incoming connections)
361
- const startNodes = dag.nodes.filter(n =>
362
- !dag.connections.some(c => c.target === n.id)
363
- );
364
-
365
- await Promise.all(startNodes.map(node => executeNode(node, execution, dag)));
366
-
367
- execution.status = 'completed';
368
- execution.completedAt = new Date().toISOString();
369
-
370
- // Collect outputs
371
- const outputNodes = workflow.nodes.filter(n => n.type === 'output:result');
372
- execution.outputs = {};
373
- for (const node of outputNodes) {
374
- const result = execution.context?.state.nodeResults[node.id];
375
- if (result) {
376
- Object.assign(execution.outputs, result);
377
- }
378
- }
379
-
380
- } catch (error) {
381
- execution.status = 'failed';
382
- execution.error = {
383
- code: 'EXECUTION_ERROR',
384
- message: error instanceof Error ? error.message : String(error),
385
- };
386
- await hooks.onError?.(execution.error, execution.context!);
387
- } finally {
388
- activeExecutions.delete(executionId);
389
- await hooks.afterExecute?.(execution);
390
- }
391
-
392
- return execution;
393
- }
394
-
395
- /**
396
- * Build DAG from workflow
397
- */
398
- function buildDAG(workflow: Workflow): { nodes: WorkflowNode[]; connections: Connection[] } {
399
- return {
400
- nodes: workflow.nodes,
401
- connections: workflow.connections,
402
- };
403
- }
404
-
405
- /**
406
- * Execute a node
407
- */
408
- async function executeNode(
409
- node: WorkflowNode,
410
- execution: WorkflowExecution,
411
- dag: { nodes: WorkflowNode[]; connections: Connection[] }
412
- ): Promise<void> {
413
- if (!execution.context) return;
414
-
415
- const nodeType = nodeTypes.get(node.type);
416
- if (!nodeType) {
417
- throw new Error(`Unknown node type: ${node.type}`);
418
- }
419
-
420
- // Check if already executed
421
- if (execution.context.state.completedNodes.includes(node.id)) {
422
- return;
423
- }
424
-
425
- // Check dependencies
426
- const incomingConnections = dag.connections.filter(c => c.target === node.id);
427
- const dependenciesMet = incomingConnections.every(c =>
428
- execution.context!.state.completedNodes.includes(c.source) ||
429
- execution.context!.state.nodeResults[c.source] !== undefined
430
- );
431
-
432
- if (!dependenciesMet) {
433
- return; // Will be executed when dependencies complete
434
- }
435
-
436
- // Mark as active
437
- execution.context.state.activeNodes.push(node.id);
438
- execution.context.state.executionPath.push(node.id);
439
-
440
- // Prepare inputs
441
- const inputs: Record<string, unknown> = {};
442
-
443
- // Get inputs from connections
444
- incomingConnections.forEach(conn => {
445
- const sourceResult = execution.context!.state.nodeResults[conn.source];
446
- if (sourceResult) {
447
- const portName = conn.sourcePort || 'data';
448
- inputs[portName] = sourceResult;
449
- }
450
- });
451
-
452
- // Apply input mappings
453
- node.inputs?.forEach(mapping => {
454
- const value = evaluateMapping(mapping.source, execution.context!);
455
- if (value !== undefined || mapping.default !== undefined) {
456
- inputs[mapping.target] = value ?? mapping.default;
457
- }
458
- });
459
-
460
- // Create node execution
461
- const nodeExecution: NodeExecution = {
462
- nodeId: node.id,
463
- status: 'running',
464
- inputs,
465
- startedAt: new Date().toISOString(),
466
- };
467
- execution.nodeExecutions = execution.nodeExecutions || [];
468
- execution.nodeExecutions.push(nodeExecution);
469
-
470
- // Trigger before node execute hook
471
- await hooks.beforeNodeExecute?.(node, execution.context);
472
-
473
- try {
474
- // Execute node
475
- const executor = nodeType.executor;
476
- if (!executor) {
477
- throw new Error(`No executor for node type: ${node.type}`);
478
- }
479
-
480
- const result = await executor(node, execution.context, inputs);
481
-
482
- // Store result
483
- execution.context.state.nodeResults[node.id] = result;
484
- execution.context.state.completedNodes.push(node.id);
485
-
486
- // Update node execution
487
- nodeExecution.status = 'completed';
488
- nodeExecution.outputs = result;
489
- nodeExecution.completedAt = new Date().toISOString();
490
-
491
- // Trigger after node execute hook
492
- await hooks.afterNodeExecute?.(node, execution.context, result);
493
-
494
- // Execute next nodes
495
- const outgoingConnections = dag.connections.filter(c => c.source === node.id);
496
-
497
- for (const conn of outgoingConnections) {
498
- // Check condition
499
- if (conn.condition) {
500
- const conditionMet = evaluateCondition(conn.condition, result, execution.context);
501
- if (!conditionMet) continue;
502
- }
503
-
504
- const nextNode = dag.nodes.find(n => n.id === conn.target);
505
- if (nextNode) {
506
- await executeNode(nextNode, execution, dag);
507
- }
508
- }
509
-
510
- } catch (error) {
511
- nodeExecution.status = 'failed';
512
- nodeExecution.error = {
513
- code: 'NODE_EXECUTION_ERROR',
514
- message: error instanceof Error ? error.message : String(error),
515
- nodeId: node.id,
516
- };
517
- execution.context.state.failedNodes.push(node.id);
518
- throw error;
519
- } finally {
520
- // Remove from active
521
- execution.context.state.activeNodes = execution.context.state.activeNodes.filter(
522
- id => id !== node.id
523
- );
524
- }
525
- }
526
-
527
- /**
528
- * Evaluate mapping expression
529
- */
530
- function evaluateMapping(expression: string, context: ExecutionContext): unknown {
531
- try {
532
- // Simple variable substitution
533
- if (expression.startsWith('${') && expression.endsWith('}')) {
534
- const path = expression.slice(2, -1);
535
- return getValueByPath(context.variables, path);
536
- }
537
- return expression;
538
- } catch {
539
- return undefined;
540
- }
541
- }
542
-
543
- /**
544
- * Evaluate condition
545
- */
546
- function evaluateCondition(
547
- condition: string,
548
- result: Record<string, unknown>,
549
- context: ExecutionContext
550
- ): boolean {
551
- try {
552
- return new Function('result', 'context', `return ${condition}`)(result, context);
553
- } catch {
554
- return false;
555
- }
556
- }
557
-
558
- /**
559
- * Get value by path
560
- */
561
- function getValueByPath(obj: Record<string, unknown>, path: string): unknown {
562
- return path.split('.').reduce((acc: unknown, key) => {
563
- if (acc && typeof acc === 'object') {
564
- return (acc as Record<string, unknown>)[key];
565
- }
566
- return undefined;
567
- }, obj);
568
- }
569
-
570
- /**
571
- * Get execution
572
- */
573
- function getExecution(executionId: string): WorkflowExecution | undefined {
574
- return executions.get(executionId);
575
- }
576
-
577
- /**
578
- * Cancel execution
579
- */
580
- function cancelExecution(executionId: string): boolean {
581
- const execution = executions.get(executionId);
582
- if (execution && execution.status === 'running') {
583
- execution.status = 'cancelled';
584
- execution.completedAt = new Date().toISOString();
585
- activeExecutions.delete(executionId);
586
- return true;
587
- }
588
- return false;
589
- }
590
-
591
- /**
592
- * Pause execution
593
- */
594
- function pauseExecution(executionId: string): boolean {
595
- const execution = executions.get(executionId);
596
- if (execution && execution.status === 'running') {
597
- execution.status = 'paused';
598
- return true;
599
- }
600
- return false;
601
- }
602
-
603
- /**
604
- * Resume execution
605
- */
606
- function resumeExecution(executionId: string): boolean {
607
- const execution = executions.get(executionId);
608
- if (execution && execution.status === 'paused') {
609
- execution.status = 'running';
610
- // TODO: Resume execution logic
611
- return true;
612
- }
613
- return false;
614
- }
615
-
616
- /**
617
- * Register node type
618
- */
619
- function registerNodeType(nodeType: NodeType): void {
620
- nodeTypes.set(nodeType.type, nodeType);
621
- }
622
-
623
- /**
624
- * Get node type
625
- */
626
- function getNodeType(type: string): NodeType | undefined {
627
- return nodeTypes.get(type);
628
- }
629
-
630
- // Initialize
631
- registerBuiltInNodeTypes();
632
-
633
- return {
634
- registerWorkflow,
635
- getWorkflow,
636
- listWorkflows,
637
- deleteWorkflow,
638
- execute,
639
- getExecution,
640
- cancelExecution,
641
- pauseExecution,
642
- resumeExecution,
643
- registerNodeType,
644
- getNodeType,
645
- };
646
- }
647
-
648
- /**
649
- * Global workflow engine instance
650
- */
651
- export const globalWorkflowEngine = createWorkflowEngine();