@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 +1 -0
- package/package.json +1 -1
- package/src/ErrorMapper.js +12 -12
- package/src/ServiceWrapper.js +107 -6
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
package/src/ErrorMapper.js
CHANGED
|
@@ -93,31 +93,31 @@ class ErrorMapper {
|
|
|
93
93
|
if (err instanceof ValidationError) {
|
|
94
94
|
if (err.phase === 'output') {
|
|
95
95
|
this._logger.error(
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
'
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
129
|
-
'
|
|
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
|
-
|
|
136
|
-
'
|
|
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
|
}
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -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:
|
|
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
|