@onlineapps/conn-orch-orchestrator 1.0.17 → 1.0.19
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 +1 -1
- package/src/WorkflowOrchestrator.js +65 -30
package/package.json
CHANGED
|
@@ -86,11 +86,14 @@ class WorkflowOrchestrator {
|
|
|
86
86
|
delivery: context?.delivery || cookbookDef?.delivery || { handler: 'none' }
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
// Find current step
|
|
90
|
-
const step = cookbookDef.steps.find(s => s.
|
|
89
|
+
// Find current step (V2: step_id only)
|
|
90
|
+
const step = cookbookDef.steps.find(s => s.step_id === current_step);
|
|
91
91
|
if (!step) {
|
|
92
92
|
throw new Error(`Step not found: ${current_step}`);
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
// Get step_id (V2)
|
|
96
|
+
const stepId = step.step_id;
|
|
94
97
|
|
|
95
98
|
// Check if this step is for this service
|
|
96
99
|
if (step.type === 'task' && step.service !== serviceName) {
|
|
@@ -112,7 +115,7 @@ class WorkflowOrchestrator {
|
|
|
112
115
|
result = await this.cache.get(cacheKey);
|
|
113
116
|
|
|
114
117
|
if (result) {
|
|
115
|
-
this.logger.info('Cache hit', { step:
|
|
118
|
+
this.logger.info('Cache hit', { step: stepId, cacheKey });
|
|
116
119
|
} else {
|
|
117
120
|
// Execute the step
|
|
118
121
|
result = await this._executeStep(step, enrichedContext, cookbookDef);
|
|
@@ -123,19 +126,27 @@ class WorkflowOrchestrator {
|
|
|
123
126
|
result = await this._executeStep(step, enrichedContext, cookbookDef);
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
// Update context with result - steps as
|
|
127
|
-
// Each step preserves its definition (
|
|
128
|
-
const currentIndex = cookbookDef.steps.findIndex(s => s.
|
|
129
|
+
// Update context with result - steps as OBJECT (V2 format, keyed by step_id)
|
|
130
|
+
// Each step preserves its definition (step_id, type, service, operation, input) and adds output
|
|
131
|
+
const currentIndex = cookbookDef.steps.findIndex(s => s.step_id === current_step);
|
|
129
132
|
const stepDefinition = cookbookDef.steps[currentIndex];
|
|
130
133
|
|
|
131
|
-
// Initialize steps
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Initialize steps object from cookbook if not present
|
|
135
|
+
// V2: steps is an object keyed by step_id, not an array
|
|
136
|
+
let existingSteps = enrichedContext.steps || {};
|
|
137
|
+
|
|
138
|
+
// Initialize from cookbook if empty
|
|
139
|
+
if (Object.keys(existingSteps).length === 0) {
|
|
140
|
+
cookbookDef.steps.forEach(s => {
|
|
141
|
+
if (s.step_id) {
|
|
142
|
+
existingSteps[s.step_id] = { ...s };
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
135
146
|
|
|
136
|
-
// Update the current step with output (preserve
|
|
137
|
-
existingSteps[
|
|
138
|
-
...stepDefinition, //
|
|
147
|
+
// Update the current step with output (preserve step_id, type, service, operation, input)
|
|
148
|
+
existingSteps[stepId] = {
|
|
149
|
+
...stepDefinition, // step_id, type, service, operation, input
|
|
139
150
|
output: result, // Add output from operation
|
|
140
151
|
status: 'completed',
|
|
141
152
|
completed_at: new Date().toISOString()
|
|
@@ -143,7 +154,7 @@ class WorkflowOrchestrator {
|
|
|
143
154
|
|
|
144
155
|
const updatedContext = {
|
|
145
156
|
...enrichedContext,
|
|
146
|
-
steps: existingSteps
|
|
157
|
+
steps: existingSteps // Object keyed by step_id
|
|
147
158
|
};
|
|
148
159
|
|
|
149
160
|
// Publish workflow.progress AFTER execution (so we have step output)
|
|
@@ -177,10 +188,11 @@ class WorkflowOrchestrator {
|
|
|
177
188
|
|
|
178
189
|
if (nextStep) {
|
|
179
190
|
// Route to next step
|
|
180
|
-
|
|
191
|
+
const nextStepId = nextStep.step_id;
|
|
192
|
+
await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id, nextStepId);
|
|
181
193
|
} else {
|
|
182
194
|
// Workflow completed - pass serviceName, cookbook and last step info for monitoring
|
|
183
|
-
await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName,
|
|
195
|
+
await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName, stepId, currentIndex);
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
return {
|
|
@@ -271,13 +283,13 @@ class WorkflowOrchestrator {
|
|
|
271
283
|
|
|
272
284
|
// Use API mapper to call the service with resolved input
|
|
273
285
|
const result = await this.apiMapper.callOperation(
|
|
274
|
-
step.operation
|
|
286
|
+
step.operation,
|
|
275
287
|
resolvedInput,
|
|
276
288
|
context
|
|
277
289
|
);
|
|
278
290
|
|
|
279
291
|
this.logger.info(`Task step executed`, {
|
|
280
|
-
step: step.
|
|
292
|
+
step: step.step_id,
|
|
281
293
|
service: step.service,
|
|
282
294
|
operation: step.operation,
|
|
283
295
|
inputResolved: !!resolvedInput
|
|
@@ -322,16 +334,28 @@ class WorkflowOrchestrator {
|
|
|
322
334
|
/**
|
|
323
335
|
* Get value from dot/bracket notation path
|
|
324
336
|
* @private
|
|
325
|
-
* @param {string} path - Path like "steps
|
|
337
|
+
* @param {string} path - Path like "steps.step_id.output.message" or "api_input.name"
|
|
326
338
|
* @param {Object} context - Context object
|
|
327
339
|
* @returns {*} Resolved value or undefined
|
|
328
340
|
*/
|
|
329
341
|
_getValueFromPath(path, context) {
|
|
330
|
-
//
|
|
331
|
-
|
|
342
|
+
// V2: steps is an object keyed by step_id, not an array
|
|
343
|
+
// Support both formats for backward compatibility during migration:
|
|
344
|
+
// - steps.step_id.output.field (V2, preferred)
|
|
345
|
+
// - steps[0].output.field (legacy, deprecated)
|
|
346
|
+
|
|
347
|
+
// Normalize bracket notation to dot notation: steps[0] -> steps.0 (legacy)
|
|
348
|
+
// But prefer step_id access: steps.step_id -> steps.step_id (V2)
|
|
349
|
+
let normalized = path;
|
|
350
|
+
|
|
351
|
+
// Handle legacy array access steps[N] - convert to numeric key
|
|
352
|
+
// But only if steps is actually an array (fallback for old data)
|
|
353
|
+
if (normalized.includes('[') && normalized.includes(']')) {
|
|
354
|
+
normalized = normalized.replace(/\[(\d+)\]/g, '.$1');
|
|
355
|
+
}
|
|
332
356
|
|
|
333
357
|
// Strip prefixes - we're already searching in context object
|
|
334
|
-
// Supports: ${context.X}, ${api_input.X}, ${steps
|
|
358
|
+
// Supports: ${context.X}, ${api_input.X}, ${steps.step_id.X} (V2)
|
|
335
359
|
if (normalized.startsWith('context.')) {
|
|
336
360
|
normalized = normalized.substring(8); // Remove 'context.'
|
|
337
361
|
}
|
|
@@ -342,6 +366,16 @@ class WorkflowOrchestrator {
|
|
|
342
366
|
let current = context;
|
|
343
367
|
for (const part of parts) {
|
|
344
368
|
if (current === undefined || current === null) return undefined;
|
|
369
|
+
|
|
370
|
+
// Special handling for steps: if accessing by numeric index and steps is object,
|
|
371
|
+
// try to find step by index in cookbook (fallback for legacy)
|
|
372
|
+
if (part === 'steps' && parts.length > 1) {
|
|
373
|
+
current = current.steps;
|
|
374
|
+
// If steps is object (V2), next part is step_id
|
|
375
|
+
// If steps is array (legacy), next part is numeric index
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
345
379
|
current = current[part];
|
|
346
380
|
}
|
|
347
381
|
return current;
|
|
@@ -376,7 +410,7 @@ class WorkflowOrchestrator {
|
|
|
376
410
|
const result = await executor.executeStep(step);
|
|
377
411
|
|
|
378
412
|
this.logger.info(`Control flow step executed`, {
|
|
379
|
-
step: step.
|
|
413
|
+
step: step.step_id,
|
|
380
414
|
type: step.type
|
|
381
415
|
});
|
|
382
416
|
|
|
@@ -391,12 +425,13 @@ class WorkflowOrchestrator {
|
|
|
391
425
|
* @param {Object} cookbookDef - Full cookbook definition
|
|
392
426
|
* @param {Object} context - Updated context
|
|
393
427
|
* @param {string} workflow_id - Workflow ID
|
|
428
|
+
* @param {string} nextStepId - Next step step_id (V2) or id (V1 fallback)
|
|
394
429
|
*/
|
|
395
|
-
async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
|
|
430
|
+
async _routeToNextStep(nextStep, cookbookDef, context, workflow_id, nextStepId) {
|
|
396
431
|
const message = {
|
|
397
432
|
workflow_id,
|
|
398
433
|
cookbook: cookbookDef,
|
|
399
|
-
current_step:
|
|
434
|
+
current_step: nextStepId,
|
|
400
435
|
context
|
|
401
436
|
};
|
|
402
437
|
|
|
@@ -405,21 +440,21 @@ class WorkflowOrchestrator {
|
|
|
405
440
|
if (this.router?.routeToService) {
|
|
406
441
|
await this.router.routeToService(nextStep.service, message);
|
|
407
442
|
this.logger.info(`Routed to service: ${nextStep.service}`, {
|
|
408
|
-
step:
|
|
443
|
+
step: nextStepId
|
|
409
444
|
});
|
|
410
445
|
} else {
|
|
411
446
|
// Fallback: publish to service's workflow queue directly
|
|
412
447
|
const serviceQueue = `${nextStep.service}.workflow`;
|
|
413
448
|
await this.mqClient.publish(serviceQueue, message);
|
|
414
449
|
this.logger.info(`Published to queue: ${serviceQueue}`, {
|
|
415
|
-
step:
|
|
450
|
+
step: nextStepId
|
|
416
451
|
});
|
|
417
452
|
}
|
|
418
453
|
} else {
|
|
419
454
|
// Control flow steps handled by workflow init queue
|
|
420
455
|
await this.mqClient.publish('workflow.init', message);
|
|
421
456
|
this.logger.info(`Routed control flow to workflow.init`, {
|
|
422
|
-
step:
|
|
457
|
+
step: nextStepId
|
|
423
458
|
});
|
|
424
459
|
}
|
|
425
460
|
}
|
|
@@ -490,12 +525,12 @@ class WorkflowOrchestrator {
|
|
|
490
525
|
const crypto = require('crypto');
|
|
491
526
|
const data = JSON.stringify({
|
|
492
527
|
service: step.service,
|
|
493
|
-
operation: step.operation
|
|
528
|
+
operation: step.operation,
|
|
494
529
|
input: step.input,
|
|
495
530
|
contextInput: context.api_input
|
|
496
531
|
});
|
|
497
532
|
const hash = crypto.createHash('sha256').update(data).digest('hex');
|
|
498
|
-
return `workflow:step:${step.service}:${step.operation
|
|
533
|
+
return `workflow:step:${step.service}:${step.operation}:${hash}`;
|
|
499
534
|
}
|
|
500
535
|
|
|
501
536
|
/**
|