@onlineapps/service-wrapper 2.3.9 → 2.4.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/API.md
CHANGED
|
@@ -53,7 +53,7 @@ ALL actual functionality is delegated to specialized connectors.
|
|
|
53
53
|
const wrapper = new ServiceWrapper({
|
|
54
54
|
service: expressApp,
|
|
55
55
|
serviceName: 'hello-service',
|
|
56
|
-
|
|
56
|
+
operations: require('./openapi.json'),
|
|
57
57
|
config: {
|
|
58
58
|
rabbitmq: process.env.RABBITMQ_URL,
|
|
59
59
|
redis: process.env.REDIS_HOST,
|
|
@@ -78,7 +78,7 @@ Create a new ServiceWrapper instance
|
|
|
78
78
|
| options | <code>Object</code> | | Configuration options |
|
|
79
79
|
| options.service | <code>Object</code> | | Express application instance |
|
|
80
80
|
| options.serviceName | <code>string</code> | | Name of the service |
|
|
81
|
-
| options.
|
|
81
|
+
| options.operations | <code>Object</code> | | Operations specification (operations.json; see operations-registry-contract.md §3) |
|
|
82
82
|
| [options.config] | <code>Object</code> | <code>{}</code> | Infrastructure configuration |
|
|
83
83
|
| [options.config.rabbitmq] | <code>string</code> | | RabbitMQ connection URL |
|
|
84
84
|
| [options.config.redis] | <code>string</code> | | Redis connection string |
|
|
@@ -106,7 +106,7 @@ ALL actual functionality is delegated to specialized connectors.
|
|
|
106
106
|
const wrapper = new ServiceWrapper({
|
|
107
107
|
service: expressApp,
|
|
108
108
|
serviceName: 'hello-service',
|
|
109
|
-
|
|
109
|
+
operations: require('./openapi.json'),
|
|
110
110
|
config: {
|
|
111
111
|
rabbitmq: process.env.RABBITMQ_URL,
|
|
112
112
|
redis: process.env.REDIS_HOST,
|
|
@@ -131,7 +131,7 @@ Create a new ServiceWrapper instance
|
|
|
131
131
|
| options | <code>Object</code> | | Configuration options |
|
|
132
132
|
| options.service | <code>Object</code> | | Express application instance |
|
|
133
133
|
| options.serviceName | <code>string</code> | | Name of the service |
|
|
134
|
-
| options.
|
|
134
|
+
| options.operations | <code>Object</code> | | Operations specification (operations.json; see operations-registry-contract.md §3) |
|
|
135
135
|
| [options.config] | <code>Object</code> | <code>{}</code> | Infrastructure configuration |
|
|
136
136
|
| [options.config.rabbitmq] | <code>string</code> | | RabbitMQ connection URL |
|
|
137
137
|
| [options.config.redis] | <code>string</code> | | Redis connection string |
|
|
@@ -214,7 +214,7 @@ ALL actual functionality is delegated to specialized connectors.
|
|
|
214
214
|
const wrapper = new ServiceWrapper({
|
|
215
215
|
service: expressApp,
|
|
216
216
|
serviceName: 'hello-service',
|
|
217
|
-
|
|
217
|
+
operations: require('./openapi.json'),
|
|
218
218
|
config: {
|
|
219
219
|
rabbitmq: process.env.RABBITMQ_URL,
|
|
220
220
|
redis: process.env.REDIS_HOST,
|
|
@@ -239,7 +239,7 @@ Create a new ServiceWrapper instance
|
|
|
239
239
|
| options | <code>Object</code> | | Configuration options |
|
|
240
240
|
| options.service | <code>Object</code> | | Express application instance |
|
|
241
241
|
| options.serviceName | <code>string</code> | | Name of the service |
|
|
242
|
-
| options.
|
|
242
|
+
| options.operations | <code>Object</code> | | Operations specification (operations.json; see operations-registry-contract.md §3) |
|
|
243
243
|
| [options.config] | <code>Object</code> | <code>{}</code> | Infrastructure configuration |
|
|
244
244
|
| [options.config.rabbitmq] | <code>string</code> | | RabbitMQ connection URL |
|
|
245
245
|
| [options.config.redis] | <code>string</code> | | Redis connection string |
|
|
@@ -267,7 +267,7 @@ ALL actual functionality is delegated to specialized connectors.
|
|
|
267
267
|
const wrapper = new ServiceWrapper({
|
|
268
268
|
service: expressApp,
|
|
269
269
|
serviceName: 'hello-service',
|
|
270
|
-
|
|
270
|
+
operations: require('./openapi.json'),
|
|
271
271
|
config: {
|
|
272
272
|
rabbitmq: process.env.RABBITMQ_URL,
|
|
273
273
|
redis: process.env.REDIS_HOST,
|
|
@@ -292,7 +292,7 @@ Create a new ServiceWrapper instance
|
|
|
292
292
|
| options | <code>Object</code> | | Configuration options |
|
|
293
293
|
| options.service | <code>Object</code> | | Express application instance |
|
|
294
294
|
| options.serviceName | <code>string</code> | | Name of the service |
|
|
295
|
-
| options.
|
|
295
|
+
| options.operations | <code>Object</code> | | Operations specification (operations.json; see operations-registry-contract.md §3) |
|
|
296
296
|
| [options.config] | <code>Object</code> | <code>{}</code> | Infrastructure configuration |
|
|
297
297
|
| [options.config.rabbitmq] | <code>string</code> | | RabbitMQ connection URL |
|
|
298
298
|
| [options.config.redis] | <code>string</code> | | Redis connection string |
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"description": "Multi-step workflow across services",
|
|
5
5
|
"steps": [
|
|
6
6
|
{
|
|
7
|
-
"service": "hello
|
|
7
|
+
"service": "biz-hello",
|
|
8
8
|
"operation": "good-day",
|
|
9
9
|
"input": {
|
|
10
10
|
"name": "Igor"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"service": "hello
|
|
14
|
+
"service": "biz-hello",
|
|
15
15
|
"operation": "hello-cert",
|
|
16
16
|
"input": {
|
|
17
17
|
"name": "Igor",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@onlineapps/conn-base-cache": "1.0.9",
|
|
28
28
|
"@onlineapps/conn-base-state": "1.0.1",
|
|
29
|
-
"@onlineapps/conn-base-monitoring": "1.0.
|
|
30
|
-
"@onlineapps/conn-infra-error-handler": "1.0.
|
|
29
|
+
"@onlineapps/conn-base-monitoring": "1.0.12",
|
|
30
|
+
"@onlineapps/conn-infra-error-handler": "1.0.11",
|
|
31
31
|
"@onlineapps/conn-infra-mq": "1.1.70",
|
|
32
32
|
"@onlineapps/conn-orch-api-mapper": "1.0.33",
|
|
33
|
-
"@onlineapps/conn-orch-cookbook": "2.0
|
|
33
|
+
"@onlineapps/conn-orch-cookbook": "2.1.0",
|
|
34
34
|
"@onlineapps/conn-orch-orchestrator": "1.0.110",
|
|
35
35
|
"@onlineapps/conn-orch-registry": "1.1.55",
|
|
36
36
|
"@onlineapps/conn-orch-validator": "2.0.33",
|
|
37
|
-
"@onlineapps/monitoring-core": "1.0.
|
|
38
|
-
"@onlineapps/service-common": "1.
|
|
37
|
+
"@onlineapps/monitoring-core": "1.0.23",
|
|
38
|
+
"@onlineapps/service-common": "1.1.2",
|
|
39
39
|
"@onlineapps/runtime-config": "1.0.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
package/src/ConfigLoader.js
CHANGED
|
@@ -372,6 +372,7 @@ class ConfigLoader {
|
|
|
372
372
|
url: serviceConfig.service.url,
|
|
373
373
|
specificationEndpoint: serviceConfig.service.specificationEndpoint,
|
|
374
374
|
description: serviceConfig.service.description,
|
|
375
|
+
workspaceScoped: serviceConfig.service.workspaceScoped,
|
|
375
376
|
env: nodeEnv
|
|
376
377
|
},
|
|
377
378
|
wrapper: mergedWrapper,
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -78,6 +78,12 @@ class ServiceWrapper {
|
|
|
78
78
|
this.validationProof = options.validationProof || null;
|
|
79
79
|
this._injectedValidationOrchestrator = options._validationOrchestrator || null;
|
|
80
80
|
|
|
81
|
+
// Fail-fast: enforce naming conventions and workspaceScoped invariants.
|
|
82
|
+
// See api/docs/standards/OPERATIONS.md and biz-service-onboarding.md §3.1 / §4.
|
|
83
|
+
// Rejection here keeps the contract authoritative at the earliest possible
|
|
84
|
+
// point (before MQ listeners, registry client, or any I/O is touched).
|
|
85
|
+
this._validateNamingAndWorkspaceScoped();
|
|
86
|
+
|
|
81
87
|
// Initialize connector placeholders
|
|
82
88
|
this.mqClient = null;
|
|
83
89
|
this.registryClient = null;
|
|
@@ -253,6 +259,79 @@ class ServiceWrapper {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Fail-fast validation of naming conventions and workspaceScoped invariants.
|
|
264
|
+
*
|
|
265
|
+
* Enforced rules (all normative — see OPERATIONS.md, biz-service-onboarding.md):
|
|
266
|
+
* 1. Every key in `operations.operations` matches kebab-case regex
|
|
267
|
+
* /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/ (no camelCase, snake_case, or PascalCase).
|
|
268
|
+
* 2. `config.service.workspaceScoped` is an explicit boolean (no default).
|
|
269
|
+
* 3. `workspaceScoped: true` ⇒ `operations['list-workspaces']` MUST exist
|
|
270
|
+
* (workspace discovery convention per onboarding §4).
|
|
271
|
+
* 4. `workspaceScoped: false` ⇒ `operations['list-workspaces']` MUST NOT
|
|
272
|
+
* exist (flag is the authoritative signal; no ambiguity).
|
|
273
|
+
*
|
|
274
|
+
* All violations throw at construction time — service will not register,
|
|
275
|
+
* not start MQ listeners, will not reach production. Misnamed operations
|
|
276
|
+
* cannot ship.
|
|
277
|
+
*
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
_validateNamingAndWorkspaceScoped() {
|
|
281
|
+
const serviceName = this.config?.service?.name || '<unnamed-service>';
|
|
282
|
+
const kebabRegex = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
283
|
+
|
|
284
|
+
// Rule 1: every operation key must be kebab-case
|
|
285
|
+
const opsMap =
|
|
286
|
+
this.operations && typeof this.operations === 'object'
|
|
287
|
+
? (this.operations.operations && typeof this.operations.operations === 'object'
|
|
288
|
+
? this.operations.operations
|
|
289
|
+
: this.operations)
|
|
290
|
+
: null;
|
|
291
|
+
if (!opsMap || typeof opsMap !== 'object') {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`[ServiceWrapper][${serviceName}] operations schema is invalid - expected object with 'operations' map of operation definitions; see api/docs/standards/OPERATIONS.md`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const offenders = Object.keys(opsMap).filter((key) => !kebabRegex.test(key));
|
|
297
|
+
if (offenders.length > 0) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`[ServiceWrapper][${serviceName}] operation key(s) violate kebab-case convention: ` +
|
|
300
|
+
`${offenders.map((k) => `'${k}'`).join(', ')} - must match /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/. ` +
|
|
301
|
+
`See api/docs/standards/OPERATIONS.md §Best Practices - Naming Convention. ` +
|
|
302
|
+
`Rename the key(s) in operations.json plus every cookbook/test/Admin UI literal; there is no alias layer.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Rule 2: workspaceScoped must be explicit boolean
|
|
307
|
+
const ws = this.config?.service?.workspaceScoped;
|
|
308
|
+
if (typeof ws !== 'boolean') {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`[ServiceWrapper][${serviceName}] service.workspaceScoped is required and MUST be an explicit boolean in config.json (got ${ws === undefined ? 'undefined' : JSON.stringify(ws)}). ` +
|
|
311
|
+
`See api/docs/standards/biz-service-onboarding.md §3.1. ` +
|
|
312
|
+
`true = service stores data keyed by workspace_id and implements list-workspaces operation; ` +
|
|
313
|
+
`false = stateless or cross-workspace.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Rules 3 & 4: workspaceScoped consistency with list-workspaces
|
|
318
|
+
const hasListWorkspaces = Object.prototype.hasOwnProperty.call(opsMap, 'list-workspaces');
|
|
319
|
+
if (ws === true && !hasListWorkspaces) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`[ServiceWrapper][${serviceName}] service.workspaceScoped is true but 'list-workspaces' operation is not declared. ` +
|
|
322
|
+
`Workspace-scoped services MUST expose list-workspaces (workspace discovery convention). ` +
|
|
323
|
+
`See api/docs/standards/biz-service-onboarding.md §4 and OPERATIONS.md §Reserved operation names.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (ws === false && hasListWorkspaces) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`[ServiceWrapper][${serviceName}] service.workspaceScoped is false but 'list-workspaces' operation is declared. ` +
|
|
329
|
+
`Stateless / cross-workspace services MUST NOT declare list-workspaces - the flag is the authoritative signal. ` +
|
|
330
|
+
`See api/docs/standards/biz-service-onboarding.md §4.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
256
335
|
/**
|
|
257
336
|
* Process and validate configuration
|
|
258
337
|
* @private
|
|
@@ -413,7 +492,7 @@ class ServiceWrapper {
|
|
|
413
492
|
|
|
414
493
|
if (this.registryClient?.queueManager) {
|
|
415
494
|
try {
|
|
416
|
-
await this.registryClient.queueManager.
|
|
495
|
+
await this.registryClient.queueManager.close();
|
|
417
496
|
console.log(`[CLEANUP] ✓ Closed Registry RabbitMQ connection`);
|
|
418
497
|
} catch (err) {
|
|
419
498
|
console.warn(`[CLEANUP] Failed to close Registry RabbitMQ connection:`, err.message);
|
|
@@ -931,11 +1010,15 @@ class ServiceWrapper {
|
|
|
931
1010
|
await this.registryClient.init();
|
|
932
1011
|
this.logger?.info('Registry client initialized');
|
|
933
1012
|
|
|
934
|
-
// Register service
|
|
1013
|
+
// Register service.
|
|
1014
|
+
// workspaceScoped propagates verbatim to Redis spec so meta/Admin UI can
|
|
1015
|
+
// filter workspace-aware services without a hardcoded allowlist. Value is
|
|
1016
|
+
// guaranteed boolean by _validateNamingAndWorkspaceScoped().
|
|
935
1017
|
const serviceInfo = {
|
|
936
1018
|
name: serviceName,
|
|
937
1019
|
url: serviceUrl,
|
|
938
1020
|
operations: this.operations?.operations || {},
|
|
1021
|
+
workspaceScoped: this.config.service?.workspaceScoped,
|
|
939
1022
|
metadata: {
|
|
940
1023
|
version: this.config.service?.version,
|
|
941
1024
|
description: this.config.service?.description || ''
|
|
@@ -1403,7 +1486,8 @@ class ServiceWrapper {
|
|
|
1403
1486
|
}
|
|
1404
1487
|
|
|
1405
1488
|
const apiMapper = ApiMapperConnector.create({
|
|
1406
|
-
|
|
1489
|
+
// operations.json contract (see operations-registry-contract.md §3)
|
|
1490
|
+
operations: this.operations,
|
|
1407
1491
|
serviceUrl: serviceUrl,
|
|
1408
1492
|
directCall: false,
|
|
1409
1493
|
logger: this.logger
|
|
@@ -2003,6 +2087,7 @@ class ServiceWrapper {
|
|
|
2003
2087
|
name: serviceName,
|
|
2004
2088
|
url: serviceUrl,
|
|
2005
2089
|
operations: this.operations?.operations || {},
|
|
2090
|
+
workspaceScoped: this.config.service?.workspaceScoped,
|
|
2006
2091
|
metadata: {
|
|
2007
2092
|
version: serviceVersion,
|
|
2008
2093
|
description: this.config.service?.description || ''
|
package/src/index.js
CHANGED
|
@@ -149,6 +149,7 @@ async function bootstrap(serviceRoot, options = {}) {
|
|
|
149
149
|
url: config.service.url,
|
|
150
150
|
specificationEndpoint: config.service.specificationEndpoint,
|
|
151
151
|
description: config.service.description,
|
|
152
|
+
workspaceScoped: config.service.workspaceScoped,
|
|
152
153
|
env: config.service.env || runtimeCfg.get('nodeEnv')
|
|
153
154
|
},
|
|
154
155
|
wrapper: config.wrapper
|