@onlineapps/conn-orch-orchestrator 2.0.0 → 2.0.2

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.2",
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
@@ -144,8 +173,29 @@ class WorkflowOrchestrator {
144
173
  async processWorkflowMessage(message, serviceName) {
145
174
  const { workflow_id, cookbook: cookbookDef, current_step } = message;
146
175
  const startTime = Date.now();
176
+ // correlation_id is required by RFC §5.8 — validated below inside the
177
+ // try block so malformed messages flow through the retry/DLQ pipeline
178
+ // and a `failed` monitoring event is emitted (no silent redelivery).
179
+ const correlationId = this._extractCorrelationId(message);
147
180
 
148
181
  try {
182
+ // Fail-fast: correlation_id MUST be present on every workflow message.
183
+ // (ContextBuilder._validateMqMessage rejects empty strings too.)
184
+ // Gateway is expected to publish it; if it does not, that is a gateway
185
+ // bug we want to surface loudly rather than silently produce 500s
186
+ // inside invokeOperation's ContextBuilder.
187
+ if (typeof correlationId !== 'string' || correlationId.length === 0) {
188
+ const err = new Error(
189
+ '[WorkflowOrchestrator] message.correlation_id is required - ' +
190
+ `Expected non-empty string on inbound workflow message (workflow_id='${workflow_id}', step='${current_step}'). ` +
191
+ 'Fix: ensure publisher (gateway / router) sets correlation_id on message body per RFC §5.8.'
192
+ );
193
+ err.errorCode = 'MISSING_CORRELATION_ID';
194
+ err.statusCode = 400;
195
+ err.type = 'VALIDATION';
196
+ throw err;
197
+ }
198
+
149
199
  // Validate cookbook structure
150
200
  this.cookbook.validateCookbook(cookbookDef);
151
201
 
@@ -199,9 +249,24 @@ class WorkflowOrchestrator {
199
249
  stepsMap[id] = s;
200
250
  }
201
251
  });
252
+ // Extract tenant/workspace/person context.
253
+ // Canonical source is cookbook._system (set by gateway); top-level
254
+ // message fields are accepted as an override for direct publishers
255
+ // that do not carry a _system block.
256
+ const sys = cookbookDef._system || {};
257
+ const tenantId = message.tenant_id !== undefined ? message.tenant_id : sys.tenant_id;
258
+ const workspaceId = message.workspace_id !== undefined
259
+ ? message.workspace_id
260
+ : (sys.workspace_id !== undefined ? sys.workspace_id : sys.default_workspace_id);
261
+ const personId = message.person_id !== undefined ? message.person_id : sys.person_id;
262
+
202
263
  const stepContext = {
203
264
  workflow_id: workflow_id,
204
265
  step_id: current_step,
266
+ correlation_id: correlationId,
267
+ tenant_id: tenantId,
268
+ workspace_id: workspaceId,
269
+ person_id: personId,
205
270
  api_input: cookbookDef.api_input || {},
206
271
  steps: stepsArrayCtx, // array (legacy consumers expect .map)
207
272
  steps_by_id: stepsMap, // fast lookup by step_id for templating
@@ -274,9 +339,16 @@ class WorkflowOrchestrator {
274
339
  });
275
340
  }
276
341
 
277
- // Route to next step or complete workflow
342
+ // Route to next step or complete workflow.
343
+ // Propagate correlation_id + tenancy so the next consumer can validate
344
+ // the envelope (RFC §5.8).
278
345
  if (nextStep) {
279
- await this._routeToNextStep(nextStep, updatedCookbook, workflow_id);
346
+ await this._routeToNextStep(nextStep, updatedCookbook, workflow_id, {
347
+ correlation_id: correlationId,
348
+ tenant_id: tenantId,
349
+ workspace_id: workspaceId,
350
+ person_id: personId
351
+ });
280
352
  } else {
281
353
  await this._completeWorkflow(workflow_id, updatedCookbook, serviceName, current_step, currentIndex);
282
354
  }
@@ -417,10 +489,19 @@ class WorkflowOrchestrator {
417
489
 
418
490
  await new Promise(resolve => setTimeout(resolve, delay));
419
491
 
420
- // Republish with UNIFIED cookbook
492
+ // Republish with UNIFIED cookbook.
493
+ // Propagate correlation_id + tenancy so the retry consumer passes
494
+ // ContextBuilder validation on the next attempt (RFC §5.8).
421
495
  const serviceQueue = `${serviceName}.workflow`;
496
+ const sysRetry = updatedCookbook?._system || {};
422
497
  await this.mqClient.publish(serviceQueue, {
423
498
  workflow_id,
499
+ correlation_id: correlationId,
500
+ tenant_id: message.tenant_id !== undefined ? message.tenant_id : sysRetry.tenant_id,
501
+ workspace_id: message.workspace_id !== undefined
502
+ ? message.workspace_id
503
+ : (sysRetry.workspace_id !== undefined ? sysRetry.workspace_id : sysRetry.default_workspace_id),
504
+ person_id: message.person_id !== undefined ? message.person_id : sysRetry.person_id,
424
505
  cookbook: updatedCookbook,
425
506
  current_step
426
507
  });
@@ -1084,13 +1165,20 @@ class WorkflowOrchestrator {
1084
1165
  * @param {Object} nextStep - Next step to execute
1085
1166
  * @param {Object} cookbook - UNIFIED cookbook (contains everything)
1086
1167
  * @param {string} workflow_id - Workflow ID
1168
+ * @param {Object} envelope - envelope to propagate (correlation_id, tenant_id, workspace_id, person_id)
1087
1169
  */
1088
- async _routeToNextStep(nextStep, cookbook, workflow_id) {
1170
+ async _routeToNextStep(nextStep, cookbook, workflow_id, envelope = {}) {
1089
1171
  const nextStepId = this._getStepId(nextStep);
1090
-
1091
- // UNIFIED COOKBOOK: no separate context, everything is in cookbook
1172
+
1173
+ // UNIFIED COOKBOOK: no separate context, everything is in cookbook.
1174
+ // correlation_id + tenancy are propagated on the message envelope so
1175
+ // the next consumer's ContextBuilder validation succeeds (RFC §5.8).
1092
1176
  const message = {
1093
1177
  workflow_id,
1178
+ correlation_id: envelope.correlation_id,
1179
+ tenant_id: envelope.tenant_id,
1180
+ workspace_id: envelope.workspace_id,
1181
+ person_id: envelope.person_id,
1094
1182
  cookbook: cookbook,
1095
1183
  current_step: nextStepId
1096
1184
  };