@open-rlb/nestjs-amqp 1.0.26 → 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.
- package/README.md +489 -170
- package/modules/broker/broker.module.js +1 -0
- package/modules/broker/broker.module.js.map +1 -1
- package/modules/broker/services/broker.service.js +2 -2
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/proxy/config/path-definition.config.d.ts +10 -1
- package/modules/proxy/services/http-auth-handler.service.d.ts +6 -0
- package/modules/proxy/services/http-auth-handler.service.js +42 -24
- package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.js +9 -3
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/modules/proxy/services/jwt.service.js +1 -1
- package/modules/proxy/services/jwt.service.js.map +1 -1
- package/modules/proxy/services/websocket.service.d.ts +30 -6
- package/modules/proxy/services/websocket.service.js +200 -82
- package/modules/proxy/services/websocket.service.js.map +1 -1
- package/package.json +4 -3
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +58 -0
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +207 -0
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +78 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +102 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +61 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +93 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +153 -0
- package/schematics/nest-add/index.js +73 -49
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +100 -68
- package/schematics/nest-add/init.schema.d.ts +2 -0
- package/schematics/nest-add/init.schema.ts +11 -1
- package/schematics/nest-add/schema.json +25 -12
- package/tsconfig.build.tsbuildinfo +1 -1
package/README.md
CHANGED
|
@@ -1,115 +1,322 @@
|
|
|
1
1
|
# @open-rlb/nestjs-amqp
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
---
|
|
52
158
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Scrivere un microservizio (AMQP)
|
|
111
318
|
|
|
112
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
import { Injectable } from '@nestjs/common';
|
|
135
|
-
import { BrokerService } from '@open-rlb/nestjs-amqp';
|
|
342
|
+
#### Sorgenti `@BrokerParam(source, name?)`
|
|
136
343
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
```
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
queue: users-rpc-q
|
|
390
|
+
```ts
|
|
391
|
+
@Injectable()
|
|
392
|
+
export class UsersClient {
|
|
393
|
+
constructor(private readonly broker: BrokerService) {}
|
|
191
394
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
408
|
+
---
|
|
203
409
|
|
|
204
|
-
|
|
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
|
-
|
|
412
|
+
Le route sono dichiarate in `gateway.paths[]` e registrate dinamicamente su Express al boot.
|
|
212
413
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
|
218
|
-
|
|
219
|
-
|
|
|
220
|
-
|
|
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
|
-
|
|
436
|
+
#### Composizione del payload (`dataSource`)
|
|
223
437
|
|
|
224
|
-
|
|
|
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
|
-
|
|
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
|
-
####
|
|
448
|
+
#### Mappatura errori → status HTTP
|
|
235
449
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
458
|
+
### Configurazione
|
|
254
459
|
|
|
255
460
|
```yaml
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
487
|
+
### Autenticazione (token nel subprotocol)
|
|
274
488
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|