@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,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();
|