@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-orchestrator",
3
- "version": "2.0.0",
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": {
@@ -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
  };