@open-rlb/nestjs-amqp 2.0.4 → 2.0.6
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 +0 -1
- package/modules/acl/const.js +0 -1
- 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-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 +2 -2
- package/modules/acl/services/acl-management.service.js +17 -20
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +1 -2
- package/modules/acl/services/acl.service.js +5 -21
- 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/decorator-paths.d.ts +1 -0
- package/modules/broker/config/decorator-paths.js +34 -4
- package/modules/broker/config/decorator-paths.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/decorators/broker-action.decorator.d.ts +2 -1
- package/modules/broker/decorators/broker-action.decorator.js +2 -2
- package/modules/broker/decorators/broker-action.decorator.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/metadata-scanner.service.js +11 -2
- package/modules/broker/services/metadata-scanner.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 +27 -3
- 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 +33 -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 +129 -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 +87 -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 +244 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +177 -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
|
@@ -7,38 +7,185 @@ const path_1 = require("path");
|
|
|
7
7
|
const formatting_1 = require("../utils/formatting");
|
|
8
8
|
const name_parser_1 = require("../utils/name.parser");
|
|
9
9
|
const source_root_helpers_1 = require("../utils/source-root.helpers");
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
imports: [ConfigModule],
|
|
21
|
-
inject: [ConfigService],
|
|
22
|
-
useFactory: async (configService: ConfigService) => ({
|
|
23
|
-
options: configService.get<RabbitMQConfig>('broker')!,
|
|
24
|
-
topics: configService.get<BrokerTopic[]>('topics')!,
|
|
25
|
-
appOptions: configService.get<AppConfig>('app'),
|
|
26
|
-
})
|
|
27
|
-
})`;
|
|
10
|
+
function defaultNames(project) {
|
|
11
|
+
return {
|
|
12
|
+
exchange: 'rlb',
|
|
13
|
+
aclQueue: 'rlb-acl',
|
|
14
|
+
adminQueue: 'rlb-gateway-admin',
|
|
15
|
+
controlTopic: 'rlb-gateway-control',
|
|
16
|
+
routeExchange: 'rlb-route-discovery',
|
|
17
|
+
routeQueue: 'rlb-route-sync',
|
|
18
|
+
serviceName: project || 'my-service',
|
|
19
|
+
};
|
|
28
20
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
async function resolveSelections(o, context) {
|
|
22
|
+
const project = (o.project || 'my-service').toString();
|
|
23
|
+
const d = defaultNames(project);
|
|
24
|
+
const flagsProvided = o.gatewayConfig !== undefined || (Array.isArray(o.features) && o.features.length > 0);
|
|
25
|
+
const canPrompt = !!process.stdout && !!process.stdout.isTTY && !process.env.CI && !flagsProvided;
|
|
26
|
+
let prompts;
|
|
27
|
+
if (canPrompt) {
|
|
28
|
+
try {
|
|
29
|
+
prompts = require('@inquirer/prompts');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
context.logger.warn('[nest-add] @inquirer/prompts not found; falling back to flags/defaults (non-interactive).');
|
|
33
|
+
prompts = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!prompts) {
|
|
37
|
+
const features = new Set((o.features || []).map((f) => String(f).trim()));
|
|
38
|
+
const gatewayConfig = o.gatewayConfig === true;
|
|
39
|
+
return {
|
|
40
|
+
gatewayConfig,
|
|
41
|
+
acl: gatewayConfig && features.has('acl'),
|
|
42
|
+
admin: gatewayConfig && features.has('gateway-admin'),
|
|
43
|
+
routeReception: gatewayConfig && features.has('route-reception'),
|
|
44
|
+
autoPublish: !gatewayConfig && features.has('auto-config-publish'),
|
|
45
|
+
skills: o.skills !== false,
|
|
46
|
+
names: {
|
|
47
|
+
exchange: o.exchange || d.exchange,
|
|
48
|
+
aclQueue: o.aclQueue || d.aclQueue,
|
|
49
|
+
adminQueue: o.adminQueue || d.adminQueue,
|
|
50
|
+
controlTopic: o.controlTopic || d.controlTopic,
|
|
51
|
+
routeExchange: o.routeExchange || d.routeExchange,
|
|
52
|
+
routeQueue: o.routeQueue || d.routeQueue,
|
|
53
|
+
serviceName: o.serviceName || d.serviceName,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const { confirm, checkbox, input } = prompts;
|
|
58
|
+
const names = { ...d };
|
|
59
|
+
let acl = false, admin = false, routeReception = false, autoPublish = false;
|
|
60
|
+
const gatewayConfig = await confirm({
|
|
61
|
+
message: 'Create a gateway (HTTP/WebSocket) configuration?',
|
|
62
|
+
default: false,
|
|
63
|
+
});
|
|
64
|
+
if (gatewayConfig) {
|
|
65
|
+
const picked = await checkbox({
|
|
66
|
+
message: 'Select gateway features to include',
|
|
67
|
+
choices: [
|
|
68
|
+
{ name: 'ACL — role-based authorization + management', value: 'acl' },
|
|
69
|
+
{ name: 'Gateway admin + auth — DB-managed routes, auth-providers, metrics', value: 'gateway-admin' },
|
|
70
|
+
{ name: 'Route reception — apply routes auto-published by microservices', value: 'route-reception' },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
acl = picked.includes('acl');
|
|
74
|
+
admin = picked.includes('gateway-admin');
|
|
75
|
+
routeReception = picked.includes('route-reception');
|
|
76
|
+
if (acl || admin || routeReception) {
|
|
77
|
+
names.exchange = await input({ message: 'Main AMQP exchange name', default: d.exchange });
|
|
78
|
+
}
|
|
79
|
+
if (acl) {
|
|
80
|
+
names.aclQueue = await input({ message: 'Queue backing the rlb-acl topic', default: d.aclQueue });
|
|
81
|
+
}
|
|
82
|
+
if (admin || routeReception) {
|
|
83
|
+
names.adminQueue = await input({ message: 'Queue backing the rlb-gateway-admin topic', default: d.adminQueue });
|
|
84
|
+
names.controlTopic = await input({ message: 'Broadcast control/reload topic name', default: d.controlTopic });
|
|
85
|
+
}
|
|
86
|
+
if (routeReception) {
|
|
87
|
+
names.routeExchange = await input({ message: 'Route-discovery exchange (must match the publishers)', default: d.routeExchange });
|
|
88
|
+
names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const picked = await checkbox({
|
|
93
|
+
message: 'Select microservice features to include',
|
|
94
|
+
choices: [
|
|
95
|
+
{ name: 'Auto-send config at startup — publish this service’s @BrokerHTTP routes to the gateway on boot', value: 'auto-config-publish' },
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
autoPublish = picked.includes('auto-config-publish');
|
|
99
|
+
if (autoPublish) {
|
|
100
|
+
names.serviceName = await input({ message: 'Service name (route ownership + AMQP connection_name)', default: d.serviceName });
|
|
101
|
+
names.routeExchange = await input({ message: 'Route-discovery exchange', default: d.routeExchange });
|
|
102
|
+
names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const skills = await confirm({ message: 'Copy the Claude skills into .claude/skills?', default: true });
|
|
106
|
+
return { gatewayConfig, acl, admin, routeReception, autoPublish, skills, names };
|
|
39
107
|
}
|
|
40
|
-
|
|
41
|
-
|
|
108
|
+
function buildConfigYaml(sel) {
|
|
109
|
+
const n = sel.names;
|
|
110
|
+
const anyAdmin = sel.admin || sel.routeReception;
|
|
111
|
+
const routeDiscoveryPub = sel.autoPublish ? `
|
|
112
|
+
# Route auto-discovery (publisher side): announce this service's @BrokerHTTP routes on boot.
|
|
113
|
+
# serviceName also fills the AMQP connection_name (none is set explicitly below).
|
|
114
|
+
routeDiscovery:
|
|
115
|
+
serviceName: "${n.serviceName}"
|
|
116
|
+
publishOnBoot: true
|
|
117
|
+
exchange: ${n.routeExchange}
|
|
118
|
+
queue: ${n.routeQueue}` : '';
|
|
119
|
+
const clientProps = sel.autoPublish ? '' : `
|
|
120
|
+
clientProperties:
|
|
121
|
+
connection_name: "<APP_NAME>"`;
|
|
122
|
+
const exchanges = [];
|
|
123
|
+
if (sel.acl || anyAdmin) {
|
|
124
|
+
exchanges.push(` - name: ${n.exchange}
|
|
125
|
+
type: "direct"
|
|
126
|
+
createExchangeIfNotExists: true
|
|
127
|
+
options:
|
|
128
|
+
durable: true`);
|
|
129
|
+
}
|
|
130
|
+
exchanges.push(` - name: example.fanout
|
|
131
|
+
type: "fanout"
|
|
132
|
+
createExchangeIfNotExists: true
|
|
133
|
+
options:
|
|
134
|
+
durable: true
|
|
135
|
+
autoDelete: false
|
|
136
|
+
internal: false`);
|
|
137
|
+
const queues = [];
|
|
138
|
+
if (sel.acl) {
|
|
139
|
+
queues.push(` - name: ${n.aclQueue}
|
|
140
|
+
exchange: ${n.exchange}
|
|
141
|
+
routingKey: ${n.aclQueue}
|
|
142
|
+
createQueueIfNotExists: true
|
|
143
|
+
options:
|
|
144
|
+
durable: true`);
|
|
145
|
+
}
|
|
146
|
+
if (anyAdmin) {
|
|
147
|
+
queues.push(` - name: ${n.adminQueue}
|
|
148
|
+
exchange: ${n.exchange}
|
|
149
|
+
routingKey: ${n.adminQueue}
|
|
150
|
+
createQueueIfNotExists: true
|
|
151
|
+
options:
|
|
152
|
+
durable: true`);
|
|
153
|
+
}
|
|
154
|
+
queues.push(` - name: example.queue
|
|
155
|
+
exchange: example.fanout
|
|
156
|
+
routingKey: example.queue
|
|
157
|
+
createQueueIfNotExists: true
|
|
158
|
+
options:
|
|
159
|
+
durable: true
|
|
160
|
+
autoDelete: false
|
|
161
|
+
exclusive: false`);
|
|
162
|
+
const topics = [];
|
|
163
|
+
if (sel.acl) {
|
|
164
|
+
topics.push(` # Fixed topic name (decorator-bound in the lib): the ACL handlers bind to 'rlb-acl'.
|
|
165
|
+
- name: rlb-acl
|
|
166
|
+
mode: rpc
|
|
167
|
+
queue: ${n.aclQueue}
|
|
168
|
+
exchange: ${n.exchange}
|
|
169
|
+
routingKey: ${n.aclQueue}`);
|
|
170
|
+
}
|
|
171
|
+
if (anyAdmin) {
|
|
172
|
+
topics.push(` # Fixed topic name (decorator-bound): the gateway-admin handlers bind to 'rlb-gateway-admin'.
|
|
173
|
+
- name: rlb-gateway-admin
|
|
174
|
+
mode: rpc
|
|
175
|
+
queue: ${n.adminQueue}
|
|
176
|
+
exchange: ${n.exchange}
|
|
177
|
+
routingKey: ${n.adminQueue}
|
|
178
|
+
# Broadcast control topic: the gateway rebuilds its routes at runtime on a 'gw-reload'.
|
|
179
|
+
- name: ${n.controlTopic}
|
|
180
|
+
mode: broadcast
|
|
181
|
+
exchange: ${n.exchange}
|
|
182
|
+
routingKey: ${n.controlTopic}`);
|
|
183
|
+
}
|
|
184
|
+
topics.push(` - name: example.topic
|
|
185
|
+
exchange: example.fanout
|
|
186
|
+
routingKey: "example.topic"
|
|
187
|
+
mode: event`);
|
|
188
|
+
let yaml = `app:
|
|
42
189
|
port: 80
|
|
43
190
|
host: 0.0.0.0
|
|
44
191
|
environment: "development"
|
|
@@ -49,72 +196,395 @@ broker:
|
|
|
49
196
|
name: "rabbitmq"
|
|
50
197
|
uri: "<AMQP_URI>"
|
|
51
198
|
defaultSubscribeErrorBehavior: "ack"
|
|
52
|
-
defaultPublishErrorBehavior: "reject"
|
|
199
|
+
defaultPublishErrorBehavior: "reject"${routeDiscoveryPub}
|
|
53
200
|
connectionManagerOptions:
|
|
54
201
|
heartbeatIntervalInSeconds: 60
|
|
55
202
|
reconnectTimeInSeconds: 60
|
|
56
|
-
connectionOptions
|
|
57
|
-
clientProperties:
|
|
58
|
-
connection_name: "<APP_NAME>"
|
|
203
|
+
connectionOptions:${clientProps}
|
|
59
204
|
credentials:
|
|
60
205
|
mechanism: PLAIN
|
|
61
206
|
username: "<AMQP_USERNAME>"
|
|
62
207
|
password: "<AMQP_PASSWORD>"
|
|
63
208
|
exchanges:
|
|
64
|
-
|
|
65
|
-
type: "fanout"
|
|
66
|
-
createExchangeIfNotExists: true
|
|
67
|
-
options:
|
|
68
|
-
durable: true
|
|
69
|
-
autoDelete: false
|
|
70
|
-
internal: false
|
|
209
|
+
${exchanges.join('\n')}
|
|
71
210
|
queues:
|
|
72
|
-
|
|
73
|
-
createQueueIfNotExists: true
|
|
74
|
-
exchange: example.fanout
|
|
75
|
-
routingKey: example.queue
|
|
76
|
-
options:
|
|
77
|
-
durable: true
|
|
78
|
-
autoDelete: false
|
|
79
|
-
exclusive: false
|
|
211
|
+
${queues.join('\n')}
|
|
80
212
|
|
|
81
213
|
topics:
|
|
82
|
-
|
|
83
|
-
exchange: example.fanout
|
|
84
|
-
routingKey: "example.topic"
|
|
85
|
-
mode: event
|
|
214
|
+
${topics.join('\n')}
|
|
86
215
|
`;
|
|
87
|
-
|
|
88
|
-
|
|
216
|
+
if (sel.gatewayConfig) {
|
|
217
|
+
yaml += '\n' + buildGatewayBlock(sel);
|
|
218
|
+
}
|
|
219
|
+
return yaml;
|
|
220
|
+
}
|
|
221
|
+
function buildGatewayBlock(sel) {
|
|
222
|
+
const n = sel.names;
|
|
223
|
+
const anyAdmin = sel.admin || sel.routeReception;
|
|
224
|
+
const paths = [];
|
|
225
|
+
if (sel.acl)
|
|
226
|
+
paths.push(ACL_PATHS);
|
|
227
|
+
if (sel.admin)
|
|
228
|
+
paths.push(adminPaths(n.controlTopic));
|
|
229
|
+
paths.push(` - name: example-path
|
|
230
|
+
method: POST
|
|
231
|
+
dataSource: body
|
|
232
|
+
path: /example
|
|
233
|
+
topic: example.topic
|
|
234
|
+
action: example-action
|
|
235
|
+
mode: event`);
|
|
236
|
+
let block = `gateway:
|
|
89
237
|
events: []
|
|
90
238
|
ws:
|
|
91
239
|
heartbeatIntervalMs: 30000
|
|
92
240
|
# Auth is declared per-event (events[].auth / requireAuth / roles / scopeClaim).
|
|
93
241
|
paths:
|
|
94
|
-
|
|
242
|
+
${paths.join('\n')}`;
|
|
243
|
+
if (anyAdmin) {
|
|
244
|
+
block += `
|
|
245
|
+
# Load DB-managed routes at boot AND on every runtime reload (ordered static-before-param).
|
|
246
|
+
loadConfig:
|
|
247
|
+
paths:
|
|
248
|
+
topic: rlb-gateway-admin
|
|
249
|
+
action: gw-path-export
|
|
250
|
+
# Broadcast topic that triggers a runtime route rebuild (see topics: ${n.controlTopic}).
|
|
251
|
+
reloadTopic: ${n.controlTopic}
|
|
252
|
+
# Per-request metrics auto-emitted to the gateway-admin metrics handler.
|
|
253
|
+
metrics:
|
|
254
|
+
topic: rlb-gateway-admin
|
|
255
|
+
action: gw-metrics-track`;
|
|
256
|
+
}
|
|
257
|
+
return block + '\n';
|
|
258
|
+
}
|
|
259
|
+
const ACL_PATHS = ` # --- ACL management: actions (name is the key — PUT upserts, GET lists, DELETE by name) ---
|
|
260
|
+
- name: acl-action-list
|
|
261
|
+
method: GET
|
|
262
|
+
path: /acl/actions
|
|
263
|
+
dataSource: query
|
|
264
|
+
topic: rlb-acl
|
|
265
|
+
action: acl-action-list
|
|
266
|
+
mode: rpc
|
|
267
|
+
- name: acl-action-get
|
|
268
|
+
method: GET
|
|
269
|
+
path: /acl/actions/get
|
|
270
|
+
dataSource: query
|
|
271
|
+
topic: rlb-acl
|
|
272
|
+
action: acl-action-get
|
|
273
|
+
mode: rpc
|
|
274
|
+
- name: acl-action-upsert
|
|
275
|
+
method: PUT
|
|
276
|
+
path: /acl/actions
|
|
277
|
+
dataSource: body
|
|
278
|
+
topic: rlb-acl
|
|
279
|
+
action: acl-action-update
|
|
280
|
+
mode: rpc
|
|
281
|
+
- name: acl-action-delete
|
|
282
|
+
method: DELETE
|
|
283
|
+
path: /acl/actions
|
|
284
|
+
dataSource: body
|
|
285
|
+
topic: rlb-acl
|
|
286
|
+
action: acl-action-delete
|
|
287
|
+
mode: rpc
|
|
288
|
+
# --- ACL management: roles ---
|
|
289
|
+
- name: acl-role-list
|
|
290
|
+
method: GET
|
|
291
|
+
path: /acl/roles
|
|
292
|
+
dataSource: query
|
|
293
|
+
topic: rlb-acl
|
|
294
|
+
action: acl-role-list
|
|
295
|
+
mode: rpc
|
|
296
|
+
- name: acl-role-get
|
|
297
|
+
method: GET
|
|
298
|
+
path: /acl/roles/get
|
|
299
|
+
dataSource: query
|
|
300
|
+
topic: rlb-acl
|
|
301
|
+
action: acl-role-get
|
|
302
|
+
mode: rpc
|
|
303
|
+
- name: acl-role-upsert
|
|
304
|
+
method: PUT
|
|
305
|
+
path: /acl/roles
|
|
306
|
+
dataSource: body
|
|
307
|
+
topic: rlb-acl
|
|
308
|
+
action: acl-role-update
|
|
309
|
+
mode: rpc
|
|
310
|
+
- name: acl-role-delete
|
|
311
|
+
method: DELETE
|
|
312
|
+
path: /acl/roles
|
|
313
|
+
dataSource: body
|
|
314
|
+
topic: rlb-acl
|
|
315
|
+
action: acl-role-delete
|
|
316
|
+
mode: rpc
|
|
317
|
+
# --- ACL grants (per-user; resourceId/companyId optional) ---
|
|
318
|
+
- name: acl-grant
|
|
95
319
|
method: POST
|
|
320
|
+
path: /acl/grants
|
|
96
321
|
dataSource: body
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
322
|
+
topic: rlb-acl
|
|
323
|
+
action: acl-grant
|
|
324
|
+
mode: rpc
|
|
325
|
+
- name: acl-revoke
|
|
326
|
+
method: DELETE
|
|
327
|
+
path: /acl/grants
|
|
328
|
+
dataSource: body
|
|
329
|
+
topic: rlb-acl
|
|
330
|
+
action: acl-revoke
|
|
331
|
+
mode: rpc
|
|
332
|
+
# --- ACL checks (GET → 200 with true/false) ---
|
|
333
|
+
- name: acl-check-gtw
|
|
334
|
+
method: GET
|
|
335
|
+
path: /acl/check
|
|
336
|
+
dataSource: query
|
|
337
|
+
topic: rlb-acl
|
|
338
|
+
action: acl-can-user-do-gtw
|
|
339
|
+
mode: rpc
|
|
340
|
+
- name: acl-check-resource
|
|
341
|
+
method: GET
|
|
342
|
+
path: /acl/check-resource
|
|
343
|
+
dataSource: query
|
|
344
|
+
topic: rlb-acl
|
|
345
|
+
action: acl-can-user-do
|
|
346
|
+
mode: rpc
|
|
347
|
+
# Lists the caller's accessible resources. Add an 'auth: <provider>' line once you declare an auth-provider.
|
|
348
|
+
- name: acl-list-resources-by-user
|
|
349
|
+
method: GET
|
|
350
|
+
path: /acl/resources
|
|
351
|
+
dataSource: query
|
|
352
|
+
topic: rlb-acl
|
|
353
|
+
action: acl-list-resources-by-user
|
|
354
|
+
mode: rpc`;
|
|
355
|
+
function adminPaths(controlTopic) {
|
|
356
|
+
return ` # --- gateway-admin: health + DB routes + auth-providers + metrics ---
|
|
357
|
+
- name: health
|
|
358
|
+
method: GET
|
|
359
|
+
path: /health
|
|
360
|
+
dataSource: query
|
|
361
|
+
topic: rlb-gateway-admin
|
|
362
|
+
action: gw-health
|
|
363
|
+
mode: rpc
|
|
364
|
+
- name: gw-path-create
|
|
365
|
+
method: POST
|
|
366
|
+
path: /admin/paths
|
|
367
|
+
dataSource: body
|
|
368
|
+
topic: rlb-gateway-admin
|
|
369
|
+
action: gw-path-create
|
|
370
|
+
mode: rpc
|
|
371
|
+
- name: gw-path-list
|
|
372
|
+
method: GET
|
|
373
|
+
path: /admin/paths
|
|
374
|
+
dataSource: query
|
|
375
|
+
topic: rlb-gateway-admin
|
|
376
|
+
action: gw-path-list
|
|
377
|
+
mode: rpc
|
|
378
|
+
- name: gw-path-export
|
|
379
|
+
method: GET
|
|
380
|
+
path: /admin/paths/export
|
|
381
|
+
dataSource: query
|
|
382
|
+
topic: rlb-gateway-admin
|
|
383
|
+
action: gw-path-export
|
|
384
|
+
mode: rpc
|
|
385
|
+
- name: gw-path-update
|
|
386
|
+
method: PUT
|
|
387
|
+
path: /admin/paths
|
|
388
|
+
dataSource: body
|
|
389
|
+
topic: rlb-gateway-admin
|
|
390
|
+
action: gw-path-update
|
|
391
|
+
mode: rpc
|
|
392
|
+
- name: gw-path-get
|
|
393
|
+
method: GET
|
|
394
|
+
path: /admin/paths/get
|
|
395
|
+
dataSource: query
|
|
396
|
+
topic: rlb-gateway-admin
|
|
397
|
+
action: gw-path-get
|
|
398
|
+
mode: rpc
|
|
399
|
+
- name: gw-path-delete
|
|
400
|
+
method: DELETE
|
|
401
|
+
path: /admin/paths
|
|
402
|
+
dataSource: body
|
|
403
|
+
topic: rlb-gateway-admin
|
|
404
|
+
action: gw-path-delete
|
|
405
|
+
mode: rpc
|
|
406
|
+
- name: gw-auth-list
|
|
407
|
+
method: GET
|
|
408
|
+
path: /admin/auth
|
|
409
|
+
dataSource: query
|
|
410
|
+
topic: rlb-gateway-admin
|
|
411
|
+
action: gw-auth-list
|
|
412
|
+
mode: rpc
|
|
413
|
+
- name: gw-auth-upsert
|
|
414
|
+
method: PUT
|
|
415
|
+
path: /admin/auth
|
|
416
|
+
dataSource: body
|
|
417
|
+
topic: rlb-gateway-admin
|
|
418
|
+
action: gw-auth-update
|
|
419
|
+
mode: rpc
|
|
420
|
+
- name: gw-auth-get
|
|
421
|
+
method: GET
|
|
422
|
+
path: /admin/auth/get
|
|
423
|
+
dataSource: query
|
|
424
|
+
topic: rlb-gateway-admin
|
|
425
|
+
action: gw-auth-get
|
|
426
|
+
mode: rpc
|
|
427
|
+
- name: gw-auth-delete
|
|
428
|
+
method: DELETE
|
|
429
|
+
path: /admin/auth
|
|
430
|
+
dataSource: body
|
|
431
|
+
topic: rlb-gateway-admin
|
|
432
|
+
action: gw-auth-delete
|
|
433
|
+
mode: rpc
|
|
434
|
+
- name: gw-metrics-get
|
|
435
|
+
method: GET
|
|
436
|
+
path: /admin/metrics
|
|
437
|
+
dataSource: query
|
|
438
|
+
topic: rlb-gateway-admin
|
|
439
|
+
action: gw-metrics-get
|
|
440
|
+
mode: rpc
|
|
441
|
+
- name: gw-metrics-series
|
|
442
|
+
method: GET
|
|
443
|
+
path: /admin/metrics/series
|
|
444
|
+
dataSource: query
|
|
445
|
+
topic: rlb-gateway-admin
|
|
446
|
+
action: gw-metrics-series
|
|
447
|
+
mode: rpc
|
|
448
|
+
- name: gw-metrics-points
|
|
449
|
+
method: GET
|
|
450
|
+
path: /admin/metrics/points
|
|
451
|
+
dataSource: query
|
|
452
|
+
topic: rlb-gateway-admin
|
|
453
|
+
action: gw-metrics-points
|
|
454
|
+
mode: rpc
|
|
455
|
+
- name: gw-metrics-track
|
|
456
|
+
method: POST
|
|
457
|
+
path: /admin/metrics/track
|
|
458
|
+
dataSource: body
|
|
459
|
+
topic: rlb-gateway-admin
|
|
460
|
+
action: gw-metrics-track
|
|
100
461
|
mode: event
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
462
|
+
- name: gw-reload
|
|
463
|
+
method: POST
|
|
464
|
+
path: /admin/reload
|
|
465
|
+
dataSource: body
|
|
466
|
+
topic: ${controlTopic}
|
|
467
|
+
action: gw-reload
|
|
468
|
+
mode: event`;
|
|
469
|
+
}
|
|
470
|
+
function buildImportStatements(sel) {
|
|
471
|
+
const libSymbols = ['AppConfig', 'BrokerModule', 'BrokerTopic', 'RabbitMQConfig'];
|
|
472
|
+
if (sel.gatewayConfig)
|
|
473
|
+
libSymbols.push('GatewayConfig', 'HandlerAuthConfig', 'ProxyModule');
|
|
474
|
+
if (sel.acl)
|
|
475
|
+
libSymbols.push('AclActionRepository', 'AclGrantRepository', 'AclModule', 'AclRoleRepository', 'AclService', 'RLB_ACL_CACHE_STORE', 'RLB_GTW_ACL_ROLE_SERVICE');
|
|
476
|
+
if (sel.admin || sel.routeReception)
|
|
477
|
+
libSymbols.push('AuthProviderRepository', 'GatewayAdminModule', 'HttpMetricRepository', 'HttpPathRepository', 'RouteSyncLogRepository');
|
|
478
|
+
const lib = `import { ${[...new Set(libSymbols)].sort().join(', ')} } from '@open-rlb/nestjs-amqp';`;
|
|
479
|
+
const lines = [lib, `import { ConfigModule, ConfigService } from '@nestjs/config';`, `import yamlConfig from './config/config.loader';`];
|
|
480
|
+
if (sel.gatewayConfig)
|
|
481
|
+
lines.push(`import { HttpModule } from '@nestjs/axios';`);
|
|
482
|
+
if (sel.acl) {
|
|
483
|
+
lines.push(`import { InMemoryAclActionRepository, InMemoryAclGrantRepository, InMemoryAclRoleRepository } from './modules/database/repository/acl.repository';`);
|
|
484
|
+
lines.push(`import { InMemoryAclStore } from './cache/in-memory-acl-store';`);
|
|
485
|
+
}
|
|
486
|
+
if (sel.admin || sel.routeReception) {
|
|
487
|
+
lines.push(`import { InMemoryAuthProviderRepository, InMemoryHttpMetricRepository, InMemoryHttpPathRepository } from './modules/database/repository/gateway.repository';`);
|
|
488
|
+
lines.push(`import { InMemoryRouteSyncLogRepository } from './modules/database/repository/route-sync.repository';`);
|
|
489
|
+
}
|
|
490
|
+
return lines.join('\n');
|
|
491
|
+
}
|
|
492
|
+
function brokerForRootAsync() {
|
|
493
|
+
return `BrokerModule.forRootAsync({
|
|
494
|
+
imports: [ConfigModule],
|
|
495
|
+
inject: [ConfigService],
|
|
496
|
+
useFactory: async (configService: ConfigService) => ({
|
|
497
|
+
options: configService.get<RabbitMQConfig>('broker')!,
|
|
498
|
+
topics: configService.get<BrokerTopic[]>('topics')!,
|
|
499
|
+
appOptions: configService.get<AppConfig>('app'),
|
|
500
|
+
})
|
|
501
|
+
})`;
|
|
502
|
+
}
|
|
503
|
+
function proxyForRootAsync(sel) {
|
|
504
|
+
const providers = sel.acl
|
|
505
|
+
? `[
|
|
506
|
+
// Role-gated paths resolve the caller's roles via AclService (in-process, no broker hop).
|
|
507
|
+
{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService },
|
|
508
|
+
]`
|
|
509
|
+
: `[]`;
|
|
510
|
+
return `ProxyModule.forRootAsync({
|
|
511
|
+
imports: [ConfigModule],
|
|
512
|
+
inject: [ConfigService],
|
|
513
|
+
useFactory: (configService: ConfigService) => ({
|
|
514
|
+
authOptions: configService.get<HandlerAuthConfig[]>('auth-providers'),
|
|
515
|
+
gatewayOptions: configService.get<GatewayConfig>('gateway'),
|
|
516
|
+
}),
|
|
517
|
+
providers: ${providers},
|
|
518
|
+
})`;
|
|
519
|
+
}
|
|
520
|
+
function aclForRoot() {
|
|
521
|
+
return `AclModule.forRoot(
|
|
522
|
+
[
|
|
523
|
+
InMemoryAclActionRepository,
|
|
524
|
+
{ provide: AclActionRepository, useExisting: InMemoryAclActionRepository },
|
|
525
|
+
InMemoryAclRoleRepository,
|
|
526
|
+
{ provide: AclRoleRepository, useExisting: InMemoryAclRoleRepository },
|
|
527
|
+
InMemoryAclGrantRepository,
|
|
528
|
+
{ provide: AclGrantRepository, useExisting: InMemoryAclGrantRepository },
|
|
529
|
+
InMemoryAclStore,
|
|
530
|
+
{ provide: RLB_ACL_CACHE_STORE, useExisting: InMemoryAclStore },
|
|
531
|
+
],
|
|
532
|
+
{ cache: { ramTtlMs: 30000, l2TtlSec: 600 } },
|
|
533
|
+
)`;
|
|
534
|
+
}
|
|
535
|
+
function gatewayAdminForRoot(sel) {
|
|
536
|
+
const options = sel.routeReception
|
|
537
|
+
? `,
|
|
538
|
+
{
|
|
539
|
+
// Consumer-side route-discovery — names MUST match the publishers' broker.routeDiscovery.
|
|
540
|
+
routeDiscovery: { exchange: '${sel.names.routeExchange}', queue: '${sel.names.routeQueue}' },
|
|
541
|
+
}`
|
|
542
|
+
: '';
|
|
543
|
+
return `GatewayAdminModule.forRoot(
|
|
544
|
+
[
|
|
545
|
+
InMemoryHttpPathRepository,
|
|
546
|
+
{ provide: HttpPathRepository, useExisting: InMemoryHttpPathRepository },
|
|
547
|
+
InMemoryAuthProviderRepository,
|
|
548
|
+
{ provide: AuthProviderRepository, useExisting: InMemoryAuthProviderRepository },
|
|
549
|
+
InMemoryHttpMetricRepository,
|
|
550
|
+
{ provide: HttpMetricRepository, useExisting: InMemoryHttpMetricRepository },
|
|
551
|
+
InMemoryRouteSyncLogRepository,
|
|
552
|
+
{ provide: RouteSyncLogRepository, useExisting: InMemoryRouteSyncLogRepository },
|
|
553
|
+
]${options},
|
|
554
|
+
)`;
|
|
555
|
+
}
|
|
556
|
+
function buildModuleEntries(sel) {
|
|
557
|
+
const entries = [];
|
|
558
|
+
entries.push(`ConfigModule.forRoot({ isGlobal: true, load: [yamlConfig] })`);
|
|
559
|
+
entries.push(brokerForRootAsync());
|
|
560
|
+
if (sel.gatewayConfig) {
|
|
561
|
+
entries.push('HttpModule');
|
|
562
|
+
entries.push(proxyForRootAsync(sel));
|
|
563
|
+
}
|
|
564
|
+
if (sel.acl)
|
|
565
|
+
entries.push(aclForRoot());
|
|
566
|
+
if (sel.admin || sel.routeReception)
|
|
567
|
+
entries.push(gatewayAdminForRoot(sel));
|
|
568
|
+
return entries;
|
|
104
569
|
}
|
|
105
570
|
function main(options) {
|
|
571
|
+
const project = (options.project || 'my-service').toString();
|
|
106
572
|
options = transform(options);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
573
|
+
return async (tree, context) => {
|
|
574
|
+
const sel = await resolveSelections({ ...options, project }, context);
|
|
575
|
+
const anyRepo = sel.acl || sel.admin || sel.routeReception;
|
|
110
576
|
return (0, schematics_1.branchAndMerge)((0, schematics_1.chain)([
|
|
111
577
|
(0, source_root_helpers_1.mergeSourceRoot)(options),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
578
|
+
addModulesToAppModule(sel),
|
|
579
|
+
createConfigLoader(),
|
|
580
|
+
updateConfigYaml(sel),
|
|
581
|
+
sel.gatewayConfig ? configureMainForGateway() : (0, schematics_1.noop)(),
|
|
582
|
+
anyRepo ? copyAsset('db-core') : (0, schematics_1.noop)(),
|
|
583
|
+
sel.acl ? copyAsset('acl') : (0, schematics_1.noop)(),
|
|
584
|
+
(sel.admin || sel.routeReception) ? copyAsset('gateway-admin') : (0, schematics_1.noop)(),
|
|
585
|
+
sel.skills ? copySkills() : (0, schematics_1.noop)(),
|
|
586
|
+
updatePackageJson(sel),
|
|
587
|
+
]));
|
|
118
588
|
};
|
|
119
589
|
}
|
|
120
590
|
function transform(source) {
|
|
@@ -128,26 +598,45 @@ function transform(source) {
|
|
|
128
598
|
target.specFileSuffix = (0, formatting_1.normalizeToKebabOrSnakeCase)(source.specFileSuffix || 'spec');
|
|
129
599
|
return target;
|
|
130
600
|
}
|
|
601
|
+
function copyAsset(name) {
|
|
602
|
+
return (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)(`./files/${name}`), [(0, schematics_1.move)((0, path_1.normalize)('.'))]), schematics_1.MergeStrategy.Overwrite);
|
|
603
|
+
}
|
|
131
604
|
function copySkills() {
|
|
132
605
|
return (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)('./files/skills'), [(0, schematics_1.move)((0, path_1.normalize)('.claude/skills'))]), schematics_1.MergeStrategy.Overwrite);
|
|
133
606
|
}
|
|
134
|
-
function
|
|
607
|
+
function createConfigLoader() {
|
|
608
|
+
return (tree) => {
|
|
609
|
+
const path = 'src/config/config.loader.ts';
|
|
610
|
+
if (tree.exists(path))
|
|
611
|
+
return tree;
|
|
612
|
+
tree.create(path, `import { readFileSync } from 'fs';
|
|
613
|
+
import * as yaml from 'js-yaml';
|
|
614
|
+
import { join } from 'path';
|
|
615
|
+
|
|
616
|
+
const YAML_CONFIG_FILENAME = 'config/config.yaml';
|
|
617
|
+
|
|
618
|
+
export default () =>
|
|
619
|
+
yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
|
|
620
|
+
`);
|
|
621
|
+
return tree;
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function updateConfigYaml(sel) {
|
|
135
625
|
return (tree) => {
|
|
136
626
|
const CONFIG_PATH = 'config/config.yaml';
|
|
137
|
-
const block =
|
|
627
|
+
const block = buildConfigYaml(sel);
|
|
138
628
|
if (!tree.exists(CONFIG_PATH)) {
|
|
139
|
-
tree.create(CONFIG_PATH, block
|
|
629
|
+
tree.create(CONFIG_PATH, block);
|
|
140
630
|
return tree;
|
|
141
631
|
}
|
|
142
632
|
const existing = tree.read(CONFIG_PATH).toString('utf-8');
|
|
143
|
-
const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(
|
|
633
|
+
const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(sel.gatewayConfig ? ['gateway:'] : [])];
|
|
144
634
|
let toAppend = '';
|
|
145
635
|
for (const key of SECTION_KEYS) {
|
|
146
636
|
if (!existing.includes(key)) {
|
|
147
637
|
const section = extractYamlSection(block, key);
|
|
148
|
-
if (section)
|
|
149
|
-
toAppend += '\n' + section;
|
|
150
|
-
}
|
|
638
|
+
if (section)
|
|
639
|
+
toAppend += '\n' + section + '\n';
|
|
151
640
|
}
|
|
152
641
|
}
|
|
153
642
|
if (toAppend.length > 0) {
|
|
@@ -158,47 +647,34 @@ function updateConfigYaml(gateway) {
|
|
|
158
647
|
}
|
|
159
648
|
function extractYamlSection(yaml, sectionKey) {
|
|
160
649
|
const lines = yaml.split('\n');
|
|
161
|
-
const startIdx = lines.findIndex(l => l.startsWith(sectionKey));
|
|
650
|
+
const startIdx = lines.findIndex((l) => l.startsWith(sectionKey));
|
|
162
651
|
if (startIdx === -1)
|
|
163
652
|
return '';
|
|
164
653
|
const endIdx = lines.findIndex((l, i) => i > startIdx && l.length > 0 && !l.startsWith(' ') && !l.startsWith('#'));
|
|
165
654
|
const sectionLines = endIdx === -1 ? lines.slice(startIdx) : lines.slice(startIdx, endIdx);
|
|
166
655
|
return sectionLines.join('\n').trimEnd();
|
|
167
656
|
}
|
|
168
|
-
function
|
|
657
|
+
function addModulesToAppModule(sel) {
|
|
169
658
|
return (tree) => {
|
|
170
|
-
const candidatePaths = [
|
|
171
|
-
|
|
172
|
-
'/app/app.module.ts',
|
|
173
|
-
'src/app.module.ts',
|
|
174
|
-
'app/app.module.ts',
|
|
175
|
-
];
|
|
176
|
-
let modulePath = candidatePaths.find(p => tree.exists(p));
|
|
177
|
-
if (!modulePath) {
|
|
178
|
-
modulePath = findFileInTree(tree, 'app.module.ts');
|
|
179
|
-
}
|
|
659
|
+
const candidatePaths = ['/src/app.module.ts', '/app/app.module.ts', 'src/app.module.ts', 'app/app.module.ts'];
|
|
660
|
+
let modulePath = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'app.module.ts');
|
|
180
661
|
if (!modulePath) {
|
|
181
|
-
console.warn('[nest-add] app.module.ts
|
|
662
|
+
console.warn('[nest-add] app.module.ts not found: AppModule wiring skipped.');
|
|
182
663
|
return tree;
|
|
183
664
|
}
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
665
|
+
const raw = tree.read(modulePath);
|
|
666
|
+
if (!raw)
|
|
667
|
+
return tree;
|
|
668
|
+
let content = raw.toString('utf-8');
|
|
669
|
+
if (content.includes('BrokerModule.forRootAsync'))
|
|
186
670
|
return tree;
|
|
187
|
-
}
|
|
188
|
-
let content = rawContent.toString('utf-8');
|
|
189
671
|
if (!content.includes('@open-rlb/nestjs-amqp')) {
|
|
190
|
-
const
|
|
191
|
-
content =
|
|
192
|
-
content.slice(0, importInsertPos) +
|
|
193
|
-
'\n' + brokerImportLine(gateway) +
|
|
194
|
-
content.slice(importInsertPos);
|
|
672
|
+
const pos = findLastImportEndIndex(content);
|
|
673
|
+
content = content.slice(0, pos) + '\n' + buildImportStatements(sel) + content.slice(pos);
|
|
195
674
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
content = insertIntoImportsArray(content, 'HttpModule');
|
|
200
|
-
}
|
|
201
|
-
content = insertIntoImportsArray(content, brokerForRootAsync());
|
|
675
|
+
const entries = buildModuleEntries(sel);
|
|
676
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
677
|
+
content = insertIntoImportsArray(content, entries[i]);
|
|
202
678
|
}
|
|
203
679
|
tree.overwrite(modulePath, content);
|
|
204
680
|
return tree;
|
|
@@ -207,37 +683,34 @@ function addBrokerModuleToAppModule(gateway) {
|
|
|
207
683
|
function configureMainForGateway() {
|
|
208
684
|
return (tree) => {
|
|
209
685
|
const candidatePaths = ['/src/main.ts', '/app/main.ts', 'src/main.ts', 'app/main.ts'];
|
|
210
|
-
const mainPath = candidatePaths.find(p => tree.exists(p)) || findFileInTree(tree, 'main.ts');
|
|
686
|
+
const mainPath = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'main.ts');
|
|
211
687
|
if (!mainPath) {
|
|
212
|
-
console.warn('[nest-add] main.ts
|
|
688
|
+
console.warn('[nest-add] main.ts not found: enable rawBody + WsAdapter manually.');
|
|
213
689
|
return tree;
|
|
214
690
|
}
|
|
215
691
|
let content = tree.read(mainPath).toString('utf-8');
|
|
216
|
-
if (content.includes('WsAdapter'))
|
|
692
|
+
if (content.includes('WsAdapter'))
|
|
217
693
|
return tree;
|
|
218
|
-
}
|
|
219
694
|
const pos = findLastImportEndIndex(content);
|
|
220
695
|
content = content.slice(0, pos) + "\nimport { WsAdapter } from '@nestjs/platform-ws';" + content.slice(pos);
|
|
221
696
|
const m = content.match(/const\s+(\w+)\s*=\s*await\s+NestFactory\.create\(\s*([A-Za-z0-9_]+)\s*(,\s*\{[^}]*\})?\s*\)\s*;?/);
|
|
222
697
|
if (m) {
|
|
223
698
|
const appVar = m[1];
|
|
224
699
|
const moduleArg = m[2];
|
|
225
|
-
const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));`;
|
|
700
|
+
const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));\n ${appVar}.enableShutdownHooks();`;
|
|
226
701
|
content = content.replace(m[0], replacement);
|
|
227
702
|
}
|
|
228
703
|
else {
|
|
229
|
-
console.warn('[nest-add] NestFactory.create()
|
|
704
|
+
console.warn('[nest-add] NestFactory.create() not found in main.ts: add { rawBody: true } + useWebSocketAdapter manually.');
|
|
230
705
|
}
|
|
231
706
|
tree.overwrite(mainPath, content);
|
|
232
707
|
return tree;
|
|
233
708
|
};
|
|
234
709
|
}
|
|
235
710
|
function insertIntoImportsArray(source, moduleEntry) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (!match) {
|
|
711
|
+
const match = /imports\s*:\s*\[/.exec(source);
|
|
712
|
+
if (!match)
|
|
239
713
|
return source;
|
|
240
|
-
}
|
|
241
714
|
const openBracketPos = source.indexOf('[', match.index);
|
|
242
715
|
let depth = 0;
|
|
243
716
|
let closeBracketPos = -1;
|
|
@@ -252,20 +725,13 @@ function insertIntoImportsArray(source, moduleEntry) {
|
|
|
252
725
|
}
|
|
253
726
|
}
|
|
254
727
|
}
|
|
255
|
-
if (closeBracketPos === -1)
|
|
728
|
+
if (closeBracketPos === -1)
|
|
256
729
|
return source;
|
|
257
|
-
}
|
|
258
730
|
const arrayContent = source.slice(openBracketPos + 1, closeBracketPos).trim();
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
else {
|
|
264
|
-
newArrayContent = `\n ${moduleEntry},\n ${arrayContent}\n `;
|
|
265
|
-
}
|
|
266
|
-
return (source.slice(0, openBracketPos + 1) +
|
|
267
|
-
newArrayContent +
|
|
268
|
-
source.slice(closeBracketPos));
|
|
731
|
+
const newArrayContent = arrayContent.length === 0
|
|
732
|
+
? `\n ${moduleEntry},\n `
|
|
733
|
+
: `\n ${moduleEntry},\n ${arrayContent}\n `;
|
|
734
|
+
return source.slice(0, openBracketPos + 1) + newArrayContent + source.slice(closeBracketPos);
|
|
269
735
|
}
|
|
270
736
|
function findLastImportEndIndex(source) {
|
|
271
737
|
const importRegex = /^import\s+.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
@@ -278,36 +744,40 @@ function findLastImportEndIndex(source) {
|
|
|
278
744
|
}
|
|
279
745
|
function findFileInTree(tree, fileName) {
|
|
280
746
|
let found;
|
|
281
|
-
tree.visit(path => {
|
|
282
|
-
if (!found && path.endsWith(`/${fileName}`))
|
|
747
|
+
tree.visit((path) => {
|
|
748
|
+
if (!found && path.endsWith(`/${fileName}`))
|
|
283
749
|
found = path;
|
|
284
|
-
}
|
|
285
750
|
});
|
|
286
751
|
return found;
|
|
287
752
|
}
|
|
288
|
-
function updatePackageJson(
|
|
753
|
+
function updatePackageJson(sel) {
|
|
289
754
|
return (host) => {
|
|
290
|
-
if (!host.exists('package.json'))
|
|
755
|
+
if (!host.exists('package.json'))
|
|
291
756
|
return host;
|
|
292
|
-
}
|
|
293
757
|
return updateJsonFile(host, 'package.json', (packageJson) => {
|
|
294
|
-
|
|
758
|
+
packageJson.dependencies = packageJson.dependencies || {};
|
|
759
|
+
const add = (name, version) => {
|
|
760
|
+
if (!packageJson.dependencies[name])
|
|
761
|
+
packageJson.dependencies[name] = version;
|
|
762
|
+
};
|
|
763
|
+
add('@nestjs/config', '^4.0.4');
|
|
764
|
+
add('js-yaml', '^4.1.0');
|
|
765
|
+
if (sel.gatewayConfig) {
|
|
766
|
+
add('@nestjs/axios', '^4.0.1');
|
|
767
|
+
add('@nestjs/platform-ws', '^11.0.1');
|
|
768
|
+
add('@nestjs/websockets', '^11.0.1');
|
|
769
|
+
add('ws', '^8.21.0');
|
|
770
|
+
}
|
|
295
771
|
});
|
|
296
772
|
};
|
|
297
773
|
}
|
|
298
774
|
function updateJsonFile(host, path, callback) {
|
|
299
775
|
const source = host.read(path);
|
|
300
776
|
if (source) {
|
|
301
|
-
const
|
|
302
|
-
const json = (0, jsonc_parser_1.parse)(sourceText);
|
|
777
|
+
const json = (0, jsonc_parser_1.parse)(source.toString('utf-8'));
|
|
303
778
|
callback(json);
|
|
304
779
|
host.overwrite(path, JSON.stringify(json, null, 2));
|
|
305
780
|
}
|
|
306
781
|
return host;
|
|
307
782
|
}
|
|
308
|
-
function updateNpmScripts(scripts, _options) {
|
|
309
|
-
if (!scripts) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
783
|
//# sourceMappingURL=index.js.map
|