@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-orchestrator",
3
- "version": "1.0.115",
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.apiMapper - API mapper for cookbook to HTTP mapping
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.apiMapper) throw new Error('apiMapper is required');
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.apiMapper = config.apiMapper;
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
- this.logger.info(`Task step executed`, {
568
- step: stepId,
569
- service: step.service,
570
- operation: step.operation,
571
- inputResolved: !!resolvedInput
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
- return result;
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.apiMapper - API mapper instance
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
- * apiMapper: new ApiMapper(),
35
+ * invoker: { invokeOperation: wrapper.invokeOperation.bind(wrapper) },
32
36
  * cookbook: new CookbookConnector()
33
37
  * });
34
38
  */