@onlineapps/conn-orch-orchestrator 2.0.0 → 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 +1 -1
- package/src/WorkflowOrchestrator.js +88 -6
package/package.json
CHANGED
|
@@ -73,6 +73,35 @@ class WorkflowOrchestrator {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
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
|
+
|
|
76
105
|
/**
|
|
77
106
|
* Get step identifier (V2.1 only: step_id)
|
|
78
107
|
* @private
|
|
@@ -145,6 +174,21 @@ class WorkflowOrchestrator {
|
|
|
145
174
|
const { workflow_id, cookbook: cookbookDef, current_step } = message;
|
|
146
175
|
const startTime = Date.now();
|
|
147
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
|
+
|
|
148
192
|
try {
|
|
149
193
|
// Validate cookbook structure
|
|
150
194
|
this.cookbook.validateCookbook(cookbookDef);
|
|
@@ -199,9 +243,24 @@ class WorkflowOrchestrator {
|
|
|
199
243
|
stepsMap[id] = s;
|
|
200
244
|
}
|
|
201
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
|
+
|
|
202
257
|
const stepContext = {
|
|
203
258
|
workflow_id: workflow_id,
|
|
204
259
|
step_id: current_step,
|
|
260
|
+
correlation_id: correlationId,
|
|
261
|
+
tenant_id: tenantId,
|
|
262
|
+
workspace_id: workspaceId,
|
|
263
|
+
person_id: personId,
|
|
205
264
|
api_input: cookbookDef.api_input || {},
|
|
206
265
|
steps: stepsArrayCtx, // array (legacy consumers expect .map)
|
|
207
266
|
steps_by_id: stepsMap, // fast lookup by step_id for templating
|
|
@@ -274,9 +333,16 @@ class WorkflowOrchestrator {
|
|
|
274
333
|
});
|
|
275
334
|
}
|
|
276
335
|
|
|
277
|
-
// 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).
|
|
278
339
|
if (nextStep) {
|
|
279
|
-
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
|
+
});
|
|
280
346
|
} else {
|
|
281
347
|
await this._completeWorkflow(workflow_id, updatedCookbook, serviceName, current_step, currentIndex);
|
|
282
348
|
}
|
|
@@ -417,10 +483,19 @@ class WorkflowOrchestrator {
|
|
|
417
483
|
|
|
418
484
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
419
485
|
|
|
420
|
-
// 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).
|
|
421
489
|
const serviceQueue = `${serviceName}.workflow`;
|
|
490
|
+
const sysRetry = updatedCookbook?._system || {};
|
|
422
491
|
await this.mqClient.publish(serviceQueue, {
|
|
423
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,
|
|
424
499
|
cookbook: updatedCookbook,
|
|
425
500
|
current_step
|
|
426
501
|
});
|
|
@@ -1084,13 +1159,20 @@ class WorkflowOrchestrator {
|
|
|
1084
1159
|
* @param {Object} nextStep - Next step to execute
|
|
1085
1160
|
* @param {Object} cookbook - UNIFIED cookbook (contains everything)
|
|
1086
1161
|
* @param {string} workflow_id - Workflow ID
|
|
1162
|
+
* @param {Object} envelope - envelope to propagate (correlation_id, tenant_id, workspace_id, person_id)
|
|
1087
1163
|
*/
|
|
1088
|
-
async _routeToNextStep(nextStep, cookbook, workflow_id) {
|
|
1164
|
+
async _routeToNextStep(nextStep, cookbook, workflow_id, envelope = {}) {
|
|
1089
1165
|
const nextStepId = this._getStepId(nextStep);
|
|
1090
|
-
|
|
1091
|
-
// 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).
|
|
1092
1170
|
const message = {
|
|
1093
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,
|
|
1094
1176
|
cookbook: cookbook,
|
|
1095
1177
|
current_step: nextStepId
|
|
1096
1178
|
};
|