@onlineapps/service-wrapper 3.0.9 → 3.0.12

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/jest.config.js CHANGED
@@ -26,6 +26,7 @@ module.exports = {
26
26
  '@onlineapps/conn-orch-registry': '<rootDir>/tests/mocks/connectors.js',
27
27
  '@onlineapps/conn-base-logger': '<rootDir>/tests/mocks/connectors.js',
28
28
  '@onlineapps/conn-orch-orchestrator': '<rootDir>/tests/mocks/connectors.js',
29
+ '@onlineapps/conn-orch-orchestrator/package.json': '<rootDir>/tests/mocks/orchestrator-package-v2.js',
29
30
  '@onlineapps/conn-orch-api-mapper': '<rootDir>/tests/mocks/connectors.js',
30
31
  '@onlineapps/conn-orch-cookbook': '<rootDir>/tests/mocks/connectors.js',
31
32
  '@onlineapps/conn-base-state': '<rootDir>/tests/mocks/connectors.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "3.0.9",
3
+ "version": "3.0.12",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -93,31 +93,31 @@ class ErrorMapper {
93
93
  if (err instanceof ValidationError) {
94
94
  if (err.phase === 'output') {
95
95
  this._logger.error(
96
- { err, operation, correlation_id, code: 'INTERNAL_ERROR', status: 500 },
97
- 'output schema validation failed - server bug'
96
+ 'output schema validation failed - server bug',
97
+ { err, operation, correlation_id, code: 'INTERNAL_ERROR', status: 500 }
98
98
  );
99
99
  return this._buildError(500, 'INTERNAL_ERROR', 'Internal validation error', undefined, correlation_id);
100
100
  }
101
101
 
102
102
  this._logger.info(
103
- { err, operation, correlation_id, code: 'VALIDATION_FAILED', status: 400 },
104
- 'input schema validation failed'
103
+ 'input schema validation failed',
104
+ { err, operation, correlation_id, code: 'VALIDATION_FAILED', status: 400 }
105
105
  );
106
106
  return this._buildError(400, 'VALIDATION_FAILED', err.message, err.details, correlation_id);
107
107
  }
108
108
 
109
109
  if (err instanceof UnknownOperationError) {
110
110
  this._logger.warn(
111
- { err, operation, correlation_id, code: 'UNKNOWN_OPERATION', status: 404 },
112
- 'unknown operation requested'
111
+ 'unknown operation requested',
112
+ { err, operation, correlation_id, code: 'UNKNOWN_OPERATION', status: 404 }
113
113
  );
114
114
  return this._buildError(404, 'UNKNOWN_OPERATION', err.message, undefined, correlation_id);
115
115
  }
116
116
 
117
117
  if (err instanceof BusinessError) {
118
118
  this._logger.info(
119
- { err, operation, correlation_id, code: err.code, status: err.status },
120
- 'handler raised BusinessError'
119
+ 'handler raised BusinessError',
120
+ { err, operation, correlation_id, code: err.code, status: err.status }
121
121
  );
122
122
  return this._buildError(err.status, err.code, err.message, err.details, correlation_id);
123
123
  }
@@ -125,15 +125,15 @@ class ErrorMapper {
125
125
  if (err instanceof AbortError || (err && err.name === 'AbortError')) {
126
126
  const message = err && err.message ? err.message : 'Invocation aborted';
127
127
  this._logger.info(
128
- { err, operation, correlation_id, code: 'CLIENT_CANCELLED', status: 499 },
129
- 'invocation aborted by client'
128
+ 'invocation aborted by client',
129
+ { err, operation, correlation_id, code: 'CLIENT_CANCELLED', status: 499 }
130
130
  );
131
131
  return this._buildError(499, 'CLIENT_CANCELLED', message, undefined, correlation_id);
132
132
  }
133
133
 
134
134
  this._logger.error(
135
- { err, operation, correlation_id, code: 'INTERNAL_ERROR', status: 500, stack: err && err.stack },
136
- 'unhandled error during operation invocation'
135
+ 'unhandled error during operation invocation',
136
+ { err, operation, correlation_id, code: 'INTERNAL_ERROR', status: 500, stack: err && err.stack }
137
137
  );
138
138
  return this._buildError(500, 'INTERNAL_ERROR', 'An internal error occurred', undefined, correlation_id);
139
139
  }
@@ -34,6 +34,40 @@ const { ErrorMapper, UnknownOperationError, ValidationError } = require('./Error
34
34
  const RUNTIME_DIR = 'conn-runtime';
35
35
  const PROOF_RELATIVE_PATH = `${RUNTIME_DIR}/validation-proof.json`;
36
36
 
37
+ /**
38
+ * service-wrapper v3 wires OrchestratorConnector with invoker RPC (conn-orch-orchestrator ≥2).
39
+ * If npm resolves orchestrator 1.x (stale tree / lockfile), WorkflowOrchestrator throws
40
+ * "apiMapper is required" — fail here with an explicit contract error instead.
41
+ * @returns {{ version: string, major: number }}
42
+ */
43
+ function readConnOrchOrchestratorContractVersion() {
44
+ let pkg;
45
+ try {
46
+ pkg = require('@onlineapps/conn-orch-orchestrator/package.json');
47
+ } catch (e) {
48
+ throw new Error(
49
+ `[ServiceWrapper] Missing dependency - cannot resolve @onlineapps/conn-orch-orchestrator/package.json — ${e.message}. Fix: npm install from service root so @onlineapps/conn-orch-orchestrator is installed.`
50
+ );
51
+ }
52
+ const version = pkg.version;
53
+ const major = Number(String(version).split('.')[0]);
54
+ if (!Number.isFinite(major)) {
55
+ throw new Error(
56
+ `[ServiceWrapper] Invalid dependency metadata - @onlineapps/conn-orch-orchestrator version "${version}" is not semver-like. Fix: reinstall node_modules from a valid registry lockfile.`
57
+ );
58
+ }
59
+ return { version, major };
60
+ }
61
+
62
+ function assertConnOrchOrchestratorV2Contract() {
63
+ const { version, major } = readConnOrchOrchestratorContractVersion();
64
+ if (major < 2) {
65
+ throw new Error(
66
+ `[ServiceWrapper] Dependency contract violation - resolved @onlineapps/conn-orch-orchestrator@${version} is incompatible with service-wrapper v3 (orchestrator invoker model). Expected major version >= 2. Fix: pin "@onlineapps/conn-orch-orchestrator":"2.0.2" (or newer 2.x) in the service package.json, delete node_modules and .oa_drive_deps_hash, then run npm ci.`
67
+ );
68
+ }
69
+ }
70
+
37
71
  const REVALIDATION_FAST_BACKOFF_MS = [30000, 60000, 120000, 300000, 600000, 1800000];
38
72
  const REVALIDATION_HOURLY_MS = 3600000;
39
73
  const REVALIDATION_HOURLY_MAX = 24;
@@ -1468,6 +1502,8 @@ class ServiceWrapper {
1468
1502
  * @private
1469
1503
  */
1470
1504
  async _initializeOrchestrator() {
1505
+ assertConnOrchOrchestratorV2Contract();
1506
+
1471
1507
  const serviceName = this.config.service?.name;
1472
1508
  const serviceVersion = this.config.service?.version;
1473
1509
  const environment = this.config.service?.env;
@@ -1628,23 +1664,71 @@ class ServiceWrapper {
1628
1664
  }
1629
1665
  }
1630
1666
 
1667
+ /**
1668
+ * If the inbound message used RPC (replyTo + correlationId), publish the JSON response
1669
+ * to the caller's reply queue with the same AMQP correlationId.
1670
+ *
1671
+ * @private
1672
+ * @param {object} rawMessage - amqplib consume message
1673
+ * @param {object|null} parsedMessage - parsed JSON body, or null when JSON.parse failed
1674
+ * @param {{ status: string, output?: object, error?: object, correlation_id?: string, workflow_id?: string }} body
1675
+ */
1676
+ async _publishWorkflowRpcReply(rawMessage, parsedMessage, body) {
1677
+ const props = rawMessage && rawMessage.properties;
1678
+ if (!this.mqClient || !props || !props.replyTo || props.correlationId == null || String(props.correlationId) === '') {
1679
+ return;
1680
+ }
1681
+ const amqpCid = String(props.correlationId);
1682
+ const correlation_id =
1683
+ body.correlation_id != null
1684
+ ? String(body.correlation_id)
1685
+ : parsedMessage && parsedMessage.correlation_id != null
1686
+ ? String(parsedMessage.correlation_id)
1687
+ : amqpCid;
1688
+ const workflow_id =
1689
+ body.workflow_id != null
1690
+ ? body.workflow_id
1691
+ : parsedMessage &&
1692
+ (parsedMessage.workflow_id != null || parsedMessage.workflowId != null)
1693
+ ? parsedMessage.workflow_id || parsedMessage.workflowId
1694
+ : undefined;
1695
+ const payload = { ...body, correlation_id };
1696
+ if (workflow_id !== undefined && payload.workflow_id === undefined) {
1697
+ payload.workflow_id = workflow_id;
1698
+ }
1699
+ await this.mqClient.publish(props.replyTo, payload, {
1700
+ correlationId: amqpCid,
1701
+ persistent: true,
1702
+ contentType: 'application/json',
1703
+ });
1704
+ }
1705
+
1631
1706
  /**
1632
1707
  * Process workflow message
1633
1708
  * @private
1634
1709
  */
1635
1710
  async _processWorkflowMessage(rawMessage, queueName) {
1711
+ let message;
1636
1712
  try {
1637
- // Parse message
1638
- let message;
1639
1713
  if (rawMessage && rawMessage.content) {
1640
- // AMQP message with content buffer
1641
1714
  const messageContent = rawMessage.content.toString();
1642
1715
  message = JSON.parse(messageContent);
1643
1716
  } else {
1644
- // Already parsed message
1645
1717
  message = rawMessage;
1646
1718
  }
1719
+ } catch (parseErr) {
1720
+ try {
1721
+ await this._publishWorkflowRpcReply(rawMessage, null, {
1722
+ status: 'error',
1723
+ error: { message: parseErr.message, code: 'INVALID_JSON' },
1724
+ });
1725
+ } catch (replyErr) {
1726
+ this.logger?.error('[ServiceWrapper] RPC reply after JSON parse error failed', replyErr);
1727
+ }
1728
+ throw parseErr;
1729
+ }
1647
1730
 
1731
+ try {
1648
1732
  // Extract and normalize flags
1649
1733
  const flags = Array.isArray(message.flags) ? message.flags : [];
1650
1734
  const isTest = flags.includes('test');
@@ -1741,6 +1825,11 @@ class ServiceWrapper {
1741
1825
  // ServiceWrapper should NOT publish here to avoid duplicate messages
1742
1826
  // If orchestrator is not used, ServiceWrapper would publish, but that's legacy path
1743
1827
 
1828
+ await this._publishWorkflowRpcReply(rawMessage, message, {
1829
+ status: 'ok',
1830
+ output: result,
1831
+ });
1832
+
1744
1833
  return result;
1745
1834
 
1746
1835
  } catch (error) {
@@ -1755,6 +1844,17 @@ class ServiceWrapper {
1755
1844
  } else {
1756
1845
  this.logger?.error(`Error processing message from ${queueName}:`, error);
1757
1846
  }
1847
+ try {
1848
+ await this._publishWorkflowRpcReply(rawMessage, message, {
1849
+ status: 'error',
1850
+ error: {
1851
+ message: error.message,
1852
+ code: error.code || 'PROCESSING_ERROR',
1853
+ },
1854
+ });
1855
+ } catch (replyErr) {
1856
+ this.logger?.error('[ServiceWrapper] RPC error reply publish failed', replyErr);
1857
+ }
1758
1858
  throw error;
1759
1859
  }
1760
1860
  }
@@ -1967,12 +2067,13 @@ class ServiceWrapper {
1967
2067
 
1968
2068
  this._contextBuilder = new ContextBuilder({
1969
2069
  connectors: {
1970
- storage: this.stateConnector || null,
2070
+ storage: null,
1971
2071
  cache: this.cacheConnector || null,
1972
2072
  http: null,
1973
2073
  secrets: null,
1974
2074
  monitoring: this.monitoring || null,
1975
- mq: this.mqClient || null
2075
+ mq: this.mqClient || null,
2076
+ state: this.stateConnector || null
1976
2077
  },
1977
2078
  config: this.config,
1978
2079
  logger