@onlineapps/service-wrapper 3.0.9 → 3.0.10

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.10",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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
  }