@onlineapps/conn-orch-orchestrator 1.0.115 → 2.0.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/package.json +2 -3
- package/src/WorkflowOrchestrator.js +135 -26
- package/src/index.js +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-orchestrator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Workflow orchestration connector for OA Drive - handles message routing and workflow execution",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"@onlineapps/conn-base-monitoring": "1.0.12",
|
|
25
25
|
"@onlineapps/conn-infra-mq": "1.1.70",
|
|
26
26
|
"@onlineapps/conn-orch-registry": "1.2.1",
|
|
27
|
-
"@onlineapps/conn-orch-cookbook": "2.1.2"
|
|
28
|
-
"@onlineapps/conn-orch-api-mapper": "1.0.34"
|
|
27
|
+
"@onlineapps/conn-orch-cookbook": "2.1.2"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
30
|
"jest": "^29.5.0",
|
|
@@ -15,7 +15,10 @@ class WorkflowOrchestrator {
|
|
|
15
15
|
* @param {Object} config - Configuration object
|
|
16
16
|
* @param {Object} config.mqClient - MQ client for message operations
|
|
17
17
|
* @param {Object} config.registryClient - Registry client for service discovery
|
|
18
|
-
* @param {Object} config.
|
|
18
|
+
* @param {Object} config.invoker - Intra-service operation invoker exposing
|
|
19
|
+
* async invokeOperation({ operation, input, envelope, workflow_id,
|
|
20
|
+
* correlation_id, step_id }) -> { status, result } | { status, error }.
|
|
21
|
+
* Replaces the retired apiMapper HTTP-loopback path (RFC §5.9, §5.10).
|
|
19
22
|
* @param {Object} config.cookbook - Cookbook connector for validation and execution
|
|
20
23
|
* @param {Object} [config.cache] - Cache connector for caching
|
|
21
24
|
* @param {Object} [config.errorHandler] - Error handler connector
|
|
@@ -25,12 +28,16 @@ class WorkflowOrchestrator {
|
|
|
25
28
|
constructor(config) {
|
|
26
29
|
if (!config.mqClient) throw new Error('mqClient is required');
|
|
27
30
|
if (!config.registryClient) throw new Error('registryClient is required');
|
|
28
|
-
if (!config.
|
|
31
|
+
if (!config.invoker || typeof config.invoker.invokeOperation !== 'function') {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'[WorkflowOrchestrator] invoker with invokeOperation(message) is required - Expected object exposing async invokeOperation({operation, input, envelope, workflow_id, correlation_id, step_id}) returning {status, result}'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
29
36
|
if (!config.cookbook) throw new Error('cookbook is required');
|
|
30
37
|
|
|
31
38
|
this.mqClient = config.mqClient;
|
|
32
39
|
this.registryClient = config.registryClient;
|
|
33
|
-
this.
|
|
40
|
+
this._invoker = config.invoker;
|
|
34
41
|
this.cookbook = config.cookbook;
|
|
35
42
|
this.cache = config.cache;
|
|
36
43
|
this.errorHandler = config.errorHandler;
|
|
@@ -66,6 +73,35 @@ class WorkflowOrchestrator {
|
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Extract correlation_id from an inbound MQ message envelope.
|
|
78
|
+
* Accepts the canonical body-level `correlation_id` and also a few known
|
|
79
|
+
* legacy placements from transports that move AMQP headers onto the
|
|
80
|
+
* parsed message object. Returns undefined if none are set - the caller
|
|
81
|
+
* is responsible for fail-fast.
|
|
82
|
+
* @private
|
|
83
|
+
* @param {Object} message - Inbound workflow message
|
|
84
|
+
* @returns {string|undefined}
|
|
85
|
+
*/
|
|
86
|
+
_extractCorrelationId(message) {
|
|
87
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
88
|
+
if (typeof message.correlation_id === 'string' && message.correlation_id.length > 0) {
|
|
89
|
+
return message.correlation_id;
|
|
90
|
+
}
|
|
91
|
+
if (message.envelope && typeof message.envelope.correlation_id === 'string' && message.envelope.correlation_id.length > 0) {
|
|
92
|
+
return message.envelope.correlation_id;
|
|
93
|
+
}
|
|
94
|
+
if (message.headers) {
|
|
95
|
+
if (typeof message.headers.correlation_id === 'string' && message.headers.correlation_id.length > 0) {
|
|
96
|
+
return message.headers.correlation_id;
|
|
97
|
+
}
|
|
98
|
+
if (typeof message.headers.correlationId === 'string' && message.headers.correlationId.length > 0) {
|
|
99
|
+
return message.headers.correlationId;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
69
105
|
/**
|
|
70
106
|
* Get step identifier (V2.1 only: step_id)
|
|
71
107
|
* @private
|
|
@@ -138,6 +174,21 @@ class WorkflowOrchestrator {
|
|
|
138
174
|
const { workflow_id, cookbook: cookbookDef, current_step } = message;
|
|
139
175
|
const startTime = Date.now();
|
|
140
176
|
|
|
177
|
+
// Fail-fast: correlation_id MUST be present on every workflow message.
|
|
178
|
+
// A workflow without a correlation_id is malformed per RFC §5.8
|
|
179
|
+
// (ContextBuilder._validateMqMessage rejects empty strings too).
|
|
180
|
+
// The gateway must publish it; if it does not, that is a gateway bug
|
|
181
|
+
// we want to surface loudly rather than silently produce 500s inside
|
|
182
|
+
// invokeOperation's ContextBuilder.
|
|
183
|
+
const correlationId = this._extractCorrelationId(message);
|
|
184
|
+
if (typeof correlationId !== 'string' || correlationId.length === 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
'[WorkflowOrchestrator] message.correlation_id is required - ' +
|
|
187
|
+
`Expected non-empty string on inbound workflow message (workflow_id='${workflow_id}', step='${current_step}'). ` +
|
|
188
|
+
'Fix: ensure publisher (gateway / router) sets correlation_id on message body per RFC §5.8.'
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
try {
|
|
142
193
|
// Validate cookbook structure
|
|
143
194
|
this.cookbook.validateCookbook(cookbookDef);
|
|
@@ -192,9 +243,24 @@ class WorkflowOrchestrator {
|
|
|
192
243
|
stepsMap[id] = s;
|
|
193
244
|
}
|
|
194
245
|
});
|
|
246
|
+
// Extract tenant/workspace/person context.
|
|
247
|
+
// Canonical source is cookbook._system (set by gateway); top-level
|
|
248
|
+
// message fields are accepted as an override for direct publishers
|
|
249
|
+
// that do not carry a _system block.
|
|
250
|
+
const sys = cookbookDef._system || {};
|
|
251
|
+
const tenantId = message.tenant_id !== undefined ? message.tenant_id : sys.tenant_id;
|
|
252
|
+
const workspaceId = message.workspace_id !== undefined
|
|
253
|
+
? message.workspace_id
|
|
254
|
+
: (sys.workspace_id !== undefined ? sys.workspace_id : sys.default_workspace_id);
|
|
255
|
+
const personId = message.person_id !== undefined ? message.person_id : sys.person_id;
|
|
256
|
+
|
|
195
257
|
const stepContext = {
|
|
196
258
|
workflow_id: workflow_id,
|
|
197
259
|
step_id: current_step,
|
|
260
|
+
correlation_id: correlationId,
|
|
261
|
+
tenant_id: tenantId,
|
|
262
|
+
workspace_id: workspaceId,
|
|
263
|
+
person_id: personId,
|
|
198
264
|
api_input: cookbookDef.api_input || {},
|
|
199
265
|
steps: stepsArrayCtx, // array (legacy consumers expect .map)
|
|
200
266
|
steps_by_id: stepsMap, // fast lookup by step_id for templating
|
|
@@ -267,9 +333,16 @@ class WorkflowOrchestrator {
|
|
|
267
333
|
});
|
|
268
334
|
}
|
|
269
335
|
|
|
270
|
-
// Route to next step or complete workflow
|
|
336
|
+
// Route to next step or complete workflow.
|
|
337
|
+
// Propagate correlation_id + tenancy so the next consumer can validate
|
|
338
|
+
// the envelope (RFC §5.8).
|
|
271
339
|
if (nextStep) {
|
|
272
|
-
await this._routeToNextStep(nextStep, updatedCookbook, workflow_id
|
|
340
|
+
await this._routeToNextStep(nextStep, updatedCookbook, workflow_id, {
|
|
341
|
+
correlation_id: correlationId,
|
|
342
|
+
tenant_id: tenantId,
|
|
343
|
+
workspace_id: workspaceId,
|
|
344
|
+
person_id: personId
|
|
345
|
+
});
|
|
273
346
|
} else {
|
|
274
347
|
await this._completeWorkflow(workflow_id, updatedCookbook, serviceName, current_step, currentIndex);
|
|
275
348
|
}
|
|
@@ -410,10 +483,19 @@ class WorkflowOrchestrator {
|
|
|
410
483
|
|
|
411
484
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
412
485
|
|
|
413
|
-
// Republish with UNIFIED cookbook
|
|
486
|
+
// Republish with UNIFIED cookbook.
|
|
487
|
+
// Propagate correlation_id + tenancy so the retry consumer passes
|
|
488
|
+
// ContextBuilder validation on the next attempt (RFC §5.8).
|
|
414
489
|
const serviceQueue = `${serviceName}.workflow`;
|
|
490
|
+
const sysRetry = updatedCookbook?._system || {};
|
|
415
491
|
await this.mqClient.publish(serviceQueue, {
|
|
416
492
|
workflow_id,
|
|
493
|
+
correlation_id: correlationId,
|
|
494
|
+
tenant_id: message.tenant_id !== undefined ? message.tenant_id : sysRetry.tenant_id,
|
|
495
|
+
workspace_id: message.workspace_id !== undefined
|
|
496
|
+
? message.workspace_id
|
|
497
|
+
: (sysRetry.workspace_id !== undefined ? sysRetry.workspace_id : sysRetry.default_workspace_id),
|
|
498
|
+
person_id: message.person_id !== undefined ? message.person_id : sysRetry.person_id,
|
|
417
499
|
cookbook: updatedCookbook,
|
|
418
500
|
current_step
|
|
419
501
|
});
|
|
@@ -552,26 +634,46 @@ class WorkflowOrchestrator {
|
|
|
552
634
|
|
|
553
635
|
const resolvedInput = await this._resolveInputReferencesAsync(step.input, context, helperContext);
|
|
554
636
|
console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${stepId} input AFTER:`, JSON.stringify(resolvedInput));
|
|
555
|
-
|
|
556
|
-
// Use API mapper to call the service with resolved input
|
|
557
|
-
const result = await this.apiMapper.callOperation(
|
|
558
|
-
step.operation || stepId,
|
|
559
|
-
resolvedInput,
|
|
560
|
-
{
|
|
561
|
-
...context,
|
|
562
|
-
workflow_id: context?.workflow_id,
|
|
563
|
-
step_id: stepId
|
|
564
|
-
}
|
|
565
|
-
);
|
|
566
637
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
operation: step.operation,
|
|
571
|
-
|
|
638
|
+
// Intra-service handler dispatch via service-wrapper v3.0.0 (RFC §5.9, §5.10).
|
|
639
|
+
// Replaces the retired apiMapper.callOperation HTTP-loopback path.
|
|
640
|
+
const envelope = await this._invoker.invokeOperation({
|
|
641
|
+
operation: step.operation || stepId,
|
|
642
|
+
input: resolvedInput,
|
|
643
|
+
envelope: {
|
|
644
|
+
tenant_id: context?.tenant_id,
|
|
645
|
+
workspace_id: context?.workspace_id,
|
|
646
|
+
person_id: context?.person_id
|
|
647
|
+
},
|
|
648
|
+
workflow_id: context?.workflow_id,
|
|
649
|
+
correlation_id: context?.correlation_id,
|
|
650
|
+
step_id: stepId
|
|
572
651
|
});
|
|
573
652
|
|
|
574
|
-
|
|
653
|
+
if (envelope && envelope.status >= 200 && envelope.status < 300) {
|
|
654
|
+
this.logger.info(`Task step executed`, {
|
|
655
|
+
step: stepId,
|
|
656
|
+
service: step.service,
|
|
657
|
+
operation: step.operation,
|
|
658
|
+
inputResolved: !!resolvedInput
|
|
659
|
+
});
|
|
660
|
+
return envelope.result;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Non-2xx: surface the typed error envelope so the outer workflow handler
|
|
664
|
+
// can record retry_history / DLQ classification per existing logic.
|
|
665
|
+
const err = new Error(
|
|
666
|
+
`[WorkflowOrchestrator] Step '${stepId}' (operation '${step.operation}') failed with status ${envelope?.status}`
|
|
667
|
+
);
|
|
668
|
+
err.status = envelope?.status;
|
|
669
|
+
err.statusCode = envelope?.status;
|
|
670
|
+
err.code = envelope?.error?.code;
|
|
671
|
+
err.errorCode = envelope?.error?.code;
|
|
672
|
+
err.details = envelope?.error?.details;
|
|
673
|
+
if (envelope?.error?.message) {
|
|
674
|
+
err.message = envelope.error.message;
|
|
675
|
+
}
|
|
676
|
+
throw err;
|
|
575
677
|
}
|
|
576
678
|
|
|
577
679
|
/**
|
|
@@ -1057,13 +1159,20 @@ class WorkflowOrchestrator {
|
|
|
1057
1159
|
* @param {Object} nextStep - Next step to execute
|
|
1058
1160
|
* @param {Object} cookbook - UNIFIED cookbook (contains everything)
|
|
1059
1161
|
* @param {string} workflow_id - Workflow ID
|
|
1162
|
+
* @param {Object} envelope - envelope to propagate (correlation_id, tenant_id, workspace_id, person_id)
|
|
1060
1163
|
*/
|
|
1061
|
-
async _routeToNextStep(nextStep, cookbook, workflow_id) {
|
|
1164
|
+
async _routeToNextStep(nextStep, cookbook, workflow_id, envelope = {}) {
|
|
1062
1165
|
const nextStepId = this._getStepId(nextStep);
|
|
1063
|
-
|
|
1064
|
-
// UNIFIED COOKBOOK: no separate context, everything is in cookbook
|
|
1166
|
+
|
|
1167
|
+
// UNIFIED COOKBOOK: no separate context, everything is in cookbook.
|
|
1168
|
+
// correlation_id + tenancy are propagated on the message envelope so
|
|
1169
|
+
// the next consumer's ContextBuilder validation succeeds (RFC §5.8).
|
|
1065
1170
|
const message = {
|
|
1066
1171
|
workflow_id,
|
|
1172
|
+
correlation_id: envelope.correlation_id,
|
|
1173
|
+
tenant_id: envelope.tenant_id,
|
|
1174
|
+
workspace_id: envelope.workspace_id,
|
|
1175
|
+
person_id: envelope.person_id,
|
|
1067
1176
|
cookbook: cookbook,
|
|
1068
1177
|
current_step: nextStepId
|
|
1069
1178
|
};
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,11 @@ const WorkflowOrchestrator = require('./WorkflowOrchestrator');
|
|
|
19
19
|
* @param {Object} config - Configuration options
|
|
20
20
|
* @param {Object} config.mqClient - MQ client instance
|
|
21
21
|
* @param {Object} config.registryClient - Registry client instance
|
|
22
|
-
* @param {Object} config.
|
|
22
|
+
* @param {Object} config.invoker - Intra-service operation invoker.
|
|
23
|
+
* Expected shape: `{ async invokeOperation({ operation, input, envelope,
|
|
24
|
+
* workflow_id, correlation_id, step_id }) -> { status, result } | { status, error } }`.
|
|
25
|
+
* Typically `{ invokeOperation: serviceWrapper.invokeOperation.bind(serviceWrapper) }`.
|
|
26
|
+
* Replaces the retired `apiMapper` HTTP-loopback dep (RFC §5.9, §5.10).
|
|
23
27
|
* @param {Object} config.cookbook - Cookbook connector instance
|
|
24
28
|
* @param {Object} [config.logger] - Logger instance
|
|
25
29
|
* @returns {WorkflowOrchestrator} New orchestrator instance
|
|
@@ -28,7 +32,7 @@ const WorkflowOrchestrator = require('./WorkflowOrchestrator');
|
|
|
28
32
|
* const orchestrator = create({
|
|
29
33
|
* mqClient: new MQClient(),
|
|
30
34
|
* registryClient: new RegistryClient(),
|
|
31
|
-
*
|
|
35
|
+
* invoker: { invokeOperation: wrapper.invokeOperation.bind(wrapper) },
|
|
32
36
|
* cookbook: new CookbookConnector()
|
|
33
37
|
* });
|
|
34
38
|
*/
|