@open-rlb/nestjs-amqp 2.0.3 → 2.0.5

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.
Files changed (71) hide show
  1. package/README.md +4 -2
  2. package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
  3. package/modules/acl/const.d.ts +1 -4
  4. package/modules/acl/const.js +1 -4
  5. package/modules/acl/const.js.map +1 -1
  6. package/modules/acl/models.d.ts +5 -7
  7. package/modules/acl/repository/acl-action.repository.d.ts +1 -5
  8. package/modules/acl/repository/acl-action.repository.js.map +1 -1
  9. package/modules/acl/repository/acl-grant.repository.d.ts +0 -1
  10. package/modules/acl/repository/acl-grant.repository.js.map +1 -1
  11. package/modules/acl/repository/acl-role.repository.d.ts +1 -5
  12. package/modules/acl/repository/acl-role.repository.js.map +1 -1
  13. package/modules/acl/services/acl-management.service.d.ts +6 -7
  14. package/modules/acl/services/acl-management.service.js +44 -61
  15. package/modules/acl/services/acl-management.service.js.map +1 -1
  16. package/modules/acl/services/acl.service.d.ts +1 -3
  17. package/modules/acl/services/acl.service.js +5 -47
  18. package/modules/acl/services/acl.service.js.map +1 -1
  19. package/modules/broker/broker.module.d.ts +2 -4
  20. package/modules/broker/broker.module.js +23 -5
  21. package/modules/broker/broker.module.js.map +1 -1
  22. package/modules/broker/config/route-discovery.config.d.ts +2 -0
  23. package/modules/broker/const.d.ts +1 -0
  24. package/modules/broker/const.js +2 -1
  25. package/modules/broker/const.js.map +1 -1
  26. package/modules/broker/services/broker.service.js +1 -1
  27. package/modules/broker/services/broker.service.js.map +1 -1
  28. package/modules/broker/services/route-discovery-publisher.service.js +7 -5
  29. package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
  30. package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
  31. package/modules/gateway-admin/const.d.ts +1 -1
  32. package/modules/gateway-admin/const.js +1 -1
  33. package/modules/gateway-admin/const.js.map +1 -1
  34. package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
  35. package/modules/gateway-admin/gateway-admin.module.js +13 -0
  36. package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
  37. package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
  38. package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
  39. package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
  40. package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
  41. package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
  42. package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
  43. package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
  44. package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
  45. package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
  46. package/modules/gateway-admin/services/route-sync.service.js +14 -8
  47. package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
  48. package/modules/proxy/services/http-handler.service.d.ts +3 -0
  49. package/modules/proxy/services/http-handler.service.js +28 -4
  50. package/modules/proxy/services/http-handler.service.js.map +1 -1
  51. package/package.json +5 -1
  52. package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
  53. package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
  54. package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
  55. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
  56. package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
  57. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +30 -5
  58. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
  59. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +120 -79
  60. package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
  61. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +49 -2
  62. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
  63. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
  64. package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +233 -0
  65. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +172 -42
  66. package/schematics/nest-add/index.js +612 -142
  67. package/schematics/nest-add/index.js.map +1 -1
  68. package/schematics/nest-add/index.ts +673 -241
  69. package/schematics/nest-add/init.schema.d.ts +10 -1
  70. package/schematics/nest-add/init.schema.ts +29 -3
  71. package/schematics/nest-add/schema.json +37 -8
@@ -7,38 +7,185 @@ const path_1 = require("path");
7
7
  const formatting_1 = require("../utils/formatting");
8
8
  const name_parser_1 = require("../utils/name.parser");
9
9
  const source_root_helpers_1 = require("../utils/source-root.helpers");
10
- function brokerImportLine(gateway) {
11
- const broker = gateway
12
- ? "import { AppConfig, BrokerModule, BrokerTopic, GatewayConfig, HandlerAuthConfig, ProxyModule, RabbitMQConfig } from '@open-rlb/nestjs-amqp';"
13
- : "import { AppConfig, BrokerModule, BrokerTopic, RabbitMQConfig } from '@open-rlb/nestjs-amqp';";
14
- const config = "\nimport { ConfigModule, ConfigService } from '@nestjs/config';";
15
- const http = gateway ? "\nimport { HttpModule } from '@nestjs/axios';" : '';
16
- return broker + config + http;
17
- }
18
- function brokerForRootAsync() {
19
- return `BrokerModule.forRootAsync({
20
- imports: [ConfigModule],
21
- inject: [ConfigService],
22
- useFactory: async (configService: ConfigService) => ({
23
- options: configService.get<RabbitMQConfig>('broker')!,
24
- topics: configService.get<BrokerTopic[]>('topics')!,
25
- appOptions: configService.get<AppConfig>('app'),
26
- })
27
- })`;
10
+ function defaultNames(project) {
11
+ return {
12
+ exchange: 'rlb',
13
+ aclQueue: 'rlb-acl',
14
+ adminQueue: 'rlb-gateway-admin',
15
+ controlTopic: 'rlb-gateway-control',
16
+ routeExchange: 'rlb-route-discovery',
17
+ routeQueue: 'rlb-route-sync',
18
+ serviceName: project || 'my-service',
19
+ };
28
20
  }
29
- function proxyForRootAsync() {
30
- return `ProxyModule.forRootAsync({
31
- imports: [ConfigModule],
32
- inject: [ConfigService],
33
- useFactory: (configService: ConfigService) => ({
34
- authOptions: configService.get<HandlerAuthConfig[]>('auth-providers'),
35
- gatewayOptions: configService.get<GatewayConfig>('gateway'),
36
- }),
37
- providers: [],
38
- })`;
21
+ async function resolveSelections(o, context) {
22
+ const project = (o.project || 'my-service').toString();
23
+ const d = defaultNames(project);
24
+ const flagsProvided = o.gatewayConfig !== undefined || (Array.isArray(o.features) && o.features.length > 0);
25
+ const canPrompt = !!process.stdout && !!process.stdout.isTTY && !process.env.CI && !flagsProvided;
26
+ let prompts;
27
+ if (canPrompt) {
28
+ try {
29
+ prompts = require('@inquirer/prompts');
30
+ }
31
+ catch {
32
+ context.logger.warn('[nest-add] @inquirer/prompts not found; falling back to flags/defaults (non-interactive).');
33
+ prompts = undefined;
34
+ }
35
+ }
36
+ if (!prompts) {
37
+ const features = new Set((o.features || []).map((f) => String(f).trim()));
38
+ const gatewayConfig = o.gatewayConfig === true;
39
+ return {
40
+ gatewayConfig,
41
+ acl: gatewayConfig && features.has('acl'),
42
+ admin: gatewayConfig && features.has('gateway-admin'),
43
+ routeReception: gatewayConfig && features.has('route-reception'),
44
+ autoPublish: !gatewayConfig && features.has('auto-config-publish'),
45
+ skills: o.skills !== false,
46
+ names: {
47
+ exchange: o.exchange || d.exchange,
48
+ aclQueue: o.aclQueue || d.aclQueue,
49
+ adminQueue: o.adminQueue || d.adminQueue,
50
+ controlTopic: o.controlTopic || d.controlTopic,
51
+ routeExchange: o.routeExchange || d.routeExchange,
52
+ routeQueue: o.routeQueue || d.routeQueue,
53
+ serviceName: o.serviceName || d.serviceName,
54
+ },
55
+ };
56
+ }
57
+ const { confirm, checkbox, input } = prompts;
58
+ const names = { ...d };
59
+ let acl = false, admin = false, routeReception = false, autoPublish = false;
60
+ const gatewayConfig = await confirm({
61
+ message: 'Create a gateway (HTTP/WebSocket) configuration?',
62
+ default: false,
63
+ });
64
+ if (gatewayConfig) {
65
+ const picked = await checkbox({
66
+ message: 'Select gateway features to include',
67
+ choices: [
68
+ { name: 'ACL — role-based authorization + management', value: 'acl' },
69
+ { name: 'Gateway admin + auth — DB-managed routes, auth-providers, metrics', value: 'gateway-admin' },
70
+ { name: 'Route reception — apply routes auto-published by microservices', value: 'route-reception' },
71
+ ],
72
+ });
73
+ acl = picked.includes('acl');
74
+ admin = picked.includes('gateway-admin');
75
+ routeReception = picked.includes('route-reception');
76
+ if (acl || admin || routeReception) {
77
+ names.exchange = await input({ message: 'Main AMQP exchange name', default: d.exchange });
78
+ }
79
+ if (acl) {
80
+ names.aclQueue = await input({ message: 'Queue backing the rlb-acl topic', default: d.aclQueue });
81
+ }
82
+ if (admin || routeReception) {
83
+ names.adminQueue = await input({ message: 'Queue backing the rlb-gateway-admin topic', default: d.adminQueue });
84
+ names.controlTopic = await input({ message: 'Broadcast control/reload topic name', default: d.controlTopic });
85
+ }
86
+ if (routeReception) {
87
+ names.routeExchange = await input({ message: 'Route-discovery exchange (must match the publishers)', default: d.routeExchange });
88
+ names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
89
+ }
90
+ }
91
+ else {
92
+ const picked = await checkbox({
93
+ message: 'Select microservice features to include',
94
+ choices: [
95
+ { name: 'Auto-send config at startup — publish this service’s @BrokerHTTP routes to the gateway on boot', value: 'auto-config-publish' },
96
+ ],
97
+ });
98
+ autoPublish = picked.includes('auto-config-publish');
99
+ if (autoPublish) {
100
+ names.serviceName = await input({ message: 'Service name (route ownership + AMQP connection_name)', default: d.serviceName });
101
+ names.routeExchange = await input({ message: 'Route-discovery exchange', default: d.routeExchange });
102
+ names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
103
+ }
104
+ }
105
+ const skills = await confirm({ message: 'Copy the Claude skills into .claude/skills?', default: true });
106
+ return { gatewayConfig, acl, admin, routeReception, autoPublish, skills, names };
39
107
  }
40
- const CONFIG_YAML_BASE = `
41
- app:
108
+ function buildConfigYaml(sel) {
109
+ const n = sel.names;
110
+ const anyAdmin = sel.admin || sel.routeReception;
111
+ const routeDiscoveryPub = sel.autoPublish ? `
112
+ # Route auto-discovery (publisher side): announce this service's @BrokerHTTP routes on boot.
113
+ # serviceName also fills the AMQP connection_name (none is set explicitly below).
114
+ routeDiscovery:
115
+ serviceName: "${n.serviceName}"
116
+ publishOnBoot: true
117
+ exchange: ${n.routeExchange}
118
+ queue: ${n.routeQueue}` : '';
119
+ const clientProps = sel.autoPublish ? '' : `
120
+ clientProperties:
121
+ connection_name: "<APP_NAME>"`;
122
+ const exchanges = [];
123
+ if (sel.acl || anyAdmin) {
124
+ exchanges.push(` - name: ${n.exchange}
125
+ type: "direct"
126
+ createExchangeIfNotExists: true
127
+ options:
128
+ durable: true`);
129
+ }
130
+ exchanges.push(` - name: example.fanout
131
+ type: "fanout"
132
+ createExchangeIfNotExists: true
133
+ options:
134
+ durable: true
135
+ autoDelete: false
136
+ internal: false`);
137
+ const queues = [];
138
+ if (sel.acl) {
139
+ queues.push(` - name: ${n.aclQueue}
140
+ exchange: ${n.exchange}
141
+ routingKey: ${n.aclQueue}
142
+ createQueueIfNotExists: true
143
+ options:
144
+ durable: true`);
145
+ }
146
+ if (anyAdmin) {
147
+ queues.push(` - name: ${n.adminQueue}
148
+ exchange: ${n.exchange}
149
+ routingKey: ${n.adminQueue}
150
+ createQueueIfNotExists: true
151
+ options:
152
+ durable: true`);
153
+ }
154
+ queues.push(` - name: example.queue
155
+ exchange: example.fanout
156
+ routingKey: example.queue
157
+ createQueueIfNotExists: true
158
+ options:
159
+ durable: true
160
+ autoDelete: false
161
+ exclusive: false`);
162
+ const topics = [];
163
+ if (sel.acl) {
164
+ topics.push(` # Fixed topic name (decorator-bound in the lib): the ACL handlers bind to 'rlb-acl'.
165
+ - name: rlb-acl
166
+ mode: rpc
167
+ queue: ${n.aclQueue}
168
+ exchange: ${n.exchange}
169
+ routingKey: ${n.aclQueue}`);
170
+ }
171
+ if (anyAdmin) {
172
+ topics.push(` # Fixed topic name (decorator-bound): the gateway-admin handlers bind to 'rlb-gateway-admin'.
173
+ - name: rlb-gateway-admin
174
+ mode: rpc
175
+ queue: ${n.adminQueue}
176
+ exchange: ${n.exchange}
177
+ routingKey: ${n.adminQueue}
178
+ # Broadcast control topic: the gateway rebuilds its routes at runtime on a 'gw-reload'.
179
+ - name: ${n.controlTopic}
180
+ mode: broadcast
181
+ exchange: ${n.exchange}
182
+ routingKey: ${n.controlTopic}`);
183
+ }
184
+ topics.push(` - name: example.topic
185
+ exchange: example.fanout
186
+ routingKey: "example.topic"
187
+ mode: event`);
188
+ let yaml = `app:
42
189
  port: 80
43
190
  host: 0.0.0.0
44
191
  environment: "development"
@@ -49,72 +196,395 @@ broker:
49
196
  name: "rabbitmq"
50
197
  uri: "<AMQP_URI>"
51
198
  defaultSubscribeErrorBehavior: "ack"
52
- defaultPublishErrorBehavior: "reject"
199
+ defaultPublishErrorBehavior: "reject"${routeDiscoveryPub}
53
200
  connectionManagerOptions:
54
201
  heartbeatIntervalInSeconds: 60
55
202
  reconnectTimeInSeconds: 60
56
- connectionOptions:
57
- clientProperties:
58
- connection_name: "<APP_NAME>"
203
+ connectionOptions:${clientProps}
59
204
  credentials:
60
205
  mechanism: PLAIN
61
206
  username: "<AMQP_USERNAME>"
62
207
  password: "<AMQP_PASSWORD>"
63
208
  exchanges:
64
- - name: example.fanout
65
- type: "fanout"
66
- createExchangeIfNotExists: true
67
- options:
68
- durable: true
69
- autoDelete: false
70
- internal: false
209
+ ${exchanges.join('\n')}
71
210
  queues:
72
- - name: example.queue
73
- createQueueIfNotExists: true
74
- exchange: example.fanout
75
- routingKey: example.queue
76
- options:
77
- durable: true
78
- autoDelete: false
79
- exclusive: false
211
+ ${queues.join('\n')}
80
212
 
81
213
  topics:
82
- - name: example.topic
83
- exchange: example.fanout
84
- routingKey: "example.topic"
85
- mode: event
214
+ ${topics.join('\n')}
86
215
  `;
87
- const CONFIG_YAML_GATEWAY = `
88
- gateway:
216
+ if (sel.gatewayConfig) {
217
+ yaml += '\n' + buildGatewayBlock(sel);
218
+ }
219
+ return yaml;
220
+ }
221
+ function buildGatewayBlock(sel) {
222
+ const n = sel.names;
223
+ const anyAdmin = sel.admin || sel.routeReception;
224
+ const paths = [];
225
+ if (sel.acl)
226
+ paths.push(ACL_PATHS);
227
+ if (sel.admin)
228
+ paths.push(adminPaths(n.controlTopic));
229
+ paths.push(` - name: example-path
230
+ method: POST
231
+ dataSource: body
232
+ path: /example
233
+ topic: example.topic
234
+ action: example-action
235
+ mode: event`);
236
+ let block = `gateway:
89
237
  events: []
90
238
  ws:
91
239
  heartbeatIntervalMs: 30000
92
240
  # Auth is declared per-event (events[].auth / requireAuth / roles / scopeClaim).
93
241
  paths:
94
- - name: example-path
242
+ ${paths.join('\n')}`;
243
+ if (anyAdmin) {
244
+ block += `
245
+ # Load DB-managed routes at boot AND on every runtime reload (ordered static-before-param).
246
+ loadConfig:
247
+ paths:
248
+ topic: rlb-gateway-admin
249
+ action: gw-path-export
250
+ # Broadcast topic that triggers a runtime route rebuild (see topics: ${n.controlTopic}).
251
+ reloadTopic: ${n.controlTopic}
252
+ # Per-request metrics auto-emitted to the gateway-admin metrics handler.
253
+ metrics:
254
+ topic: rlb-gateway-admin
255
+ action: gw-metrics-track`;
256
+ }
257
+ return block + '\n';
258
+ }
259
+ const ACL_PATHS = ` # --- ACL management: actions (name is the key — PUT upserts, GET lists, DELETE by name) ---
260
+ - name: acl-action-list
261
+ method: GET
262
+ path: /acl/actions
263
+ dataSource: query
264
+ topic: rlb-acl
265
+ action: acl-action-list
266
+ mode: rpc
267
+ - name: acl-action-get
268
+ method: GET
269
+ path: /acl/actions/get
270
+ dataSource: query
271
+ topic: rlb-acl
272
+ action: acl-action-get
273
+ mode: rpc
274
+ - name: acl-action-upsert
275
+ method: PUT
276
+ path: /acl/actions
277
+ dataSource: body
278
+ topic: rlb-acl
279
+ action: acl-action-update
280
+ mode: rpc
281
+ - name: acl-action-delete
282
+ method: DELETE
283
+ path: /acl/actions
284
+ dataSource: body
285
+ topic: rlb-acl
286
+ action: acl-action-delete
287
+ mode: rpc
288
+ # --- ACL management: roles ---
289
+ - name: acl-role-list
290
+ method: GET
291
+ path: /acl/roles
292
+ dataSource: query
293
+ topic: rlb-acl
294
+ action: acl-role-list
295
+ mode: rpc
296
+ - name: acl-role-get
297
+ method: GET
298
+ path: /acl/roles/get
299
+ dataSource: query
300
+ topic: rlb-acl
301
+ action: acl-role-get
302
+ mode: rpc
303
+ - name: acl-role-upsert
304
+ method: PUT
305
+ path: /acl/roles
306
+ dataSource: body
307
+ topic: rlb-acl
308
+ action: acl-role-update
309
+ mode: rpc
310
+ - name: acl-role-delete
311
+ method: DELETE
312
+ path: /acl/roles
313
+ dataSource: body
314
+ topic: rlb-acl
315
+ action: acl-role-delete
316
+ mode: rpc
317
+ # --- ACL grants (per-user; resourceId/companyId optional) ---
318
+ - name: acl-grant
95
319
  method: POST
320
+ path: /acl/grants
96
321
  dataSource: body
97
- path: /example
98
- topic: example.topic
99
- action: example-action
322
+ topic: rlb-acl
323
+ action: acl-grant
324
+ mode: rpc
325
+ - name: acl-revoke
326
+ method: DELETE
327
+ path: /acl/grants
328
+ dataSource: body
329
+ topic: rlb-acl
330
+ action: acl-revoke
331
+ mode: rpc
332
+ # --- ACL checks (GET → 200 with true/false) ---
333
+ - name: acl-check-gtw
334
+ method: GET
335
+ path: /acl/check
336
+ dataSource: query
337
+ topic: rlb-acl
338
+ action: acl-can-user-do-gtw
339
+ mode: rpc
340
+ - name: acl-check-resource
341
+ method: GET
342
+ path: /acl/check-resource
343
+ dataSource: query
344
+ topic: rlb-acl
345
+ action: acl-can-user-do
346
+ mode: rpc
347
+ # Lists the caller's accessible resources. Add an 'auth: <provider>' line once you declare an auth-provider.
348
+ - name: acl-list-resources-by-user
349
+ method: GET
350
+ path: /acl/resources
351
+ dataSource: query
352
+ topic: rlb-acl
353
+ action: acl-list-resources-by-user
354
+ mode: rpc`;
355
+ function adminPaths(controlTopic) {
356
+ return ` # --- gateway-admin: health + DB routes + auth-providers + metrics ---
357
+ - name: health
358
+ method: GET
359
+ path: /health
360
+ dataSource: query
361
+ topic: rlb-gateway-admin
362
+ action: gw-health
363
+ mode: rpc
364
+ - name: gw-path-create
365
+ method: POST
366
+ path: /admin/paths
367
+ dataSource: body
368
+ topic: rlb-gateway-admin
369
+ action: gw-path-create
370
+ mode: rpc
371
+ - name: gw-path-list
372
+ method: GET
373
+ path: /admin/paths
374
+ dataSource: query
375
+ topic: rlb-gateway-admin
376
+ action: gw-path-list
377
+ mode: rpc
378
+ - name: gw-path-export
379
+ method: GET
380
+ path: /admin/paths/export
381
+ dataSource: query
382
+ topic: rlb-gateway-admin
383
+ action: gw-path-export
384
+ mode: rpc
385
+ - name: gw-path-update
386
+ method: PUT
387
+ path: /admin/paths
388
+ dataSource: body
389
+ topic: rlb-gateway-admin
390
+ action: gw-path-update
391
+ mode: rpc
392
+ - name: gw-path-get
393
+ method: GET
394
+ path: /admin/paths/get
395
+ dataSource: query
396
+ topic: rlb-gateway-admin
397
+ action: gw-path-get
398
+ mode: rpc
399
+ - name: gw-path-delete
400
+ method: DELETE
401
+ path: /admin/paths
402
+ dataSource: body
403
+ topic: rlb-gateway-admin
404
+ action: gw-path-delete
405
+ mode: rpc
406
+ - name: gw-auth-list
407
+ method: GET
408
+ path: /admin/auth
409
+ dataSource: query
410
+ topic: rlb-gateway-admin
411
+ action: gw-auth-list
412
+ mode: rpc
413
+ - name: gw-auth-upsert
414
+ method: PUT
415
+ path: /admin/auth
416
+ dataSource: body
417
+ topic: rlb-gateway-admin
418
+ action: gw-auth-update
419
+ mode: rpc
420
+ - name: gw-auth-get
421
+ method: GET
422
+ path: /admin/auth/get
423
+ dataSource: query
424
+ topic: rlb-gateway-admin
425
+ action: gw-auth-get
426
+ mode: rpc
427
+ - name: gw-auth-delete
428
+ method: DELETE
429
+ path: /admin/auth
430
+ dataSource: body
431
+ topic: rlb-gateway-admin
432
+ action: gw-auth-delete
433
+ mode: rpc
434
+ - name: gw-metrics-get
435
+ method: GET
436
+ path: /admin/metrics
437
+ dataSource: query
438
+ topic: rlb-gateway-admin
439
+ action: gw-metrics-get
440
+ mode: rpc
441
+ - name: gw-metrics-series
442
+ method: GET
443
+ path: /admin/metrics/series
444
+ dataSource: query
445
+ topic: rlb-gateway-admin
446
+ action: gw-metrics-series
447
+ mode: rpc
448
+ - name: gw-metrics-points
449
+ method: GET
450
+ path: /admin/metrics/points
451
+ dataSource: query
452
+ topic: rlb-gateway-admin
453
+ action: gw-metrics-points
454
+ mode: rpc
455
+ - name: gw-metrics-track
456
+ method: POST
457
+ path: /admin/metrics/track
458
+ dataSource: body
459
+ topic: rlb-gateway-admin
460
+ action: gw-metrics-track
100
461
  mode: event
101
- `;
102
- function configYaml(gateway) {
103
- return gateway ? CONFIG_YAML_BASE.trimEnd() + '\n' + CONFIG_YAML_GATEWAY : CONFIG_YAML_BASE;
462
+ - name: gw-reload
463
+ method: POST
464
+ path: /admin/reload
465
+ dataSource: body
466
+ topic: ${controlTopic}
467
+ action: gw-reload
468
+ mode: event`;
469
+ }
470
+ function buildImportStatements(sel) {
471
+ const libSymbols = ['AppConfig', 'BrokerModule', 'BrokerTopic', 'RabbitMQConfig'];
472
+ if (sel.gatewayConfig)
473
+ libSymbols.push('GatewayConfig', 'HandlerAuthConfig', 'ProxyModule');
474
+ if (sel.acl)
475
+ libSymbols.push('AclActionRepository', 'AclGrantRepository', 'AclModule', 'AclRoleRepository', 'AclService', 'RLB_ACL_CACHE_STORE', 'RLB_GTW_ACL_ROLE_SERVICE');
476
+ if (sel.admin || sel.routeReception)
477
+ libSymbols.push('AuthProviderRepository', 'GatewayAdminModule', 'HttpMetricRepository', 'HttpPathRepository', 'RouteSyncLogRepository');
478
+ const lib = `import { ${[...new Set(libSymbols)].sort().join(', ')} } from '@open-rlb/nestjs-amqp';`;
479
+ const lines = [lib, `import { ConfigModule, ConfigService } from '@nestjs/config';`, `import yamlConfig from './config/config.loader';`];
480
+ if (sel.gatewayConfig)
481
+ lines.push(`import { HttpModule } from '@nestjs/axios';`);
482
+ if (sel.acl) {
483
+ lines.push(`import { InMemoryAclActionRepository, InMemoryAclGrantRepository, InMemoryAclRoleRepository } from './modules/database/repository/acl.repository';`);
484
+ lines.push(`import { InMemoryAclStore } from './cache/in-memory-acl-store';`);
485
+ }
486
+ if (sel.admin || sel.routeReception) {
487
+ lines.push(`import { InMemoryAuthProviderRepository, InMemoryHttpMetricRepository, InMemoryHttpPathRepository } from './modules/database/repository/gateway.repository';`);
488
+ lines.push(`import { InMemoryRouteSyncLogRepository } from './modules/database/repository/route-sync.repository';`);
489
+ }
490
+ return lines.join('\n');
491
+ }
492
+ function brokerForRootAsync() {
493
+ return `BrokerModule.forRootAsync({
494
+ imports: [ConfigModule],
495
+ inject: [ConfigService],
496
+ useFactory: async (configService: ConfigService) => ({
497
+ options: configService.get<RabbitMQConfig>('broker')!,
498
+ topics: configService.get<BrokerTopic[]>('topics')!,
499
+ appOptions: configService.get<AppConfig>('app'),
500
+ })
501
+ })`;
502
+ }
503
+ function proxyForRootAsync(sel) {
504
+ const providers = sel.acl
505
+ ? `[
506
+ // Role-gated paths resolve the caller's roles via AclService (in-process, no broker hop).
507
+ { provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService },
508
+ ]`
509
+ : `[]`;
510
+ return `ProxyModule.forRootAsync({
511
+ imports: [ConfigModule],
512
+ inject: [ConfigService],
513
+ useFactory: (configService: ConfigService) => ({
514
+ authOptions: configService.get<HandlerAuthConfig[]>('auth-providers'),
515
+ gatewayOptions: configService.get<GatewayConfig>('gateway'),
516
+ }),
517
+ providers: ${providers},
518
+ })`;
519
+ }
520
+ function aclForRoot() {
521
+ return `AclModule.forRoot(
522
+ [
523
+ InMemoryAclActionRepository,
524
+ { provide: AclActionRepository, useExisting: InMemoryAclActionRepository },
525
+ InMemoryAclRoleRepository,
526
+ { provide: AclRoleRepository, useExisting: InMemoryAclRoleRepository },
527
+ InMemoryAclGrantRepository,
528
+ { provide: AclGrantRepository, useExisting: InMemoryAclGrantRepository },
529
+ InMemoryAclStore,
530
+ { provide: RLB_ACL_CACHE_STORE, useExisting: InMemoryAclStore },
531
+ ],
532
+ { cache: { ramTtlMs: 30000, l2TtlSec: 600 } },
533
+ )`;
534
+ }
535
+ function gatewayAdminForRoot(sel) {
536
+ const options = sel.routeReception
537
+ ? `,
538
+ {
539
+ // Consumer-side route-discovery — names MUST match the publishers' broker.routeDiscovery.
540
+ routeDiscovery: { exchange: '${sel.names.routeExchange}', queue: '${sel.names.routeQueue}' },
541
+ }`
542
+ : '';
543
+ return `GatewayAdminModule.forRoot(
544
+ [
545
+ InMemoryHttpPathRepository,
546
+ { provide: HttpPathRepository, useExisting: InMemoryHttpPathRepository },
547
+ InMemoryAuthProviderRepository,
548
+ { provide: AuthProviderRepository, useExisting: InMemoryAuthProviderRepository },
549
+ InMemoryHttpMetricRepository,
550
+ { provide: HttpMetricRepository, useExisting: InMemoryHttpMetricRepository },
551
+ InMemoryRouteSyncLogRepository,
552
+ { provide: RouteSyncLogRepository, useExisting: InMemoryRouteSyncLogRepository },
553
+ ]${options},
554
+ )`;
555
+ }
556
+ function buildModuleEntries(sel) {
557
+ const entries = [];
558
+ entries.push(`ConfigModule.forRoot({ isGlobal: true, load: [yamlConfig] })`);
559
+ entries.push(brokerForRootAsync());
560
+ if (sel.gatewayConfig) {
561
+ entries.push('HttpModule');
562
+ entries.push(proxyForRootAsync(sel));
563
+ }
564
+ if (sel.acl)
565
+ entries.push(aclForRoot());
566
+ if (sel.admin || sel.routeReception)
567
+ entries.push(gatewayAdminForRoot(sel));
568
+ return entries;
104
569
  }
105
570
  function main(options) {
571
+ const project = (options.project || 'my-service').toString();
106
572
  options = transform(options);
107
- const gateway = options.gateway !== false;
108
- const skills = options.skills !== false;
109
- return (tree, context) => {
573
+ return async (tree, context) => {
574
+ const sel = await resolveSelections({ ...options, project }, context);
575
+ const anyRepo = sel.acl || sel.admin || sel.routeReception;
110
576
  return (0, schematics_1.branchAndMerge)((0, schematics_1.chain)([
111
577
  (0, source_root_helpers_1.mergeSourceRoot)(options),
112
- addBrokerModuleToAppModule(gateway),
113
- updateConfigYaml(gateway),
114
- gateway ? configureMainForGateway() : (0, schematics_1.noop)(),
115
- updatePackageJson(options),
116
- skills ? copySkills() : (0, schematics_1.noop)(),
117
- ]))(tree, context);
578
+ addModulesToAppModule(sel),
579
+ createConfigLoader(),
580
+ updateConfigYaml(sel),
581
+ sel.gatewayConfig ? configureMainForGateway() : (0, schematics_1.noop)(),
582
+ anyRepo ? copyAsset('db-core') : (0, schematics_1.noop)(),
583
+ sel.acl ? copyAsset('acl') : (0, schematics_1.noop)(),
584
+ (sel.admin || sel.routeReception) ? copyAsset('gateway-admin') : (0, schematics_1.noop)(),
585
+ sel.skills ? copySkills() : (0, schematics_1.noop)(),
586
+ updatePackageJson(sel),
587
+ ]));
118
588
  };
119
589
  }
120
590
  function transform(source) {
@@ -128,26 +598,45 @@ function transform(source) {
128
598
  target.specFileSuffix = (0, formatting_1.normalizeToKebabOrSnakeCase)(source.specFileSuffix || 'spec');
129
599
  return target;
130
600
  }
601
+ function copyAsset(name) {
602
+ return (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)(`./files/${name}`), [(0, schematics_1.move)((0, path_1.normalize)('.'))]), schematics_1.MergeStrategy.Overwrite);
603
+ }
131
604
  function copySkills() {
132
605
  return (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)('./files/skills'), [(0, schematics_1.move)((0, path_1.normalize)('.claude/skills'))]), schematics_1.MergeStrategy.Overwrite);
133
606
  }
134
- function updateConfigYaml(gateway) {
607
+ function createConfigLoader() {
608
+ return (tree) => {
609
+ const path = 'src/config/config.loader.ts';
610
+ if (tree.exists(path))
611
+ return tree;
612
+ tree.create(path, `import { readFileSync } from 'fs';
613
+ import * as yaml from 'js-yaml';
614
+ import { join } from 'path';
615
+
616
+ const YAML_CONFIG_FILENAME = 'config/config.yaml';
617
+
618
+ export default () =>
619
+ yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
620
+ `);
621
+ return tree;
622
+ };
623
+ }
624
+ function updateConfigYaml(sel) {
135
625
  return (tree) => {
136
626
  const CONFIG_PATH = 'config/config.yaml';
137
- const block = configYaml(gateway);
627
+ const block = buildConfigYaml(sel);
138
628
  if (!tree.exists(CONFIG_PATH)) {
139
- tree.create(CONFIG_PATH, block.trimStart());
629
+ tree.create(CONFIG_PATH, block);
140
630
  return tree;
141
631
  }
142
632
  const existing = tree.read(CONFIG_PATH).toString('utf-8');
143
- const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(gateway ? ['gateway:'] : [])];
633
+ const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(sel.gatewayConfig ? ['gateway:'] : [])];
144
634
  let toAppend = '';
145
635
  for (const key of SECTION_KEYS) {
146
636
  if (!existing.includes(key)) {
147
637
  const section = extractYamlSection(block, key);
148
- if (section) {
149
- toAppend += '\n' + section;
150
- }
638
+ if (section)
639
+ toAppend += '\n' + section + '\n';
151
640
  }
152
641
  }
153
642
  if (toAppend.length > 0) {
@@ -158,47 +647,34 @@ function updateConfigYaml(gateway) {
158
647
  }
159
648
  function extractYamlSection(yaml, sectionKey) {
160
649
  const lines = yaml.split('\n');
161
- const startIdx = lines.findIndex(l => l.startsWith(sectionKey));
650
+ const startIdx = lines.findIndex((l) => l.startsWith(sectionKey));
162
651
  if (startIdx === -1)
163
652
  return '';
164
653
  const endIdx = lines.findIndex((l, i) => i > startIdx && l.length > 0 && !l.startsWith(' ') && !l.startsWith('#'));
165
654
  const sectionLines = endIdx === -1 ? lines.slice(startIdx) : lines.slice(startIdx, endIdx);
166
655
  return sectionLines.join('\n').trimEnd();
167
656
  }
168
- function addBrokerModuleToAppModule(gateway) {
657
+ function addModulesToAppModule(sel) {
169
658
  return (tree) => {
170
- const candidatePaths = [
171
- '/src/app.module.ts',
172
- '/app/app.module.ts',
173
- 'src/app.module.ts',
174
- 'app/app.module.ts',
175
- ];
176
- let modulePath = candidatePaths.find(p => tree.exists(p));
177
- if (!modulePath) {
178
- modulePath = findFileInTree(tree, 'app.module.ts');
179
- }
659
+ const candidatePaths = ['/src/app.module.ts', '/app/app.module.ts', 'src/app.module.ts', 'app/app.module.ts'];
660
+ let modulePath = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'app.module.ts');
180
661
  if (!modulePath) {
181
- console.warn('[nest-add] app.module.ts non trovato: BrokerModule non aggiunto.');
662
+ console.warn('[nest-add] app.module.ts not found: AppModule wiring skipped.');
182
663
  return tree;
183
664
  }
184
- const rawContent = tree.read(modulePath);
185
- if (!rawContent) {
665
+ const raw = tree.read(modulePath);
666
+ if (!raw)
667
+ return tree;
668
+ let content = raw.toString('utf-8');
669
+ if (content.includes('BrokerModule.forRootAsync'))
186
670
  return tree;
187
- }
188
- let content = rawContent.toString('utf-8');
189
671
  if (!content.includes('@open-rlb/nestjs-amqp')) {
190
- const importInsertPos = findLastImportEndIndex(content);
191
- content =
192
- content.slice(0, importInsertPos) +
193
- '\n' + brokerImportLine(gateway) +
194
- content.slice(importInsertPos);
672
+ const pos = findLastImportEndIndex(content);
673
+ content = content.slice(0, pos) + '\n' + buildImportStatements(sel) + content.slice(pos);
195
674
  }
196
- if (!content.includes('BrokerModule.forRootAsync')) {
197
- if (gateway) {
198
- content = insertIntoImportsArray(content, proxyForRootAsync());
199
- content = insertIntoImportsArray(content, 'HttpModule');
200
- }
201
- content = insertIntoImportsArray(content, brokerForRootAsync());
675
+ const entries = buildModuleEntries(sel);
676
+ for (let i = entries.length - 1; i >= 0; i--) {
677
+ content = insertIntoImportsArray(content, entries[i]);
202
678
  }
203
679
  tree.overwrite(modulePath, content);
204
680
  return tree;
@@ -207,37 +683,34 @@ function addBrokerModuleToAppModule(gateway) {
207
683
  function configureMainForGateway() {
208
684
  return (tree) => {
209
685
  const candidatePaths = ['/src/main.ts', '/app/main.ts', 'src/main.ts', 'app/main.ts'];
210
- const mainPath = candidatePaths.find(p => tree.exists(p)) || findFileInTree(tree, 'main.ts');
686
+ const mainPath = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'main.ts');
211
687
  if (!mainPath) {
212
- console.warn('[nest-add] main.ts non trovato: abilita manualmente rawBody e WsAdapter.');
688
+ console.warn('[nest-add] main.ts not found: enable rawBody + WsAdapter manually.');
213
689
  return tree;
214
690
  }
215
691
  let content = tree.read(mainPath).toString('utf-8');
216
- if (content.includes('WsAdapter')) {
692
+ if (content.includes('WsAdapter'))
217
693
  return tree;
218
- }
219
694
  const pos = findLastImportEndIndex(content);
220
695
  content = content.slice(0, pos) + "\nimport { WsAdapter } from '@nestjs/platform-ws';" + content.slice(pos);
221
696
  const m = content.match(/const\s+(\w+)\s*=\s*await\s+NestFactory\.create\(\s*([A-Za-z0-9_]+)\s*(,\s*\{[^}]*\})?\s*\)\s*;?/);
222
697
  if (m) {
223
698
  const appVar = m[1];
224
699
  const moduleArg = m[2];
225
- const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));`;
700
+ const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));\n ${appVar}.enableShutdownHooks();`;
226
701
  content = content.replace(m[0], replacement);
227
702
  }
228
703
  else {
229
- console.warn('[nest-add] NestFactory.create() non trovato in main.ts: aggiungi { rawBody: true } e useWebSocketAdapter manualmente.');
704
+ console.warn('[nest-add] NestFactory.create() not found in main.ts: add { rawBody: true } + useWebSocketAdapter manually.');
230
705
  }
231
706
  tree.overwrite(mainPath, content);
232
707
  return tree;
233
708
  };
234
709
  }
235
710
  function insertIntoImportsArray(source, moduleEntry) {
236
- const importsArrayRegex = /imports\s*:\s*\[/;
237
- const match = importsArrayRegex.exec(source);
238
- if (!match) {
711
+ const match = /imports\s*:\s*\[/.exec(source);
712
+ if (!match)
239
713
  return source;
240
- }
241
714
  const openBracketPos = source.indexOf('[', match.index);
242
715
  let depth = 0;
243
716
  let closeBracketPos = -1;
@@ -252,20 +725,13 @@ function insertIntoImportsArray(source, moduleEntry) {
252
725
  }
253
726
  }
254
727
  }
255
- if (closeBracketPos === -1) {
728
+ if (closeBracketPos === -1)
256
729
  return source;
257
- }
258
730
  const arrayContent = source.slice(openBracketPos + 1, closeBracketPos).trim();
259
- let newArrayContent;
260
- if (arrayContent.length === 0) {
261
- newArrayContent = `\n ${moduleEntry},\n `;
262
- }
263
- else {
264
- newArrayContent = `\n ${moduleEntry},\n ${arrayContent}\n `;
265
- }
266
- return (source.slice(0, openBracketPos + 1) +
267
- newArrayContent +
268
- source.slice(closeBracketPos));
731
+ const newArrayContent = arrayContent.length === 0
732
+ ? `\n ${moduleEntry},\n `
733
+ : `\n ${moduleEntry},\n ${arrayContent}\n `;
734
+ return source.slice(0, openBracketPos + 1) + newArrayContent + source.slice(closeBracketPos);
269
735
  }
270
736
  function findLastImportEndIndex(source) {
271
737
  const importRegex = /^import\s+.+from\s+['"][^'"]+['"];?\s*$/gm;
@@ -278,36 +744,40 @@ function findLastImportEndIndex(source) {
278
744
  }
279
745
  function findFileInTree(tree, fileName) {
280
746
  let found;
281
- tree.visit(path => {
282
- if (!found && path.endsWith(`/${fileName}`)) {
747
+ tree.visit((path) => {
748
+ if (!found && path.endsWith(`/${fileName}`))
283
749
  found = path;
284
- }
285
750
  });
286
751
  return found;
287
752
  }
288
- function updatePackageJson(options) {
753
+ function updatePackageJson(sel) {
289
754
  return (host) => {
290
- if (!host.exists('package.json')) {
755
+ if (!host.exists('package.json'))
291
756
  return host;
292
- }
293
757
  return updateJsonFile(host, 'package.json', (packageJson) => {
294
- updateNpmScripts(packageJson.scripts, options);
758
+ packageJson.dependencies = packageJson.dependencies || {};
759
+ const add = (name, version) => {
760
+ if (!packageJson.dependencies[name])
761
+ packageJson.dependencies[name] = version;
762
+ };
763
+ add('@nestjs/config', '^4.0.4');
764
+ add('js-yaml', '^4.1.0');
765
+ if (sel.gatewayConfig) {
766
+ add('@nestjs/axios', '^4.0.1');
767
+ add('@nestjs/platform-ws', '^11.0.1');
768
+ add('@nestjs/websockets', '^11.0.1');
769
+ add('ws', '^8.21.0');
770
+ }
295
771
  });
296
772
  };
297
773
  }
298
774
  function updateJsonFile(host, path, callback) {
299
775
  const source = host.read(path);
300
776
  if (source) {
301
- const sourceText = source.toString('utf-8');
302
- const json = (0, jsonc_parser_1.parse)(sourceText);
777
+ const json = (0, jsonc_parser_1.parse)(source.toString('utf-8'));
303
778
  callback(json);
304
779
  host.overwrite(path, JSON.stringify(json, null, 2));
305
780
  }
306
781
  return host;
307
782
  }
308
- function updateNpmScripts(scripts, _options) {
309
- if (!scripts) {
310
- return;
311
- }
312
- }
313
783
  //# sourceMappingURL=index.js.map