@open-rlb/nestjs-amqp 1.0.27 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +572 -169
- package/amqp-lib/amqp/connection.d.ts +1 -1
- package/common/errors.d.ts +13 -0
- package/common/errors.js +26 -0
- package/common/errors.js.map +1 -0
- package/common/flatten.util.d.ts +1 -0
- package/common/flatten.util.js +29 -0
- package/common/flatten.util.js.map +1 -0
- package/common/index.d.ts +3 -0
- package/common/index.js +20 -0
- package/common/index.js.map +1 -0
- package/common/pagination.model.d.ts +6 -0
- package/common/pagination.model.js +3 -0
- package/common/pagination.model.js.map +1 -0
- package/index.d.ts +3 -0
- package/index.js +3 -0
- package/index.js.map +1 -1
- package/modules/acl/acl.module.d.ts +5 -0
- package/modules/acl/acl.module.js +36 -0
- package/modules/acl/acl.module.js.map +1 -0
- package/modules/acl/cache/acl-cache.service.d.ts +15 -0
- package/modules/acl/cache/acl-cache.service.js +98 -0
- package/modules/acl/cache/acl-cache.service.js.map +1 -0
- package/modules/acl/cache/cache-store.d.ts +6 -0
- package/modules/acl/cache/cache-store.js +3 -0
- package/modules/acl/cache/cache-store.js.map +1 -0
- package/modules/acl/config/acl.config.d.ts +8 -0
- package/modules/acl/config/acl.config.js +3 -0
- package/modules/acl/config/acl.config.js.map +1 -0
- package/modules/acl/const.d.ts +17 -0
- package/modules/acl/const.js +21 -0
- package/modules/acl/const.js.map +1 -0
- package/modules/acl/index.d.ts +11 -0
- package/modules/acl/index.js +28 -0
- package/modules/acl/index.js.map +1 -0
- package/modules/acl/models.d.ts +19 -0
- package/modules/acl/models.js +3 -0
- package/modules/acl/models.js.map +1 -0
- package/modules/acl/repository/acl-action.repository.d.ts +11 -0
- package/modules/acl/repository/acl-action.repository.js +7 -0
- package/modules/acl/repository/acl-action.repository.js.map +1 -0
- package/modules/acl/repository/acl-grant.repository.d.ts +11 -0
- package/modules/acl/repository/acl-grant.repository.js +7 -0
- package/modules/acl/repository/acl-grant.repository.js.map +1 -0
- package/modules/acl/repository/acl-role.repository.d.ts +10 -0
- package/modules/acl/repository/acl-role.repository.js +7 -0
- package/modules/acl/repository/acl-role.repository.js.map +1 -0
- package/modules/acl/services/acl-management.service.d.ts +26 -0
- package/modules/acl/services/acl-management.service.js +202 -0
- package/modules/acl/services/acl-management.service.js.map +1 -0
- package/modules/acl/services/acl.service.d.ts +11 -0
- package/modules/acl/services/acl.service.js +63 -0
- package/modules/acl/services/acl.service.js.map +1 -0
- package/modules/broker/broker.module.d.ts +1 -7
- package/modules/broker/broker.module.js +1 -27
- 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/gateway-admin/config/gateway-admin.config.d.ts +3 -0
- package/modules/gateway-admin/config/gateway-admin.config.js +3 -0
- package/modules/gateway-admin/config/gateway-admin.config.js.map +1 -0
- package/modules/gateway-admin/const.d.ts +18 -0
- package/modules/gateway-admin/const.js +22 -0
- package/modules/gateway-admin/const.js.map +1 -0
- package/modules/gateway-admin/gateway-admin.module.d.ts +5 -0
- package/modules/gateway-admin/gateway-admin.module.js +35 -0
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -0
- package/modules/gateway-admin/index.d.ts +11 -0
- package/modules/gateway-admin/index.js +28 -0
- package/modules/gateway-admin/index.js.map +1 -0
- package/modules/gateway-admin/models.d.ts +22 -0
- package/modules/gateway-admin/models.js +3 -0
- package/modules/gateway-admin/models.js.map +1 -0
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +15 -0
- package/modules/gateway-admin/repository/auth-provider.repository.js +7 -0
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -0
- package/modules/gateway-admin/repository/http-metric.repository.d.ts +7 -0
- package/modules/gateway-admin/repository/http-metric.repository.js +7 -0
- package/modules/gateway-admin/repository/http-metric.repository.js.map +1 -0
- package/modules/gateway-admin/repository/http-path.repository.d.ts +15 -0
- package/modules/gateway-admin/repository/http-path.repository.js +7 -0
- package/modules/gateway-admin/repository/http-path.repository.js.map +1 -0
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +14 -0
- package/modules/gateway-admin/services/gateway-auth.service.js +100 -0
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -0
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +11 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js +59 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -0
- package/modules/gateway-admin/services/gateway-path.service.d.ts +14 -0
- package/modules/gateway-admin/services/gateway-path.service.js +106 -0
- package/modules/gateway-admin/services/gateway-path.service.js.map +1 -0
- package/modules/gateway-admin/util/path-order.d.ts +3 -0
- package/modules/gateway-admin/util/path-order.js +36 -0
- package/modules/gateway-admin/util/path-order.js.map +1 -0
- package/modules/proxy/config/path-definition.config.d.ts +14 -1
- package/modules/proxy/proxy.module.d.ts +15 -2
- package/modules/proxy/proxy.module.js +24 -4
- package/modules/proxy/proxy.module.js.map +1 -1
- package/modules/proxy/services/http-auth-handler.service.d.ts +6 -0
- package/modules/proxy/services/http-auth-handler.service.js +48 -24
- package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +5 -1
- package/modules/proxy/services/http-handler.service.js +79 -10
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/modules/proxy/services/jwt.service.d.ts +3 -0
- package/modules/proxy/services/jwt.service.js +66 -9
- package/modules/proxy/services/jwt.service.js.map +1 -1
- package/modules/proxy/services/websocket.service.d.ts +33 -6
- package/modules/proxy/services/websocket.service.js +250 -82
- package/modules/proxy/services/websocket.service.js.map +1 -1
- package/package.json +29 -28
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +59 -0
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +214 -0
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +95 -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 +160 -0
- package/schematics/nest-add/index.js +82 -49
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +113 -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 +0 -1
package/README.md
CHANGED
|
@@ -1,115 +1,331 @@
|
|
|
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 a `BrokerModule` solo `{ options, topics, appOptions }` e non importa `ProxyModule`/`HttpModule`; con il gateway attivo aggiunge `ProxyModule.forRootAsync(...)` (che riceve `authOptions` + `gatewayOptions`), `HttpModule` e il `WsAdapter` in `main.ts`. Lo schematic è idempotente (non tocca un `AppModule` che già importa `BrokerModule`).
|
|
26
|
+
|
|
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
|
|
14
93
|
|
|
15
|
-
### `AppModule`
|
|
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'),
|
|
34
115
|
}),
|
|
35
116
|
}),
|
|
36
117
|
HttpModule,
|
|
37
|
-
ProxyModule
|
|
118
|
+
// auth-providers + gateway config → ProxyModule (non più BrokerModule)
|
|
119
|
+
ProxyModule.forRootAsync({
|
|
120
|
+
imports: [ConfigModule],
|
|
121
|
+
inject: [ConfigService],
|
|
122
|
+
useFactory: (config: ConfigService) => ({
|
|
123
|
+
authOptions: config.get<HandlerAuthConfig[]>('auth-providers'),
|
|
124
|
+
gatewayOptions: config.get<GatewayConfig>('gateway'),
|
|
125
|
+
}),
|
|
126
|
+
providers: [
|
|
127
|
+
// { provide: RLB_GTW_ACL_ROLE_SERVICE, useClass: MyAclService }, // solo se usi `roles`
|
|
128
|
+
],
|
|
129
|
+
}),
|
|
38
130
|
],
|
|
39
131
|
})
|
|
40
132
|
export class AppModule {}
|
|
41
133
|
```
|
|
42
134
|
|
|
43
|
-
### Bootstrap
|
|
135
|
+
### 2. Bootstrap (`main.ts`)
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { NestFactory } from '@nestjs/core';
|
|
139
|
+
import { WsAdapter } from '@nestjs/platform-ws';
|
|
140
|
+
import { AppModule } from './app.module';
|
|
141
|
+
|
|
142
|
+
async function bootstrap() {
|
|
143
|
+
// rawBody: true è OBBLIGATORIO se usi parseRaw nelle route del gateway
|
|
144
|
+
const app = await NestFactory.create(AppModule, { rawBody: true });
|
|
145
|
+
app.useWebSocketAdapter(new WsAdapter(app)); // solo se usi il gateway WebSocket
|
|
146
|
+
await app.listen(3000, '0.0.0.0');
|
|
147
|
+
}
|
|
148
|
+
bootstrap();
|
|
149
|
+
```
|
|
44
150
|
|
|
45
|
-
|
|
151
|
+
### 3. Config loader (`config/config.loader.ts`)
|
|
46
152
|
|
|
47
153
|
```ts
|
|
48
|
-
|
|
154
|
+
import { readFileSync } from 'fs';
|
|
155
|
+
import * as yaml from 'js-yaml';
|
|
156
|
+
import { join } from 'path';
|
|
157
|
+
|
|
158
|
+
const YAML_CONFIG_FILENAME = 'config/config.yaml';
|
|
159
|
+
|
|
160
|
+
export default () =>
|
|
161
|
+
yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
|
|
49
162
|
```
|
|
50
163
|
|
|
51
|
-
|
|
164
|
+
---
|
|
52
165
|
|
|
53
|
-
|
|
54
|
-
|
|
166
|
+
## Configurazione completa
|
|
167
|
+
|
|
168
|
+
Il file `config.yaml` ha cinque sezioni di primo livello: `app`, `broker`, `topics`, `auth-providers`, `gateway`.
|
|
55
169
|
|
|
170
|
+
### `app`
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
56
173
|
app:
|
|
57
|
-
|
|
174
|
+
port: 3000
|
|
175
|
+
host: 0.0.0.0
|
|
176
|
+
environment: development # 'development' | 'production' (controlla il dettaglio degli errori esposti)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> In `production` gli errori restituiti dal gateway sono ridotti a `{ message, name }`; in `development` viene incluso lo stack/dettaglio. Vedi `UtilsService.error2Object`.
|
|
58
180
|
|
|
181
|
+
### `broker`
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
59
184
|
broker:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
185
|
+
name: rabbitmq
|
|
186
|
+
uri: "amqp://user:pass@localhost:5672/vhost" # stringa o array di URI (failover)
|
|
187
|
+
prefetchCount: 10
|
|
188
|
+
defaultRpcTimeout: 10000 # ms, default per requestData
|
|
189
|
+
defaultSubscribeErrorBehavior: ack # ack | reject | requeue (comportamento di default sugli errori consumer)
|
|
190
|
+
|
|
191
|
+
connectionManagerOptions: # opzioni amqp-connection-manager
|
|
64
192
|
heartbeatIntervalInSeconds: 60
|
|
65
193
|
reconnectTimeInSeconds: 60
|
|
66
194
|
connectionOptions:
|
|
67
195
|
clientProperties:
|
|
68
|
-
connection_name:
|
|
196
|
+
connection_name: my-service # OBBLIGATORIO per broadcast e per il gateway WebSocket
|
|
69
197
|
credentials:
|
|
70
|
-
mechanism: PLAIN
|
|
198
|
+
mechanism: PLAIN # PLAIN | EXTERNAL | AMQPLAIN
|
|
71
199
|
username: guest
|
|
72
200
|
password: guest
|
|
73
201
|
|
|
74
202
|
exchanges:
|
|
75
203
|
- name: users-ex
|
|
76
|
-
type: direct
|
|
77
|
-
createExchangeIfNotExists: true
|
|
78
|
-
options:
|
|
79
|
-
durable: true
|
|
204
|
+
type: direct # direct | topic | fanout | headers
|
|
205
|
+
createExchangeIfNotExists: true # false → checkExchange (deve già esistere)
|
|
206
|
+
options: { durable: true }
|
|
80
207
|
|
|
81
208
|
queues:
|
|
82
209
|
- name: users-rpc-q
|
|
83
210
|
exchange: users-ex
|
|
84
|
-
routingKey: users.rpc
|
|
211
|
+
routingKey: users.rpc # string | string[]; OBBLIGATORIO se exchange è di tipo `topic`
|
|
85
212
|
createQueueIfNotExists: true
|
|
86
|
-
options:
|
|
87
|
-
|
|
213
|
+
options: { durable: true }
|
|
214
|
+
|
|
215
|
+
replyQueues: # mappa exchange → reply queue per le risposte RPC
|
|
216
|
+
users-ex: users-reply-q # se omesso si usa la direct-reply-to di RabbitMQ
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `topics`
|
|
88
220
|
|
|
89
|
-
|
|
90
|
-
|
|
221
|
+
Un topic mappa un nome logico (azione/microservizio) su un percorso AMQP. Il `mode` decide la semantica.
|
|
222
|
+
|
|
223
|
+
| `mode` | Quando usarlo | Campi richiesti | Semantica |
|
|
224
|
+
| ----------- | -------------------------------------- | ------------------------------------------------------- | ------------------------------------ |
|
|
225
|
+
| `rpc` | request/response | `name`, `queue` (o `exchange`+`routingKey`) | risposta immediata + timeout |
|
|
226
|
+
| `handle` | worker su una coda | `name`, `queue` | consumer di coda semplice |
|
|
227
|
+
| `broadcast` | un messaggio a molti consumer | `name`, `exchange`, `routingKey` | fanout/topic; richiede `connection_name` |
|
|
228
|
+
| `event` | publish senza risposta | `name`, `queue` **oppure** `exchange`+`routingKey` | fire-and-forget |
|
|
91
229
|
|
|
230
|
+
```yaml
|
|
92
231
|
topics:
|
|
93
232
|
- name: users-rpc
|
|
94
233
|
mode: rpc
|
|
95
|
-
queue: users-rpc-q
|
|
234
|
+
queue: users-rpc-q # deve esistere in broker.queues[]
|
|
235
|
+
|
|
236
|
+
- name: invoice-handle
|
|
237
|
+
mode: handle
|
|
238
|
+
queue: invoice-handle-q
|
|
239
|
+
|
|
240
|
+
- name: notify-broadcast
|
|
241
|
+
mode: broadcast
|
|
242
|
+
exchange: notify-ex
|
|
243
|
+
routingKey: notify.#
|
|
244
|
+
|
|
245
|
+
- name: audit-event
|
|
246
|
+
mode: event
|
|
247
|
+
exchange: audit-ex
|
|
248
|
+
routingKey: audit.created
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> `toObservable: true` su un topic `handle` instrada i messaggi su `BrokerService.events$` (Observable RxJS) invece che a un handler registrato.
|
|
252
|
+
|
|
253
|
+
### `auth-providers`
|
|
254
|
+
|
|
255
|
+
Provider di autenticazione usati dalle route del gateway (`gateway.paths[].auth`) e dagli eventi WebSocket (`gateway.events[].auth`).
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
auth-providers:
|
|
259
|
+
- name: gateway-jwks
|
|
260
|
+
type: jwks # jwt | jwks | basic | str-compare | none
|
|
261
|
+
issuer: https://issuer.example.com/realms/main
|
|
262
|
+
jwksUri: https://issuer.example.com/certs
|
|
263
|
+
algorithms: [RS256]
|
|
264
|
+
httpsAllowUnauthorized: false # true SOLO per issuer self-signed in dev
|
|
265
|
+
jwtMap: # claim del token → claim mappato (header-prefixed)
|
|
266
|
+
- sub:userId
|
|
267
|
+
- roles:roles
|
|
268
|
+
headerPrefix: X-GTW-AUTH- # prefisso degli header propagati ai microservizi
|
|
269
|
+
uidClaim: USERID # dest (uppercase) usato come user id per l'ACL
|
|
270
|
+
usernameClaim: USERNAME
|
|
271
|
+
aclTopic: acl # topic RPC interrogato per i ruoli
|
|
272
|
+
aclAction: can-user-do
|
|
273
|
+
|
|
274
|
+
- name: gateway-jwt
|
|
275
|
+
type: jwt
|
|
276
|
+
secret: your-jwt-secret
|
|
277
|
+
issuer: https://issuer.example.com/realms/main
|
|
278
|
+
audience: your-audience
|
|
279
|
+
algorithms: [HS256]
|
|
280
|
+
jwtMap: [sub:userId, roles:roles]
|
|
281
|
+
headerPrefix: X-GTW-AUTH-
|
|
282
|
+
uidClaim: USERID
|
|
283
|
+
usernameClaim: USERNAME
|
|
284
|
+
aclTopic: acl
|
|
285
|
+
aclAction: can-user-do
|
|
286
|
+
|
|
287
|
+
- name: gateway-basic
|
|
288
|
+
type: basic
|
|
289
|
+
clientId: my-user
|
|
290
|
+
clientSecret: my-pass
|
|
291
|
+
headerPrefix: X-GTW-AUTH-
|
|
292
|
+
|
|
293
|
+
- name: gateway-str
|
|
294
|
+
type: str-compare
|
|
295
|
+
secret: your-static-token
|
|
296
|
+
headerPrefix: Bearer # prefisso atteso nell'header Authorization
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
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')`.
|
|
300
|
+
|
|
301
|
+
> **Sicurezza dei provider**: `algorithms` è **obbligatorio** per `jwt`/`jwks` (se omesso la verifica è negata → previene l'algorithm-confusion); per `jwks` solo algoritmi asimmetrici (RS\*/ES\*/PS\*), `HS*`/`none` rifiutati. `str-compare` senza `secret` e `basic` senza `clientSecret` fanno **pass-through** (richiesta considerata autenticata — provider di fatto aperto/disabilitato; usalo consapevolmente). Senza `jwtMap` i claim vengono propagati non mappati: definiscilo sempre.
|
|
302
|
+
|
|
303
|
+
### `gateway`
|
|
96
304
|
|
|
305
|
+
```yaml
|
|
97
306
|
gateway:
|
|
98
307
|
mode: gateway
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
308
|
+
headerPrefix: X-GTW- # prefisso per gli header inoltrati (forwardHeaders)
|
|
309
|
+
|
|
310
|
+
ws: # opzioni WebSocket — solo livello connessione
|
|
311
|
+
maxConnections: 5000
|
|
312
|
+
maxSubscriptionsPerClient: 50
|
|
313
|
+
heartbeatIntervalMs: 30000
|
|
314
|
+
# auth/roles/scope sono dichiarati PER-EVENTO (events[].auth/requireAuth/roles/...)
|
|
315
|
+
|
|
316
|
+
loadConfig: # caricamento remoto di paths/events via RPC (opzionale)
|
|
317
|
+
paths: { topic: gtw.config, action: get-paths }
|
|
318
|
+
events: { topic: gtw.config, action: get-events }
|
|
319
|
+
|
|
320
|
+
paths: [ ... ] # vedi "Gateway HTTP"
|
|
321
|
+
events: [ ... ] # vedi "Gateway WebSocket"
|
|
108
322
|
```
|
|
109
323
|
|
|
110
|
-
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Scrivere un microservizio (AMQP)
|
|
111
327
|
|
|
112
|
-
###
|
|
328
|
+
### Handler con i decoratori
|
|
113
329
|
|
|
114
330
|
```ts
|
|
115
331
|
import { Injectable } from '@nestjs/common';
|
|
@@ -117,6 +333,8 @@ import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
|
|
|
117
333
|
|
|
118
334
|
@Injectable()
|
|
119
335
|
export class UsersActionService {
|
|
336
|
+
// @BrokerAction(topic, action, type?) — il `type` è documentativo: l'handler è
|
|
337
|
+
// SEMPRE raggiungibile sia in rpc sia in event (vedi "Doppio comportamento").
|
|
120
338
|
@BrokerAction('users-rpc', 'user.create', 'rpc')
|
|
121
339
|
async createUser(
|
|
122
340
|
@BrokerParam('body', 'email') email: string,
|
|
@@ -128,100 +346,105 @@ export class UsersActionService {
|
|
|
128
346
|
}
|
|
129
347
|
```
|
|
130
348
|
|
|
131
|
-
|
|
349
|
+
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
350
|
|
|
133
|
-
|
|
134
|
-
import { Injectable } from '@nestjs/common';
|
|
135
|
-
import { BrokerService } from '@open-rlb/nestjs-amqp';
|
|
351
|
+
#### Sorgenti `@BrokerParam(source, name?)`
|
|
136
352
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
353
|
+
| Source | Valore iniettato |
|
|
354
|
+
| ----------- | ----------------------------------------------------- |
|
|
355
|
+
| `body` | `payload[name ?? nomeParametro]` |
|
|
356
|
+
| `body-full` | payload completo |
|
|
357
|
+
| `header` | `headers[name ?? nomeParametro]` |
|
|
358
|
+
| `tag` | consumer tag AMQP |
|
|
359
|
+
| `action` | action del messaggio |
|
|
360
|
+
| `topic` | topic corrente |
|
|
140
361
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
362
|
+
> Se ometti `@BrokerParam` su un parametro, il default è `{ source: 'body' }` con chiave = nome del parametro.
|
|
363
|
+
|
|
364
|
+
### Doppio comportamento RPC / event
|
|
365
|
+
|
|
366
|
+
Ogni `@BrokerAction` è eseguibile **sia in RPC sia in event**, senza modifiche al servizio. Cambia solo cosa attende il chiamante.
|
|
367
|
+
|
|
368
|
+
| Modalità | Come si invoca | Cosa si attende |
|
|
369
|
+
| -------- | ------------------------------------------------- | ------------------------------------------------------- |
|
|
370
|
+
| `rpc` | `broker.requestData(...)` / path `mode: rpc` | la **risposta** del metodo (request/response) |
|
|
371
|
+
| `event` | `broker.publishMessage(...)` / path `mode: event` | solo che il **broker prenda in carico** (publisher confirm) |
|
|
372
|
+
|
|
373
|
+
`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.
|
|
374
|
+
|
|
375
|
+
```yaml
|
|
376
|
+
# Lo stesso topic/action esposto nei due modi
|
|
377
|
+
gateway:
|
|
378
|
+
paths:
|
|
379
|
+
- { name: users-create-sync, method: POST, path: /users, topic: users-rpc, action: user.create, mode: rpc }
|
|
380
|
+
- { name: users-create-async, method: POST, path: /users/async, topic: users-rpc, action: user.create, mode: event }
|
|
151
381
|
```
|
|
152
382
|
|
|
153
|
-
###
|
|
383
|
+
### Consumer manuali (senza decoratori)
|
|
154
384
|
|
|
155
385
|
```ts
|
|
386
|
+
// RPC
|
|
156
387
|
await broker.registerRpc<{ id: string }, { ok: boolean }>('health-rpc', async (event) => {
|
|
157
388
|
return { ok: !!event.payload?.id };
|
|
158
389
|
});
|
|
159
390
|
|
|
391
|
+
// handle / broadcast (gli handler devono restituire void)
|
|
160
392
|
await broker.registerHandler<{ invoiceId: string }>('invoice-handle', async (event) => {
|
|
161
393
|
console.log(event.payload.invoiceId);
|
|
162
394
|
});
|
|
163
395
|
```
|
|
164
396
|
|
|
165
|
-
|
|
397
|
+
### Pubblicare / chiamare da codice
|
|
166
398
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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:
|
|
185
|
-
|
|
186
|
-
```yaml
|
|
187
|
-
topics:
|
|
188
|
-
- name: users-rpc
|
|
189
|
-
mode: rpc
|
|
190
|
-
queue: users-rpc-q
|
|
399
|
+
```ts
|
|
400
|
+
@Injectable()
|
|
401
|
+
export class UsersClient {
|
|
402
|
+
constructor(private readonly broker: BrokerService) {}
|
|
191
403
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
404
|
+
// RPC: attende la risposta
|
|
405
|
+
createUserRpc() {
|
|
406
|
+
return this.broker.requestData('users-rpc', 'user.create',
|
|
407
|
+
{ email: 'a@b.c', role: 'admin' }, { 'X-Tenant': 'acme' }, 5000);
|
|
408
|
+
}
|
|
195
409
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
410
|
+
// Event: attende solo che il broker prenda in carico
|
|
411
|
+
async emitAudit() {
|
|
412
|
+
await this.broker.publishMessage('audit-event', 'audit.created', { entity: 'user', id: 'u_1' });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
200
415
|
```
|
|
201
416
|
|
|
202
|
-
|
|
417
|
+
---
|
|
203
418
|
|
|
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 |
|
|
419
|
+
## Gateway HTTP
|
|
210
420
|
|
|
211
|
-
|
|
421
|
+
Le route sono dichiarate in `gateway.paths[]` e registrate dinamicamente su Express al boot.
|
|
212
422
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
|
218
|
-
|
|
219
|
-
|
|
|
220
|
-
|
|
423
|
+
```yaml
|
|
424
|
+
gateway:
|
|
425
|
+
paths:
|
|
426
|
+
- name: users-create
|
|
427
|
+
method: POST # GET | POST | PUT | DELETE | PATCH
|
|
428
|
+
path: /users/:tenant? # supporta route param Express
|
|
429
|
+
dataSource: body # body | query | params | body-query | query-body
|
|
430
|
+
topic: users-rpc
|
|
431
|
+
action: user.create
|
|
432
|
+
mode: rpc # rpc | event
|
|
433
|
+
timeout: 7000 # solo rpc
|
|
434
|
+
auth: gateway-jwks # nome di un auth-provider
|
|
435
|
+
allowAnonymous: false # true → consente l'accesso anche senza auth valida
|
|
436
|
+
roles: [users.create] # richiede un IAclRoleService registrato
|
|
437
|
+
successStatusCode: 201
|
|
438
|
+
binary: false # true → risposta come Buffer base64-decoded
|
|
439
|
+
redirect: 302 # se valorizzato, redirect alla URL contenuta nella risposta
|
|
440
|
+
headers: { Cache-Control: no-store } # header statici sulla risposta
|
|
441
|
+
forwardHeaders: { Tenant: x-tenant } # header della richiesta da inoltrare al microservizio
|
|
442
|
+
parseRaw: false # true → inoltra il body raw come $raw (richiede rawBody:true nel bootstrap)
|
|
443
|
+
```
|
|
221
444
|
|
|
222
|
-
|
|
445
|
+
#### Composizione del payload (`dataSource`)
|
|
223
446
|
|
|
224
|
-
|
|
|
447
|
+
| Valore | Payload inviato al broker |
|
|
225
448
|
| ------------ | -------------------------------- |
|
|
226
449
|
| `body` | `{...params, ...body}` |
|
|
227
450
|
| `query` | `{...params, ...query}` |
|
|
@@ -229,72 +452,252 @@ topics:
|
|
|
229
452
|
| `body-query` | `{...params, ...query, ...body}` |
|
|
230
453
|
| `query-body` | `{...params, ...body, ...query}` |
|
|
231
454
|
|
|
232
|
-
|
|
455
|
+
> 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`.
|
|
456
|
+
|
|
457
|
+
#### Mappatura errori → status HTTP
|
|
233
458
|
|
|
234
|
-
|
|
459
|
+
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.
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Gateway WebSocket
|
|
464
|
+
|
|
465
|
+
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).
|
|
466
|
+
|
|
467
|
+
### Configurazione
|
|
235
468
|
|
|
236
469
|
```yaml
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
470
|
+
gateway:
|
|
471
|
+
ws: # solo livello connessione
|
|
472
|
+
maxConnections: 5000 # limite connessioni per istanza
|
|
473
|
+
maxSubscriptionsPerClient: 50 # limite sottoscrizioni per client
|
|
474
|
+
heartbeatIntervalMs: 30000 # ping/pong per chiudere le connessioni morte
|
|
475
|
+
allowedOrigins: # allowlist Origin dell'handshake (omessa → tutte)
|
|
476
|
+
- https://app.example.com
|
|
477
|
+
maxMessageBytes: 16384 # scarta i frame client più grandi (default 16KB)
|
|
478
|
+
|
|
479
|
+
events:
|
|
480
|
+
- name: orders
|
|
481
|
+
type: ws # ws | http (webhook)
|
|
482
|
+
exchange: orders-ex
|
|
483
|
+
routingKey: orders.#
|
|
484
|
+
auth: gateway-jwks # provider che verifica il token e mappa i claim PER QUESTO evento
|
|
485
|
+
requireAuth: true # default true quando `auth` è impostato; false → auth opzionale
|
|
486
|
+
roles: [orders.read] # verifica ACL via IAclRoleService
|
|
487
|
+
scopeClaim: X-GTW-AUTH-USERID # inoltra solo i messaggi dell'utente...
|
|
488
|
+
payloadKey: userId # ...dove payload.userId === claim dell'utente
|
|
489
|
+
|
|
490
|
+
- name: invoices # forwarding webhook
|
|
491
|
+
type: http
|
|
492
|
+
exchange: inv-ex
|
|
493
|
+
routingKey: inv.#
|
|
494
|
+
url: https://hooks.example.com/invoices
|
|
495
|
+
method: POST
|
|
496
|
+
timeout: 8000
|
|
251
497
|
```
|
|
252
498
|
|
|
253
|
-
|
|
499
|
+
### Autenticazione (token nel subprotocol)
|
|
254
500
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
501
|
+
I browser non possono impostare header custom sull'handshake, quindi il token JWT viaggia nel **subprotocol** (`Sec-WebSocket-Protocol`):
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
const ws = new WebSocket('ws://localhost:3000', [token]); // oppure ['bearer', token]
|
|
271
505
|
```
|
|
272
506
|
|
|
273
|
-
|
|
507
|
+
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.
|
|
274
508
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
509
|
+
### Protocollo client
|
|
510
|
+
|
|
511
|
+
```js
|
|
512
|
+
ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders', select: { status: 'open' } }));
|
|
513
|
+
ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'orders' }));
|
|
514
|
+
// messaggi in arrivo: { topic: 'onOrders', data: <payload> }
|
|
515
|
+
// errori: { topic: 'onError', data: { event, error } }
|
|
281
516
|
```
|
|
282
517
|
|
|
283
|
-
###
|
|
518
|
+
### Sicurezza e scalabilità
|
|
284
519
|
|
|
285
|
-
|
|
286
|
-
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
520
|
+
- **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.
|
|
521
|
+
- **Authz per evento**: `roles` (ACL via `IAclRoleService`) sull'identità ricavata da `auth`.
|
|
522
|
+
- **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).
|
|
523
|
+
- **Sessione limitata dalla scadenza del token**: l'`exp` del JWT viene catturato alla prima verifica e la connessione viene chiusa (`1008 token expired`) appena scade — niente consegne dopo la scadenza.
|
|
524
|
+
- **Origin allowlist**: `gateway.ws.allowedOrigins` rifiuta gli handshake cross-site (se omessa, tutte le origin sono accettate e lo si segnala a boot).
|
|
525
|
+
- **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.
|
|
526
|
+
- **Hardening**: heartbeat ping/pong, limiti connessioni/sottoscrizioni, limite dimensione frame (`maxMessageBytes`), cleanup robusto su `close`/`error`.
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Remote config
|
|
531
|
+
|
|
532
|
+
`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.
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Moduli opzionali `AclModule` e `GatewayAdminModule` (persistenza fornita dal consumer)
|
|
537
|
+
|
|
538
|
+
Due moduli **opzionali** per gestire ACL e configurazione gateway a database. **La lib non dipende da Mongo/Redis**: definisce i servizi/cache + i **contratti repository (classi astratte)** e l'interfaccia `AclCacheStore`; **il consumer fornisce le implementazioni** (es. Mongo + Redis). Esempio completo e funzionante: **[`apps/gateway-2`](apps/gateway-2)** — per restare autonomo usa **repository in-RAM** (`InMemory*Repository`) e una **cache L2 in-RAM** (`InMemoryAclStore`), così gira solo con RabbitMQ; in produzione si rimpiazzano con implementazioni Mongo/Redis senza toccare la lib.
|
|
539
|
+
|
|
540
|
+
### `AclModule` — ACL DB-backed con cache 2-livelli
|
|
541
|
+
|
|
542
|
+
ACL (azioni → ruoli → grant per-utente) con `canUserDo` corretto e **cache RAM + L2 pluggable** (TTL diversi) e invalidazione che forza il DB.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
import { AclModule, AclService, AclActionRepository, AclRoleRepository, AclGrantRepository,
|
|
546
|
+
RLB_ACL_CACHE_STORE, RLB_GTW_ACL_ROLE_SERVICE } from '@open-rlb/nestjs-amqp';
|
|
547
|
+
|
|
548
|
+
@Module({
|
|
549
|
+
imports: [
|
|
550
|
+
BrokerModule.forRootAsync({ /* ... */ }),
|
|
551
|
+
// ProxyModule riceve auth/gateway config e usa AclService come IAclRoleService (AclModule è @Global):
|
|
552
|
+
ProxyModule.forRootAsync({
|
|
553
|
+
imports: [ConfigModule],
|
|
554
|
+
inject: [ConfigService],
|
|
555
|
+
useFactory: (config: ConfigService) => ({
|
|
556
|
+
authOptions: config.get<HandlerAuthConfig[]>('auth-providers'),
|
|
557
|
+
gatewayOptions: config.get<GatewayConfig>('gateway'),
|
|
558
|
+
}),
|
|
559
|
+
providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }],
|
|
560
|
+
}),
|
|
561
|
+
AclModule.forRoot(
|
|
562
|
+
[
|
|
563
|
+
...aclMongoModelProviders, // provider dei model Mongoose
|
|
564
|
+
{ provide: AclActionRepository, useClass: MongoAclActionRepository },
|
|
565
|
+
{ provide: AclRoleRepository, useClass: MongoAclRoleRepository },
|
|
566
|
+
{ provide: AclGrantRepository, useClass: MongoAclGrantRepository },
|
|
567
|
+
InMemoryAclStore, // implementa AclCacheStore
|
|
568
|
+
{ provide: RLB_ACL_CACHE_STORE, useExisting: InMemoryAclStore },// L2 opzionale (omesso → solo RAM)
|
|
569
|
+
],
|
|
570
|
+
{ cache: { ramTtlMs: 30000, l2TtlSec: 600 } },
|
|
571
|
+
),
|
|
572
|
+
],
|
|
573
|
+
})
|
|
574
|
+
export class AppModule {}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
- I handler sono esposti su `BrokerService` con topic **`rlb-acl`** (costante `ACL_TOPIC`): `acl-can-user-do` (rpc), `acl-grant`/`acl-revoke`, `acl-action-*`, `acl-role-*`. Definisci nel tuo `broker.topics` un topic `rlb-acl` e imposta negli auth-provider `aclTopic: rlb-acl`, `aclAction: acl-can-user-do`.
|
|
578
|
+
- `AclService.canUserDo(topic, action, userId)` serve dalla cache; sul miss interroga il DB (`checkActions`: i ruoli del grant devono coprire l'azione) e ripopola RAM+L2.
|
|
579
|
+
- **Invalidazione**: ogni mutazione (grant/role/action) svuota L1 e L2 → la prossima verifica pesca dal DB. Senza L2, la coerenza multi-istanza è limitata dal `ramTtlMs`.
|
|
580
|
+
- **Cache L2 pluggable**: il consumer fornisce `{ provide: RLB_ACL_CACHE_STORE, useClass/useExisting }` che implementa `AclCacheStore` (`get/set/del/keys`). In `gateway-2` è `InMemoryAclStore` (mock in RAM, nessuna dipendenza esterna); in produzione plugga uno store condiviso (es. Redis).
|
|
581
|
+
|
|
582
|
+
### `GatewayAdminModule` — CRUD rotte/auth + liste + metriche
|
|
583
|
+
|
|
584
|
+
CRUD di rotte HTTP e auth-providers (repo forniti dal consumer), con **liste esportabili** per il gateway (in aggiunta allo YAML), **metriche a contatori** e **ordinamento path static-before-param**.
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
import { GatewayAdminModule, HttpPathRepository, AuthProviderRepository, HttpMetricRepository } from '@open-rlb/nestjs-amqp';
|
|
588
|
+
|
|
589
|
+
GatewayAdminModule.forRoot([
|
|
590
|
+
...gatewayAdminMongoModelProviders,
|
|
591
|
+
{ provide: HttpPathRepository, useClass: MongoHttpPathRepository },
|
|
592
|
+
{ provide: AuthProviderRepository, useClass: MongoAuthProviderRepository },
|
|
593
|
+
{ provide: HttpMetricRepository, useClass: MongoHttpMetricRepository },
|
|
594
|
+
]);
|
|
294
595
|
```
|
|
295
596
|
|
|
296
|
-
|
|
597
|
+
Handler su topic **`rlb-gateway-admin`** (`GATEWAY_ADMIN_TOPIC`):
|
|
598
|
+
- CRUD rotte: `gw-path-create/update/delete/get/list`; **`gw-path-export` (rpc)** → tutte le rotte abilitate come `PathDefinition[]` **ordinate** (statiche prima delle parametriche). Punta `gateway.loadConfig.paths` a `{ topic: rlb-gateway-admin, action: gw-path-export }`.
|
|
599
|
+
- CRUD auth: `gw-auth-create/.../list`; **`gw-auth-export` (rpc)** → `HandlerAuthConfig[]` abilitati (per frontend / merge lato gateway).
|
|
600
|
+
- Metriche: **`gw-metrics-track` (event)** incrementa i contatori per `(method, route)`; **`gw-metrics-get` (rpc)** restituisce count/errori/durata media per il frontend.
|
|
601
|
+
|
|
602
|
+
> **Ordinamento path**: `gw-path-export` usa `orderPaths()` così `resources/path` precede `resources/:varName` — necessario perché Express, registrando prima la rotta parametrica, intercetterebbe il segmento statico.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## API `BrokerService`
|
|
607
|
+
|
|
608
|
+
| Metodo | Uso |
|
|
609
|
+
| ------------------------------------------------------------ | ------------------------------------------------ |
|
|
610
|
+
| `requestData(topic, action, payload?, headers?, timeout?)` | RPC request/response (attende la risposta) |
|
|
611
|
+
| `publishMessage(topic, action, payload, headers?)` → `Promise<boolean>` | event fire-and-forget con publisher confirm |
|
|
612
|
+
| `registerRpc(topic, handler)` | consumer RPC manuale |
|
|
613
|
+
| `registerHandler(topic, handler)` | consumer `handle` / `broadcast` (ritorna void) |
|
|
614
|
+
| `getRpc(topic)` / `getHandler(topic)` | recupera l'handler registrato |
|
|
615
|
+
| `events$` / `getEvents$<T>()` | Observable degli eventi dei topic `toObservable` |
|
|
616
|
+
|
|
617
|
+
### Decoratori
|
|
618
|
+
|
|
619
|
+
| Decoratore | Uso |
|
|
620
|
+
| ------------------------------------------------------------- | ------------------------------------ |
|
|
621
|
+
| `@BrokerAction(topic, action, type?)` | lega un metodo a topic/action |
|
|
622
|
+
| `@BrokerParam(source, name?)` | mappa i parametri dai dati messaggio |
|
|
623
|
+
| `@BrokerAuth(authName, allowAnonymous?, roles?)` | metadati di auth (usati dallo scanner) |
|
|
624
|
+
| `@BrokerHTTP(method, path, dataSource?, timeout?, parseRaw?)` | metadati HTTP (usati dallo scanner) |
|
|
625
|
+
|
|
626
|
+
### Pipe utility
|
|
627
|
+
|
|
628
|
+
`BooleanPipe` e `NumberPipe` convertono valori stringa/numerici (es. da query string). Esportate da `@open-rlb/nestjs-amqp`.
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## ⚠️ Gotcha e casi a rischio bug
|
|
633
|
+
|
|
634
|
+
Questi sono i punti che causano più frequentemente bug silenziosi. **Leggili prima di estendere la lib.**
|
|
635
|
+
|
|
636
|
+
### Decoratori e handler
|
|
637
|
+
|
|
638
|
+
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.
|
|
639
|
+
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`.
|
|
640
|
+
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.
|
|
641
|
+
|
|
642
|
+
### Wiring topic ↔ queue ↔ exchange
|
|
643
|
+
|
|
644
|
+
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`.
|
|
645
|
+
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`).
|
|
646
|
+
6. **Exchange `type: topic` → il queue DEVE avere `routingKey`**, altrimenti l'avvio lancia `Queue ... has no routing key`.
|
|
647
|
+
7. **`mode: broadcast` e gateway WebSocket richiedono `connection_name`** (`clientProperties.connection_name`), altrimenti throw.
|
|
648
|
+
|
|
649
|
+
### RPC / timeout / errori
|
|
650
|
+
|
|
651
|
+
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.
|
|
652
|
+
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.
|
|
653
|
+
10. **Timeout di default 10s** (o `broker.defaultRpcTimeout`). Per RPC lente imposta `timeout` sulla path o sull'argomento di `requestData`.
|
|
654
|
+
|
|
655
|
+
### Gateway HTTP
|
|
656
|
+
|
|
657
|
+
11. **`parseRaw: true` richiede `NestFactory.create(AppModule, { rawBody: true })`**, altrimenti `$raw` è `undefined`.
|
|
658
|
+
12. **I route param vincono sul body/query** (ri-applicati per ultimi). Attento alle collisioni di chiave (`:id` vs `body.id`).
|
|
659
|
+
13. **Gli upload sono in `$files`** (multer `.any()`); i buffer vengono convertiti in stringa binaria — rigestiscili con cura lato consumer.
|
|
660
|
+
|
|
661
|
+
### Auth / ACL
|
|
662
|
+
|
|
663
|
+
14. **`roles` su una path o evento richiede un `IAclRoleService`** registrato via `RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`. L'auth-provider deve definire `aclTopic`, `aclAction`, `uidClaim`, `usernameClaim`, e `uidClaim` deve corrispondere a un `dest` del `jwtMap`. Mancante → throw. Nota: `authOptions`/`gatewayOptions` si passano a `ProxyModule`, non a `BrokerModule`.
|
|
664
|
+
15. **Gli header propagati sono uppercase e prefissati** (`${headerPrefix}${DEST}`): leggi `X-GTW-AUTH-USERID`, non `userId`.
|
|
665
|
+
|
|
666
|
+
### WebSocket
|
|
667
|
+
|
|
668
|
+
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.
|
|
669
|
+
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).
|
|
670
|
+
|
|
671
|
+
### Publish / event
|
|
672
|
+
|
|
673
|
+
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.
|
|
674
|
+
19. **Gli handler `handle`/`broadcast` devono restituire `void`**: un valore di ritorno genera un warning (`Subscribe handlers should only return void`).
|
|
675
|
+
|
|
676
|
+
### TLS / credenziali
|
|
677
|
+
|
|
678
|
+
20. **JWKS verifica il TLS di default.** Usa `httpsAllowUnauthorized: true` su un provider solo per issuer self-signed in sviluppo.
|
|
679
|
+
21. **`mechanism` credenziali**: `PLAIN` | `EXTERNAL` | `AMQPLAIN` (case-insensitive). Un valore sconosciuto non imposta la `response` → autenticazione fallita.
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## Errori comuni
|
|
684
|
+
|
|
685
|
+
- `Topic <name> not found in configuration`: controlla `topics[].name`, `@BrokerAction`, `requestData`/`publishMessage`, `gateway.paths[].topic`.
|
|
686
|
+
- `Queue <name> not found in configuration`: verifica che `topics[].queue` esista in `broker.queues[]`.
|
|
687
|
+
- `Queue <name> has no routing key`: l'exchange è di tipo `topic` ma il queue non ha `routingKey`.
|
|
688
|
+
- `Client name is required ...`: manca `connection_name` (richiesto da broadcast e WebSocket).
|
|
689
|
+
- `ACL Role Service not found`: stai usando `roles` senza aver registrato `RLB_GTW_ACL_ROLE_SERVICE`.
|
|
690
|
+
- `401/403` dal gateway: controlla `auth`, `auth-providers[]`, e l'ACL service quando usi `roles`.
|
|
691
|
+
- Timeout RPC: `replyQueues` errato, `action` non gestita da alcun servizio, o handler troppo lento (`timeout`).
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
## Sviluppo
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
npm run build # compila (tsc)
|
|
699
|
+
npm test # jest
|
|
700
|
+
npm run start:dev # nest start --watch (app gateway di esempio)
|
|
701
|
+
```
|
|
297
702
|
|
|
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`.
|
|
703
|
+
Licenza: MIT.
|