@onlineapps/conn-orch-orchestrator 1.0.18 → 1.0.20
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 +80 -30
package/package.json
CHANGED
|
@@ -75,6 +75,9 @@ class WorkflowOrchestrator {
|
|
|
75
75
|
const { workflow_id, cookbook: cookbookDef, current_step, context } = message;
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
+
// FAIL-FAST: Validate V2 format BEFORE anything else
|
|
79
|
+
this._validateV2Format(cookbookDef);
|
|
80
|
+
|
|
78
81
|
// Validate cookbook structure
|
|
79
82
|
this.cookbook.validateCookbook(cookbookDef);
|
|
80
83
|
|
|
@@ -86,14 +89,14 @@ class WorkflowOrchestrator {
|
|
|
86
89
|
delivery: context?.delivery || cookbookDef?.delivery || { handler: 'none' }
|
|
87
90
|
};
|
|
88
91
|
|
|
89
|
-
// Find current step (V2: step_id
|
|
90
|
-
const step = cookbookDef.steps.find(s =>
|
|
92
|
+
// Find current step (V2: step_id only)
|
|
93
|
+
const step = cookbookDef.steps.find(s => s.step_id === current_step);
|
|
91
94
|
if (!step) {
|
|
92
|
-
throw new Error(`Step not found: ${current_step}`);
|
|
95
|
+
throw new Error(`Step not found: ${current_step}. Available steps: ${cookbookDef.steps.map(s => s.step_id).join(', ')}`);
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
// Get step_id (V2)
|
|
96
|
-
const stepId = step.step_id
|
|
98
|
+
// Get step_id (V2)
|
|
99
|
+
const stepId = step.step_id;
|
|
97
100
|
|
|
98
101
|
// Check if this step is for this service
|
|
99
102
|
if (step.type === 'task' && step.service !== serviceName) {
|
|
@@ -128,28 +131,18 @@ class WorkflowOrchestrator {
|
|
|
128
131
|
|
|
129
132
|
// Update context with result - steps as OBJECT (V2 format, keyed by step_id)
|
|
130
133
|
// Each step preserves its definition (step_id, type, service, operation, input) and adds output
|
|
131
|
-
const currentIndex = cookbookDef.steps.findIndex(s =>
|
|
134
|
+
const currentIndex = cookbookDef.steps.findIndex(s => s.step_id === current_step);
|
|
132
135
|
const stepDefinition = cookbookDef.steps[currentIndex];
|
|
133
136
|
|
|
134
137
|
// Initialize steps object from cookbook if not present
|
|
135
138
|
// V2: steps is an object keyed by step_id, not an array
|
|
136
139
|
let existingSteps = enrichedContext.steps || {};
|
|
137
140
|
|
|
138
|
-
//
|
|
139
|
-
if (
|
|
140
|
-
existingSteps = {};
|
|
141
|
-
cookbookDef.steps.forEach(s => {
|
|
142
|
-
const sid = s.step_id || s.id;
|
|
143
|
-
if (sid) {
|
|
144
|
-
existingSteps[sid] = { ...s };
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
} else if (Object.keys(existingSteps).length === 0) {
|
|
148
|
-
// Initialize from cookbook
|
|
141
|
+
// Initialize from cookbook if empty
|
|
142
|
+
if (Object.keys(existingSteps).length === 0) {
|
|
149
143
|
cookbookDef.steps.forEach(s => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
existingSteps[sid] = { ...s };
|
|
144
|
+
if (s.step_id) {
|
|
145
|
+
existingSteps[s.step_id] = { ...s };
|
|
153
146
|
}
|
|
154
147
|
});
|
|
155
148
|
}
|
|
@@ -198,7 +191,7 @@ class WorkflowOrchestrator {
|
|
|
198
191
|
|
|
199
192
|
if (nextStep) {
|
|
200
193
|
// Route to next step
|
|
201
|
-
const nextStepId = nextStep.step_id
|
|
194
|
+
const nextStepId = nextStep.step_id;
|
|
202
195
|
await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id, nextStepId);
|
|
203
196
|
} else {
|
|
204
197
|
// Workflow completed - pass serviceName, cookbook and last step info for monitoring
|
|
@@ -293,13 +286,13 @@ class WorkflowOrchestrator {
|
|
|
293
286
|
|
|
294
287
|
// Use API mapper to call the service with resolved input
|
|
295
288
|
const result = await this.apiMapper.callOperation(
|
|
296
|
-
step.operation
|
|
289
|
+
step.operation,
|
|
297
290
|
resolvedInput,
|
|
298
291
|
context
|
|
299
292
|
);
|
|
300
293
|
|
|
301
294
|
this.logger.info(`Task step executed`, {
|
|
302
|
-
step: step.step_id
|
|
295
|
+
step: step.step_id,
|
|
303
296
|
service: step.service,
|
|
304
297
|
operation: step.operation,
|
|
305
298
|
inputResolved: !!resolvedInput
|
|
@@ -420,7 +413,7 @@ class WorkflowOrchestrator {
|
|
|
420
413
|
const result = await executor.executeStep(step);
|
|
421
414
|
|
|
422
415
|
this.logger.info(`Control flow step executed`, {
|
|
423
|
-
step: step.step_id
|
|
416
|
+
step: step.step_id,
|
|
424
417
|
type: step.type
|
|
425
418
|
});
|
|
426
419
|
|
|
@@ -441,7 +434,7 @@ class WorkflowOrchestrator {
|
|
|
441
434
|
const message = {
|
|
442
435
|
workflow_id,
|
|
443
436
|
cookbook: cookbookDef,
|
|
444
|
-
current_step: nextStepId
|
|
437
|
+
current_step: nextStepId,
|
|
445
438
|
context
|
|
446
439
|
};
|
|
447
440
|
|
|
@@ -450,21 +443,21 @@ class WorkflowOrchestrator {
|
|
|
450
443
|
if (this.router?.routeToService) {
|
|
451
444
|
await this.router.routeToService(nextStep.service, message);
|
|
452
445
|
this.logger.info(`Routed to service: ${nextStep.service}`, {
|
|
453
|
-
step: nextStepId
|
|
446
|
+
step: nextStepId
|
|
454
447
|
});
|
|
455
448
|
} else {
|
|
456
449
|
// Fallback: publish to service's workflow queue directly
|
|
457
450
|
const serviceQueue = `${nextStep.service}.workflow`;
|
|
458
451
|
await this.mqClient.publish(serviceQueue, message);
|
|
459
452
|
this.logger.info(`Published to queue: ${serviceQueue}`, {
|
|
460
|
-
step: nextStepId
|
|
453
|
+
step: nextStepId
|
|
461
454
|
});
|
|
462
455
|
}
|
|
463
456
|
} else {
|
|
464
457
|
// Control flow steps handled by workflow init queue
|
|
465
458
|
await this.mqClient.publish('workflow.init', message);
|
|
466
459
|
this.logger.info(`Routed control flow to workflow.init`, {
|
|
467
|
-
step: nextStepId
|
|
460
|
+
step: nextStepId
|
|
468
461
|
});
|
|
469
462
|
}
|
|
470
463
|
}
|
|
@@ -535,12 +528,12 @@ class WorkflowOrchestrator {
|
|
|
535
528
|
const crypto = require('crypto');
|
|
536
529
|
const data = JSON.stringify({
|
|
537
530
|
service: step.service,
|
|
538
|
-
operation: step.operation
|
|
531
|
+
operation: step.operation,
|
|
539
532
|
input: step.input,
|
|
540
533
|
contextInput: context.api_input
|
|
541
534
|
});
|
|
542
535
|
const hash = crypto.createHash('sha256').update(data).digest('hex');
|
|
543
|
-
return `workflow:step:${step.service}:${step.operation
|
|
536
|
+
return `workflow:step:${step.service}:${step.operation}:${hash}`;
|
|
544
537
|
}
|
|
545
538
|
|
|
546
539
|
/**
|
|
@@ -578,6 +571,63 @@ class WorkflowOrchestrator {
|
|
|
578
571
|
const mapper = new ResponseMapper();
|
|
579
572
|
return mapper.mapResponse(response, mapping);
|
|
580
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* FAIL-FAST: Validate V2 format strictly
|
|
577
|
+
* Rejects V1 format immediately with clear error message
|
|
578
|
+
* @private
|
|
579
|
+
* @param {Object} cookbook - Cookbook definition
|
|
580
|
+
* @throws {Error} If cookbook is not V2 format
|
|
581
|
+
*/
|
|
582
|
+
_validateV2Format(cookbook) {
|
|
583
|
+
if (!cookbook) {
|
|
584
|
+
throw new Error('FAIL-FAST: Cookbook is required');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check version format
|
|
588
|
+
const version = cookbook.version;
|
|
589
|
+
if (!version) {
|
|
590
|
+
throw new Error('FAIL-FAST: Cookbook version is required');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// V1 format detection - REJECT immediately
|
|
594
|
+
if (/^1\.\d+\.\d+$/.test(version)) {
|
|
595
|
+
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.`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Must be V2 format
|
|
599
|
+
if (!/^2\.\d+\.\d+$/.test(version)) {
|
|
600
|
+
throw new Error(`FAIL-FAST: Invalid version format '${version}'. Must be 2.x.x (e.g., 2.0.0)`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check steps
|
|
604
|
+
if (!cookbook.steps || !Array.isArray(cookbook.steps) || cookbook.steps.length === 0) {
|
|
605
|
+
throw new Error('FAIL-FAST: Cookbook must have at least one step');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Validate each step has step_id (V2) not id (V1)
|
|
609
|
+
const invalidSteps = [];
|
|
610
|
+
cookbook.steps.forEach((step, index) => {
|
|
611
|
+
if (step.id && !step.step_id) {
|
|
612
|
+
invalidSteps.push(`Step ${index}: has 'id' (V1) but missing 'step_id' (V2)`);
|
|
613
|
+
}
|
|
614
|
+
if (!step.step_id) {
|
|
615
|
+
invalidSteps.push(`Step ${index}: missing required 'step_id'`);
|
|
616
|
+
}
|
|
617
|
+
if (!step.type) {
|
|
618
|
+
invalidSteps.push(`Step ${index} (${step.step_id || 'unknown'}): missing required 'type'`);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (invalidSteps.length > 0) {
|
|
623
|
+
throw new Error(`FAIL-FAST: Invalid V2 cookbook format:\n${invalidSteps.join('\n')}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
this.logger.info('[WorkflowOrchestrator] V2 format validation passed', {
|
|
627
|
+
version: cookbook.version,
|
|
628
|
+
steps: cookbook.steps.map(s => s.step_id)
|
|
629
|
+
});
|
|
630
|
+
}
|
|
581
631
|
}
|
|
582
632
|
|
|
583
633
|
module.exports = WorkflowOrchestrator;
|