@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
@@ -9,52 +9,234 @@ import { InitOptions } from './init.schema';
9
9
  type UpdateJsonFn<T> = (obj: T) => T | void;
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // Costanti per BrokerModule la parte gateway è inclusa solo se `gateway` è on
12
+ // Resolved selections (from interactive prompts or flags/defaults)
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
- function brokerImportLine(gateway: boolean): string {
16
- // BrokerModule only owns broker/topics/app. When gateway is on, auth-providers and the
17
- // gateway config are wired into ProxyModule instead (so import those types there).
18
- const broker = gateway
19
- ? "import { AppConfig, BrokerModule, BrokerTopic, GatewayConfig, HandlerAuthConfig, ProxyModule, RabbitMQConfig } from '@open-rlb/nestjs-amqp';"
20
- : "import { AppConfig, BrokerModule, BrokerTopic, RabbitMQConfig } from '@open-rlb/nestjs-amqp';";
21
- const config = "\nimport { ConfigModule, ConfigService } from '@nestjs/config';";
22
- const http = gateway ? "\nimport { HttpModule } from '@nestjs/axios';" : '';
23
- return broker + config + http;
15
+ interface Names {
16
+ exchange: string; // main AMQP exchange backing acl/admin queues (default rlb)
17
+ aclQueue: string; // queue backing the fixed rlb-acl topic
18
+ adminQueue: string; // queue backing the fixed rlb-gateway-admin topic
19
+ controlTopic: string; // broadcast control/reload topic
20
+ routeExchange: string; // route-discovery fanout exchange
21
+ routeQueue: string; // route-sync durable queue
22
+ serviceName: string; // route-publish ownership + connection_name
24
23
  }
25
24
 
26
- function brokerForRootAsync(): string {
27
- return `BrokerModule.forRootAsync({
28
- imports: [ConfigModule],
29
- inject: [ConfigService],
30
- useFactory: async (configService: ConfigService) => ({
31
- options: configService.get<RabbitMQConfig>('broker')!,
32
- topics: configService.get<BrokerTopic[]>('topics')!,
33
- appOptions: configService.get<AppConfig>('app'),
34
- })
35
- })`;
25
+ interface Resolved {
26
+ gatewayConfig: boolean; // create the HTTP/WebSocket gateway config
27
+ acl: boolean; // AclModule + ACL paths
28
+ admin: boolean; // GatewayAdminModule + DB routes/auth/metrics paths
29
+ routeReception: boolean;// gateway consumes routes auto-published by microservices
30
+ autoPublish: boolean; // microservice publishes its @BrokerHTTP routes on boot
31
+ skills: boolean; // copy the Claude skills
32
+ names: Names;
36
33
  }
37
34
 
38
- function proxyForRootAsync(): string {
39
- // Gateway owns auth-providers + gateway config (moved out of BrokerModule). Add ACL/role
40
- // service bindings in the `providers` array when needed.
41
- return `ProxyModule.forRootAsync({
42
- imports: [ConfigModule],
43
- inject: [ConfigService],
44
- useFactory: (configService: ConfigService) => ({
45
- authOptions: configService.get<HandlerAuthConfig[]>('auth-providers'),
46
- gatewayOptions: configService.get<GatewayConfig>('gateway'),
47
- }),
48
- providers: [],
49
- })`;
35
+ function defaultNames(project: string): Names {
36
+ return {
37
+ exchange: 'rlb',
38
+ aclQueue: 'rlb-acl',
39
+ adminQueue: 'rlb-gateway-admin',
40
+ controlTopic: 'rlb-gateway-control',
41
+ routeExchange: 'rlb-route-discovery',
42
+ routeQueue: 'rlb-route-sync',
43
+ serviceName: project || 'my-service',
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Resolves the install selections. When stdin is a TTY and no flags were passed, it drives a
49
+ * branching interactive flow with @inquirer/prompts:
50
+ * - "Create a gateway configuration?" → yes: pick {acl, gateway-admin, route-reception} and the
51
+ * topic/queue/exchange names (rlb-* defaults); no: pick {auto-config-publish} (+ names).
52
+ * Otherwise it falls back to the provided flags + rlb-* defaults (non-interactive / CI).
53
+ */
54
+ async function resolveSelections(o: InitOptions, context: SchematicContext): Promise<Resolved> {
55
+ const project = (o.project || 'my-service').toString();
56
+ const d = defaultNames(project);
57
+ const flagsProvided = o.gatewayConfig !== undefined || (Array.isArray(o.features) && o.features.length > 0);
58
+ const canPrompt = !!process.stdout && !!process.stdout.isTTY && !process.env.CI && !flagsProvided;
59
+
60
+ let prompts: any;
61
+ if (canPrompt) {
62
+ try {
63
+ prompts = require('@inquirer/prompts');
64
+ } catch {
65
+ context.logger.warn('[nest-add] @inquirer/prompts not found; falling back to flags/defaults (non-interactive).');
66
+ prompts = undefined;
67
+ }
68
+ }
69
+
70
+ // ---- Non-interactive: derive everything from flags + defaults --------------
71
+ if (!prompts) {
72
+ const features = new Set((o.features || []).map((f) => String(f).trim()));
73
+ const gatewayConfig = o.gatewayConfig === true;
74
+ return {
75
+ gatewayConfig,
76
+ acl: gatewayConfig && features.has('acl'),
77
+ admin: gatewayConfig && features.has('gateway-admin'),
78
+ routeReception: gatewayConfig && features.has('route-reception'),
79
+ autoPublish: !gatewayConfig && features.has('auto-config-publish'),
80
+ skills: o.skills !== false,
81
+ names: {
82
+ exchange: o.exchange || d.exchange,
83
+ aclQueue: o.aclQueue || d.aclQueue,
84
+ adminQueue: o.adminQueue || d.adminQueue,
85
+ controlTopic: o.controlTopic || d.controlTopic,
86
+ routeExchange: o.routeExchange || d.routeExchange,
87
+ routeQueue: o.routeQueue || d.routeQueue,
88
+ serviceName: o.serviceName || d.serviceName,
89
+ },
90
+ };
91
+ }
92
+
93
+ // ---- Interactive branching flow -------------------------------------------
94
+ const { confirm, checkbox, input } = prompts;
95
+ const names: Names = { ...d };
96
+ let acl = false, admin = false, routeReception = false, autoPublish = false;
97
+
98
+ const gatewayConfig: boolean = await confirm({
99
+ message: 'Create a gateway (HTTP/WebSocket) configuration?',
100
+ default: false,
101
+ });
102
+
103
+ if (gatewayConfig) {
104
+ const picked: string[] = await checkbox({
105
+ message: 'Select gateway features to include',
106
+ choices: [
107
+ { name: 'ACL — role-based authorization + management', value: 'acl' },
108
+ { name: 'Gateway admin + auth — DB-managed routes, auth-providers, metrics', value: 'gateway-admin' },
109
+ { name: 'Route reception — apply routes auto-published by microservices', value: 'route-reception' },
110
+ ],
111
+ });
112
+ acl = picked.includes('acl');
113
+ admin = picked.includes('gateway-admin');
114
+ routeReception = picked.includes('route-reception');
115
+
116
+ if (acl || admin || routeReception) {
117
+ names.exchange = await input({ message: 'Main AMQP exchange name', default: d.exchange });
118
+ }
119
+ if (acl) {
120
+ names.aclQueue = await input({ message: 'Queue backing the rlb-acl topic', default: d.aclQueue });
121
+ }
122
+ if (admin || routeReception) {
123
+ names.adminQueue = await input({ message: 'Queue backing the rlb-gateway-admin topic', default: d.adminQueue });
124
+ names.controlTopic = await input({ message: 'Broadcast control/reload topic name', default: d.controlTopic });
125
+ }
126
+ if (routeReception) {
127
+ names.routeExchange = await input({ message: 'Route-discovery exchange (must match the publishers)', default: d.routeExchange });
128
+ names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
129
+ }
130
+ } else {
131
+ const picked: string[] = await checkbox({
132
+ message: 'Select microservice features to include',
133
+ choices: [
134
+ { name: 'Auto-send config at startup — publish this service’s @BrokerHTTP routes to the gateway on boot', value: 'auto-config-publish' },
135
+ ],
136
+ });
137
+ autoPublish = picked.includes('auto-config-publish');
138
+ if (autoPublish) {
139
+ names.serviceName = await input({ message: 'Service name (route ownership + AMQP connection_name)', default: d.serviceName });
140
+ names.routeExchange = await input({ message: 'Route-discovery exchange', default: d.routeExchange });
141
+ names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
142
+ }
143
+ }
144
+
145
+ const skills: boolean = await confirm({ message: 'Copy the Claude skills into .claude/skills?', default: true });
146
+ return { gatewayConfig, acl, admin, routeReception, autoPublish, skills, names };
50
147
  }
51
148
 
52
149
  // ---------------------------------------------------------------------------
53
- // Costanti per config.yaml — la sezione `gateway` è inclusa solo se gateway è on
150
+ // config/config.yaml builder
54
151
  // ---------------------------------------------------------------------------
55
152
 
56
- const CONFIG_YAML_BASE = `
57
- app:
153
+ function buildConfigYaml(sel: Resolved): string {
154
+ const n = sel.names;
155
+ const anyAdmin = sel.admin || sel.routeReception;
156
+
157
+ const routeDiscoveryPub = sel.autoPublish ? `
158
+ # Route auto-discovery (publisher side): announce this service's @BrokerHTTP routes on boot.
159
+ # serviceName also fills the AMQP connection_name (none is set explicitly below).
160
+ routeDiscovery:
161
+ serviceName: "${n.serviceName}"
162
+ publishOnBoot: true
163
+ exchange: ${n.routeExchange}
164
+ queue: ${n.routeQueue}` : '';
165
+
166
+ const clientProps = sel.autoPublish ? '' : `
167
+ clientProperties:
168
+ connection_name: "<APP_NAME>"`;
169
+
170
+ const exchanges: string[] = [];
171
+ if (sel.acl || anyAdmin) {
172
+ exchanges.push(` - name: ${n.exchange}
173
+ type: "direct"
174
+ createExchangeIfNotExists: true
175
+ options:
176
+ durable: true`);
177
+ }
178
+ exchanges.push(` - name: example.fanout
179
+ type: "fanout"
180
+ createExchangeIfNotExists: true
181
+ options:
182
+ durable: true
183
+ autoDelete: false
184
+ internal: false`);
185
+
186
+ const queues: string[] = [];
187
+ if (sel.acl) {
188
+ queues.push(` - name: ${n.aclQueue}
189
+ exchange: ${n.exchange}
190
+ routingKey: ${n.aclQueue}
191
+ createQueueIfNotExists: true
192
+ options:
193
+ durable: true`);
194
+ }
195
+ if (anyAdmin) {
196
+ queues.push(` - name: ${n.adminQueue}
197
+ exchange: ${n.exchange}
198
+ routingKey: ${n.adminQueue}
199
+ createQueueIfNotExists: true
200
+ options:
201
+ durable: true`);
202
+ }
203
+ queues.push(` - name: example.queue
204
+ exchange: example.fanout
205
+ routingKey: example.queue
206
+ createQueueIfNotExists: true
207
+ options:
208
+ durable: true
209
+ autoDelete: false
210
+ exclusive: false`);
211
+
212
+ const topics: string[] = [];
213
+ if (sel.acl) {
214
+ topics.push(` # Fixed topic name (decorator-bound in the lib): the ACL handlers bind to 'rlb-acl'.
215
+ - name: rlb-acl
216
+ mode: rpc
217
+ queue: ${n.aclQueue}
218
+ exchange: ${n.exchange}
219
+ routingKey: ${n.aclQueue}`);
220
+ }
221
+ if (anyAdmin) {
222
+ topics.push(` # Fixed topic name (decorator-bound): the gateway-admin handlers bind to 'rlb-gateway-admin'.
223
+ - name: rlb-gateway-admin
224
+ mode: rpc
225
+ queue: ${n.adminQueue}
226
+ exchange: ${n.exchange}
227
+ routingKey: ${n.adminQueue}
228
+ # Broadcast control topic: the gateway rebuilds its routes at runtime on a 'gw-reload'.
229
+ - name: ${n.controlTopic}
230
+ mode: broadcast
231
+ exchange: ${n.exchange}
232
+ routingKey: ${n.controlTopic}`);
233
+ }
234
+ topics.push(` - name: example.topic
235
+ exchange: example.fanout
236
+ routingKey: "example.topic"
237
+ mode: event`);
238
+
239
+ let yaml = `app:
58
240
  port: 80
59
241
  host: 0.0.0.0
60
242
  environment: "development"
@@ -65,60 +247,386 @@ broker:
65
247
  name: "rabbitmq"
66
248
  uri: "<AMQP_URI>"
67
249
  defaultSubscribeErrorBehavior: "ack"
68
- defaultPublishErrorBehavior: "reject"
250
+ defaultPublishErrorBehavior: "reject"${routeDiscoveryPub}
69
251
  connectionManagerOptions:
70
252
  heartbeatIntervalInSeconds: 60
71
253
  reconnectTimeInSeconds: 60
72
- connectionOptions:
73
- clientProperties:
74
- connection_name: "<APP_NAME>"
254
+ connectionOptions:${clientProps}
75
255
  credentials:
76
256
  mechanism: PLAIN
77
257
  username: "<AMQP_USERNAME>"
78
258
  password: "<AMQP_PASSWORD>"
79
259
  exchanges:
80
- - name: example.fanout
81
- type: "fanout"
82
- createExchangeIfNotExists: true
83
- options:
84
- durable: true
85
- autoDelete: false
86
- internal: false
260
+ ${exchanges.join('\n')}
87
261
  queues:
88
- - name: example.queue
89
- createQueueIfNotExists: true
90
- exchange: example.fanout
91
- routingKey: example.queue
92
- options:
93
- durable: true
94
- autoDelete: false
95
- exclusive: false
262
+ ${queues.join('\n')}
96
263
 
97
264
  topics:
98
- - name: example.topic
99
- exchange: example.fanout
100
- routingKey: "example.topic"
101
- mode: event
265
+ ${topics.join('\n')}
102
266
  `;
103
267
 
104
- const CONFIG_YAML_GATEWAY = `
105
- gateway:
268
+ if (sel.gatewayConfig) {
269
+ yaml += '\n' + buildGatewayBlock(sel);
270
+ }
271
+ return yaml;
272
+ }
273
+
274
+ function buildGatewayBlock(sel: Resolved): string {
275
+ const n = sel.names;
276
+ const anyAdmin = sel.admin || sel.routeReception;
277
+ const paths: string[] = [];
278
+
279
+ if (sel.acl) paths.push(ACL_PATHS);
280
+ if (sel.admin) paths.push(adminPaths(n.controlTopic));
281
+ paths.push(` - name: example-path
282
+ method: POST
283
+ dataSource: body
284
+ path: /example
285
+ topic: example.topic
286
+ action: example-action
287
+ mode: event`);
288
+
289
+ let block = `gateway:
106
290
  events: []
107
291
  ws:
108
292
  heartbeatIntervalMs: 30000
109
293
  # Auth is declared per-event (events[].auth / requireAuth / roles / scopeClaim).
110
294
  paths:
111
- - name: example-path
295
+ ${paths.join('\n')}`;
296
+
297
+ if (anyAdmin) {
298
+ block += `
299
+ # Load DB-managed routes at boot AND on every runtime reload (ordered static-before-param).
300
+ loadConfig:
301
+ paths:
302
+ topic: rlb-gateway-admin
303
+ action: gw-path-export
304
+ # Broadcast topic that triggers a runtime route rebuild (see topics: ${n.controlTopic}).
305
+ reloadTopic: ${n.controlTopic}
306
+ # Per-request metrics auto-emitted to the gateway-admin metrics handler.
307
+ metrics:
308
+ topic: rlb-gateway-admin
309
+ action: gw-metrics-track`;
310
+ }
311
+ return block + '\n';
312
+ }
313
+
314
+ const ACL_PATHS = ` # --- ACL management: actions (name is the key — PUT upserts, GET lists, DELETE by name) ---
315
+ - name: acl-action-list
316
+ method: GET
317
+ path: /acl/actions
318
+ dataSource: query
319
+ topic: rlb-acl
320
+ action: acl-action-list
321
+ mode: rpc
322
+ - name: acl-action-get
323
+ method: GET
324
+ path: /acl/actions/get
325
+ dataSource: query
326
+ topic: rlb-acl
327
+ action: acl-action-get
328
+ mode: rpc
329
+ - name: acl-action-upsert
330
+ method: PUT
331
+ path: /acl/actions
332
+ dataSource: body
333
+ topic: rlb-acl
334
+ action: acl-action-update
335
+ mode: rpc
336
+ - name: acl-action-delete
337
+ method: DELETE
338
+ path: /acl/actions
339
+ dataSource: body
340
+ topic: rlb-acl
341
+ action: acl-action-delete
342
+ mode: rpc
343
+ # --- ACL management: roles ---
344
+ - name: acl-role-list
345
+ method: GET
346
+ path: /acl/roles
347
+ dataSource: query
348
+ topic: rlb-acl
349
+ action: acl-role-list
350
+ mode: rpc
351
+ - name: acl-role-get
352
+ method: GET
353
+ path: /acl/roles/get
354
+ dataSource: query
355
+ topic: rlb-acl
356
+ action: acl-role-get
357
+ mode: rpc
358
+ - name: acl-role-upsert
359
+ method: PUT
360
+ path: /acl/roles
361
+ dataSource: body
362
+ topic: rlb-acl
363
+ action: acl-role-update
364
+ mode: rpc
365
+ - name: acl-role-delete
366
+ method: DELETE
367
+ path: /acl/roles
368
+ dataSource: body
369
+ topic: rlb-acl
370
+ action: acl-role-delete
371
+ mode: rpc
372
+ # --- ACL grants (per-user; resourceId/companyId optional) ---
373
+ - name: acl-grant
112
374
  method: POST
375
+ path: /acl/grants
113
376
  dataSource: body
114
- path: /example
115
- topic: example.topic
116
- action: example-action
377
+ topic: rlb-acl
378
+ action: acl-grant
379
+ mode: rpc
380
+ - name: acl-revoke
381
+ method: DELETE
382
+ path: /acl/grants
383
+ dataSource: body
384
+ topic: rlb-acl
385
+ action: acl-revoke
386
+ mode: rpc
387
+ # --- ACL checks (GET → 200 with true/false) ---
388
+ - name: acl-check-gtw
389
+ method: GET
390
+ path: /acl/check
391
+ dataSource: query
392
+ topic: rlb-acl
393
+ action: acl-can-user-do-gtw
394
+ mode: rpc
395
+ - name: acl-check-resource
396
+ method: GET
397
+ path: /acl/check-resource
398
+ dataSource: query
399
+ topic: rlb-acl
400
+ action: acl-can-user-do
401
+ mode: rpc
402
+ # Lists the caller's accessible resources. Add an 'auth: <provider>' line once you declare an auth-provider.
403
+ - name: acl-list-resources-by-user
404
+ method: GET
405
+ path: /acl/resources
406
+ dataSource: query
407
+ topic: rlb-acl
408
+ action: acl-list-resources-by-user
409
+ mode: rpc`;
410
+
411
+ function adminPaths(controlTopic: string): string {
412
+ return ` # --- gateway-admin: health + DB routes + auth-providers + metrics ---
413
+ - name: health
414
+ method: GET
415
+ path: /health
416
+ dataSource: query
417
+ topic: rlb-gateway-admin
418
+ action: gw-health
419
+ mode: rpc
420
+ - name: gw-path-create
421
+ method: POST
422
+ path: /admin/paths
423
+ dataSource: body
424
+ topic: rlb-gateway-admin
425
+ action: gw-path-create
426
+ mode: rpc
427
+ - name: gw-path-list
428
+ method: GET
429
+ path: /admin/paths
430
+ dataSource: query
431
+ topic: rlb-gateway-admin
432
+ action: gw-path-list
433
+ mode: rpc
434
+ - name: gw-path-export
435
+ method: GET
436
+ path: /admin/paths/export
437
+ dataSource: query
438
+ topic: rlb-gateway-admin
439
+ action: gw-path-export
440
+ mode: rpc
441
+ - name: gw-path-update
442
+ method: PUT
443
+ path: /admin/paths
444
+ dataSource: body
445
+ topic: rlb-gateway-admin
446
+ action: gw-path-update
447
+ mode: rpc
448
+ - name: gw-path-get
449
+ method: GET
450
+ path: /admin/paths/get
451
+ dataSource: query
452
+ topic: rlb-gateway-admin
453
+ action: gw-path-get
454
+ mode: rpc
455
+ - name: gw-path-delete
456
+ method: DELETE
457
+ path: /admin/paths
458
+ dataSource: body
459
+ topic: rlb-gateway-admin
460
+ action: gw-path-delete
461
+ mode: rpc
462
+ - name: gw-auth-list
463
+ method: GET
464
+ path: /admin/auth
465
+ dataSource: query
466
+ topic: rlb-gateway-admin
467
+ action: gw-auth-list
468
+ mode: rpc
469
+ - name: gw-auth-upsert
470
+ method: PUT
471
+ path: /admin/auth
472
+ dataSource: body
473
+ topic: rlb-gateway-admin
474
+ action: gw-auth-update
475
+ mode: rpc
476
+ - name: gw-auth-get
477
+ method: GET
478
+ path: /admin/auth/get
479
+ dataSource: query
480
+ topic: rlb-gateway-admin
481
+ action: gw-auth-get
482
+ mode: rpc
483
+ - name: gw-auth-delete
484
+ method: DELETE
485
+ path: /admin/auth
486
+ dataSource: body
487
+ topic: rlb-gateway-admin
488
+ action: gw-auth-delete
489
+ mode: rpc
490
+ - name: gw-metrics-get
491
+ method: GET
492
+ path: /admin/metrics
493
+ dataSource: query
494
+ topic: rlb-gateway-admin
495
+ action: gw-metrics-get
496
+ mode: rpc
497
+ - name: gw-metrics-series
498
+ method: GET
499
+ path: /admin/metrics/series
500
+ dataSource: query
501
+ topic: rlb-gateway-admin
502
+ action: gw-metrics-series
503
+ mode: rpc
504
+ - name: gw-metrics-points
505
+ method: GET
506
+ path: /admin/metrics/points
507
+ dataSource: query
508
+ topic: rlb-gateway-admin
509
+ action: gw-metrics-points
510
+ mode: rpc
511
+ - name: gw-metrics-track
512
+ method: POST
513
+ path: /admin/metrics/track
514
+ dataSource: body
515
+ topic: rlb-gateway-admin
516
+ action: gw-metrics-track
117
517
  mode: event
118
- `;
518
+ - name: gw-reload
519
+ method: POST
520
+ path: /admin/reload
521
+ dataSource: body
522
+ topic: ${controlTopic}
523
+ action: gw-reload
524
+ mode: event`;
525
+ }
526
+
527
+ // ---------------------------------------------------------------------------
528
+ // AppModule wiring (import statements + @Module imports entries)
529
+ // ---------------------------------------------------------------------------
530
+
531
+ function buildImportStatements(sel: Resolved): string {
532
+ const libSymbols = ['AppConfig', 'BrokerModule', 'BrokerTopic', 'RabbitMQConfig'];
533
+ if (sel.gatewayConfig) libSymbols.push('GatewayConfig', 'HandlerAuthConfig', 'ProxyModule');
534
+ if (sel.acl) libSymbols.push('AclActionRepository', 'AclGrantRepository', 'AclModule', 'AclRoleRepository', 'AclService', 'RLB_ACL_CACHE_STORE', 'RLB_GTW_ACL_ROLE_SERVICE');
535
+ if (sel.admin || sel.routeReception) libSymbols.push('AuthProviderRepository', 'GatewayAdminModule', 'HttpMetricRepository', 'HttpPathRepository', 'RouteSyncLogRepository');
536
+ const lib = `import { ${[...new Set(libSymbols)].sort().join(', ')} } from '@open-rlb/nestjs-amqp';`;
537
+
538
+ const lines = [lib, `import { ConfigModule, ConfigService } from '@nestjs/config';`, `import yamlConfig from './config/config.loader';`];
539
+ if (sel.gatewayConfig) lines.push(`import { HttpModule } from '@nestjs/axios';`);
540
+ if (sel.acl) {
541
+ lines.push(`import { InMemoryAclActionRepository, InMemoryAclGrantRepository, InMemoryAclRoleRepository } from './modules/database/repository/acl.repository';`);
542
+ lines.push(`import { InMemoryAclStore } from './cache/in-memory-acl-store';`);
543
+ }
544
+ if (sel.admin || sel.routeReception) {
545
+ lines.push(`import { InMemoryAuthProviderRepository, InMemoryHttpMetricRepository, InMemoryHttpPathRepository } from './modules/database/repository/gateway.repository';`);
546
+ lines.push(`import { InMemoryRouteSyncLogRepository } from './modules/database/repository/route-sync.repository';`);
547
+ }
548
+ return lines.join('\n');
549
+ }
550
+
551
+ function brokerForRootAsync(): string {
552
+ return `BrokerModule.forRootAsync({
553
+ imports: [ConfigModule],
554
+ inject: [ConfigService],
555
+ useFactory: async (configService: ConfigService) => ({
556
+ options: configService.get<RabbitMQConfig>('broker')!,
557
+ topics: configService.get<BrokerTopic[]>('topics')!,
558
+ appOptions: configService.get<AppConfig>('app'),
559
+ })
560
+ })`;
561
+ }
562
+
563
+ function proxyForRootAsync(sel: Resolved): string {
564
+ const providers = sel.acl
565
+ ? `[
566
+ // Role-gated paths resolve the caller's roles via AclService (in-process, no broker hop).
567
+ { provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService },
568
+ ]`
569
+ : `[]`;
570
+ return `ProxyModule.forRootAsync({
571
+ imports: [ConfigModule],
572
+ inject: [ConfigService],
573
+ useFactory: (configService: ConfigService) => ({
574
+ authOptions: configService.get<HandlerAuthConfig[]>('auth-providers'),
575
+ gatewayOptions: configService.get<GatewayConfig>('gateway'),
576
+ }),
577
+ providers: ${providers},
578
+ })`;
579
+ }
119
580
 
120
- function configYaml(gateway: boolean): string {
121
- return gateway ? CONFIG_YAML_BASE.trimEnd() + '\n' + CONFIG_YAML_GATEWAY : CONFIG_YAML_BASE;
581
+ function aclForRoot(): string {
582
+ return `AclModule.forRoot(
583
+ [
584
+ InMemoryAclActionRepository,
585
+ { provide: AclActionRepository, useExisting: InMemoryAclActionRepository },
586
+ InMemoryAclRoleRepository,
587
+ { provide: AclRoleRepository, useExisting: InMemoryAclRoleRepository },
588
+ InMemoryAclGrantRepository,
589
+ { provide: AclGrantRepository, useExisting: InMemoryAclGrantRepository },
590
+ InMemoryAclStore,
591
+ { provide: RLB_ACL_CACHE_STORE, useExisting: InMemoryAclStore },
592
+ ],
593
+ { cache: { ramTtlMs: 30000, l2TtlSec: 600 } },
594
+ )`;
595
+ }
596
+
597
+ function gatewayAdminForRoot(sel: Resolved): string {
598
+ const options = sel.routeReception
599
+ ? `,
600
+ {
601
+ // Consumer-side route-discovery — names MUST match the publishers' broker.routeDiscovery.
602
+ routeDiscovery: { exchange: '${sel.names.routeExchange}', queue: '${sel.names.routeQueue}' },
603
+ }`
604
+ : '';
605
+ return `GatewayAdminModule.forRoot(
606
+ [
607
+ InMemoryHttpPathRepository,
608
+ { provide: HttpPathRepository, useExisting: InMemoryHttpPathRepository },
609
+ InMemoryAuthProviderRepository,
610
+ { provide: AuthProviderRepository, useExisting: InMemoryAuthProviderRepository },
611
+ InMemoryHttpMetricRepository,
612
+ { provide: HttpMetricRepository, useExisting: InMemoryHttpMetricRepository },
613
+ InMemoryRouteSyncLogRepository,
614
+ { provide: RouteSyncLogRepository, useExisting: InMemoryRouteSyncLogRepository },
615
+ ]${options},
616
+ )`;
617
+ }
618
+
619
+ function buildModuleEntries(sel: Resolved): string[] {
620
+ const entries: string[] = [];
621
+ entries.push(`ConfigModule.forRoot({ isGlobal: true, load: [yamlConfig] })`);
622
+ entries.push(brokerForRootAsync());
623
+ if (sel.gatewayConfig) {
624
+ entries.push('HttpModule');
625
+ entries.push(proxyForRootAsync(sel));
626
+ }
627
+ if (sel.acl) entries.push(aclForRoot());
628
+ if (sel.admin || sel.routeReception) entries.push(gatewayAdminForRoot(sel));
629
+ return entries;
122
630
  }
123
631
 
124
632
  // ---------------------------------------------------------------------------
@@ -126,25 +634,30 @@ function configYaml(gateway: boolean): string {
126
634
  // ---------------------------------------------------------------------------
127
635
 
128
636
  export function main(options: InitOptions): Rule {
637
+ const project = (options.project || 'my-service').toString();
129
638
  options = transform(options);
130
- const gateway = options.gateway !== false; // default on
131
- const skills = options.skills !== false; // default on
132
- return (tree: Tree, context: SchematicContext) => {
639
+ return async (tree: Tree, context: SchematicContext) => {
640
+ const sel = await resolveSelections({ ...options, project } as InitOptions, context);
641
+ const anyRepo = sel.acl || sel.admin || sel.routeReception;
133
642
  return branchAndMerge(
134
643
  chain([
135
644
  mergeSourceRoot(options),
136
- addBrokerModuleToAppModule(gateway),
137
- updateConfigYaml(gateway),
138
- gateway ? configureMainForGateway() : noop(),
139
- updatePackageJson(options),
140
- skills ? copySkills() : noop(),
645
+ addModulesToAppModule(sel),
646
+ createConfigLoader(),
647
+ updateConfigYaml(sel),
648
+ sel.gatewayConfig ? configureMainForGateway() : noop(),
649
+ anyRepo ? copyAsset('db-core') : noop(),
650
+ sel.acl ? copyAsset('acl') : noop(),
651
+ (sel.admin || sel.routeReception) ? copyAsset('gateway-admin') : noop(),
652
+ sel.skills ? copySkills() : noop(),
653
+ updatePackageJson(sel),
141
654
  ]),
142
- )(tree, context);
655
+ );
143
656
  };
144
657
  }
145
658
 
146
659
  // ---------------------------------------------------------------------------
147
- // Helpers interni
660
+ // Helpers
148
661
  // ---------------------------------------------------------------------------
149
662
 
150
663
  function transform(source: InitOptions): InitOptions {
@@ -155,129 +668,101 @@ function transform(source: InitOptions): InitOptions {
155
668
  const location: Location = new NameParser().parse({ ...target, name: 'init' });
156
669
  target.name = '';
157
670
  target.path = normalizeToKebabOrSnakeCase(location.path);
158
- target.specFileSuffix = normalizeToKebabOrSnakeCase(
159
- source.specFileSuffix || 'spec',
160
- );
671
+ target.specFileSuffix = normalizeToKebabOrSnakeCase(source.specFileSuffix || 'spec');
161
672
  return target;
162
673
  }
163
674
 
164
- // ---------------------------------------------------------------------------
165
- // Rule: copia le skill Claude in .claude/skills del progetto consumer
166
- // ---------------------------------------------------------------------------
675
+ /** Copy a static asset tree under ./files/<name> to the project root (no templating). */
676
+ function copyAsset(name: string): Rule {
677
+ return mergeWith(apply(url(`./files/${name}`), [move(normalize('.'))]), MergeStrategy.Overwrite);
678
+ }
167
679
 
680
+ /** Copy the Claude skills tree to .claude/skills. */
168
681
  function copySkills(): Rule {
169
- // Static asset tree under ./files/skills → moved to .claude/skills (no templating).
170
- return mergeWith(
171
- apply(url('./files/skills'), [move(normalize('.claude/skills'))]),
172
- MergeStrategy.Overwrite,
173
- );
682
+ return mergeWith(apply(url('./files/skills'), [move(normalize('.claude/skills'))]), MergeStrategy.Overwrite);
174
683
  }
175
684
 
176
- // ---------------------------------------------------------------------------
177
- // Rule: crea o aggiorna config/config.yaml con il blocco di configurazione
178
- // ---------------------------------------------------------------------------
685
+ /** Create src/config/config.loader.ts (js-yaml loader) if it doesn't exist yet. */
686
+ function createConfigLoader(): Rule {
687
+ return (tree: Tree) => {
688
+ const path = 'src/config/config.loader.ts';
689
+ if (tree.exists(path)) return tree;
690
+ tree.create(path, `import { readFileSync } from 'fs';
691
+ import * as yaml from 'js-yaml';
692
+ import { join } from 'path';
693
+
694
+ const YAML_CONFIG_FILENAME = 'config/config.yaml';
695
+
696
+ export default () =>
697
+ yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
698
+ `);
699
+ return tree;
700
+ };
701
+ }
179
702
 
180
- function updateConfigYaml(gateway: boolean): Rule {
703
+ function updateConfigYaml(sel: Resolved): Rule {
181
704
  return (tree: Tree) => {
182
705
  const CONFIG_PATH = 'config/config.yaml';
183
- const block = configYaml(gateway);
706
+ const block = buildConfigYaml(sel);
184
707
 
185
708
  if (!tree.exists(CONFIG_PATH)) {
186
- // Crea il file da zero con l'intero blocco template
187
- tree.create(CONFIG_PATH, block.trimStart());
709
+ tree.create(CONFIG_PATH, block);
188
710
  return tree;
189
711
  }
190
712
 
191
- // File esistente: appende solo le sezioni ancora mancanti
713
+ // Existing file: append only the still-missing top-level sections.
192
714
  const existing = tree.read(CONFIG_PATH)!.toString('utf-8');
193
- const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(gateway ? ['gateway:'] : [])] as const;
715
+ const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(sel.gatewayConfig ? ['gateway:'] : [])] as const;
194
716
 
195
717
  let toAppend = '';
196
718
  for (const key of SECTION_KEYS) {
197
719
  if (!existing.includes(key)) {
198
720
  const section = extractYamlSection(block, key);
199
- if (section) {
200
- toAppend += '\n' + section;
201
- }
721
+ if (section) toAppend += '\n' + section + '\n';
202
722
  }
203
723
  }
204
-
205
724
  if (toAppend.length > 0) {
206
725
  tree.overwrite(CONFIG_PATH, existing.trimEnd() + '\n' + toAppend);
207
726
  }
208
-
209
727
  return tree;
210
728
  };
211
729
  }
212
730
 
213
- /**
214
- * Estrae dal template YAML il blocco che inizia con `sectionKey`
215
- * e termina prima del prossimo blocco di primo livello (chiave senza indentazione).
216
- */
731
+ /** Extracts the YAML block starting at `sectionKey` up to the next top-level key. */
217
732
  function extractYamlSection(yaml: string, sectionKey: string): string {
218
733
  const lines = yaml.split('\n');
219
- const startIdx = lines.findIndex(l => l.startsWith(sectionKey));
734
+ const startIdx = lines.findIndex((l) => l.startsWith(sectionKey));
220
735
  if (startIdx === -1) return '';
221
-
222
- const endIdx = lines.findIndex(
223
- (l, i) => i > startIdx && l.length > 0 && !l.startsWith(' ') && !l.startsWith('#'),
224
- );
225
-
736
+ const endIdx = lines.findIndex((l, i) => i > startIdx && l.length > 0 && !l.startsWith(' ') && !l.startsWith('#'));
226
737
  const sectionLines = endIdx === -1 ? lines.slice(startIdx) : lines.slice(startIdx, endIdx);
227
738
  return sectionLines.join('\n').trimEnd();
228
739
  }
229
740
 
230
- // ---------------------------------------------------------------------------
231
- // Rule: aggiunge BrokerModule.forRootAsync(...) (+ gateway) ad app.module.ts
232
- // ---------------------------------------------------------------------------
233
-
234
- function addBrokerModuleToAppModule(gateway: boolean): Rule {
741
+ function addModulesToAppModule(sel: Resolved): Rule {
235
742
  return (tree: Tree) => {
236
- // Cerca app.module.ts nella posizione canonica; fallback su ricerca ricorsiva
237
- const candidatePaths = [
238
- '/src/app.module.ts',
239
- '/app/app.module.ts',
240
- 'src/app.module.ts',
241
- 'app/app.module.ts',
242
- ];
243
-
244
- let modulePath: string | undefined = candidatePaths.find(p => tree.exists(p));
245
-
743
+ const candidatePaths = ['/src/app.module.ts', '/app/app.module.ts', 'src/app.module.ts', 'app/app.module.ts'];
744
+ let modulePath: string | undefined = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'app.module.ts');
246
745
  if (!modulePath) {
247
- // Ricerca ricorsiva come ultima risorsa
248
- modulePath = findFileInTree(tree, 'app.module.ts');
249
- }
250
-
251
- if (!modulePath) {
252
- console.warn('[nest-add] app.module.ts non trovato: BrokerModule non aggiunto.');
746
+ console.warn('[nest-add] app.module.ts not found: AppModule wiring skipped.');
253
747
  return tree;
254
748
  }
255
749
 
256
- const rawContent = tree.read(modulePath);
257
- if (!rawContent) {
258
- return tree;
259
- }
750
+ const raw = tree.read(modulePath);
751
+ if (!raw) return tree;
752
+ let content = raw.toString('utf-8');
260
753
 
261
- let content = rawContent.toString('utf-8');
754
+ // Sentinel: BrokerModule.forRootAsync means "already wired" → idempotent no-op.
755
+ if (content.includes('BrokerModule.forRootAsync')) return tree;
262
756
 
263
- // ---- 1. Aggiunge l'import statement se non già presente ----------------
264
757
  if (!content.includes('@open-rlb/nestjs-amqp')) {
265
- const importInsertPos = findLastImportEndIndex(content);
266
- content =
267
- content.slice(0, importInsertPos) +
268
- '\n' + brokerImportLine(gateway) +
269
- content.slice(importInsertPos);
758
+ const pos = findLastImportEndIndex(content);
759
+ content = content.slice(0, pos) + '\n' + buildImportStatements(sel) + content.slice(pos);
270
760
  }
271
761
 
272
- // ---- 2. Aggiunge le voci nell'array imports (solo il necessario) -------
273
- // BrokerModule.forRootAsync is the sentinel for "already wired": guard all the
274
- // entries on it so HttpModule/ProxyModule aren't skipped by their import line.
275
- if (!content.includes('BrokerModule.forRootAsync')) {
276
- if (gateway) {
277
- content = insertIntoImportsArray(content, proxyForRootAsync());
278
- content = insertIntoImportsArray(content, 'HttpModule');
279
- }
280
- content = insertIntoImportsArray(content, brokerForRootAsync());
762
+ // Insert the module entries (reverse so the final order matches buildModuleEntries()).
763
+ const entries = buildModuleEntries(sel);
764
+ for (let i = entries.length - 1; i >= 0; i--) {
765
+ content = insertIntoImportsArray(content, entries[i]);
281
766
  }
282
767
 
283
768
  tree.overwrite(modulePath, content);
@@ -285,36 +770,28 @@ function addBrokerModuleToAppModule(gateway: boolean): Rule {
285
770
  };
286
771
  }
287
772
 
288
- // ---------------------------------------------------------------------------
289
- // Rule: abilita rawBody + WsAdapter in main.ts (solo gateway)
290
- // ---------------------------------------------------------------------------
291
-
292
773
  function configureMainForGateway(): Rule {
293
774
  return (tree: Tree) => {
294
775
  const candidatePaths = ['/src/main.ts', '/app/main.ts', 'src/main.ts', 'app/main.ts'];
295
- const mainPath = candidatePaths.find(p => tree.exists(p)) || findFileInTree(tree, 'main.ts');
776
+ const mainPath = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'main.ts');
296
777
  if (!mainPath) {
297
- console.warn('[nest-add] main.ts non trovato: abilita manualmente rawBody e WsAdapter.');
778
+ console.warn('[nest-add] main.ts not found: enable rawBody + WsAdapter manually.');
298
779
  return tree;
299
780
  }
300
781
  let content = tree.read(mainPath)!.toString('utf-8');
301
- if (content.includes('WsAdapter')) {
302
- return tree; // già configurato
303
- }
782
+ if (content.includes('WsAdapter')) return tree;
304
783
 
305
- // import WsAdapter dopo l'ultimo import
306
784
  const pos = findLastImportEndIndex(content);
307
785
  content = content.slice(0, pos) + "\nimport { WsAdapter } from '@nestjs/platform-ws';" + content.slice(pos);
308
786
 
309
- // abilita rawBody e registra il WS adapter subito dopo NestFactory.create(...)
310
787
  const m = content.match(/const\s+(\w+)\s*=\s*await\s+NestFactory\.create\(\s*([A-Za-z0-9_]+)\s*(,\s*\{[^}]*\})?\s*\)\s*;?/);
311
788
  if (m) {
312
789
  const appVar = m[1];
313
790
  const moduleArg = m[2];
314
- const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));`;
791
+ const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));\n ${appVar}.enableShutdownHooks();`;
315
792
  content = content.replace(m[0], replacement);
316
793
  } else {
317
- console.warn('[nest-add] NestFactory.create() non trovato in main.ts: aggiungi { rawBody: true } e useWebSocketAdapter manualmente.');
794
+ console.warn('[nest-add] NestFactory.create() not found in main.ts: add { rawBody: true } + useWebSocketAdapter manually.');
318
795
  }
319
796
 
320
797
  tree.overwrite(mainPath, content);
@@ -322,64 +799,30 @@ function configureMainForGateway(): Rule {
322
799
  };
323
800
  }
324
801
 
325
- /**
326
- * Inserisce `moduleEntry` all'interno del primo array `imports: [...]`
327
- * trovato nel decoratore @Module di NestJS.
328
- *
329
- * Gestisce sia array su singola riga sia array multi-riga.
330
- */
802
+ /** Inserts `moduleEntry` at the head of the first `imports: [...]` array in the @Module decorator. */
331
803
  function insertIntoImportsArray(source: string, moduleEntry: string): string {
332
- // Trova "imports:" seguito (eventualmente con spazi/newline) da "["
333
- const importsArrayRegex = /imports\s*:\s*\[/;
334
- const match = importsArrayRegex.exec(source);
335
- if (!match) {
336
- return source;
337
- }
338
-
339
- // Posizione del "[" che apre l'array
804
+ const match = /imports\s*:\s*\[/.exec(source);
805
+ if (!match) return source;
340
806
  const openBracketPos = source.indexOf('[', match.index);
341
807
 
342
- // Trova il corrispondente "]" tenendo conto di bracket annidati
343
808
  let depth = 0;
344
809
  let closeBracketPos = -1;
345
810
  for (let i = openBracketPos; i < source.length; i++) {
346
811
  if (source[i] === '[') depth++;
347
812
  else if (source[i] === ']') {
348
813
  depth--;
349
- if (depth === 0) {
350
- closeBracketPos = i;
351
- break;
352
- }
814
+ if (depth === 0) { closeBracketPos = i; break; }
353
815
  }
354
816
  }
817
+ if (closeBracketPos === -1) return source;
355
818
 
356
- if (closeBracketPos === -1) {
357
- return source; // array non chiuso correttamente
358
- }
359
-
360
- // Determina il contenuto corrente dell'array (senza le parentesi quadre)
361
819
  const arrayContent = source.slice(openBracketPos + 1, closeBracketPos).trim();
362
-
363
- let newArrayContent: string;
364
- if (arrayContent.length === 0) {
365
- // Array vuoto inserisce direttamente
366
- newArrayContent = `\n ${moduleEntry},\n `;
367
- } else {
368
- // Array con contenuto → aggiunge in testa (prima entry), con virgola finale
369
- newArrayContent = `\n ${moduleEntry},\n ${arrayContent}\n `;
370
- }
371
-
372
- return (
373
- source.slice(0, openBracketPos + 1) +
374
- newArrayContent +
375
- source.slice(closeBracketPos)
376
- );
820
+ const newArrayContent = arrayContent.length === 0
821
+ ? `\n ${moduleEntry},\n `
822
+ : `\n ${moduleEntry},\n ${arrayContent}\n `;
823
+ return source.slice(0, openBracketPos + 1) + newArrayContent + source.slice(closeBracketPos);
377
824
  }
378
825
 
379
- /**
380
- * Restituisce la posizione di fine dell'ultimo statement `import` nel file,
381
- * utile per inserire un nuovo import subito dopo l'ultimo esistente.
382
- */
383
826
  function findLastImportEndIndex(source: string): number {
384
827
  const importRegex = /^import\s+.+from\s+['"][^'"]+['"];?\s*$/gm;
385
828
  let lastEnd = 0;
@@ -390,55 +833,44 @@ function findLastImportEndIndex(source: string): number {
390
833
  return lastEnd;
391
834
  }
392
835
 
393
- /**
394
- * Ricerca ricorsiva di un file per nome all'interno dell'albero Schematics.
395
- */
396
836
  function findFileInTree(tree: Tree, fileName: string): string | undefined {
397
837
  let found: string | undefined;
398
- tree.visit(path => {
399
- if (!found && path.endsWith(`/${fileName}`)) {
400
- found = path;
401
- }
838
+ tree.visit((path) => {
839
+ if (!found && path.endsWith(`/${fileName}`)) found = path;
402
840
  });
403
841
  return found;
404
842
  }
405
843
 
406
844
  // ---------------------------------------------------------------------------
407
- // Rule: aggiorna package.json (hook per script futuri)
845
+ // package.json: add the runtime deps the generated code imports.
408
846
  // ---------------------------------------------------------------------------
409
847
 
410
- function updatePackageJson(options: InitOptions) {
848
+ function updatePackageJson(sel: Resolved): Rule {
411
849
  return (host: Tree) => {
412
- if (!host.exists('package.json')) {
413
- return host;
414
- }
415
- return updateJsonFile(
416
- host,
417
- 'package.json',
418
- (packageJson: Record<string, any>) => {
419
- updateNpmScripts(packageJson.scripts, options);
420
- },
421
- );
850
+ if (!host.exists('package.json')) return host;
851
+ return updateJsonFile(host, 'package.json', (packageJson: Record<string, any>) => {
852
+ packageJson.dependencies = packageJson.dependencies || {};
853
+ const add = (name: string, version: string) => {
854
+ if (!packageJson.dependencies[name]) packageJson.dependencies[name] = version;
855
+ };
856
+ add('@nestjs/config', '^4.0.4');
857
+ add('js-yaml', '^4.1.0');
858
+ if (sel.gatewayConfig) {
859
+ add('@nestjs/axios', '^4.0.1');
860
+ add('@nestjs/platform-ws', '^11.0.1');
861
+ add('@nestjs/websockets', '^11.0.1');
862
+ add('ws', '^8.21.0');
863
+ }
864
+ });
422
865
  };
423
866
  }
424
867
 
425
- function updateJsonFile<T>(
426
- host: Tree,
427
- path: string,
428
- callback: UpdateJsonFn<T>,
429
- ): Tree {
868
+ function updateJsonFile<T>(host: Tree, path: string, callback: UpdateJsonFn<T>): Tree {
430
869
  const source = host.read(path);
431
870
  if (source) {
432
- const sourceText = source.toString('utf-8');
433
- const json = parse(sourceText);
871
+ const json = parse(source.toString('utf-8'));
434
872
  callback(json as unknown as T);
435
873
  host.overwrite(path, JSON.stringify(json, null, 2));
436
874
  }
437
875
  return host;
438
876
  }
439
-
440
- function updateNpmScripts(scripts: Record<string, any>, _options: InitOptions) {
441
- if (!scripts) {
442
- return;
443
- }
444
- }