@onlineapps/service-wrapper 2.3.8 → 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
- openApiSpec: require('./openapi.json'),
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.openApiSpec | <code>Object</code> | | OpenAPI specification |
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
- openApiSpec: require('./openapi.json'),
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.openApiSpec | <code>Object</code> | | OpenAPI specification |
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
- openApiSpec: require('./openapi.json'),
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.openApiSpec | <code>Object</code> | | OpenAPI specification |
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
- openApiSpec: require('./openapi.json'),
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.openApiSpec | <code>Object</code> | | OpenAPI specification |
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,7 +4,7 @@
4
4
  "description": "Operation with file output (MinIO storage)",
5
5
  "steps": [
6
6
  {
7
- "service": "hello-service",
7
+ "service": "biz-hello",
8
8
  "operation": "hello-cert",
9
9
  "input": {
10
10
  "name": "Igor",
@@ -4,14 +4,14 @@
4
4
  "description": "Multi-step workflow across services",
5
5
  "steps": [
6
6
  {
7
- "service": "hello-service",
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-service",
14
+ "service": "biz-hello",
15
15
  "operation": "hello-cert",
16
16
  "input": {
17
17
  "name": "Igor",
@@ -4,7 +4,7 @@
4
4
  "description": "Single operation example",
5
5
  "steps": [
6
6
  {
7
- "service": "hello-service",
7
+ "service": "biz-hello",
8
8
  "operation": "good-day",
9
9
  "input": {
10
10
  "name": "Igor"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.3.8",
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.10",
30
- "@onlineapps/conn-infra-error-handler": "1.0.9",
31
- "@onlineapps/conn-infra-mq": "1.1.69",
29
+ "@onlineapps/conn-base-monitoring": "1.0.12",
30
+ "@onlineapps/conn-infra-error-handler": "1.0.11",
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.38",
34
- "@onlineapps/conn-orch-orchestrator": "1.0.109",
33
+ "@onlineapps/conn-orch-cookbook": "2.1.0",
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.21",
38
- "@onlineapps/service-common": "1.0.18",
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": {
@@ -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,
@@ -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.disconnect();
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
- openApiSpec: this.operations,
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