@open-rlb/nestjs-amqp 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
- package/modules/acl/const.d.ts +1 -4
- package/modules/acl/const.js +1 -4
- package/modules/acl/const.js.map +1 -1
- package/modules/acl/models.d.ts +5 -7
- package/modules/acl/repository/acl-action.repository.d.ts +1 -5
- package/modules/acl/repository/acl-action.repository.js.map +1 -1
- package/modules/acl/repository/acl-grant.repository.d.ts +0 -1
- package/modules/acl/repository/acl-grant.repository.js.map +1 -1
- package/modules/acl/repository/acl-role.repository.d.ts +1 -5
- package/modules/acl/repository/acl-role.repository.js.map +1 -1
- package/modules/acl/services/acl-management.service.d.ts +6 -7
- package/modules/acl/services/acl-management.service.js +44 -61
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +1 -3
- package/modules/acl/services/acl.service.js +5 -47
- package/modules/acl/services/acl.service.js.map +1 -1
- package/modules/broker/broker.module.d.ts +2 -4
- package/modules/broker/broker.module.js +23 -5
- package/modules/broker/broker.module.js.map +1 -1
- package/modules/broker/config/route-discovery.config.d.ts +2 -0
- package/modules/broker/const.d.ts +1 -0
- package/modules/broker/const.js +2 -1
- package/modules/broker/const.js.map +1 -1
- package/modules/broker/services/broker.service.js +1 -1
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/broker/services/route-discovery-publisher.service.js +7 -5
- package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
- package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
- package/modules/gateway-admin/const.d.ts +1 -1
- package/modules/gateway-admin/const.js +1 -1
- package/modules/gateway-admin/const.js.map +1 -1
- package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
- package/modules/gateway-admin/gateway-admin.module.js +13 -0
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
- package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
- package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
- package/modules/gateway-admin/services/route-sync.service.js +14 -8
- package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +3 -0
- package/modules/proxy/services/http-handler.service.js +28 -4
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/package.json +5 -1
- package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
- package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
- package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +30 -5
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +120 -79
- package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +49 -2
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
- package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +233 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +172 -42
- package/schematics/nest-add/index.js +612 -142
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +673 -241
- package/schematics/nest-add/init.schema.d.ts +10 -1
- package/schematics/nest-add/init.schema.ts +29 -3
- package/schematics/nest-add/schema.json +37 -8
|
@@ -8,38 +8,47 @@ description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nes
|
|
|
8
8
|
Read first:
|
|
9
9
|
- `.claude/skills/rlb-amqp/references/config-schema.md` (the `gateway.events[]` + `gateway.ws`
|
|
10
10
|
sections)
|
|
11
|
-
- `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, ACL item 15)
|
|
11
|
+
- `.claude/skills/rlb-amqp/references/gotchas.md` (WebSocket items 16–18, 26–27; ACL item 15)
|
|
12
12
|
|
|
13
|
-
A WS event
|
|
14
|
-
clients of EVERY gateway instance. Secure it by default.
|
|
13
|
+
A WS event binds a broker `exchange`/`routingKey` to a named client-facing stream and fans
|
|
14
|
+
each message out to the connected clients of EVERY gateway instance. Secure it by default.
|
|
15
15
|
|
|
16
16
|
## Decide
|
|
17
17
|
|
|
18
18
|
- **type**: `ws` (push to clients) or `http` (forward each message to a webhook `url`).
|
|
19
|
+
(`mqtt` is also accepted by the type union but `ws`/`http` are the supported paths.)
|
|
19
20
|
- **source**: `exchange` + `routingKey` (the exchange must exist in `broker.exchanges[]`;
|
|
20
|
-
`connection_name`
|
|
21
|
+
each instance needs a distinct `connection_name` — gotcha 8).
|
|
22
|
+
- **client-facing name**: clients subscribe by `name`; messages arrive as `on<Name>`
|
|
23
|
+
(first letter capitalized — `chat` → `onChat`).
|
|
21
24
|
- **security**:
|
|
22
25
|
- `auth: <provider>` → the provider that verifies the connection token AND maps its claims
|
|
23
|
-
for THIS event (at subscribe time). When set, a valid token is
|
|
26
|
+
for THIS event (at subscribe time, memoized per provider). When set, a valid token is
|
|
27
|
+
required to subscribe.
|
|
24
28
|
- `requireAuth: false` → makes `auth` optional (anonymous allowed; claims mapped if a token
|
|
25
29
|
is present — handy with `scopeClaim`). Defaults to `true` when `auth` is set.
|
|
26
30
|
- `roles: [...]` → ACL check (needs `IAclRoleService`); requires `auth` for the identity.
|
|
27
|
-
- `scopeClaim` + `payloadKey` → per-user
|
|
31
|
+
- `scopeClaim` + `payloadKey` → per-user isolation: a client only receives messages where
|
|
28
32
|
`payload[payloadKey] === claims[scopeClaim]`. `scopeClaim` is the MAPPED claim
|
|
29
33
|
(with `headerPrefix`, e.g. `X-GTW-AUTH-USERID`). Without `payloadKey` it denies all
|
|
30
|
-
(gotcha 16).
|
|
34
|
+
(gotcha 16). With `auth` but no `scopeClaim`/`payloadKey`, every authorized subscriber
|
|
35
|
+
gets ALL messages (warned at boot).
|
|
31
36
|
|
|
32
|
-
> Auth/roles/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits
|
|
33
|
-
> and
|
|
37
|
+
> Auth/roles/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits,
|
|
38
|
+
> heartbeat, origin allowlist and message-size cap (no auth fields). Different events may use
|
|
39
|
+
> different providers.
|
|
34
40
|
|
|
35
41
|
## YAML fragments
|
|
36
42
|
|
|
37
43
|
```yaml
|
|
38
44
|
gateway:
|
|
39
45
|
ws: # connection-level only (optional)
|
|
40
|
-
maxConnections: 5000
|
|
41
|
-
maxSubscriptionsPerClient: 50
|
|
42
|
-
heartbeatIntervalMs: 30000
|
|
46
|
+
maxConnections: 5000 # max concurrent connections this instance accepts
|
|
47
|
+
maxSubscriptionsPerClient: 50 # max active subscriptions per client
|
|
48
|
+
heartbeatIntervalMs: 30000 # ping/pong; also drops dead sockets + expired tokens
|
|
49
|
+
allowedOrigins: # reject cross-site handshakes; omit → all origins (logged)
|
|
50
|
+
- https://app.example.com
|
|
51
|
+
maxMessageBytes: 16384 # inbound client frame cap; larger frames dropped (default)
|
|
43
52
|
|
|
44
53
|
events:
|
|
45
54
|
- name: orders
|
|
@@ -49,8 +58,8 @@ gateway:
|
|
|
49
58
|
auth: gateway-jwks # verifies token + maps claims for this event
|
|
50
59
|
requireAuth: true # default true when auth is set; false → optional
|
|
51
60
|
roles: [orders.read] # optional → needs IAclRoleService
|
|
52
|
-
scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping
|
|
53
|
-
payloadKey: userId
|
|
61
|
+
scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping (MAPPED claim)
|
|
62
|
+
payloadKey: userId # message field compared to scopeClaim
|
|
54
63
|
|
|
55
64
|
- name: invoices # webhook variant
|
|
56
65
|
type: http
|
|
@@ -58,6 +67,7 @@ gateway:
|
|
|
58
67
|
routingKey: inv.#
|
|
59
68
|
url: https://hooks.example.com/invoices
|
|
60
69
|
method: POST
|
|
70
|
+
headers: { x-api-key: secret }
|
|
61
71
|
timeout: 8000
|
|
62
72
|
```
|
|
63
73
|
|
|
@@ -75,19 +85,31 @@ broker:
|
|
|
75
85
|
## Required wiring to flag
|
|
76
86
|
|
|
77
87
|
- The app bootstrap must register the WS adapter:
|
|
78
|
-
`app.useWebSocketAdapter(new WsAdapter(app))
|
|
88
|
+
`app.useWebSocketAdapter(new WsAdapter(app))` (see
|
|
89
|
+
`sample/config-sample/gateway-in-memory/src/main.ts`).
|
|
79
90
|
- `events[].auth` must reference a `jwt`/`jwks` provider; subscribing without a valid token
|
|
80
91
|
yields `{ topic:'onError', data:{ event, error:'unauthorized' } }` (unless `requireAuth:false`).
|
|
81
|
-
|
|
92
|
+
A failed role check yields `error:'forbidden'`.
|
|
93
|
+
- `roles` → `IAclRoleService` via `RLB_GTW_ACL_ROLE_SERVICE` in
|
|
94
|
+
`ProxyModule.forRootAsync({ providers: [...] })` (gotcha 15).
|
|
82
95
|
- Do NOT add a fixed durable queue for the event — the lib creates a per-instance exclusive
|
|
83
|
-
queue for fan-out (gotcha 17).
|
|
96
|
+
ephemeral auto-delete queue for fan-out (gotcha 17).
|
|
97
|
+
- WS sessions are bounded by the token `exp`: the socket is closed (`1008`) when the JWT
|
|
98
|
+
expires; long-lived sockets need refresh + reconnect (gotcha 26).
|
|
84
99
|
|
|
85
100
|
## Client snippet (for docs/testing)
|
|
86
101
|
|
|
87
102
|
```js
|
|
88
|
-
|
|
103
|
+
// token in subprotocol — browsers can't set custom handshake headers (gotcha 18).
|
|
104
|
+
// single value = token; ['bearer', token] / ['jwt', token] pairs also accepted.
|
|
105
|
+
const ws = new WebSocket('ws://localhost:3000', [token]);
|
|
89
106
|
ws.onopen = () => ws.send(JSON.stringify({ action: 'subscribe', topic: 'orders' }));
|
|
90
107
|
ws.onmessage = (e) => console.log(JSON.parse(e.data)); // { topic:'onOrders', data } | { topic:'onError', ... }
|
|
108
|
+
// unsubscribe: ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'orders' }));
|
|
91
109
|
```
|
|
92
110
|
|
|
111
|
+
> An optional `select: { key: value }` in the subscribe frame is a client-side filter
|
|
112
|
+
> (forwarded only when `payload[key] === value` for every key). It is INTERSECTED with the
|
|
113
|
+
> server-enforced `scopeClaim` isolation — a `select` can never widen what a user receives.
|
|
114
|
+
|
|
93
115
|
Output the YAML fragments (with parent paths) and the bootstrap/ACL items still required.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-amqp-gateway-admin
|
|
3
|
+
description: Drive the @open-rlb/nestjs-amqp gateway-admin management plane and route auto-discovery. Use for DB-managed HTTP routes (gw-path-*), DB auth-providers CRUD (gw-auth-*, name-keyed PUT-upsert), gateway metrics (gw-metrics-get/series/points/track) and the gw-health probe, runtime route reload (gw-reload via the broadcast control topic), the in-proxy metrics hook (RLB_GTW_METRICS_HOOK), and wiring GatewayAdminModule.forRoot/forRootAsync. Also covers route auto-discovery: publisher (broker.routeDiscovery serviceName/publishOnBoot/exchange/queue) vs consumer (GatewayAdminModule routeDiscovery exchange/queue).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gateway-admin module + route auto-discovery
|
|
7
|
+
|
|
8
|
+
Read first:
|
|
9
|
+
- `docs/gateway-admin.md` (full reference)
|
|
10
|
+
- `sample/config-sample/gateway-admin.yaml` (annotated YAML for every action)
|
|
11
|
+
- `sample/config-sample/gateway-db/` (runnable wiring; `apps/gateway-2` was retired)
|
|
12
|
+
|
|
13
|
+
The gateway-admin module is the gateway's management plane: DB-stored routes,
|
|
14
|
+
DB auth-providers, metrics, runtime reload, and the consumer side of route
|
|
15
|
+
auto-discovery. All handlers bind to the topic `rlb-gateway-admin`
|
|
16
|
+
(`GATEWAY_ADMIN_TOPIC`). The topic NAME and every action string are
|
|
17
|
+
**decorator-bound — NOT configurable**. You drive them by adding
|
|
18
|
+
`gateway.paths[]` that forward to that topic/action.
|
|
19
|
+
|
|
20
|
+
## Fixed vs configurable
|
|
21
|
+
|
|
22
|
+
- FIXED (write exactly): topic names `rlb-gateway-admin`, control action
|
|
23
|
+
`gw-reload`, and all action strings `gw-path-*` / `gw-auth-*` / `gw-metrics-*`
|
|
24
|
+
/ `gw-health` (from `GW_ADMIN_ACTIONS` + `GW_RELOAD_ACTION`).
|
|
25
|
+
- CONFIGURABLE: each topic's exchange/queue/routingKey, and the route-discovery
|
|
26
|
+
exchange/queue (defaults `rlb-route-discovery` / `rlb-route-sync`) — which must
|
|
27
|
+
match on the publisher AND consumer sides.
|
|
28
|
+
|
|
29
|
+
## Nest wiring — `GatewayAdminModule`
|
|
30
|
+
|
|
31
|
+
`forRoot(providers, options)`: repo bindings FIRST, options SECOND. You supply
|
|
32
|
+
the four repositories (any store); the module ships the services and wires
|
|
33
|
+
`RouteSyncService` internally. Tokens are re-exported from `@open-rlb/nestjs-amqp`.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
GatewayAdminModule.forRoot([
|
|
37
|
+
{ provide: HttpPathRepository, useExisting: MongoHttpPathRepository },
|
|
38
|
+
{ provide: AuthProviderRepository, useExisting: MongoAuthProviderRepository },
|
|
39
|
+
{ provide: HttpMetricRepository, useExisting: MongoHttpMetricRepository },
|
|
40
|
+
{ provide: RouteSyncLogRepository, useExisting: MongoRouteSyncLogRepository }, // route-discovery journal
|
|
41
|
+
]),
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `forRootAsync` to resolve the **consumer-side** `routeDiscovery { exchange, queue }`
|
|
45
|
+
from config (see Route auto-discovery). Exports `GatewayPathService`,
|
|
46
|
+
`GatewayAuthService`, `GatewayMetricsService`.
|
|
47
|
+
|
|
48
|
+
## Broker topic + queue (required)
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
broker:
|
|
52
|
+
queues:
|
|
53
|
+
- name: rlb-gateway-admin
|
|
54
|
+
exchange: rlb
|
|
55
|
+
routingKey: rlb-gateway-admin
|
|
56
|
+
createQueueIfNotExists: true
|
|
57
|
+
options: { durable: true }
|
|
58
|
+
topics:
|
|
59
|
+
- name: rlb-gateway-admin # MUST be this name
|
|
60
|
+
mode: rpc
|
|
61
|
+
queue: rlb-gateway-admin
|
|
62
|
+
exchange: rlb
|
|
63
|
+
routingKey: rlb-gateway-admin
|
|
64
|
+
- name: rlb-gateway-control # name is yours; must match gateway.reloadTopic + gw-reload path
|
|
65
|
+
mode: broadcast
|
|
66
|
+
exchange: rlb
|
|
67
|
+
routingKey: rlb-gateway-control
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Point `loadConfig.paths` at the export responder so DB routes merge with YAML
|
|
71
|
+
`gateway.paths` on boot and on every reload (merged static-before-param):
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
gateway:
|
|
75
|
+
loadConfig:
|
|
76
|
+
paths: { topic: rlb-gateway-admin, action: gw-path-export }
|
|
77
|
+
reloadTopic: rlb-gateway-control
|
|
78
|
+
metrics: { topic: rlb-gateway-admin, action: gw-metrics-track } # auto-emit track per request
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Route management — `gw-path-*` (id-keyed)
|
|
82
|
+
|
|
83
|
+
`gw-path-create` is a **POST** (DB paths have an `id`) — unlike the name-keyed
|
|
84
|
+
auth/ACL resources. `create` rejects a `(method, path)` collision (409).
|
|
85
|
+
`export` returns enabled paths, ordered, and feeds `loadConfig.paths`.
|
|
86
|
+
|
|
87
|
+
| Method | Path | action | dataSource | Notes |
|
|
88
|
+
| --- | --- | --- | --- | --- |
|
|
89
|
+
| POST | `/admin/paths` | `gw-path-create` | body | needs `name,method,path,topic`; 409 on collision |
|
|
90
|
+
| GET | `/admin/paths` | `gw-path-list` | query | `?page=&limit=` |
|
|
91
|
+
| GET | `/admin/paths/export` | `gw-path-export` | query | enabled, ordered; used by `loadConfig.paths` |
|
|
92
|
+
| PUT | `/admin/paths` | `gw-path-update` | body | needs `id`; re-checks collisions |
|
|
93
|
+
| GET | `/admin/paths/get` | `gw-path-get` | query | `?id=` |
|
|
94
|
+
| DELETE | `/admin/paths` | `gw-path-delete` | body | `{ id }` |
|
|
95
|
+
|
|
96
|
+
## Auth-provider management — `gw-auth-*` (name-keyed PUT-upsert)
|
|
97
|
+
|
|
98
|
+
No `id`, no POST — a single `PUT` creates-or-updates by `name`. These are the
|
|
99
|
+
DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
|
|
100
|
+
|
|
101
|
+
| Method | Path | action | dataSource | Notes |
|
|
102
|
+
| --- | --- | --- | --- | --- |
|
|
103
|
+
| GET | `/admin/auth` | `gw-auth-list` | query | `?page=&limit=` |
|
|
104
|
+
| PUT | `/admin/auth` | `gw-auth-update` | body | upsert by name; `{ name, type, ... }` |
|
|
105
|
+
| GET | `/admin/auth/get` | `gw-auth-get` | query | `?name=` |
|
|
106
|
+
| DELETE | `/admin/auth` | `gw-auth-delete` | body | `{ name }` |
|
|
107
|
+
|
|
108
|
+
`gw-auth-export` (dump all enabled) also exists; not in the sample YAML.
|
|
109
|
+
|
|
110
|
+
## Metrics — `gw-metrics-*` + `gw-health`
|
|
111
|
+
|
|
112
|
+
| Method | Path | action | mode | dataSource | Returns |
|
|
113
|
+
| --- | --- | --- | --- | --- | --- |
|
|
114
|
+
| GET | `/admin/metrics` | `gw-metrics-get` | rpc | query | counters/route (`count,errorCount,avgDurationMs`); `?route=` |
|
|
115
|
+
| GET | `/admin/metrics/series` | `gw-metrics-series` | rpc | query | buckets: `?bucketMs=60000&from=&to=&method=&route=&name=` |
|
|
116
|
+
| GET | `/admin/metrics/points` | `gw-metrics-points` | rpc | query | raw points newest-first: `?method=&route=&from=&to=&limit=` |
|
|
117
|
+
| POST | `/admin/metrics/track` | `gw-metrics-track` | event | body | fire-and-forget sink (wired via `gateway.metrics`) |
|
|
118
|
+
|
|
119
|
+
`gw-health` → `{ status: 'ok' }`, a real 200 liveness probe (NOT a metrics dump):
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- name: health
|
|
123
|
+
method: GET
|
|
124
|
+
path: /health
|
|
125
|
+
dataSource: query
|
|
126
|
+
topic: rlb-gateway-admin
|
|
127
|
+
action: gw-health
|
|
128
|
+
mode: rpc
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### In-proxy metrics hook — `RLB_GTW_METRICS_HOOK`
|
|
132
|
+
|
|
133
|
+
Independent of the broker `gateway.metrics` sink: the gateway invokes a hook
|
|
134
|
+
once per request, after the response is flushed. Register under
|
|
135
|
+
`RLB_GTW_METRICS_HOOK` in `ProxyModule.forRootAsync`'s `providers`. Both sinks
|
|
136
|
+
can be active; the hook must not throw and should be cheap/async.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
export interface GatewayMetricsHook { track(p: GatewayMetricPoint): void | Promise<void>; }
|
|
140
|
+
// GatewayMetricPoint: { ts, method, route, name?, topic?, action?, mode?, status?, durationMs? }
|
|
141
|
+
|
|
142
|
+
ProxyModule.forRootAsync({ /* ... */ providers: [
|
|
143
|
+
{ provide: RLB_GTW_METRICS_HOOK, useClass: InfluxMetricsHook },
|
|
144
|
+
]}),
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
(`sample/config-sample/gateway-db` ships an `InfluxMetricsHook` that is a no-op
|
|
148
|
+
until `INFLUX_URL/TOKEN/ORG` env are set.)
|
|
149
|
+
|
|
150
|
+
## Runtime reload — `gw-reload`
|
|
151
|
+
|
|
152
|
+
The ONLY control action that forces a route rebuild. Published to the
|
|
153
|
+
**broadcast control topic** (`gateway.reloadTopic`, NOT `rlb-gateway-admin`),
|
|
154
|
+
`mode: event`. The control-topic subscriber ignores everything except
|
|
155
|
+
`gw-reload`, staying decoupled from other control traffic.
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
- name: gw-reload
|
|
159
|
+
method: POST
|
|
160
|
+
path: /admin/reload
|
|
161
|
+
dataSource: body
|
|
162
|
+
topic: rlb-gateway-control # the broadcast control topic
|
|
163
|
+
action: gw-reload
|
|
164
|
+
mode: event
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Seed DB routes via `POST /admin/paths`, then `POST /admin/reload` — no restart.
|
|
168
|
+
|
|
169
|
+
## Route auto-discovery (publisher vs consumer)
|
|
170
|
+
|
|
171
|
+
A microservice announces its own `@BrokerHTTP`/`@BrokerAction` routes; the
|
|
172
|
+
gateway persists + registers them, no YAML edits. Two halves must agree on the
|
|
173
|
+
same exchange/queue (defaults `rlb-route-discovery` / `rlb-route-sync`).
|
|
174
|
+
|
|
175
|
+
### Publisher (microservice → gateway) — `broker.routeDiscovery`
|
|
176
|
+
|
|
177
|
+
Lives in `BrokerModule`, so its config is INSIDE the broker block.
|
|
178
|
+
|
|
179
|
+
| Field | Default | Purpose |
|
|
180
|
+
| --- | --- | --- |
|
|
181
|
+
| `serviceName` | — | **Required to publish**; ownership key. PROMOTES to AMQP `connection_name` if none set. |
|
|
182
|
+
| `publishOnBoot` | `true` | Announce manifest on bootstrap (durable/persistent message; queue buffers it). |
|
|
183
|
+
| `exchange` | `rlb-route-discovery` | Fanout exchange the manifest is published to. |
|
|
184
|
+
| `queue` | `rlb-route-sync` | Durable shared work-queue the gateway consumes. |
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
# in the MICROSERVICE config.yaml:
|
|
188
|
+
broker:
|
|
189
|
+
routeDiscovery:
|
|
190
|
+
serviceName: demo-ms # required; also fills connection_name if unset
|
|
191
|
+
publishOnBoot: true
|
|
192
|
+
# exchange/queue default; override only to namespace per env (then match the consumer)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Routes are declared `@BrokerHTTP` over a `@BrokerAction` method. The gateway must
|
|
196
|
+
still declare the microservice's broker topic so it can route forwarded calls.
|
|
197
|
+
|
|
198
|
+
### Consumer (gateway ← microservice) — `GatewayAdminModule` `routeDiscovery`
|
|
199
|
+
|
|
200
|
+
Wired by `GatewayAdminModule` (NOT YAML). Asserts the fanout exchange, subscribes
|
|
201
|
+
to the durable queue (competing consumers), then per manifest: diffs vs DB scoped
|
|
202
|
+
to the publishing service, applies only changes (insert/update; soft-disable stale
|
|
203
|
+
to `enabled:false`; skip collisions — existing owner keeps `(method,path)`),
|
|
204
|
+
journals every event via `RouteSyncLogRepository`, and publishes `gw-reload` when
|
|
205
|
+
anything changed. Never throws (acks; no poison loop).
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
GatewayAdminModule.forRootAsync({
|
|
209
|
+
imports: [ConfigModule], inject: [ConfigService],
|
|
210
|
+
useFactory: (c: ConfigService) => ({
|
|
211
|
+
routeDiscovery: {
|
|
212
|
+
exchange: c.get('routeDiscovery.exchange'), // default rlb-route-discovery
|
|
213
|
+
queue: c.get('routeDiscovery.queue'), // default rlb-route-sync — MUST match publishers
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
providers: [ /* HttpPathRepository, AuthProviderRepository, HttpMetricRepository, RouteSyncLogRepository */ ],
|
|
217
|
+
}),
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The consumer has NO `serviceName` (it only receives). The exchange/queue MUST
|
|
221
|
+
match every publisher's `broker.routeDiscovery`.
|
|
222
|
+
|
|
223
|
+
## Verify
|
|
224
|
+
|
|
225
|
+
- Topic `rlb-gateway-admin` (+ queue) declared; `reloadTopic` matches the
|
|
226
|
+
broadcast control topic name and the `gw-reload` path's `topic`.
|
|
227
|
+
- Action strings written exactly (`gw-path-*`, `gw-auth-*`, `gw-metrics-*`,
|
|
228
|
+
`gw-health`, `gw-reload`).
|
|
229
|
+
- `loadConfig.paths` → `gw-path-export`; `gateway.metrics` → `gw-metrics-track`.
|
|
230
|
+
- Route-discovery `exchange`/`queue` identical on publisher and consumer.
|
|
231
|
+
- All four repositories bound; `npm run build`.
|
|
232
|
+
|
|
233
|
+
See also: `rlb-amqp` (schema/gotchas) · `rlb-amqp-add-route` · `docs/gateway.md` · `docs/acl.md`
|
|
@@ -9,10 +9,83 @@ Read first:
|
|
|
9
9
|
- `.claude/skills/rlb-amqp/references/config-schema.md`
|
|
10
10
|
- `.claude/skills/rlb-amqp/references/gotchas.md`
|
|
11
11
|
|
|
12
|
-
Decide the role: a **microservice** (only `@BrokerAction` handlers
|
|
13
|
-
|
|
12
|
+
Decide the role: a **microservice** (only `@BrokerAction` / `@BrokerHTTP` handlers, no
|
|
13
|
+
HTTP server) or a **gateway** (HTTP/WS exposure in front of microservices). Two paths:
|
|
14
|
+
the `nest add` schematic (fast, patches in place) or manual wiring. Canonical runnable
|
|
15
|
+
examples live under `sample/config-sample/` (`gateway-in-memory`, `gateway-db`,
|
|
16
|
+
`calculator.ms`) — mirror those, not the retired `apps/gateway-2`.
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Path A — `nest add` schematic (preferred)
|
|
21
|
+
|
|
22
|
+
From a NestJS project root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
nest add @open-rlb/nestjs-amqp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It **patches in place** (does not scaffold a new app): edits `src/app.module.ts` and
|
|
29
|
+
`src/main.ts`, creates `src/config/config.loader.ts` + `config/config.yaml`, copies
|
|
30
|
+
RUNNABLE in-memory repositories for the selected features, adds deps, and copies the
|
|
31
|
+
Claude skills.
|
|
32
|
+
|
|
33
|
+
### Interactive flow
|
|
34
|
+
|
|
35
|
+
1. **"Create a gateway (HTTP/WebSocket) configuration? y/N"**
|
|
36
|
+
2. **YES** → checkbox of gateway features:
|
|
37
|
+
- `acl` — `AclModule` + ACL management/grant/check paths
|
|
38
|
+
- `gateway-admin` — `GatewayAdminModule` + DB-managed routes/auth-providers/metrics paths
|
|
39
|
+
- `route-reception` — gateway consumes routes auto-published by microservices
|
|
40
|
+
Then prompts for names (defaults shown): exchange `rlb`, ACL queue `rlb-acl`,
|
|
41
|
+
admin queue `rlb-gateway-admin`, control topic `rlb-gateway-control`,
|
|
42
|
+
route exchange `rlb-route-discovery`, route queue `rlb-route-sync`.
|
|
43
|
+
3. **NO** (plain microservice) → checkbox:
|
|
44
|
+
- `auto-config-publish` — publish this service's `@BrokerHTTP` routes to the gateway
|
|
45
|
+
on boot (adds `broker.routeDiscovery`). Prompts service name + route exchange/queue.
|
|
46
|
+
4. "Copy the Claude skills into .claude/skills? Y/n"
|
|
47
|
+
|
|
48
|
+
### Non-interactive flags (CI / scripted)
|
|
49
|
+
|
|
50
|
+
Passing `--gatewayConfig` or any `--features` skips the prompts; everything else falls
|
|
51
|
+
back to `rlb-*` defaults.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Gateway with ACL + admin + route reception
|
|
55
|
+
nest add @open-rlb/nestjs-amqp \
|
|
56
|
+
--gatewayConfig --features acl --features gateway-admin --features route-reception
|
|
57
|
+
|
|
58
|
+
# Plain microservice that auto-publishes its @BrokerHTTP routes on boot
|
|
59
|
+
nest add @open-rlb/nestjs-amqp \
|
|
60
|
+
--gatewayConfig=false --features auto-config-publish \
|
|
61
|
+
--serviceName my-service --routeExchange rlb-route-discovery --routeQueue rlb-route-sync
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Flag | Purpose |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `--gatewayConfig` | `true` = gateway, `false` = microservice (default false non-interactive). |
|
|
67
|
+
| `--features <f>` | Repeatable. Gateway: `acl`, `gateway-admin`, `route-reception`. MS: `auto-config-publish`. |
|
|
68
|
+
| `--exchange` | Main AMQP exchange backing acl/admin queues. Default `rlb`. |
|
|
69
|
+
| `--aclQueue` / `--adminQueue` | Queues backing the fixed `rlb-acl` / `rlb-gateway-admin` topics. |
|
|
70
|
+
| `--controlTopic` | Broadcast control/reload topic. Default `rlb-gateway-control`. |
|
|
71
|
+
| `--routeExchange` / `--routeQueue` | Route-discovery exchange/queue. Defaults `rlb-route-discovery` / `rlb-route-sync` — **must match both publisher and gateway**. |
|
|
72
|
+
| `--serviceName` | Route-publish ownership key + AMQP `connection_name`. Default = project name. |
|
|
73
|
+
| `--skills` | Copy Claude skills. Default `true`; `--skills=false` to skip. |
|
|
74
|
+
|
|
75
|
+
> `app.module.ts` patch is idempotent (keys off `BrokerModule.forRootAsync`). If it can't
|
|
76
|
+
> locate `app.module.ts` / `main.ts` it warns and leaves you the manual wiring below.
|
|
77
|
+
|
|
78
|
+
After scaffolding, edit `config/config.yaml` (fill `<AMQP_URI>` / credentials) and replace
|
|
79
|
+
the `<APP_NAME>` `connection_name` placeholder. Then use `rlb-amqp-add-action` /
|
|
80
|
+
`rlb-amqp-add-route` / `rlb-amqp-add-ws-event` to grow the service.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Path B — manual wiring
|
|
85
|
+
|
|
86
|
+
Mirrors `sample/config-sample/gateway-in-memory` (gateway) and `calculator.ms` (pure MS).
|
|
87
|
+
|
|
88
|
+
### 1. `src/config/config.loader.ts`
|
|
16
89
|
|
|
17
90
|
```ts
|
|
18
91
|
import { readFileSync } from 'fs';
|
|
@@ -20,23 +93,25 @@ import * as yaml from 'js-yaml';
|
|
|
20
93
|
import { join } from 'path';
|
|
21
94
|
|
|
22
95
|
const YAML_CONFIG_FILENAME = 'config/config.yaml';
|
|
96
|
+
|
|
23
97
|
export default () =>
|
|
24
98
|
yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
|
|
25
99
|
```
|
|
26
100
|
|
|
27
|
-
(`js-yaml`
|
|
101
|
+
(`js-yaml` + `@nestjs/config` are deps; the gateway also needs `@nestjs/axios`,
|
|
102
|
+
`@nestjs/platform-ws`, `@nestjs/websockets`, `ws`.)
|
|
28
103
|
|
|
29
|
-
|
|
104
|
+
### 2. `src/app.module.ts`
|
|
30
105
|
|
|
31
106
|
```ts
|
|
32
|
-
import { HttpModule } from '@nestjs/axios';
|
|
107
|
+
import { HttpModule } from '@nestjs/axios'; // gateway only
|
|
33
108
|
import { Module } from '@nestjs/common';
|
|
34
109
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
110
|
+
import {
|
|
111
|
+
AppConfig, BrokerModule, BrokerTopic, RabbitMQConfig,
|
|
112
|
+
GatewayConfig, HandlerAuthConfig, ProxyModule, // gateway only
|
|
113
|
+
} from '@open-rlb/nestjs-amqp';
|
|
38
114
|
import yamlConfig from './config/config.loader';
|
|
39
|
-
// import { MyActionService } from './my-action.service';
|
|
40
115
|
|
|
41
116
|
@Module({
|
|
42
117
|
imports: [
|
|
@@ -45,13 +120,15 @@ import yamlConfig from './config/config.loader';
|
|
|
45
120
|
imports: [ConfigModule],
|
|
46
121
|
inject: [ConfigService],
|
|
47
122
|
useFactory: async (config: ConfigService) => ({
|
|
48
|
-
|
|
49
|
-
|
|
123
|
+
// broker.routeDiscovery (publisher side), when used, lives INSIDE this `broker` block.
|
|
124
|
+
options: config.get<RabbitMQConfig>('broker')!,
|
|
125
|
+
topics: config.get<BrokerTopic[]>('topics')!,
|
|
50
126
|
appOptions: config.get<AppConfig>('app'),
|
|
51
127
|
}),
|
|
52
128
|
}),
|
|
129
|
+
|
|
130
|
+
// --- gateway only: omit ProxyModule + HttpModule for a pure microservice ---
|
|
53
131
|
HttpModule,
|
|
54
|
-
// Gateway: auth-providers + gateway config live in ProxyModule (NOT BrokerModule).
|
|
55
132
|
ProxyModule.forRootAsync({
|
|
56
133
|
imports: [ConfigModule],
|
|
57
134
|
inject: [ConfigService],
|
|
@@ -60,33 +137,59 @@ import yamlConfig from './config/config.loader';
|
|
|
60
137
|
gatewayOptions: config.get<GatewayConfig>('gateway'),
|
|
61
138
|
}),
|
|
62
139
|
providers: [
|
|
63
|
-
//
|
|
140
|
+
// Role-gated paths resolve the caller's roles in-process via this token (NO broker hop):
|
|
141
|
+
// { provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }, // required if any path uses `roles`
|
|
142
|
+
// Optional in-proxy per-request metrics hook (independent of gateway.metrics):
|
|
143
|
+
// { provide: RLB_GTW_METRICS_HOOK, useClass: InfluxMetricsHook },
|
|
64
144
|
],
|
|
65
145
|
}),
|
|
66
146
|
],
|
|
67
|
-
providers: [/*
|
|
147
|
+
providers: [/* AppService — your @BrokerAction / @BrokerHTTP handlers */],
|
|
68
148
|
})
|
|
69
149
|
export class AppModule {}
|
|
70
150
|
```
|
|
71
151
|
|
|
72
|
-
>
|
|
152
|
+
> The gateway's `auth-providers` + `gateway` config belong to **`ProxyModule`**, not
|
|
153
|
+
> `BrokerModule`. For ACL add `AclModule.forRoot([...repos], { cache })` and bind
|
|
154
|
+
> `RLB_GTW_ACL_ROLE_SERVICE`; for DB routes/auth/metrics add
|
|
155
|
+
> `GatewayAdminModule.forRoot([...repos])` — see the `gateway-in-memory` sample and the
|
|
156
|
+
> ACL / Gateway Admin docs.
|
|
157
|
+
|
|
158
|
+
### 3. `src/main.ts`
|
|
73
159
|
|
|
74
|
-
|
|
160
|
+
**Gateway** (needs `rawBody` + `WsAdapter`):
|
|
75
161
|
|
|
76
162
|
```ts
|
|
163
|
+
import { ConfigService } from '@nestjs/config';
|
|
77
164
|
import { NestFactory } from '@nestjs/core';
|
|
78
165
|
import { WsAdapter } from '@nestjs/platform-ws';
|
|
79
166
|
import { AppModule } from './app.module';
|
|
80
167
|
|
|
81
168
|
async function bootstrap() {
|
|
82
|
-
const app = await NestFactory.create(AppModule, { rawBody: true }); // rawBody
|
|
83
|
-
app.useWebSocketAdapter(new WsAdapter(app)); //
|
|
84
|
-
|
|
169
|
+
const app = await NestFactory.create(AppModule, { rawBody: true }); // rawBody: raw-body/webhook routes
|
|
170
|
+
app.useWebSocketAdapter(new WsAdapter(app)); // WS events
|
|
171
|
+
app.enableShutdownHooks();
|
|
172
|
+
const cfg = app.get(ConfigService).get<{ port?: number; host?: string }>('app');
|
|
173
|
+
await app.listen(Number(process.env.PORT) || cfg?.port || 3000, cfg?.host || '0.0.0.0');
|
|
85
174
|
}
|
|
86
175
|
bootstrap();
|
|
87
176
|
```
|
|
88
177
|
|
|
89
|
-
|
|
178
|
+
**Pure microservice** (no HTTP server — `init()`, not `listen()`):
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { NestFactory } from '@nestjs/core';
|
|
182
|
+
import { AppModule } from './app.module';
|
|
183
|
+
|
|
184
|
+
async function bootstrap() {
|
|
185
|
+
const app = await NestFactory.create(AppModule);
|
|
186
|
+
app.enableShutdownHooks(); // drain in-flight RPC + close AMQP cleanly on SIGINT/SIGTERM
|
|
187
|
+
await app.init(); // @BrokerAction handlers start consuming once initialized
|
|
188
|
+
}
|
|
189
|
+
bootstrap();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 4. `config/config.yaml` (starter)
|
|
90
193
|
|
|
91
194
|
```yaml
|
|
92
195
|
app:
|
|
@@ -94,28 +197,29 @@ app:
|
|
|
94
197
|
host: 0.0.0.0
|
|
95
198
|
environment: development
|
|
96
199
|
|
|
97
|
-
auth-providers: []
|
|
200
|
+
auth-providers: [] # gateway only — JWT/JWKS providers
|
|
98
201
|
|
|
99
202
|
broker:
|
|
203
|
+
name: rabbitmq
|
|
100
204
|
uri: "amqp://guest:guest@localhost:5672/"
|
|
101
|
-
defaultRpcTimeout: 10000
|
|
102
205
|
defaultSubscribeErrorBehavior: ack
|
|
206
|
+
defaultPublishErrorBehavior: reject
|
|
103
207
|
connectionManagerOptions:
|
|
104
208
|
heartbeatIntervalInSeconds: 60
|
|
105
209
|
reconnectTimeInSeconds: 60
|
|
106
210
|
connectionOptions:
|
|
107
211
|
clientProperties:
|
|
108
|
-
connection_name: my-service
|
|
212
|
+
connection_name: my-service # distinct per instance; needed for broadcast/WebSocket
|
|
109
213
|
credentials: { mechanism: PLAIN, username: guest, password: guest }
|
|
110
214
|
exchanges:
|
|
111
|
-
- name:
|
|
215
|
+
- name: rlb
|
|
112
216
|
type: direct
|
|
113
217
|
createExchangeIfNotExists: true
|
|
114
218
|
options: { durable: true }
|
|
115
219
|
queues:
|
|
116
220
|
- name: my-rpc-q
|
|
117
|
-
exchange:
|
|
118
|
-
routingKey: my
|
|
221
|
+
exchange: rlb
|
|
222
|
+
routingKey: my-rpc-q
|
|
119
223
|
createQueueIfNotExists: true
|
|
120
224
|
options: { durable: true }
|
|
121
225
|
|
|
@@ -123,38 +227,64 @@ topics:
|
|
|
123
227
|
- name: my-rpc
|
|
124
228
|
mode: rpc
|
|
125
229
|
queue: my-rpc-q
|
|
230
|
+
exchange: rlb
|
|
231
|
+
routingKey: my-rpc-q
|
|
126
232
|
|
|
233
|
+
# --- gateway only ---
|
|
127
234
|
gateway:
|
|
128
235
|
mode: gateway
|
|
236
|
+
events: []
|
|
129
237
|
paths:
|
|
130
|
-
- name:
|
|
238
|
+
- name: health
|
|
131
239
|
method: GET
|
|
132
|
-
path: /
|
|
240
|
+
path: /health
|
|
133
241
|
dataSource: query
|
|
134
|
-
topic:
|
|
135
|
-
action:
|
|
242
|
+
topic: rlb-gateway-admin # gateway-admin gw-health → 200 { status: 'ok' }
|
|
243
|
+
action: gw-health
|
|
136
244
|
mode: rpc
|
|
137
|
-
events: []
|
|
138
245
|
```
|
|
139
246
|
|
|
140
|
-
|
|
247
|
+
### Route auto-discovery (publisher side, optional)
|
|
248
|
+
|
|
249
|
+
A microservice can announce its `@BrokerHTTP` routes on boot. Add INSIDE the `broker`
|
|
250
|
+
block — `serviceName` is the ownership key AND fills `connection_name` when none is set
|
|
251
|
+
explicitly. `exchange`/`queue` default to `rlb-route-discovery` / `rlb-route-sync` and
|
|
252
|
+
must match the gateway's `GatewayAdminModule` `routeDiscovery { exchange, queue }`.
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
broker:
|
|
256
|
+
# ...
|
|
257
|
+
routeDiscovery:
|
|
258
|
+
serviceName: my-service
|
|
259
|
+
publishOnBoot: true
|
|
260
|
+
# exchange: rlb-route-discovery # override to namespace per env (must match the gateway)
|
|
261
|
+
# queue: rlb-route-sync
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 5. Sample handler (one method, both transports)
|
|
141
265
|
|
|
142
266
|
```ts
|
|
143
267
|
import { Injectable } from '@nestjs/common';
|
|
144
|
-
import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
|
|
268
|
+
import { BrokerAction, BrokerHTTP, BrokerParam } from '@open-rlb/nestjs-amqp';
|
|
145
269
|
|
|
146
270
|
@Injectable()
|
|
147
|
-
export class
|
|
148
|
-
@BrokerAction('my-rpc', 'ping',
|
|
149
|
-
|
|
150
|
-
|
|
271
|
+
export class AppService {
|
|
272
|
+
@BrokerAction('my-rpc', 'ping') // AMQP RPC on topic my-rpc, action ping
|
|
273
|
+
@BrokerHTTP('POST', '/ping', 'body') // route metadata for gateway auto-discovery
|
|
274
|
+
async ping(@BrokerParam('body', 'name') name: string) { // flat params, one decorator each
|
|
275
|
+
return { pong: true, name };
|
|
151
276
|
}
|
|
152
277
|
}
|
|
153
278
|
```
|
|
154
279
|
|
|
155
280
|
## Verify
|
|
156
|
-
- topic/queue/exchange names line up (gotchas 5–7);
|
|
157
|
-
|
|
281
|
+
- topic/queue/exchange names line up across `broker`/`topics`/paths (gotchas 5–7);
|
|
282
|
+
`connection_name` set if using broadcast/WebSocket (8).
|
|
283
|
+
- Route-discovery `exchange`/`queue` identical on publisher and gateway.
|
|
284
|
+
- Reload DB routes at runtime via the `gw-reload` action on the broadcast control topic
|
|
285
|
+
(default `rlb-gateway-control`).
|
|
286
|
+
- `npm run build`, start with a reachable RabbitMQ, hit `/health` (gateway) or publish to
|
|
287
|
+
the RPC topic (microservice).
|
|
158
288
|
|
|
159
|
-
After scaffolding, use `rlb-amqp-add-action` / `rlb-amqp-add-route` /
|
|
160
|
-
to grow the service.
|
|
289
|
+
After scaffolding, use `rlb-amqp-add-action` / `rlb-amqp-add-route` /
|
|
290
|
+
`rlb-amqp-add-ws-event` to grow the service.
|