@open-rlb/nestjs-amqp 1.0.27 → 1.0.28

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 (29) hide show
  1. package/README.md +489 -170
  2. package/modules/broker/services/broker.service.js +2 -2
  3. package/modules/broker/services/broker.service.js.map +1 -1
  4. package/modules/proxy/config/path-definition.config.d.ts +10 -1
  5. package/modules/proxy/services/http-auth-handler.service.d.ts +6 -0
  6. package/modules/proxy/services/http-auth-handler.service.js +42 -24
  7. package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
  8. package/modules/proxy/services/http-handler.service.js +9 -3
  9. package/modules/proxy/services/http-handler.service.js.map +1 -1
  10. package/modules/proxy/services/jwt.service.js +1 -1
  11. package/modules/proxy/services/jwt.service.js.map +1 -1
  12. package/modules/proxy/services/websocket.service.d.ts +30 -6
  13. package/modules/proxy/services/websocket.service.js +200 -82
  14. package/modules/proxy/services/websocket.service.js.map +1 -1
  15. package/package.json +4 -3
  16. package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +58 -0
  17. package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +207 -0
  18. package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +78 -0
  19. package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +102 -0
  20. package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +61 -0
  21. package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +93 -0
  22. package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +153 -0
  23. package/schematics/nest-add/index.js +73 -49
  24. package/schematics/nest-add/index.js.map +1 -1
  25. package/schematics/nest-add/index.ts +100 -68
  26. package/schematics/nest-add/init.schema.d.ts +2 -0
  27. package/schematics/nest-add/init.schema.ts +11 -1
  28. package/schematics/nest-add/schema.json +25 -12
  29. package/tsconfig.build.tsbuildinfo +1 -1
package/README.md CHANGED
@@ -1,115 +1,322 @@
1
1
  # @open-rlb/nestjs-amqp
2
2
 
3
- Quick guide for using the npm package.
3
+ Libreria **NestJS** che fornisce un'astrazione di alto livello su **RabbitMQ/AMQP**, più un **API Gateway HTTP/WebSocket** che traduce le richieste esterne in messaggi sul broker.
4
4
 
5
- Guide scope: `rpc`, `handle`, `broadcast`.
6
-
7
- ## Installation
5
+ È il cuore di un'architettura a microservizi event-driven: i servizi comunicano tra loro via RabbitMQ con semplici decoratori, e un gateway espone tutto al mondo esterno via HTTP/WS, il tutto guidato dalla configurazione YAML.
8
6
 
9
7
  ```bash
10
8
  npm i @open-rlb/nestjs-amqp
11
9
  ```
12
10
 
13
- ## Basic setup
11
+ ### Installazione automatica (`nest add`)
12
+
13
+ Uno **schematic** wira la libreria nel tuo progetto NestJS: aggiunge i moduli all'`AppModule`, crea il config loader e un `config.yaml`, copia le **skill Claude** in `.claude/skills/` e — in base alla modalità gateway — include o meno la parte HTTP/WebSocket (sia nello YAML sia nella factory dei moduli).
14
+
15
+ ```bash
16
+ # con gateway HTTP/WebSocket (default)
17
+ nest add @open-rlb/nestjs-amqp
18
+
19
+ # solo microservizio AMQP (niente gateway)
20
+ nest g @open-rlb/nestjs-amqp:nest-add --gateway=false
21
+ ```
22
+
23
+ Opzioni: `--gateway` (on/off, default on), `--module` (default `src/app.module.ts`), `--main` (default `src/main.ts`), `--config` (default `config/config.yaml`), `--skills` (copia le skill, default on), `--skip-install`.
24
+
25
+ Con `--gateway=false` la factory passa solo `{ options, topics, appOptions, authOptions }` e non importa `ProxyModule`/`HttpModule`; con il gateway attivo aggiunge anche `gatewayOptions`, `ProxyModule.forRoot([])`, `HttpModule` e il `WsAdapter` in `main.ts`. Lo schematic è idempotente (non tocca un `AppModule` che già importa `BrokerModule`).
14
26
 
15
- ### `AppModule`
27
+ > Documentazione completa. Indice:
28
+ > [Architettura](#architettura) ·
29
+ > [Quick start](#quick-start) ·
30
+ > [Configurazione](#configurazione-completa) ·
31
+ > [Scrivere un microservizio (AMQP)](#scrivere-un-microservizio-amqp) ·
32
+ > [Gateway HTTP](#gateway-http) ·
33
+ > [Gateway WebSocket](#gateway-websocket) ·
34
+ > [Remote config](#remote-config) ·
35
+ > [API `BrokerService`](#api-brokerservice) ·
36
+ > [⚠️ Gotcha e casi a rischio bug](#️-gotcha-e-casi-a-rischio-bug) ·
37
+ > [Errori comuni](#errori-comuni)
38
+
39
+ ---
40
+
41
+ ## Architettura
42
+
43
+ Monorepo NestJS (vedi `nest-cli.json`):
44
+
45
+ | Progetto | Tipo | Descrizione |
46
+ | ------------------------ | ----------- | ------------------------------------------------ |
47
+ | `libs/rlb-nestjs-amqp` | library | La libreria vera e propria (il prodotto npm) |
48
+ | `apps/gateway` | application | App di esempio/riferimento che usa la libreria |
49
+
50
+ ```
51
+ ┌─────────────────────────────────────────────────────────┐
52
+ │ Client esterni (HTTP, WebSocket) │
53
+ └───────────────┬─────────────────────────────────────────┘
54
+
55
+ ┌───────▼────────┐ modules/proxy ── Gateway
56
+ │ HttpHandler │ - registra route HTTP dinamiche
57
+ │ WebSocketSvc │ - auth (jwt/jwks/basic/str-compare) + ACL/ruoli
58
+ │ JwtService │ - traduce HTTP/WS → messaggi broker
59
+ └───────┬────────┘
60
+
61
+ ┌───────▼────────┐ modules/broker ── Astrazione AMQP
62
+ │ BrokerService │ - rpc / handle / broadcast / event
63
+ │ MetadataScanner│ - decoratori @BrokerAction / @BrokerParam
64
+ │ HandlerRegistry│ - auto-discovery dei metodi via reflect-metadata
65
+ └───────┬────────┘
66
+
67
+ ┌───────▼────────┐ amqp-lib ── Driver AMQP a basso livello
68
+ │ AmqpConnection │ - connessione gestita (riconnessione, canali)
69
+ │ │ - publish/consume, RPC con correlationId, Nack
70
+ └───────┬────────┘
71
+
72
+ ┌─────▼─────┐
73
+ │ RabbitMQ │
74
+ └───────────┘
75
+ ```
76
+
77
+ ### I tre strati
78
+
79
+ 1. **`amqp-lib`** — driver a basso livello (`AmqpConnection`): connessione resiliente (`amqp-connection-manager`), canali gestiti, setup di exchange/queue/binding al boot, RPC con `correlationId` + *direct-reply-to*, consumer con gestione errori (`Nack` → ack/reject/requeue), graceful shutdown.
80
+ 2. **`modules/broker`** — astrazione di business: `BrokerService`, decoratori `@BrokerAction`/`@BrokerParam`, `MetadataScannerService` (auto-discovery dei metodi decorati e registrazione automatica dei consumer).
81
+ 3. **`modules/proxy`** — gateway HTTP/WebSocket: registrazione dinamica di route Express, auth pluggable, ACL/ruoli, WebSocket sicuro e scalabile, forwarding webhook.
82
+
83
+ ### Flusso di una richiesta
84
+
85
+ ```
86
+ HTTP/WS request → Gateway → (RPC | event) su RabbitMQ → microservizio (@BrokerAction)
87
+ → risposta (solo RPC) → HTTP/WS response
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Quick start
93
+
94
+ ### 1. `AppModule`
16
95
 
17
96
  ```ts
18
97
  import { HttpModule } from '@nestjs/axios';
19
98
  import { Module } from '@nestjs/common';
20
99
  import { ConfigModule, ConfigService } from '@nestjs/config';
21
- import { BrokerModule, ProxyModule } from '@open-rlb/nestjs-amqp';
100
+ import { AppConfig, BrokerModule, BrokerTopic, GatewayConfig, ProxyModule } from '@open-rlb/nestjs-amqp';
101
+ import { RabbitMQConfig } from '@open-rlb/nestjs-amqp/amqp-lib/config/rabbitmq.config';
102
+ import { HandlerAuthConfig } from '@open-rlb/nestjs-amqp/modules/broker/config/handler-auth.config';
103
+ import yamlConfig from './config/config.loader';
22
104
 
23
105
  @Module({
24
106
  imports: [
107
+ ConfigModule.forRoot({ isGlobal: true, load: [yamlConfig] }),
25
108
  BrokerModule.forRootAsync({
26
109
  imports: [ConfigModule],
27
110
  inject: [ConfigService],
28
111
  useFactory: async (config: ConfigService) => ({
29
- options: config.get('broker'),
30
- topics: config.get('topics'),
31
- appOptions: config.get('app'),
32
- authOptions: config.get('auth-providers'),
33
- gatewayOptions: config.get('gateway'),
112
+ options: config.get<RabbitMQConfig>('broker'),
113
+ topics: config.get<BrokerTopic[]>('topics'),
114
+ appOptions: config.get<AppConfig>('app'),
115
+ authOptions: config.get<HandlerAuthConfig[]>('auth-providers'),
116
+ gatewayOptions: config.get<GatewayConfig>('gateway'),
34
117
  }),
35
118
  }),
36
119
  HttpModule,
37
- ProxyModule.forRoot([]),
120
+ ProxyModule.forRoot([
121
+ // { provide: RLB_GTW_ACL_ROLE_SERVICE, useClass: MyAclService }, // solo se usi `roles`
122
+ ]),
38
123
  ],
39
124
  })
40
125
  export class AppModule {}
41
126
  ```
42
127
 
43
- ### Bootstrap
128
+ ### 2. Bootstrap (`main.ts`)
44
129
 
45
- If you use `parseRaw: true` in gateway routes:
130
+ ```ts
131
+ import { NestFactory } from '@nestjs/core';
132
+ import { WsAdapter } from '@nestjs/platform-ws';
133
+ import { AppModule } from './app.module';
134
+
135
+ async function bootstrap() {
136
+ // rawBody: true è OBBLIGATORIO se usi parseRaw nelle route del gateway
137
+ const app = await NestFactory.create(AppModule, { rawBody: true });
138
+ app.useWebSocketAdapter(new WsAdapter(app)); // solo se usi il gateway WebSocket
139
+ await app.listen(3000, '0.0.0.0');
140
+ }
141
+ bootstrap();
142
+ ```
143
+
144
+ ### 3. Config loader (`config/config.loader.ts`)
46
145
 
47
146
  ```ts
48
- const app = await NestFactory.create(AppModule, { rawBody: true });
147
+ import { readFileSync } from 'fs';
148
+ import * as yaml from 'js-yaml';
149
+ import { join } from 'path';
150
+
151
+ const YAML_CONFIG_FILENAME = 'config/config.yaml';
152
+
153
+ export default () =>
154
+ yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
49
155
  ```
50
156
 
51
- ## Minimal `config.yaml`
157
+ ---
52
158
 
53
- ```yaml
54
- # yaml-language-server: $schema=./schema.json
159
+ ## Configurazione completa
160
+
161
+ Il file `config.yaml` ha cinque sezioni di primo livello: `app`, `broker`, `topics`, `auth-providers`, `gateway`.
162
+
163
+ ### `app`
55
164
 
165
+ ```yaml
56
166
  app:
57
- environment: development
167
+ port: 3000
168
+ host: 0.0.0.0
169
+ environment: development # 'development' | 'production' (controlla il dettaglio degli errori esposti)
170
+ ```
171
+
172
+ > In `production` gli errori restituiti dal gateway sono ridotti a `{ message, name }`; in `development` viene incluso lo stack/dettaglio. Vedi `UtilsService.error2Object`.
58
173
 
174
+ ### `broker`
175
+
176
+ ```yaml
59
177
  broker:
60
- uri: "amqp://localhost/<vhost>"
61
- defaultSubscribeErrorBehavior: "ack"
62
- defaultPublishErrorBehavior: "reject"
63
- connectionManagerOptions:
178
+ name: rabbitmq
179
+ uri: "amqp://user:pass@localhost:5672/vhost" # stringa o array di URI (failover)
180
+ prefetchCount: 10
181
+ defaultRpcTimeout: 10000 # ms, default per requestData
182
+ defaultSubscribeErrorBehavior: ack # ack | reject | requeue (comportamento di default sugli errori consumer)
183
+
184
+ connectionManagerOptions: # opzioni amqp-connection-manager
64
185
  heartbeatIntervalInSeconds: 60
65
186
  reconnectTimeInSeconds: 60
66
187
  connectionOptions:
67
188
  clientProperties:
68
- connection_name: "connection-name"
189
+ connection_name: my-service # OBBLIGATORIO per broadcast e per il gateway WebSocket
69
190
  credentials:
70
- mechanism: PLAIN
191
+ mechanism: PLAIN # PLAIN | EXTERNAL | AMQPLAIN
71
192
  username: guest
72
193
  password: guest
73
194
 
74
195
  exchanges:
75
196
  - name: users-ex
76
- type: direct
77
- createExchangeIfNotExists: true
78
- options:
79
- durable: true
197
+ type: direct # direct | topic | fanout | headers
198
+ createExchangeIfNotExists: true # false → checkExchange (deve già esistere)
199
+ options: { durable: true }
80
200
 
81
201
  queues:
82
202
  - name: users-rpc-q
83
203
  exchange: users-ex
84
- routingKey: users.rpc
204
+ routingKey: users.rpc # string | string[]; OBBLIGATORIO se exchange è di tipo `topic`
85
205
  createQueueIfNotExists: true
86
- options:
87
- durable: true
206
+ options: { durable: true }
207
+
208
+ replyQueues: # mappa exchange → reply queue per le risposte RPC
209
+ users-ex: users-reply-q # se omesso si usa la direct-reply-to di RabbitMQ
210
+ ```
211
+
212
+ ### `topics`
213
+
214
+ Un topic mappa un nome logico (azione/microservizio) su un percorso AMQP. Il `mode` decide la semantica.
88
215
 
89
- replyQueues:
90
- users-ex: users-reply-q
216
+ | `mode` | Quando usarlo | Campi richiesti | Semantica |
217
+ | ----------- | -------------------------------------- | ------------------------------------------------------- | ------------------------------------ |
218
+ | `rpc` | request/response | `name`, `queue` (o `exchange`+`routingKey`) | risposta immediata + timeout |
219
+ | `handle` | worker su una coda | `name`, `queue` | consumer di coda semplice |
220
+ | `broadcast` | un messaggio a molti consumer | `name`, `exchange`, `routingKey` | fanout/topic; richiede `connection_name` |
221
+ | `event` | publish senza risposta | `name`, `queue` **oppure** `exchange`+`routingKey` | fire-and-forget |
91
222
 
223
+ ```yaml
92
224
  topics:
93
225
  - name: users-rpc
94
226
  mode: rpc
95
- queue: users-rpc-q
227
+ queue: users-rpc-q # deve esistere in broker.queues[]
228
+
229
+ - name: invoice-handle
230
+ mode: handle
231
+ queue: invoice-handle-q
232
+
233
+ - name: notify-broadcast
234
+ mode: broadcast
235
+ exchange: notify-ex
236
+ routingKey: notify.#
237
+
238
+ - name: audit-event
239
+ mode: event
240
+ exchange: audit-ex
241
+ routingKey: audit.created
242
+ ```
243
+
244
+ > `toObservable: true` su un topic `handle` instrada i messaggi su `BrokerService.events$` (Observable RxJS) invece che a un handler registrato.
96
245
 
246
+ ### `auth-providers`
247
+
248
+ Provider di autenticazione usati dalle route del gateway (`gateway.paths[].auth`) e dagli eventi WebSocket (`gateway.events[].auth`).
249
+
250
+ ```yaml
251
+ auth-providers:
252
+ - name: gateway-jwks
253
+ type: jwks # jwt | jwks | basic | str-compare | none
254
+ issuer: https://issuer.example.com/realms/main
255
+ jwksUri: https://issuer.example.com/certs
256
+ algorithms: [RS256]
257
+ httpsAllowUnauthorized: false # true SOLO per issuer self-signed in dev
258
+ jwtMap: # claim del token → claim mappato (header-prefixed)
259
+ - sub:userId
260
+ - roles:roles
261
+ headerPrefix: X-GTW-AUTH- # prefisso degli header propagati ai microservizi
262
+ uidClaim: USERID # dest (uppercase) usato come user id per l'ACL
263
+ usernameClaim: USERNAME
264
+ aclTopic: acl # topic RPC interrogato per i ruoli
265
+ aclAction: can-user-do
266
+
267
+ - name: gateway-jwt
268
+ type: jwt
269
+ secret: your-jwt-secret
270
+ issuer: https://issuer.example.com/realms/main
271
+ audience: your-audience
272
+ algorithms: [HS256]
273
+ jwtMap: [sub:userId, roles:roles]
274
+ headerPrefix: X-GTW-AUTH-
275
+ uidClaim: USERID
276
+ usernameClaim: USERNAME
277
+ aclTopic: acl
278
+ aclAction: can-user-do
279
+
280
+ - name: gateway-basic
281
+ type: basic
282
+ clientId: my-user
283
+ clientSecret: my-pass
284
+ headerPrefix: X-GTW-AUTH-
285
+
286
+ - name: gateway-str
287
+ type: str-compare
288
+ secret: your-static-token
289
+ headerPrefix: Bearer # prefisso atteso nell'header Authorization
290
+ ```
291
+
292
+ Mapping dei claim: un token con `{ sub: "u_1", roles: [...] }` e `jwtMap: [sub:userId]`, `headerPrefix: X-GTW-AUTH-` produce l'header `X-GTW-AUTH-USERID = u_1` propagato al microservizio. Leggilo con `@BrokerParam('header', 'X-GTW-AUTH-USERID')`.
293
+
294
+ ### `gateway`
295
+
296
+ ```yaml
97
297
  gateway:
98
298
  mode: gateway
99
- paths:
100
- - name: users-create
101
- method: POST
102
- path: /users
103
- dataSource: body
104
- topic: users-rpc
105
- action: user.create
106
- mode: rpc
107
- events: []
299
+ headerPrefix: X-GTW- # prefisso per gli header inoltrati (forwardHeaders)
300
+
301
+ ws: # opzioni WebSocket — solo livello connessione
302
+ maxConnections: 5000
303
+ maxSubscriptionsPerClient: 50
304
+ heartbeatIntervalMs: 30000
305
+ # auth/roles/scope sono dichiarati PER-EVENTO (events[].auth/requireAuth/roles/...)
306
+
307
+ loadConfig: # caricamento remoto di paths/events via RPC (opzionale)
308
+ paths: { topic: gtw.config, action: get-paths }
309
+ events: { topic: gtw.config, action: get-events }
310
+
311
+ paths: [ ... ] # vedi "Gateway HTTP"
312
+ events: [ ... ] # vedi "Gateway WebSocket"
108
313
  ```
109
314
 
110
- ## Quick usage
315
+ ---
316
+
317
+ ## Scrivere un microservizio (AMQP)
111
318
 
112
- ### RPC handler with decorators
319
+ ### Handler con i decoratori
113
320
 
114
321
  ```ts
115
322
  import { Injectable } from '@nestjs/common';
@@ -117,6 +324,8 @@ import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
117
324
 
118
325
  @Injectable()
119
326
  export class UsersActionService {
327
+ // @BrokerAction(topic, action, type?) — il `type` è documentativo: l'handler è
328
+ // SEMPRE raggiungibile sia in rpc sia in event (vedi "Doppio comportamento").
120
329
  @BrokerAction('users-rpc', 'user.create', 'rpc')
121
330
  async createUser(
122
331
  @BrokerParam('body', 'email') email: string,
@@ -128,100 +337,105 @@ export class UsersActionService {
128
337
  }
129
338
  ```
130
339
 
131
- ### RPC call from code
340
+ Registra il servizio come provider in un modulo NestJS qualunque: il `MetadataScannerService` lo scopre all'avvio e registra automaticamente il consumer per il topic.
132
341
 
133
- ```ts
134
- import { Injectable } from '@nestjs/common';
135
- import { BrokerService } from '@open-rlb/nestjs-amqp';
342
+ #### Sorgenti `@BrokerParam(source, name?)`
136
343
 
137
- @Injectable()
138
- export class UsersClientService {
139
- constructor(private readonly broker: BrokerService) {}
344
+ | Source | Valore iniettato |
345
+ | ----------- | ----------------------------------------------------- |
346
+ | `body` | `payload[name ?? nomeParametro]` |
347
+ | `body-full` | payload completo |
348
+ | `header` | `headers[name ?? nomeParametro]` |
349
+ | `tag` | consumer tag AMQP |
350
+ | `action` | action del messaggio |
351
+ | `topic` | topic corrente |
140
352
 
141
- async createUser() {
142
- return this.broker.requestData(
143
- 'users-rpc',
144
- 'user.create',
145
- { email: 'john@example.com', role: 'admin' },
146
- { 'X-Tenant': 'acme' },
147
- 5000,
148
- );
149
- }
150
- }
353
+ > Se ometti `@BrokerParam` su un parametro, il default è `{ source: 'body' }` con chiave = nome del parametro.
354
+
355
+ ### Doppio comportamento RPC / event
356
+
357
+ Ogni `@BrokerAction` è eseguibile **sia in RPC sia in event**, senza modifiche al servizio. Cambia solo cosa attende il chiamante.
358
+
359
+ | Modalità | Come si invoca | Cosa si attende |
360
+ | -------- | ------------------------------------------------- | ------------------------------------------------------- |
361
+ | `rpc` | `broker.requestData(...)` / path `mode: rpc` | la **risposta** del metodo (request/response) |
362
+ | `event` | `broker.publishMessage(...)` / path `mode: event` | solo che il **broker prenda in carico** (publisher confirm) |
363
+
364
+ `publishMessage` è `async` e si risolve solo al publisher confirm (rigetta su nack/errore). Sul gateway, una path `mode: event` risponde `202` **dopo** il confirm e `503` se il broker non accetta.
365
+
366
+ ```yaml
367
+ # Lo stesso topic/action esposto nei due modi
368
+ gateway:
369
+ paths:
370
+ - { name: users-create-sync, method: POST, path: /users, topic: users-rpc, action: user.create, mode: rpc }
371
+ - { name: users-create-async, method: POST, path: /users/async, topic: users-rpc, action: user.create, mode: event }
151
372
  ```
152
373
 
153
- ### Manual consumer (without decorators)
374
+ ### Consumer manuali (senza decoratori)
154
375
 
155
376
  ```ts
377
+ // RPC
156
378
  await broker.registerRpc<{ id: string }, { ok: boolean }>('health-rpc', async (event) => {
157
379
  return { ok: !!event.payload?.id };
158
380
  });
159
381
 
382
+ // handle / broadcast (gli handler devono restituire void)
160
383
  await broker.registerHandler<{ invoiceId: string }>('invoice-handle', async (event) => {
161
384
  console.log(event.payload.invoiceId);
162
385
  });
163
386
  ```
164
387
 
165
- ## Quick reference
166
-
167
- ### `BrokerService`
168
-
169
- | Method | Use |
170
- | ---------------------------------------------------------- | ------------------------------- |
171
- | `requestData(topic, action, payload?, headers?, timeout?)` | RPC request/response |
172
- | `registerRpc(topic, handler)` | manual RPC consumer |
173
- | `registerHandler(topic, handler)` | `handle` / `broadcast` consumer |
174
-
175
- ### Topic types (`topics[].mode`)
176
-
177
- | Type | Use it when | Minimal config | Why |
178
- | ----------- | -------------------------------------- | ------------------------------------------------------- | ------------------------------------ |
179
- | `rpc` | you need request/response | `name`, `mode: rpc`, `queue` (or `exchange+routingKey`) | immediate response + timeout control |
180
- | `handle` | you need a worker on one queue | `name`, `mode: handle`, `queue` | simple queue consumer |
181
- | `broadcast` | you need one message to many consumers | `name`, `mode: broadcast`, `exchange`, `routingKey` | fanout/topic pattern |
182
- | `event` | you need publish without response | `name`, `mode: event`, `queue` or `exchange+routingKey` | fire-and-forget (not covered here) |
183
-
184
- Quick snippet:
388
+ ### Pubblicare / chiamare da codice
185
389
 
186
- ```yaml
187
- topics:
188
- - name: users-rpc
189
- mode: rpc
190
- queue: users-rpc-q
390
+ ```ts
391
+ @Injectable()
392
+ export class UsersClient {
393
+ constructor(private readonly broker: BrokerService) {}
191
394
 
192
- - name: invoice-handle
193
- mode: handle
194
- queue: invoice-handle-q
395
+ // RPC: attende la risposta
396
+ createUserRpc() {
397
+ return this.broker.requestData('users-rpc', 'user.create',
398
+ { email: 'a@b.c', role: 'admin' }, { 'X-Tenant': 'acme' }, 5000);
399
+ }
195
400
 
196
- - name: notify-broadcast
197
- mode: broadcast
198
- exchange: notify-ex
199
- routingKey: notify.#
401
+ // Event: attende solo che il broker prenda in carico
402
+ async emitAudit() {
403
+ await this.broker.publishMessage('audit-event', 'audit.created', { entity: 'user', id: 'u_1' });
404
+ }
405
+ }
200
406
  ```
201
407
 
202
- ### Decorators
408
+ ---
203
409
 
204
- | Decorator | Use |
205
- | ------------------------------------------------------------- | ------------------------------------ |
206
- | `@BrokerAction(topic, action, type?)` | binds method to topic/action |
207
- | `@BrokerParam(source, name?)` | maps method params from message data |
208
- | `@BrokerAuth(authName, allowAnonymous?, roles?)` | auth metadata |
209
- | `@BrokerHTTP(method, path, dataSource?, timeout?, parseRaw?)` | HTTP metadata |
410
+ ## Gateway HTTP
210
411
 
211
- ### `@BrokerParam` sources
412
+ Le route sono dichiarate in `gateway.paths[]` e registrate dinamicamente su Express al boot.
212
413
 
213
- | Source | Injected value |
214
- | ----------- | --------------------------------- |
215
- | `body` | `payload[name or parameter name]` |
216
- | `body-full` | full payload |
217
- | `header` | `headers[name or parameter name]` |
218
- | `tag` | AMQP consumer tag |
219
- | `action` | message action |
220
- | `topic` | current topic |
414
+ ```yaml
415
+ gateway:
416
+ paths:
417
+ - name: users-create
418
+ method: POST # GET | POST | PUT | DELETE | PATCH
419
+ path: /users/:tenant? # supporta route param Express
420
+ dataSource: body # body | query | params | body-query | query-body
421
+ topic: users-rpc
422
+ action: user.create
423
+ mode: rpc # rpc | event
424
+ timeout: 7000 # solo rpc
425
+ auth: gateway-jwks # nome di un auth-provider
426
+ allowAnonymous: false # true → consente l'accesso anche senza auth valida
427
+ roles: [users.create] # richiede un IAclRoleService registrato
428
+ successStatusCode: 201
429
+ binary: false # true → risposta come Buffer base64-decoded
430
+ redirect: 302 # se valorizzato, redirect alla URL contenuta nella risposta
431
+ headers: { Cache-Control: no-store } # header statici sulla risposta
432
+ forwardHeaders: { Tenant: x-tenant } # header della richiesta da inoltrare al microservizio
433
+ parseRaw: false # true → inoltra il body raw come $raw (richiede rawBody:true nel bootstrap)
434
+ ```
221
435
 
222
- ### `gateway.paths[].dataSource`
436
+ #### Composizione del payload (`dataSource`)
223
437
 
224
- | Value | Payload |
438
+ | Valore | Payload inviato al broker |
225
439
  | ------------ | -------------------------------- |
226
440
  | `body` | `{...params, ...body}` |
227
441
  | `query` | `{...params, ...query}` |
@@ -229,72 +443,177 @@ topics:
229
443
  | `body-query` | `{...params, ...query, ...body}` |
230
444
  | `query-body` | `{...params, ...body, ...query}` |
231
445
 
232
- ### Auth providers
446
+ > I route param (`req.params`) vengono **ri-applicati per ultimi** su `data`: a parità di chiave vincono sempre sul body/query. Gli upload multipart finiscono in `$files`; il body raw (se `parseRaw`) in `$raw`.
233
447
 
234
- #### Type: `jwks`
448
+ #### Mappatura errori → status HTTP
235
449
 
236
- ```yaml
237
- auth-providers:
238
- - name: gateway-jwks
239
- type: jwks
240
- issuer: https://issuer.example.com/realms/main
241
- jwksUri: https://issuer.example.com/certs
242
- algorithms: [RS256]
243
- jwtMap:
244
- - sub:userId
245
- - roles:roles
246
- headerPrefix: X-GTW-AUTH-
247
- uidClaim: USERID
248
- usernameClaim: USERNAME
249
- aclTopic: acl
250
- aclAction: can-user-do
251
- ```
450
+ Il `name` dell'errore lanciato dal microservizio determina lo status: `BadRequestError`/`InvalidParamsErrror` → 400, `UnauthorizedError` → 401, `ForbiddenError` → 403, `NotFoundError` → 404, altrimenti → 500. In `mode: event` un confirm fallito → 503.
451
+
452
+ ---
453
+
454
+ ## Gateway WebSocket
455
+
456
+ Il gateway WebSocket inoltra eventi del broker ai client connessi (o a webhook HTTP), con autenticazione, autorizzazione per evento e funzionamento corretto in **multi-istanza** (fan-out).
252
457
 
253
- #### Type: `jwt`
458
+ ### Configurazione
254
459
 
255
460
  ```yaml
256
- auth-providers:
257
- - name: gateway-jwt
258
- type: jwt
259
- secret: your-jwt-secret
260
- issuer: https://issuer.example.com/realms/main
261
- audience: your-audience
262
- algorithms: [HS256]
263
- jwtMap:
264
- - sub:userId
265
- - roles:roles
266
- headerPrefix: X-GTW-AUTH-
267
- uidClaim: USERID
268
- usernameClaim: USERNAME
269
- aclTopic: acl
270
- aclAction: can-user-do
461
+ gateway:
462
+ ws: # solo livello connessione
463
+ maxConnections: 5000 # limite connessioni per istanza
464
+ maxSubscriptionsPerClient: 50 # limite sottoscrizioni per client
465
+ heartbeatIntervalMs: 30000 # ping/pong per chiudere le connessioni morte
466
+
467
+ events:
468
+ - name: orders
469
+ type: ws # ws | http (webhook)
470
+ exchange: orders-ex
471
+ routingKey: orders.#
472
+ auth: gateway-jwks # provider che verifica il token e mappa i claim PER QUESTO evento
473
+ requireAuth: true # default true quando `auth` è impostato; false → auth opzionale
474
+ roles: [orders.read] # verifica ACL via IAclRoleService
475
+ scopeClaim: X-GTW-AUTH-USERID # inoltra solo i messaggi dell'utente...
476
+ payloadKey: userId # ...dove payload.userId === claim dell'utente
477
+
478
+ - name: invoices # forwarding webhook
479
+ type: http
480
+ exchange: inv-ex
481
+ routingKey: inv.#
482
+ url: https://hooks.example.com/invoices
483
+ method: POST
484
+ timeout: 8000
271
485
  ```
272
486
 
273
- #### Type: `str-compare`
487
+ ### Autenticazione (token nel subprotocol)
274
488
 
275
- ```yaml
276
- auth-providers:
277
- - name: gateway-str
278
- type: str-compare
279
- secret: your-static-token
280
- headerPrefix: Bearer
489
+ I browser non possono impostare header custom sull'handshake, quindi il token JWT viaggia nel **subprotocol** (`Sec-WebSocket-Protocol`):
490
+
491
+ ```js
492
+ const ws = new WebSocket('ws://localhost:3000', [token]); // oppure ['bearer', token]
281
493
  ```
282
494
 
283
- ### Gateway path (RPC)
495
+ Il token viene conservato sulla connessione e **verificato al momento del `subscribe` con il provider dichiarato dall'evento** (`events[].auth`), che ne mappa anche i claim. La verifica è memoizzata per provider: lo stesso token è verificato al più una volta per provider. Eventi diversi possono usare provider diversi.
284
496
 
285
- ```yaml
286
- - name: users-create
287
- method: POST
288
- path: /users
289
- dataSource: body
290
- topic: users-rpc
291
- action: user.create
292
- mode: rpc
293
- timeout: 7000
497
+ ### Protocollo client
498
+
499
+ ```js
500
+ ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders', select: { status: 'open' } }));
501
+ ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'orders' }));
502
+ // messaggi in arrivo: { topic: 'onOrders', data: <payload> }
503
+ // errori: { topic: 'onError', data: { event, error } }
294
504
  ```
295
505
 
296
- ## Common errors
506
+ ### Sicurezza e scalabilità
507
+
508
+ - **Auth per evento**: `events[].auth` indica il provider che verifica il token e mappa i claim per quell'evento; `requireAuth: false` rende l'auth opzionale (anonimi ammessi, claim mappati se il token c'è). Subscribe negato (`onError: unauthorized`) se l'auth è richiesta e il token non è valido.
509
+ - **Authz per evento**: `roles` (ACL via `IAclRoleService`) sull'identità ricavata da `auth`.
510
+ - **Scoping per-utente**: `scopeClaim` + `payloadKey` impediscono a un client di ricevere dati altrui tramite un `select` arbitrario (il filtro server-side è intersecato con quello del client, mai allargato). Se `scopeClaim` è impostato senza `payloadKey`, **nega tutto** (safe default).
511
+ - **Multi-istanza**: ogni istanza crea una coda AMQP **effimera ed esclusiva** (nome unico per processo) → tutte le repliche ricevono ogni evento e lo inoltrano ai rispettivi client.
512
+ - **Hardening**: heartbeat ping/pong, limiti connessioni/sottoscrizioni, cleanup robusto su `close`/`error`.
513
+
514
+ ---
515
+
516
+ ## Remote config
517
+
518
+ `RemoteConfigService` permette ai microservizi di **registrare le proprie route nel gateway a runtime**, pubblicando le loro `PathDefinition` su un exchange fanout `config.ms`. Il gateway le riceve e chiama `HttpHandlerService.registerPath()` dinamicamente. In alternativa, `gateway.loadConfig` carica paths/events tramite una singola chiamata RPC all'avvio.
519
+
520
+ ---
521
+
522
+ ## API `BrokerService`
523
+
524
+ | Metodo | Uso |
525
+ | ------------------------------------------------------------ | ------------------------------------------------ |
526
+ | `requestData(topic, action, payload?, headers?, timeout?)` | RPC request/response (attende la risposta) |
527
+ | `publishMessage(topic, action, payload, headers?)` → `Promise<boolean>` | event fire-and-forget con publisher confirm |
528
+ | `registerRpc(topic, handler)` | consumer RPC manuale |
529
+ | `registerHandler(topic, handler)` | consumer `handle` / `broadcast` (ritorna void) |
530
+ | `getRpc(topic)` / `getHandler(topic)` | recupera l'handler registrato |
531
+ | `events$` / `getEvents$<T>()` | Observable degli eventi dei topic `toObservable` |
532
+
533
+ ### Decoratori
534
+
535
+ | Decoratore | Uso |
536
+ | ------------------------------------------------------------- | ------------------------------------ |
537
+ | `@BrokerAction(topic, action, type?)` | lega un metodo a topic/action |
538
+ | `@BrokerParam(source, name?)` | mappa i parametri dai dati messaggio |
539
+ | `@BrokerAuth(authName, allowAnonymous?, roles?)` | metadati di auth (usati dallo scanner) |
540
+ | `@BrokerHTTP(method, path, dataSource?, timeout?, parseRaw?)` | metadati HTTP (usati dallo scanner) |
541
+
542
+ ### Pipe utility
543
+
544
+ `BooleanPipe` e `NumberPipe` convertono valori stringa/numerici (es. da query string). Esportate da `@open-rlb/nestjs-amqp`.
545
+
546
+ ---
547
+
548
+ ## ⚠️ Gotcha e casi a rischio bug
549
+
550
+ Questi sono i punti che causano più frequentemente bug silenziosi. **Leggili prima di estendere la lib.**
551
+
552
+ ### Decoratori e handler
553
+
554
+ 1. **Niente destructuring nei parametri dell'handler.** `@BrokerParam` associa i parametri leggendo il *source* della funzione con una regex (`getParamNames`). Una firma come `fn({ a, b })` rompe l'allineamento degli indici. Usa parametri semplici.
555
+ 2. **Evita i valori di default nei parametri.** C'è uno strip basilare (`removeDefaultsFromParams`), ma default complessi (oggetti, chiamate) disallineano la mappatura. Passa sempre un `name` esplicito a `@BrokerParam`.
556
+ 3. **`(topic, action)` deve essere unico.** Tutti gli `@BrokerAction` dello stesso topic condividono **una sola coda/consumer** e vengono smistati per `action`. Due metodi con lo stesso `(topic, action)` → il secondo sovrascrive il primo in silenzio.
557
+
558
+ ### Wiring topic ↔ queue ↔ exchange
559
+
560
+ 4. **Il `name` del topic deve coincidere ovunque**: `@BrokerAction(topic)`, `topics[].name`, `requestData/publishMessage(topic)`, `gateway.paths[].topic`/`events[]`. Un typo → `Topic X not found in configuration`.
561
+ 5. **`mode: rpc`/`handle` richiedono che `topics[].queue` esista in `broker.queues[]`**, e che il `queue.exchange` esista in `broker.exchanges[]`. In `handle` un queue mancante causa un NPE all'avvio (`queue.exchange`).
562
+ 6. **Exchange `type: topic` → il queue DEVE avere `routingKey`**, altrimenti l'avvio lancia `Queue ... has no routing key`.
563
+ 7. **`mode: broadcast` e gateway WebSocket richiedono `connection_name`** (`clientProperties.connection_name`), altrimenti throw.
564
+
565
+ ### RPC / timeout / errori
566
+
567
+ 8. **Reply RPC**: `requestData` risolve `replyTo` da `broker.replyQueues[exchange]`; se assente usa la direct-reply-to di RabbitMQ. Un `replyQueues` con la chiave exchange sbagliata → nessuna risposta → timeout.
568
+ 9. **Le eccezioni dell'handler RPC NON propagano come throw lato consumer**: vengono restituite come `{ success: false, error }` e `requestData` rilancia l'errore al chiamante. Sul gateway lo status dipende dal `error.name` (vedi tabella). Dai agli errori un `name` coerente.
569
+ 10. **Timeout di default 10s** (o `broker.defaultRpcTimeout`). Per RPC lente imposta `timeout` sulla path o sull'argomento di `requestData`.
570
+
571
+ ### Gateway HTTP
572
+
573
+ 11. **`parseRaw: true` richiede `NestFactory.create(AppModule, { rawBody: true })`**, altrimenti `$raw` è `undefined`.
574
+ 12. **I route param vincono sul body/query** (ri-applicati per ultimi). Attento alle collisioni di chiave (`:id` vs `body.id`).
575
+ 13. **Gli upload sono in `$files`** (multer `.any()`); i buffer vengono convertiti in stringa binaria — rigestiscili con cura lato consumer.
576
+
577
+ ### Auth / ACL
578
+
579
+ 14. **`roles` su una path o evento richiede un `IAclRoleService`** registrato via `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRoot([...])`. L'auth-provider deve definire `aclTopic`, `aclAction`, `uidClaim`, `usernameClaim`, e `uidClaim` deve corrispondere a un `dest` del `jwtMap`. Mancante → throw.
580
+ 15. **Gli header propagati sono uppercase e prefissati** (`${headerPrefix}${DEST}`): leggi `X-GTW-AUTH-USERID`, non `userId`.
581
+
582
+ ### WebSocket
583
+
584
+ 16. **`scopeClaim` referenzia il claim MAPPATO** (con `headerPrefix`, es. `X-GTW-AUTH-USERID`), non il claim grezzo del token. `payloadKey` è la chiave nel payload dell'evento. Senza `payloadKey`, lo scope nega tutto.
585
+ 17. **Non usare code durevoli condivise per gli eventi WS**: la lib crea una coda esclusiva per istanza apposta per il fan-out. Una coda fissa farebbe competere le istanze (i client di un'istanza perderebbero messaggi).
586
+
587
+ ### Publish / event
588
+
589
+ 18. **`publishMessage` è `async`: devi fare `await`** per ottenere la garanzia di publisher confirm e per intercettare i fallimenti. Senza `await` è fire-and-forget senza garanzia.
590
+ 19. **Gli handler `handle`/`broadcast` devono restituire `void`**: un valore di ritorno genera un warning (`Subscribe handlers should only return void`).
591
+
592
+ ### TLS / credenziali
593
+
594
+ 20. **JWKS verifica il TLS di default.** Usa `httpsAllowUnauthorized: true` su un provider solo per issuer self-signed in sviluppo.
595
+ 21. **`mechanism` credenziali**: `PLAIN` | `EXTERNAL` | `AMQPLAIN` (case-insensitive). Un valore sconosciuto non imposta la `response` → autenticazione fallita.
596
+
597
+ ---
598
+
599
+ ## Errori comuni
600
+
601
+ - `Topic <name> not found in configuration`: controlla `topics[].name`, `@BrokerAction`, `requestData`/`publishMessage`, `gateway.paths[].topic`.
602
+ - `Queue <name> not found in configuration`: verifica che `topics[].queue` esista in `broker.queues[]`.
603
+ - `Queue <name> has no routing key`: l'exchange è di tipo `topic` ma il queue non ha `routingKey`.
604
+ - `Client name is required ...`: manca `connection_name` (richiesto da broadcast e WebSocket).
605
+ - `ACL Role Service not found`: stai usando `roles` senza aver registrato `RLB_GTW_ACL_ROLE_SERVICE`.
606
+ - `401/403` dal gateway: controlla `auth`, `auth-providers[]`, e l'ACL service quando usi `roles`.
607
+ - Timeout RPC: `replyQueues` errato, `action` non gestita da alcun servizio, o handler troppo lento (`timeout`).
608
+
609
+ ---
610
+
611
+ ## Sviluppo
612
+
613
+ ```bash
614
+ npm run build # compila (tsc)
615
+ npm test # jest
616
+ npm run start:dev # nest start --watch (app gateway di esempio)
617
+ ```
297
618
 
298
- - `Topic <name> not found in configuration`: check `topics[].name`, `@BrokerAction`, `requestData`, `gateway.paths[].topic`.
299
- - `Queue <name> not found in configuration`: check that `topics[].queue` exists in `broker.queues[]`.
300
- - `401/403` from gateway: check `gateway.paths[].auth`, `auth-providers[]`, ACL service when using `roles`.
619
+ Licenza: MIT.