@onlineapps/conn-orch-orchestrator 1.0.30 → 1.0.32
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 +4 -5
- package/src/WorkflowOrchestrator.js +60 -303
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-orchestrator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "Workflow orchestration connector for OA Drive - handles message routing and workflow execution",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,14 +22,13 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@onlineapps/conn-base-monitoring": "^1.0.0",
|
|
24
24
|
"@onlineapps/conn-infra-mq": "^1.1.0",
|
|
25
|
-
"@onlineapps/conn-orch-api-mapper": "^1.0.8",
|
|
26
|
-
"@onlineapps/conn-orch-cookbook": "^2.0.0",
|
|
27
25
|
"@onlineapps/conn-orch-registry": "^1.1.4",
|
|
28
|
-
"@onlineapps/cookbook
|
|
26
|
+
"@onlineapps/conn-orch-cookbook": "^2.0.0",
|
|
27
|
+
"@onlineapps/conn-orch-api-mapper": "^1.0.0"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
|
-
"eslint": "^8.30.0",
|
|
32
30
|
"jest": "^29.5.0",
|
|
31
|
+
"eslint": "^8.30.0",
|
|
33
32
|
"jsdoc-to-markdown": "^8.0.0"
|
|
34
33
|
},
|
|
35
34
|
"jest": {
|
|
@@ -51,6 +51,16 @@ class WorkflowOrchestrator {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Get step identifier (supports both V1 'id' and V2 'step_id')
|
|
56
|
+
* @private
|
|
57
|
+
* @param {Object} step - Step object
|
|
58
|
+
* @returns {string} Step identifier
|
|
59
|
+
*/
|
|
60
|
+
_getStepId(step) {
|
|
61
|
+
return step?.step_id || step?.id;
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
/**
|
|
55
65
|
* Process a workflow message
|
|
56
66
|
* @async
|
|
@@ -75,10 +85,7 @@ class WorkflowOrchestrator {
|
|
|
75
85
|
const { workflow_id, cookbook: cookbookDef, current_step, context } = message;
|
|
76
86
|
|
|
77
87
|
try {
|
|
78
|
-
//
|
|
79
|
-
this._validateV2Format(cookbookDef);
|
|
80
|
-
|
|
81
|
-
// Validate against JSON Schema (V2 schema - steps as object)
|
|
88
|
+
// Validate cookbook structure
|
|
82
89
|
this.cookbook.validateCookbook(cookbookDef);
|
|
83
90
|
|
|
84
91
|
// Ensure delivery configuration is preserved in context (required by Delivery Dispatcher)
|
|
@@ -89,14 +96,11 @@ class WorkflowOrchestrator {
|
|
|
89
96
|
delivery: context?.delivery || cookbookDef?.delivery || { handler: 'none' }
|
|
90
97
|
};
|
|
91
98
|
|
|
92
|
-
// Find current step (
|
|
93
|
-
const step = cookbookDef.steps
|
|
99
|
+
// Find current step (supports both V1 'id' and V2 'step_id')
|
|
100
|
+
const step = cookbookDef.steps.find(s => this._getStepId(s) === current_step);
|
|
94
101
|
if (!step) {
|
|
95
|
-
throw new Error(`Step not found: ${current_step}
|
|
102
|
+
throw new Error(`Step not found: ${current_step}`);
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
// Get step_id (V2)
|
|
99
|
-
const stepId = step.step_id;
|
|
100
104
|
|
|
101
105
|
// Check if this step is for this service
|
|
102
106
|
if (step.type === 'task' && step.service !== serviceName) {
|
|
@@ -118,7 +122,7 @@ class WorkflowOrchestrator {
|
|
|
118
122
|
result = await this.cache.get(cacheKey);
|
|
119
123
|
|
|
120
124
|
if (result) {
|
|
121
|
-
this.logger.info('Cache hit', { step:
|
|
125
|
+
this.logger.info('Cache hit', { step: this._getStepId(step), cacheKey });
|
|
122
126
|
} else {
|
|
123
127
|
// Execute the step
|
|
124
128
|
result = await this._executeStep(step, enrichedContext, cookbookDef);
|
|
@@ -129,55 +133,29 @@ class WorkflowOrchestrator {
|
|
|
129
133
|
result = await this._executeStep(step, enrichedContext, cookbookDef);
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
// Update context with result - steps as
|
|
133
|
-
// Each step preserves its definition (step_id, type, service, operation, input) and adds output
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
// Initialize steps object from cookbook if not present
|
|
137
|
-
// V2: steps is an object keyed by step_id
|
|
138
|
-
let existingSteps = enrichedContext.steps || {};
|
|
136
|
+
// Update context with result - steps as ARRAY (consistent with cookbook.steps)
|
|
137
|
+
// Each step preserves its definition (id/step_id, type, service, operation, input) and adds output
|
|
138
|
+
const currentIndex = cookbookDef.steps.findIndex(s => this._getStepId(s) === current_step);
|
|
139
|
+
const stepDefinition = cookbookDef.steps[currentIndex];
|
|
139
140
|
|
|
140
|
-
// Initialize from cookbook if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
}
|
|
141
|
+
// Initialize steps array from cookbook if not present
|
|
142
|
+
const existingSteps = Array.isArray(enrichedContext.steps)
|
|
143
|
+
? [...enrichedContext.steps]
|
|
144
|
+
: cookbookDef.steps.map(s => ({ ...s })); // Deep copy of step definitions
|
|
146
145
|
|
|
147
|
-
// Update the current step with output (preserve
|
|
148
|
-
existingSteps[
|
|
149
|
-
...stepDefinition, //
|
|
146
|
+
// Update the current step with output (preserve id, type, service, operation, input)
|
|
147
|
+
existingSteps[currentIndex] = {
|
|
148
|
+
...stepDefinition, // id, type, service, operation, input
|
|
150
149
|
output: result, // Add output from operation
|
|
151
150
|
status: 'completed',
|
|
152
151
|
completed_at: new Date().toISOString()
|
|
153
152
|
};
|
|
154
153
|
|
|
155
|
-
// === STRUCTURED LOG: Context update ===
|
|
156
|
-
console.log(`[WorkflowOrchestrator:CONTEXT_UPDATE] ${JSON.stringify({
|
|
157
|
-
layer: 'orchestrator',
|
|
158
|
-
workflow_id: workflow_id,
|
|
159
|
-
step_id: stepId,
|
|
160
|
-
stored_output: {
|
|
161
|
-
_descriptor: result?._descriptor,
|
|
162
|
-
type: result?.type,
|
|
163
|
-
storage_ref: result?.storage_ref,
|
|
164
|
-
outputKeys: result && typeof result === 'object' ? Object.keys(result) : null
|
|
165
|
-
},
|
|
166
|
-
timestamp: new Date().toISOString()
|
|
167
|
-
})}`);
|
|
168
|
-
|
|
169
|
-
|
|
170
154
|
const updatedContext = {
|
|
171
155
|
...enrichedContext,
|
|
172
|
-
steps: existingSteps
|
|
156
|
+
steps: existingSteps
|
|
173
157
|
};
|
|
174
158
|
|
|
175
|
-
// V2: Get step order from object keys (JavaScript preserves insertion order for string keys)
|
|
176
|
-
const stepKeys = Object.keys(cookbookDef.steps);
|
|
177
|
-
const currentIndex = stepKeys.indexOf(current_step);
|
|
178
|
-
const nextStepKey = stepKeys[currentIndex + 1];
|
|
179
|
-
const nextStep = nextStepKey ? cookbookDef.steps[nextStepKey] : null;
|
|
180
|
-
|
|
181
159
|
// Publish workflow.progress AFTER execution (so we have step output)
|
|
182
160
|
const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
|
|
183
161
|
try {
|
|
@@ -204,13 +182,15 @@ class WorkflowOrchestrator {
|
|
|
204
182
|
});
|
|
205
183
|
}
|
|
206
184
|
|
|
185
|
+
// Find next step (currentIndex already defined above)
|
|
186
|
+
const nextStep = cookbookDef.steps[currentIndex + 1];
|
|
187
|
+
|
|
207
188
|
if (nextStep) {
|
|
208
189
|
// Route to next step
|
|
209
|
-
|
|
210
|
-
await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id, nextStepId);
|
|
190
|
+
await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id);
|
|
211
191
|
} else {
|
|
212
192
|
// Workflow completed - pass serviceName, cookbook and last step info for monitoring
|
|
213
|
-
await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName,
|
|
193
|
+
await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName, current_step, currentIndex);
|
|
214
194
|
}
|
|
215
195
|
|
|
216
196
|
return {
|
|
@@ -295,78 +275,25 @@ class WorkflowOrchestrator {
|
|
|
295
275
|
* @returns {Promise<Object>} API call result
|
|
296
276
|
*/
|
|
297
277
|
async _executeTaskStep(step, context) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Debug: log context keys and api_input
|
|
306
|
-
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context keys:`, Object.keys(context || {}));
|
|
278
|
+
const stepId = this._getStepId(step);
|
|
279
|
+
// Resolve variable references in step.input (e.g. ${steps[0].output.message})
|
|
280
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${stepId} input BEFORE:`, JSON.stringify(step.input));
|
|
281
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Context keys:`, Object.keys(context || {}));
|
|
282
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Context.input_file:`, context?.input_file);
|
|
283
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Context.steps:`, JSON.stringify(context.steps?.map(s => ({ id: this._getStepId(s), output: s.output }))));
|
|
307
284
|
|
|
308
|
-
// FAIL-FAST: Log api_input state (but don't fail if missing - it might not be needed)
|
|
309
|
-
if (context.api_input === undefined) {
|
|
310
|
-
console.warn(`[WorkflowOrchestrator] [WARNING] Step ${step.step_id} - context.api_input is undefined - template references like {{api_input.field}} will fail`);
|
|
311
|
-
} else if (context.api_input === null) {
|
|
312
|
-
console.warn(`[WorkflowOrchestrator] [WARNING] Step ${step.step_id} - context.api_input is null - template references like {{api_input.field}} will fail`);
|
|
313
|
-
} else if (typeof context.api_input !== 'object' || Array.isArray(context.api_input)) {
|
|
314
|
-
const error = new Error(`Invalid api_input type for step ${step.step_id}: expected object, got ${typeof context.api_input}`);
|
|
315
|
-
console.error(`[WorkflowOrchestrator] [FAIL-FAST] Invalid api_input type:`, {
|
|
316
|
-
step_id: step.step_id,
|
|
317
|
-
api_input_type: typeof context.api_input,
|
|
318
|
-
api_input_value: context.api_input,
|
|
319
|
-
is_array: Array.isArray(context.api_input)
|
|
320
|
-
});
|
|
321
|
-
throw error;
|
|
322
|
-
} else {
|
|
323
|
-
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context.api_input:`, JSON.stringify(context.api_input));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context.steps keys:`,
|
|
327
|
-
context.steps ? Object.keys(context.steps) : 'NO STEPS');
|
|
328
|
-
|
|
329
|
-
// Resolve variable references in step.input (e.g. {{steps.step_id.output.field}})
|
|
330
|
-
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} input BEFORE:`, JSON.stringify(step.input));
|
|
331
285
|
const resolvedInput = this._resolveInputReferences(step.input, context);
|
|
332
|
-
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${
|
|
333
|
-
|
|
334
|
-
// FAIL-FAST: Check if template references were resolved
|
|
335
|
-
const inputStr = JSON.stringify(resolvedInput);
|
|
336
|
-
if (inputStr.includes('{{api_input.') || inputStr.includes('${api_input.')) {
|
|
337
|
-
console.error(`[WorkflowOrchestrator] [FAIL-FAST] Unresolved api_input template in step ${step.step_id}:`, {
|
|
338
|
-
step_id: step.step_id,
|
|
339
|
-
resolved_input: resolvedInput,
|
|
340
|
-
context_api_input: context.api_input,
|
|
341
|
-
context_keys: Object.keys(context)
|
|
342
|
-
});
|
|
343
|
-
throw new Error(`Template reference to api_input not resolved in step ${step.step_id} - check that api_input is in context`);
|
|
344
|
-
}
|
|
286
|
+
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${stepId} input AFTER:`, JSON.stringify(resolvedInput));
|
|
345
287
|
|
|
346
288
|
// Use API mapper to call the service with resolved input
|
|
347
289
|
const result = await this.apiMapper.callOperation(
|
|
348
|
-
step.operation,
|
|
290
|
+
step.operation || stepId,
|
|
349
291
|
resolvedInput,
|
|
350
292
|
context
|
|
351
293
|
);
|
|
352
294
|
|
|
353
|
-
// === STRUCTURED LOG: Step result ===
|
|
354
|
-
console.log(`[WorkflowOrchestrator:STEP_RESULT] ${JSON.stringify({
|
|
355
|
-
layer: 'orchestrator',
|
|
356
|
-
step_id: step.step_id,
|
|
357
|
-
service: step.service,
|
|
358
|
-
operation: step.operation,
|
|
359
|
-
result: {
|
|
360
|
-
_descriptor: result?._descriptor,
|
|
361
|
-
type: result?.type,
|
|
362
|
-
storage_ref: result?.storage_ref,
|
|
363
|
-
resultKeys: result && typeof result === 'object' ? Object.keys(result) : null
|
|
364
|
-
},
|
|
365
|
-
timestamp: new Date().toISOString()
|
|
366
|
-
})}`);
|
|
367
|
-
|
|
368
295
|
this.logger.info(`Task step executed`, {
|
|
369
|
-
step:
|
|
296
|
+
step: stepId,
|
|
370
297
|
service: step.service,
|
|
371
298
|
operation: step.operation,
|
|
372
299
|
inputResolved: !!resolvedInput
|
|
@@ -377,9 +304,7 @@ class WorkflowOrchestrator {
|
|
|
377
304
|
|
|
378
305
|
/**
|
|
379
306
|
* Resolve variable references in input object
|
|
380
|
-
* Supports
|
|
381
|
-
* - {{steps.step_id.output.field}}, {{api_input.field}} (preferred, V2)
|
|
382
|
-
* - ${steps[N].output.field}, ${api_input.field} (legacy)
|
|
307
|
+
* Supports ${steps[N].output.field}, ${api_input.field}, ${context.field}
|
|
383
308
|
* @private
|
|
384
309
|
* @param {Object} input - Input object with potential references
|
|
385
310
|
* @param {Object} context - Current context with steps, api_input, etc
|
|
@@ -389,39 +314,12 @@ class WorkflowOrchestrator {
|
|
|
389
314
|
if (!input) return input;
|
|
390
315
|
|
|
391
316
|
const resolveValue = (value) => {
|
|
392
|
-
if (typeof value === 'string') {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const resolved = this._getValueFromPath(path.trim(), context);
|
|
399
|
-
if (resolved !== undefined) {
|
|
400
|
-
// If resolved value is an object, stringify it for embedding in strings
|
|
401
|
-
if (typeof resolved === 'object' && result !== match) {
|
|
402
|
-
return JSON.stringify(resolved);
|
|
403
|
-
}
|
|
404
|
-
return resolved;
|
|
405
|
-
}
|
|
406
|
-
return match;
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Replace ${...} references (legacy syntax)
|
|
411
|
-
if (result.includes('${')) {
|
|
412
|
-
result = result.replace(/\$\{([^}]+)\}/g, (match, path) => {
|
|
413
|
-
const resolved = this._getValueFromPath(path.trim(), context);
|
|
414
|
-
if (resolved !== undefined) {
|
|
415
|
-
if (typeof resolved === 'object' && result !== match) {
|
|
416
|
-
return JSON.stringify(resolved);
|
|
417
|
-
}
|
|
418
|
-
return resolved;
|
|
419
|
-
}
|
|
420
|
-
return match;
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return result;
|
|
317
|
+
if (typeof value === 'string' && value.includes('${')) {
|
|
318
|
+
// Replace ${...} references
|
|
319
|
+
return value.replace(/\$\{([^}]+)\}/g, (match, path) => {
|
|
320
|
+
const resolved = this._getValueFromPath(path.trim(), context);
|
|
321
|
+
return resolved !== undefined ? resolved : match;
|
|
322
|
+
});
|
|
425
323
|
}
|
|
426
324
|
if (Array.isArray(value)) {
|
|
427
325
|
return value.map(resolveValue);
|
|
@@ -440,34 +338,16 @@ class WorkflowOrchestrator {
|
|
|
440
338
|
/**
|
|
441
339
|
* Get value from dot/bracket notation path
|
|
442
340
|
* @private
|
|
443
|
-
* @param {string} path - Path like "steps.
|
|
341
|
+
* @param {string} path - Path like "steps[0].output.message" or "api_input.name"
|
|
444
342
|
* @param {Object} context - Context object
|
|
445
343
|
* @returns {*} Resolved value or undefined
|
|
446
344
|
*/
|
|
447
345
|
_getValueFromPath(path, context) {
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
console.error('[WorkflowOrchestrator] [FAIL-FAST] _getValueFromPath called with null/undefined context', { path });
|
|
451
|
-
throw new Error(`Cannot resolve path '${path}': context is null or undefined`);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// V2: steps is an object keyed by step_id, not an array
|
|
455
|
-
// Support both formats for backward compatibility during migration:
|
|
456
|
-
// - steps.step_id.output.field (V2, preferred)
|
|
457
|
-
// - steps[0].output.field (legacy, deprecated)
|
|
458
|
-
|
|
459
|
-
// Normalize bracket notation to dot notation: steps[0] -> steps.0 (legacy)
|
|
460
|
-
// But prefer step_id access: steps.step_id -> steps.step_id (V2)
|
|
461
|
-
let normalized = path;
|
|
462
|
-
|
|
463
|
-
// Handle legacy array access steps[N] - convert to numeric key
|
|
464
|
-
// But only if steps is actually an array (fallback for old data)
|
|
465
|
-
if (normalized.includes('[') && normalized.includes(']')) {
|
|
466
|
-
normalized = normalized.replace(/\[(\d+)\]/g, '.$1');
|
|
467
|
-
}
|
|
346
|
+
// Normalize bracket notation to dot notation: steps[0] -> steps.0
|
|
347
|
+
let normalized = path.replace(/\[(\d+)\]/g, '.$1');
|
|
468
348
|
|
|
469
349
|
// Strip prefixes - we're already searching in context object
|
|
470
|
-
// Supports: ${context.X}, ${api_input.X}, ${steps.
|
|
350
|
+
// Supports: ${context.X}, ${api_input.X}, ${steps[N].X}
|
|
471
351
|
if (normalized.startsWith('context.')) {
|
|
472
352
|
normalized = normalized.substring(8); // Remove 'context.'
|
|
473
353
|
}
|
|
@@ -475,46 +355,9 @@ class WorkflowOrchestrator {
|
|
|
475
355
|
|
|
476
356
|
const parts = normalized.split('.');
|
|
477
357
|
|
|
478
|
-
// FAIL-FAST: Log api_input access attempts
|
|
479
|
-
if (normalized.startsWith('api_input.')) {
|
|
480
|
-
console.log(`[WorkflowOrchestrator] [GET_VALUE] Resolving api_input path: ${path} -> ${normalized}`);
|
|
481
|
-
console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input exists:`, context.api_input !== undefined);
|
|
482
|
-
console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input type:`, typeof context.api_input);
|
|
483
|
-
if (context.api_input) {
|
|
484
|
-
console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input keys:`, Object.keys(context.api_input));
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
358
|
let current = context;
|
|
489
359
|
for (const part of parts) {
|
|
490
|
-
if (current === undefined || current === null)
|
|
491
|
-
// FAIL-FAST: Log failed resolution for api_input
|
|
492
|
-
if (normalized.startsWith('api_input.')) {
|
|
493
|
-
console.error(`[WorkflowOrchestrator] [FAIL-FAST] Failed to resolve api_input path: ${path}`, {
|
|
494
|
-
path,
|
|
495
|
-
normalized,
|
|
496
|
-
parts,
|
|
497
|
-
failed_at_part: part,
|
|
498
|
-
current_type: typeof current,
|
|
499
|
-
context_keys: Object.keys(context),
|
|
500
|
-
context_has_api_input: 'api_input' in context,
|
|
501
|
-
api_input_type: typeof context.api_input,
|
|
502
|
-
api_input_keys: context.api_input ? Object.keys(context.api_input) : null
|
|
503
|
-
});
|
|
504
|
-
throw new Error(`Failed to resolve api_input path '${path}': '${part}' is undefined in context`);
|
|
505
|
-
}
|
|
506
|
-
return undefined;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Special handling for steps: if accessing by numeric index and steps is object,
|
|
510
|
-
// try to find step by index in cookbook (fallback for legacy)
|
|
511
|
-
if (part === 'steps' && parts.length > 1) {
|
|
512
|
-
current = current.steps;
|
|
513
|
-
// If steps is object (V2), next part is step_id
|
|
514
|
-
// If steps is array (legacy), next part is numeric index
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
|
|
360
|
+
if (current === undefined || current === null) return undefined;
|
|
518
361
|
current = current[part];
|
|
519
362
|
}
|
|
520
363
|
return current;
|
|
@@ -549,7 +392,7 @@ class WorkflowOrchestrator {
|
|
|
549
392
|
const result = await executor.executeStep(step);
|
|
550
393
|
|
|
551
394
|
this.logger.info(`Control flow step executed`, {
|
|
552
|
-
step: step
|
|
395
|
+
step: this._getStepId(step),
|
|
553
396
|
type: step.type
|
|
554
397
|
});
|
|
555
398
|
|
|
@@ -564,9 +407,9 @@ class WorkflowOrchestrator {
|
|
|
564
407
|
* @param {Object} cookbookDef - Full cookbook definition
|
|
565
408
|
* @param {Object} context - Updated context
|
|
566
409
|
* @param {string} workflow_id - Workflow ID
|
|
567
|
-
* @param {string} nextStepId - Next step step_id (V2) or id (V1 fallback)
|
|
568
410
|
*/
|
|
569
|
-
async _routeToNextStep(nextStep, cookbookDef, context, workflow_id
|
|
411
|
+
async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
|
|
412
|
+
const nextStepId = this._getStepId(nextStep);
|
|
570
413
|
const message = {
|
|
571
414
|
workflow_id,
|
|
572
415
|
cookbook: cookbookDef,
|
|
@@ -624,21 +467,6 @@ class WorkflowOrchestrator {
|
|
|
624
467
|
// Delivery Dispatcher requires: workflow_id, status, delivery (must be object, not null)
|
|
625
468
|
const delivery = finalContext?.delivery || cookbookDef?.delivery || { handler: 'none' };
|
|
626
469
|
|
|
627
|
-
// Extract api_output from the last completed step
|
|
628
|
-
// steps is an object keyed by step_id in V2
|
|
629
|
-
let api_output = null;
|
|
630
|
-
if (lastStepId && finalContext?.steps?.[lastStepId]?.output) {
|
|
631
|
-
api_output = finalContext.steps[lastStepId].output;
|
|
632
|
-
} else if (finalContext?.steps) {
|
|
633
|
-
// Fallback: find any step with output
|
|
634
|
-
const stepKeys = Object.keys(finalContext.steps);
|
|
635
|
-
for (const key of stepKeys) {
|
|
636
|
-
if (finalContext.steps[key]?.output) {
|
|
637
|
-
api_output = finalContext.steps[key].output;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
470
|
// Build message in format expected by Delivery Dispatcher
|
|
643
471
|
const workflowCompletedMessage = {
|
|
644
472
|
workflow_id,
|
|
@@ -646,11 +474,10 @@ class WorkflowOrchestrator {
|
|
|
646
474
|
service: serviceName, // Service that completed the workflow (for monitoring)
|
|
647
475
|
step_id: lastStepId, // Last step ID for monitoring
|
|
648
476
|
step_index: lastStepIndex, // Last step index for monitoring
|
|
649
|
-
api_output: api_output, // Output from last step - for frontend/delivery
|
|
650
477
|
cookbook: cookbookDef, // Full cookbook for monitoring trace
|
|
651
478
|
delivery: delivery, // Delivery configuration from Gateway context (must be object)
|
|
652
479
|
context: finalContext, // Full context for output resolution
|
|
653
|
-
steps: finalContext?.steps ||
|
|
480
|
+
steps: finalContext?.steps || [], // Steps results (array) for output resolution
|
|
654
481
|
completed_at: new Date().toISOString()
|
|
655
482
|
};
|
|
656
483
|
|
|
@@ -678,14 +505,15 @@ class WorkflowOrchestrator {
|
|
|
678
505
|
*/
|
|
679
506
|
_getCacheKey(step, context) {
|
|
680
507
|
const crypto = require('crypto');
|
|
508
|
+
const stepId = this._getStepId(step);
|
|
681
509
|
const data = JSON.stringify({
|
|
682
510
|
service: step.service,
|
|
683
|
-
operation: step.operation,
|
|
511
|
+
operation: step.operation || stepId,
|
|
684
512
|
input: step.input,
|
|
685
513
|
contextInput: context.api_input
|
|
686
514
|
});
|
|
687
515
|
const hash = crypto.createHash('sha256').update(data).digest('hex');
|
|
688
|
-
return `workflow:step:${step.service}:${step.operation}:${hash}`;
|
|
516
|
+
return `workflow:step:${step.service}:${step.operation || stepId}:${hash}`;
|
|
689
517
|
}
|
|
690
518
|
|
|
691
519
|
/**
|
|
@@ -723,77 +551,6 @@ class WorkflowOrchestrator {
|
|
|
723
551
|
const mapper = new ResponseMapper();
|
|
724
552
|
return mapper.mapResponse(response, mapping);
|
|
725
553
|
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* FAIL-FAST: Validate V2 format strictly
|
|
729
|
-
* Rejects V1 format immediately with clear error message
|
|
730
|
-
* @private
|
|
731
|
-
* @param {Object} cookbook - Cookbook definition
|
|
732
|
-
* @throws {Error} If cookbook is not V2 format
|
|
733
|
-
*/
|
|
734
|
-
_validateV2Format(cookbook) {
|
|
735
|
-
if (!cookbook) {
|
|
736
|
-
throw new Error('FAIL-FAST: Cookbook is required');
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// Check version format
|
|
740
|
-
const version = cookbook.version;
|
|
741
|
-
if (!version) {
|
|
742
|
-
throw new Error('FAIL-FAST: Cookbook version is required');
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// V1 format detection - REJECT immediately
|
|
746
|
-
if (/^1\.\d+\.\d+$/.test(version)) {
|
|
747
|
-
throw new Error(`FAIL-FAST: V1 format (${version}) is DEPRECATED and no longer supported. Use V2 format (2.x.x) with step_id instead of id.`);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Must be V2 format
|
|
751
|
-
if (!/^2\.\d+\.\d+$/.test(version)) {
|
|
752
|
-
throw new Error(`FAIL-FAST: Invalid version format '${version}'. Must be 2.x.x (e.g., 2.0.0)`);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Check steps - V2 FORMAT: steps MUST be an OBJECT (keyed by step_id), NOT an array!
|
|
756
|
-
if (!cookbook.steps || typeof cookbook.steps !== 'object') {
|
|
757
|
-
throw new Error('FAIL-FAST: Cookbook must have steps object');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// V1 format (array) is BANNED!
|
|
761
|
-
if (Array.isArray(cookbook.steps)) {
|
|
762
|
-
throw new Error('FAIL-FAST: V1 FORMAT BANNED! Cookbook steps must be an OBJECT (keyed by step_id), NOT an array!');
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const stepKeys = Object.keys(cookbook.steps);
|
|
766
|
-
if (stepKeys.length === 0) {
|
|
767
|
-
throw new Error('FAIL-FAST: Cookbook must have at least one step');
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Validate each step has step_id (V2) not id (V1)
|
|
771
|
-
const invalidSteps = [];
|
|
772
|
-
stepKeys.forEach((stepKey) => {
|
|
773
|
-
const step = cookbook.steps[stepKey];
|
|
774
|
-
if (step.id && !step.step_id) {
|
|
775
|
-
invalidSteps.push(`Step '${stepKey}': has 'id' (V1) but missing 'step_id' (V2)`);
|
|
776
|
-
}
|
|
777
|
-
if (!step.step_id) {
|
|
778
|
-
invalidSteps.push(`Step '${stepKey}': missing required 'step_id'`);
|
|
779
|
-
}
|
|
780
|
-
if (step.step_id !== stepKey) {
|
|
781
|
-
invalidSteps.push(`Step '${stepKey}': step_id '${step.step_id}' does not match key '${stepKey}'`);
|
|
782
|
-
}
|
|
783
|
-
if (!step.type) {
|
|
784
|
-
invalidSteps.push(`Step '${stepKey}': missing required 'type'`);
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
if (invalidSteps.length > 0) {
|
|
789
|
-
throw new Error(`FAIL-FAST: Invalid V2 cookbook format:\n${invalidSteps.join('\n')}`);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
this.logger.info('[WorkflowOrchestrator] V2 format validation passed', {
|
|
793
|
-
version: cookbook.version,
|
|
794
|
-
steps: Object.keys(cookbook.steps)
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
554
|
}
|
|
798
555
|
|
|
799
556
|
module.exports = WorkflowOrchestrator;
|