@open-rlb/nestjs-amqp 2.1.1 → 2.2.0
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/package.json +6 -5
- package/schematics/add-auth-provider/index.d.ts +3 -0
- package/schematics/add-auth-provider/index.js +60 -0
- package/schematics/add-auth-provider/index.js.map +1 -0
- package/schematics/add-auth-provider/index.spec.ts +83 -0
- package/schematics/add-auth-provider/index.ts +83 -0
- package/schematics/add-auth-provider/schema.d.ts +18 -0
- package/schematics/add-auth-provider/schema.js +3 -0
- package/schematics/add-auth-provider/schema.js.map +1 -0
- package/schematics/add-auth-provider/schema.json +59 -0
- package/schematics/add-auth-provider/schema.ts +35 -0
- package/schematics/add-exchange/index.d.ts +3 -0
- package/schematics/add-exchange/index.js +49 -0
- package/schematics/add-exchange/index.js.map +1 -0
- package/schematics/add-exchange/index.spec.ts +65 -0
- package/schematics/add-exchange/index.ts +65 -0
- package/schematics/add-exchange/schema.d.ts +14 -0
- package/schematics/add-exchange/schema.js +3 -0
- package/schematics/add-exchange/schema.js.map +1 -0
- package/schematics/add-exchange/schema.json +50 -0
- package/schematics/add-exchange/schema.ts +26 -0
- package/schematics/add-exchange-binding/index.d.ts +3 -0
- package/schematics/add-exchange-binding/index.js +53 -0
- package/schematics/add-exchange-binding/index.js.map +1 -0
- package/schematics/add-exchange-binding/index.spec.ts +73 -0
- package/schematics/add-exchange-binding/index.ts +64 -0
- package/schematics/add-exchange-binding/schema.d.ts +8 -0
- package/schematics/add-exchange-binding/schema.js +3 -0
- package/schematics/add-exchange-binding/schema.js.map +1 -0
- package/schematics/add-exchange-binding/schema.json +36 -0
- package/schematics/add-exchange-binding/schema.ts +14 -0
- package/schematics/add-gateway/index.d.ts +4 -0
- package/schematics/add-gateway/index.js +105 -0
- package/schematics/add-gateway/index.js.map +1 -0
- package/schematics/add-gateway/index.spec.ts +163 -0
- package/schematics/add-gateway/index.ts +141 -0
- package/schematics/add-gateway/paths.d.ts +12 -0
- package/schematics/add-gateway/paths.js +87 -0
- package/schematics/add-gateway/paths.js.map +1 -0
- package/schematics/add-gateway/paths.ts +103 -0
- package/schematics/add-gateway/schema.d.ts +13 -0
- package/schematics/add-gateway/schema.js +3 -0
- package/schematics/add-gateway/schema.js.map +1 -0
- package/schematics/add-gateway/schema.json +63 -0
- package/schematics/add-gateway/schema.ts +24 -0
- package/schematics/add-gateway/wiring.d.ts +14 -0
- package/schematics/add-gateway/wiring.js +102 -0
- package/schematics/add-gateway/wiring.js.map +1 -0
- package/schematics/add-gateway/wiring.ts +115 -0
- package/schematics/add-metrics-topic/index.d.ts +3 -0
- package/schematics/add-metrics-topic/index.js +31 -0
- package/schematics/add-metrics-topic/index.js.map +1 -0
- package/schematics/add-metrics-topic/index.spec.ts +79 -0
- package/schematics/add-metrics-topic/index.ts +49 -0
- package/schematics/add-metrics-topic/schema.d.ts +10 -0
- package/schematics/add-metrics-topic/schema.js +3 -0
- package/schematics/add-metrics-topic/schema.js.map +1 -0
- package/schematics/add-metrics-topic/schema.json +48 -0
- package/schematics/add-metrics-topic/schema.ts +18 -0
- package/schematics/add-queue/index.d.ts +3 -0
- package/schematics/add-queue/index.js +48 -0
- package/schematics/add-queue/index.js.map +1 -0
- package/schematics/add-queue/index.spec.ts +75 -0
- package/schematics/add-queue/index.ts +74 -0
- package/schematics/add-queue/schema.d.ts +17 -0
- package/schematics/add-queue/schema.js +3 -0
- package/schematics/add-queue/schema.js.map +1 -0
- package/schematics/add-queue/schema.json +54 -0
- package/schematics/add-queue/schema.ts +32 -0
- package/schematics/add-reply-queue/index.d.ts +3 -0
- package/schematics/add-reply-queue/index.js +26 -0
- package/schematics/add-reply-queue/index.js.map +1 -0
- package/schematics/add-reply-queue/index.spec.ts +69 -0
- package/schematics/add-reply-queue/index.ts +33 -0
- package/schematics/add-reply-queue/schema.d.ts +6 -0
- package/schematics/add-reply-queue/schema.js +3 -0
- package/schematics/add-reply-queue/schema.js.map +1 -0
- package/schematics/add-reply-queue/schema.json +28 -0
- package/schematics/add-reply-queue/schema.ts +10 -0
- package/schematics/add-route/index.d.ts +3 -0
- package/schematics/add-route/index.js +57 -0
- package/schematics/add-route/index.js.map +1 -0
- package/schematics/add-route/index.spec.ts +75 -0
- package/schematics/add-route/index.ts +74 -0
- package/schematics/add-route/schema.d.ts +22 -0
- package/schematics/add-route/schema.js +3 -0
- package/schematics/add-route/schema.js.map +1 -0
- package/schematics/add-route/schema.json +74 -0
- package/schematics/add-route/schema.ts +43 -0
- package/schematics/add-topic/index.d.ts +3 -0
- package/schematics/add-topic/index.js +69 -0
- package/schematics/add-topic/index.js.map +1 -0
- package/schematics/add-topic/index.spec.ts +100 -0
- package/schematics/add-topic/index.ts +104 -0
- package/schematics/add-topic/schema.d.ts +18 -0
- package/schematics/add-topic/schema.js +3 -0
- package/schematics/add-topic/schema.js.map +1 -0
- package/schematics/add-topic/schema.json +63 -0
- package/schematics/add-topic/schema.ts +34 -0
- package/schematics/add-ws-event/index.d.ts +3 -0
- package/schematics/add-ws-event/index.js +53 -0
- package/schematics/add-ws-event/index.js.map +1 -0
- package/schematics/add-ws-event/index.spec.ts +85 -0
- package/schematics/add-ws-event/index.ts +72 -0
- package/schematics/add-ws-event/schema.d.ts +17 -0
- package/schematics/add-ws-event/schema.js +3 -0
- package/schematics/add-ws-event/schema.js.map +1 -0
- package/schematics/add-ws-event/schema.json +53 -0
- package/schematics/add-ws-event/schema.ts +33 -0
- package/schematics/collection.json +81 -1
- package/schematics/configure-broker/index.d.ts +3 -0
- package/schematics/configure-broker/index.js +41 -0
- package/schematics/configure-broker/index.js.map +1 -0
- package/schematics/configure-broker/index.spec.ts +62 -0
- package/schematics/configure-broker/index.ts +51 -0
- package/schematics/configure-broker/schema.d.ts +11 -0
- package/schematics/configure-broker/schema.js +3 -0
- package/schematics/configure-broker/schema.js.map +1 -0
- package/schematics/configure-broker/schema.json +47 -0
- package/schematics/configure-broker/schema.ts +20 -0
- package/schematics/enable-load-config/index.d.ts +3 -0
- package/schematics/enable-load-config/index.js +32 -0
- package/schematics/enable-load-config/index.js.map +1 -0
- package/schematics/enable-load-config/index.spec.ts +61 -0
- package/schematics/enable-load-config/index.ts +39 -0
- package/schematics/enable-load-config/schema.d.ts +8 -0
- package/schematics/enable-load-config/schema.js +3 -0
- package/schematics/enable-load-config/schema.js.map +1 -0
- package/schematics/enable-load-config/schema.json +36 -0
- package/schematics/enable-load-config/schema.ts +14 -0
- package/schematics/enable-retry/index.d.ts +3 -0
- package/schematics/enable-retry/index.js +51 -0
- package/schematics/enable-retry/index.js.map +1 -0
- package/schematics/enable-retry/index.spec.ts +73 -0
- package/schematics/enable-retry/index.ts +62 -0
- package/schematics/enable-retry/schema.d.ts +13 -0
- package/schematics/enable-retry/schema.js +3 -0
- package/schematics/enable-retry/schema.js.map +1 -0
- package/schematics/enable-retry/schema.json +64 -0
- package/schematics/enable-retry/schema.ts +24 -0
- package/schematics/enable-route-discovery/index.d.ts +3 -0
- package/schematics/enable-route-discovery/index.js +36 -0
- package/schematics/enable-route-discovery/index.js.map +1 -0
- package/schematics/enable-route-discovery/index.spec.ts +55 -0
- package/schematics/enable-route-discovery/index.ts +48 -0
- package/schematics/enable-route-discovery/schema.d.ts +9 -0
- package/schematics/enable-route-discovery/schema.js +3 -0
- package/schematics/enable-route-discovery/schema.js.map +1 -0
- package/schematics/enable-route-discovery/schema.json +44 -0
- package/schematics/enable-route-discovery/schema.ts +16 -0
- package/schematics/harden-gateway/index.d.ts +3 -0
- package/schematics/harden-gateway/index.js +93 -0
- package/schematics/harden-gateway/index.js.map +1 -0
- package/schematics/harden-gateway/index.spec.ts +105 -0
- package/schematics/harden-gateway/index.ts +108 -0
- package/schematics/harden-gateway/schema.d.ts +11 -0
- package/schematics/harden-gateway/schema.js +3 -0
- package/schematics/harden-gateway/schema.js.map +1 -0
- package/schematics/harden-gateway/schema.json +47 -0
- package/schematics/harden-gateway/schema.ts +20 -0
- package/schematics/nest-add/index.js +55 -832
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.spec.ts +73 -0
- package/schematics/nest-add/index.ts +76 -917
- package/schematics/nest-add/init.schema.d.ts +1 -0
- package/schematics/nest-add/init.schema.ts +3 -0
- package/schematics/nest-add/schema.json +4 -43
- package/schematics/set-connection-name/index.d.ts +3 -0
- package/schematics/set-connection-name/index.js +34 -0
- package/schematics/set-connection-name/index.js.map +1 -0
- package/schematics/set-connection-name/index.spec.ts +48 -0
- package/schematics/set-connection-name/index.ts +42 -0
- package/schematics/set-connection-name/schema.d.ts +5 -0
- package/schematics/set-connection-name/schema.js +3 -0
- package/schematics/set-connection-name/schema.js.map +1 -0
- package/schematics/set-connection-name/schema.json +24 -0
- package/schematics/set-connection-name/schema.ts +8 -0
- package/schematics/utils/broker-yaml.util.d.ts +53 -0
- package/schematics/utils/broker-yaml.util.js +91 -0
- package/schematics/utils/broker-yaml.util.js.map +1 -0
- package/schematics/utils/broker-yaml.util.ts +172 -0
- package/schematics/utils/nest-wiring.util.d.ts +30 -0
- package/schematics/utils/nest-wiring.util.js +170 -0
- package/schematics/utils/nest-wiring.util.js.map +1 -0
- package/schematics/utils/nest-wiring.util.ts +211 -0
- package/schematics/utils/schematic-prompt.util.d.ts +7 -0
- package/schematics/utils/schematic-prompt.util.js +57 -0
- package/schematics/utils/schematic-prompt.util.js.map +1 -0
- package/schematics/utils/schematic-prompt.util.ts +82 -0
- package/schematics/utils/yaml-config.util.d.ts +22 -0
- package/schematics/utils/yaml-config.util.js +119 -0
- package/schematics/utils/yaml-config.util.js.map +1 -0
- package/schematics/utils/yaml-config.util.ts +167 -0
- /package/schematics/{nest-add → add-gateway}/files/acl/src/cache/in-memory-acl-store.ts +0 -0
- /package/schematics/{nest-add → add-gateway}/files/acl/src/modules/database/repository/acl.repository.ts +0 -0
- /package/schematics/{nest-add → add-gateway}/files/db-core/src/modules/database/repository/in-memory-collection.ts +0 -0
- /package/schematics/{nest-add → add-gateway}/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +0 -0
- /package/schematics/{nest-add → add-gateway}/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +0 -0
|
@@ -1,953 +1,112 @@
|
|
|
1
|
-
import { apply,
|
|
2
|
-
import { parse } from 'jsonc-parser';
|
|
1
|
+
import { apply, MergeStrategy, mergeWith, move, noop, Rule, SchematicContext, Tree, url } from '@angular-devkit/schematics';
|
|
3
2
|
import { normalize } from 'path';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
addDeps,
|
|
5
|
+
brokerModuleEntry,
|
|
6
|
+
configModuleEntry,
|
|
7
|
+
coreImportLines,
|
|
8
|
+
coreLibSymbols,
|
|
9
|
+
createConfigLoader,
|
|
10
|
+
wireAppModule,
|
|
11
|
+
} from '../utils/nest-wiring.util';
|
|
12
|
+
import { askConfirm, loadPrompts } from '../utils/schematic-prompt.util';
|
|
13
|
+
import { findConfigYaml, readConfigDoc, setIn, writeConfigDoc } from '../utils/yaml-config.util';
|
|
7
14
|
import { InitOptions } from './init.schema';
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `nest add @open-rlb/nestjs-amqp` — the CORE bootstrap. The `nest add` runner installs the package;
|
|
18
|
+
* this schematic then lays down the minimum a plain AMQP microservice needs: a base `config.yaml`,
|
|
19
|
+
* the config loader, and the ConfigModule + BrokerModule wiring in AppModule. It stops there — the
|
|
20
|
+
* gateway, ACL, topics, routes, retry, … are added on demand by the focused schematics (`add-gateway`,
|
|
21
|
+
* `add-topic`, `add-route`, `enable-retry`, …). Finally it offers to drop the Claude skills / project
|
|
22
|
+
* docs into `.claude/skills`. Every step is idempotent.
|
|
23
|
+
*/
|
|
24
|
+
export function main(options: InitOptions): Rule {
|
|
25
|
+
return async (tree: Tree, context: SchematicContext) => {
|
|
26
|
+
const prompts = loadPrompts(context, options.skills !== undefined);
|
|
27
|
+
const skills = options.skills ?? (await askConfirm(prompts, 'Add the Claude skills / project docs into .claude/skills?', true));
|
|
28
|
+
|
|
29
|
+
// 1. Base config.yaml (create when missing; only backfill missing top-level sections otherwise).
|
|
30
|
+
ensureBaseConfig(tree, options.config, context);
|
|
31
|
+
|
|
32
|
+
// 2. Config loader + core module wiring + runtime deps.
|
|
33
|
+
createConfigLoader(tree);
|
|
34
|
+
const modulePath = wireAppModule(tree, {
|
|
35
|
+
libSymbols: coreLibSymbols(),
|
|
36
|
+
importLines: coreImportLines(),
|
|
37
|
+
entries: [configModuleEntry(), brokerModuleEntry()],
|
|
38
|
+
});
|
|
39
|
+
if (!modulePath) {
|
|
40
|
+
context.logger.warn('[nest-add] app.module.ts not found — wire ConfigModule + BrokerModule.forRootAsync manually.');
|
|
41
|
+
}
|
|
42
|
+
addDeps(tree, { '@nestjs/config': '^4.0.4', 'js-yaml': '^4.1.0' });
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
gatewayConfig: boolean; // create the HTTP/WebSocket gateway config
|
|
27
|
-
acl: boolean; // AclModule + ACL paths
|
|
28
|
-
admin: boolean; // GatewayAdminModule + DB routes/auth/metrics paths
|
|
29
|
-
routeReception: boolean;// gateway consumes routes auto-published by microservices
|
|
30
|
-
autoPublish: boolean; // microservice publishes its @BrokerHTTP routes on boot
|
|
31
|
-
skills: boolean; // copy the Claude skills
|
|
32
|
-
names: Names;
|
|
33
|
-
}
|
|
44
|
+
context.logger.info('[nest-add] core ready. Use `add-gateway`, `add-topic`, `add-route`, `enable-retry`, … to add features.');
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
exchange: 'rlb',
|
|
38
|
-
aclQueue: 'rlb-acl',
|
|
39
|
-
adminQueue: 'rlb-gateway-admin',
|
|
40
|
-
controlTopic: 'rlb-gateway-control',
|
|
41
|
-
routeExchange: 'rlb-route-discovery',
|
|
42
|
-
routeQueue: 'rlb-route-sync',
|
|
43
|
-
serviceName: project || 'my-service',
|
|
46
|
+
// 3. Skills / project docs (applied by the engine as a returned Rule).
|
|
47
|
+
return skills ? copySkills() : noop();
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* - "Create a gateway configuration?" → yes: pick {acl, gateway-admin, route-reception} and the
|
|
51
|
-
* topic/queue/exchange names (rlb-* defaults); no: pick {auto-config-publish} (+ names).
|
|
52
|
-
* Otherwise it falls back to the provided flags + rlb-* defaults (non-interactive / CI).
|
|
52
|
+
* Ensure a base config.yaml exists. A missing file gets the full commented template; an existing one
|
|
53
|
+
* is only backfilled with any missing top-level section (app/broker/topics) so user edits survive.
|
|
53
54
|
*/
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let prompts: any;
|
|
61
|
-
if (canPrompt) {
|
|
62
|
-
try {
|
|
63
|
-
prompts = require('@inquirer/prompts');
|
|
64
|
-
} catch {
|
|
65
|
-
context.logger.warn('[nest-add] @inquirer/prompts not found; falling back to flags/defaults (non-interactive).');
|
|
66
|
-
prompts = undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ---- Non-interactive: derive everything from flags + defaults --------------
|
|
71
|
-
if (!prompts) {
|
|
72
|
-
const features = new Set((o.features || []).map((f) => String(f).trim()));
|
|
73
|
-
const gatewayConfig = o.gatewayConfig === true;
|
|
74
|
-
return {
|
|
75
|
-
gatewayConfig,
|
|
76
|
-
acl: gatewayConfig && features.has('acl'),
|
|
77
|
-
admin: gatewayConfig && features.has('gateway-admin'),
|
|
78
|
-
routeReception: gatewayConfig && features.has('route-reception'),
|
|
79
|
-
autoPublish: !gatewayConfig && features.has('auto-config-publish'),
|
|
80
|
-
skills: o.skills !== false,
|
|
81
|
-
names: {
|
|
82
|
-
exchange: o.exchange || d.exchange,
|
|
83
|
-
aclQueue: o.aclQueue || d.aclQueue,
|
|
84
|
-
adminQueue: o.adminQueue || d.adminQueue,
|
|
85
|
-
controlTopic: o.controlTopic || d.controlTopic,
|
|
86
|
-
routeExchange: o.routeExchange || d.routeExchange,
|
|
87
|
-
routeQueue: o.routeQueue || d.routeQueue,
|
|
88
|
-
serviceName: o.serviceName || d.serviceName,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---- Interactive branching flow -------------------------------------------
|
|
94
|
-
const { confirm, checkbox, input } = prompts;
|
|
95
|
-
const names: Names = { ...d };
|
|
96
|
-
let acl = false, admin = false, routeReception = false, autoPublish = false;
|
|
97
|
-
|
|
98
|
-
const gatewayConfig: boolean = await confirm({
|
|
99
|
-
message: 'Create a gateway (HTTP/WebSocket) configuration?',
|
|
100
|
-
default: false,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
if (gatewayConfig) {
|
|
104
|
-
const picked: string[] = await checkbox({
|
|
105
|
-
message: 'Select gateway features to include',
|
|
106
|
-
choices: [
|
|
107
|
-
{ name: 'ACL — role-based authorization + management', value: 'acl' },
|
|
108
|
-
{ name: 'Gateway admin + auth — DB-managed routes, auth-providers, metrics', value: 'gateway-admin' },
|
|
109
|
-
{ name: 'Route reception — apply routes auto-published by microservices', value: 'route-reception' },
|
|
110
|
-
],
|
|
111
|
-
});
|
|
112
|
-
acl = picked.includes('acl');
|
|
113
|
-
admin = picked.includes('gateway-admin');
|
|
114
|
-
routeReception = picked.includes('route-reception');
|
|
115
|
-
|
|
116
|
-
if (acl || admin || routeReception) {
|
|
117
|
-
names.exchange = await input({ message: 'Main AMQP exchange name', default: d.exchange });
|
|
118
|
-
}
|
|
119
|
-
if (acl) {
|
|
120
|
-
names.aclQueue = await input({ message: 'Queue backing the rlb-acl topic', default: d.aclQueue });
|
|
121
|
-
}
|
|
122
|
-
if (admin || routeReception) {
|
|
123
|
-
names.adminQueue = await input({ message: 'Queue backing the rlb-gateway-admin topic', default: d.adminQueue });
|
|
124
|
-
names.controlTopic = await input({ message: 'Broadcast control/reload topic name', default: d.controlTopic });
|
|
125
|
-
}
|
|
126
|
-
if (routeReception) {
|
|
127
|
-
names.routeExchange = await input({ message: 'Route-discovery exchange (must match the publishers)', default: d.routeExchange });
|
|
128
|
-
names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
const picked: string[] = await checkbox({
|
|
132
|
-
message: 'Select microservice features to include',
|
|
133
|
-
choices: [
|
|
134
|
-
{ name: 'Auto-send config at startup — publish this service’s @BrokerHTTP routes to the gateway on boot', value: 'auto-config-publish' },
|
|
135
|
-
],
|
|
136
|
-
});
|
|
137
|
-
autoPublish = picked.includes('auto-config-publish');
|
|
138
|
-
if (autoPublish) {
|
|
139
|
-
names.serviceName = await input({ message: 'Service name (route ownership + AMQP connection_name)', default: d.serviceName });
|
|
140
|
-
names.routeExchange = await input({ message: 'Route-discovery exchange', default: d.routeExchange });
|
|
141
|
-
names.routeQueue = await input({ message: 'Route-sync queue', default: d.routeQueue });
|
|
142
|
-
}
|
|
55
|
+
function ensureBaseConfig(tree: Tree, preferred: string | undefined, context: SchematicContext): void {
|
|
56
|
+
const configPath = findConfigYaml(tree, preferred);
|
|
57
|
+
if (!tree.exists(configPath)) {
|
|
58
|
+
tree.create(configPath, BASE_CONFIG_YAML);
|
|
59
|
+
context.logger.info(`[nest-add] created ${configPath}.`);
|
|
60
|
+
return;
|
|
143
61
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
62
|
+
const { doc } = readConfigDoc(tree, configPath);
|
|
63
|
+
if (!doc.hasIn(['app'])) setIn(doc, ['app'], { port: 80, host: '0.0.0.0', environment: 'development' });
|
|
64
|
+
if (!doc.hasIn(['broker'])) setIn(doc, ['broker'], { name: 'rabbitmq', uri: '<AMQP_URI>' });
|
|
65
|
+
if (!doc.hasIn(['topics'])) setIn(doc, ['topics'], []);
|
|
66
|
+
writeConfigDoc(tree, configPath, doc);
|
|
147
67
|
}
|
|
148
68
|
|
|
149
|
-
|
|
150
|
-
// config/config.yaml builder
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
function buildConfigYaml(sel: Resolved): string {
|
|
154
|
-
const n = sel.names;
|
|
155
|
-
const anyAdmin = sel.admin || sel.routeReception;
|
|
156
|
-
|
|
157
|
-
const routeDiscoveryPub = sel.autoPublish ? `
|
|
158
|
-
# Route auto-discovery (publisher side): announce this service's @BrokerHTTP routes on boot.
|
|
159
|
-
# serviceName also fills the AMQP connection_name (none is set explicitly below).
|
|
160
|
-
routeDiscovery:
|
|
161
|
-
serviceName: "${n.serviceName}"
|
|
162
|
-
publishOnBoot: true
|
|
163
|
-
exchange: ${n.routeExchange}
|
|
164
|
-
queue: ${n.routeQueue}` : '';
|
|
165
|
-
|
|
166
|
-
const clientProps = sel.autoPublish ? '' : `
|
|
167
|
-
clientProperties:
|
|
168
|
-
connection_name: "<APP_NAME>"`;
|
|
169
|
-
|
|
170
|
-
const exchanges: string[] = [];
|
|
171
|
-
if (sel.acl || anyAdmin) {
|
|
172
|
-
exchanges.push(` - name: ${n.exchange}
|
|
173
|
-
type: "direct"
|
|
174
|
-
createExchangeIfNotExists: true
|
|
175
|
-
options:
|
|
176
|
-
durable: true`);
|
|
177
|
-
}
|
|
178
|
-
exchanges.push(` - name: example.fanout
|
|
179
|
-
type: "fanout"
|
|
180
|
-
createExchangeIfNotExists: true
|
|
181
|
-
options:
|
|
182
|
-
durable: true
|
|
183
|
-
autoDelete: false
|
|
184
|
-
internal: false`);
|
|
185
|
-
|
|
186
|
-
const queues: string[] = [];
|
|
187
|
-
if (sel.acl) {
|
|
188
|
-
queues.push(` - name: ${n.aclQueue}
|
|
189
|
-
exchange: ${n.exchange}
|
|
190
|
-
routingKey: ${n.aclQueue}
|
|
191
|
-
createQueueIfNotExists: true
|
|
192
|
-
options:
|
|
193
|
-
durable: true`);
|
|
194
|
-
}
|
|
195
|
-
if (anyAdmin) {
|
|
196
|
-
queues.push(` - name: ${n.adminQueue}
|
|
197
|
-
exchange: ${n.exchange}
|
|
198
|
-
routingKey: ${n.adminQueue}
|
|
199
|
-
createQueueIfNotExists: true
|
|
200
|
-
options:
|
|
201
|
-
durable: true`);
|
|
202
|
-
}
|
|
203
|
-
queues.push(` - name: example.queue
|
|
204
|
-
exchange: example.fanout
|
|
205
|
-
routingKey: example.queue
|
|
206
|
-
createQueueIfNotExists: true
|
|
207
|
-
options:
|
|
208
|
-
durable: true
|
|
209
|
-
autoDelete: false
|
|
210
|
-
exclusive: false`);
|
|
211
|
-
|
|
212
|
-
const topics: string[] = [];
|
|
213
|
-
if (sel.acl) {
|
|
214
|
-
topics.push(` # Fixed topic name (decorator-bound in the lib): the ACL handlers bind to 'rlb-acl'.
|
|
215
|
-
- name: rlb-acl
|
|
216
|
-
mode: rpc
|
|
217
|
-
queue: ${n.aclQueue}
|
|
218
|
-
exchange: ${n.exchange}
|
|
219
|
-
routingKey: ${n.aclQueue}`);
|
|
220
|
-
}
|
|
221
|
-
if (anyAdmin) {
|
|
222
|
-
topics.push(` # Fixed topic name (decorator-bound): the gateway-admin handlers bind to 'rlb-gateway-admin'.
|
|
223
|
-
- name: rlb-gateway-admin
|
|
224
|
-
mode: rpc
|
|
225
|
-
queue: ${n.adminQueue}
|
|
226
|
-
exchange: ${n.exchange}
|
|
227
|
-
routingKey: ${n.adminQueue}
|
|
228
|
-
# Broadcast control topic: the gateway rebuilds its routes at runtime on a 'gw-reload'.
|
|
229
|
-
- name: ${n.controlTopic}
|
|
230
|
-
mode: broadcast
|
|
231
|
-
exchange: ${n.exchange}
|
|
232
|
-
routingKey: ${n.controlTopic}`);
|
|
233
|
-
}
|
|
234
|
-
topics.push(` - name: example.topic
|
|
235
|
-
exchange: example.fanout
|
|
236
|
-
routingKey: "example.topic"
|
|
237
|
-
mode: event`);
|
|
238
|
-
|
|
239
|
-
let yaml = `app:
|
|
69
|
+
const BASE_CONFIG_YAML = `app:
|
|
240
70
|
port: 80
|
|
241
71
|
host: 0.0.0.0
|
|
242
72
|
environment: "development"
|
|
243
73
|
|
|
244
|
-
auth-providers: []
|
|
245
|
-
|
|
246
74
|
broker:
|
|
247
75
|
name: "rabbitmq"
|
|
248
76
|
uri: "<AMQP_URI>"
|
|
249
|
-
defaultSubscribeErrorBehavior: "ack"
|
|
250
|
-
defaultPublishErrorBehavior: "reject"${routeDiscoveryPub}
|
|
251
77
|
connectionManagerOptions:
|
|
252
78
|
heartbeatIntervalInSeconds: 60
|
|
253
79
|
reconnectTimeInSeconds: 60
|
|
254
|
-
connectionOptions
|
|
80
|
+
connectionOptions:
|
|
81
|
+
clientProperties:
|
|
82
|
+
# Logical name — the library appends -<hostname>-<pid> per instance.
|
|
83
|
+
connection_name: "<APP_NAME>"
|
|
255
84
|
credentials:
|
|
256
85
|
mechanism: PLAIN
|
|
257
86
|
username: "<AMQP_USERNAME>"
|
|
258
87
|
password: "<AMQP_PASSWORD>"
|
|
259
88
|
exchanges:
|
|
260
|
-
|
|
89
|
+
- name: example.fanout
|
|
90
|
+
type: "fanout"
|
|
91
|
+
createExchangeIfNotExists: true
|
|
92
|
+
options:
|
|
93
|
+
durable: true
|
|
261
94
|
queues:
|
|
262
|
-
|
|
95
|
+
- name: example.queue
|
|
96
|
+
exchange: example.fanout
|
|
97
|
+
routingKey: example.queue
|
|
98
|
+
createQueueIfNotExists: true
|
|
99
|
+
options:
|
|
100
|
+
durable: true
|
|
263
101
|
|
|
264
102
|
topics:
|
|
265
|
-
|
|
103
|
+
- name: example.topic
|
|
104
|
+
exchange: example.fanout
|
|
105
|
+
routingKey: "example.topic"
|
|
106
|
+
mode: event
|
|
266
107
|
`;
|
|
267
108
|
|
|
268
|
-
|
|
269
|
-
yaml += '\n' + buildGatewayBlock(sel);
|
|
270
|
-
}
|
|
271
|
-
return yaml;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function buildGatewayBlock(sel: Resolved): string {
|
|
275
|
-
const n = sel.names;
|
|
276
|
-
const anyAdmin = sel.admin || sel.routeReception;
|
|
277
|
-
const paths: string[] = [];
|
|
278
|
-
|
|
279
|
-
if (sel.acl) paths.push(ACL_PATHS);
|
|
280
|
-
if (sel.admin) paths.push(adminPaths(n.controlTopic));
|
|
281
|
-
paths.push(` - name: example-path
|
|
282
|
-
method: POST
|
|
283
|
-
dataSource: body
|
|
284
|
-
path: /example
|
|
285
|
-
topic: example.topic
|
|
286
|
-
action: example-action
|
|
287
|
-
mode: event`);
|
|
288
|
-
|
|
289
|
-
let block = `gateway:
|
|
290
|
-
events: []
|
|
291
|
-
ws:
|
|
292
|
-
heartbeatIntervalMs: 30000
|
|
293
|
-
# Auth is declared per-event (events[].auth / requireAuth / actions / scopeClaim).
|
|
294
|
-
paths:
|
|
295
|
-
${paths.join('\n')}`;
|
|
296
|
-
|
|
297
|
-
if (anyAdmin) {
|
|
298
|
-
block += `
|
|
299
|
-
# Load DB-managed routes at boot AND on every runtime reload (ordered static-before-param).
|
|
300
|
-
loadConfig:
|
|
301
|
-
paths:
|
|
302
|
-
topic: rlb-gateway-admin
|
|
303
|
-
action: gw-path-export
|
|
304
|
-
# Broadcast topic that triggers a runtime route rebuild (see topics: ${n.controlTopic}).
|
|
305
|
-
reloadTopic: ${n.controlTopic}
|
|
306
|
-
# Per-request metrics auto-emitted to the gateway-admin metrics handler.
|
|
307
|
-
metrics:
|
|
308
|
-
topic: rlb-gateway-admin
|
|
309
|
-
action: gw-metrics-track`;
|
|
310
|
-
}
|
|
311
|
-
return block + '\n';
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const ACL_PATHS = ` # --- ACL management: actions (name is the key — PUT upserts, GET lists, DELETE by name) ---
|
|
315
|
-
- name: acl-action-list
|
|
316
|
-
method: GET
|
|
317
|
-
path: /acl/actions
|
|
318
|
-
dataSource: query
|
|
319
|
-
topic: rlb-acl
|
|
320
|
-
action: acl-action-list
|
|
321
|
-
mode: rpc
|
|
322
|
-
- name: acl-action-get
|
|
323
|
-
method: GET
|
|
324
|
-
path: /acl/actions/get
|
|
325
|
-
dataSource: query
|
|
326
|
-
topic: rlb-acl
|
|
327
|
-
action: acl-action-get
|
|
328
|
-
mode: rpc
|
|
329
|
-
- name: acl-action-upsert
|
|
330
|
-
method: PUT
|
|
331
|
-
path: /acl/actions
|
|
332
|
-
dataSource: body
|
|
333
|
-
topic: rlb-acl
|
|
334
|
-
action: acl-action-update
|
|
335
|
-
mode: rpc
|
|
336
|
-
- name: acl-action-delete
|
|
337
|
-
method: DELETE
|
|
338
|
-
path: /acl/actions
|
|
339
|
-
dataSource: body
|
|
340
|
-
topic: rlb-acl
|
|
341
|
-
action: acl-action-delete
|
|
342
|
-
mode: rpc
|
|
343
|
-
- name: acl-action-search # query: ?q=&limit=
|
|
344
|
-
method: GET
|
|
345
|
-
path: /acl/actions/search
|
|
346
|
-
dataSource: query
|
|
347
|
-
topic: rlb-acl
|
|
348
|
-
action: acl-action-search
|
|
349
|
-
mode: rpc
|
|
350
|
-
# --- ACL management: roles ---
|
|
351
|
-
- name: acl-role-list
|
|
352
|
-
method: GET
|
|
353
|
-
path: /acl/roles
|
|
354
|
-
dataSource: query
|
|
355
|
-
topic: rlb-acl
|
|
356
|
-
action: acl-role-list
|
|
357
|
-
mode: rpc
|
|
358
|
-
- name: acl-role-get
|
|
359
|
-
method: GET
|
|
360
|
-
path: /acl/roles/get
|
|
361
|
-
dataSource: query
|
|
362
|
-
topic: rlb-acl
|
|
363
|
-
action: acl-role-get
|
|
364
|
-
mode: rpc
|
|
365
|
-
- name: acl-role-upsert
|
|
366
|
-
method: PUT
|
|
367
|
-
path: /acl/roles
|
|
368
|
-
dataSource: body
|
|
369
|
-
topic: rlb-acl
|
|
370
|
-
action: acl-role-update
|
|
371
|
-
mode: rpc
|
|
372
|
-
- name: acl-role-delete
|
|
373
|
-
method: DELETE
|
|
374
|
-
path: /acl/roles
|
|
375
|
-
dataSource: body
|
|
376
|
-
topic: rlb-acl
|
|
377
|
-
action: acl-role-delete
|
|
378
|
-
mode: rpc
|
|
379
|
-
- name: acl-role-search # query: ?q=&limit=
|
|
380
|
-
method: GET
|
|
381
|
-
path: /acl/roles/search
|
|
382
|
-
dataSource: query
|
|
383
|
-
topic: rlb-acl
|
|
384
|
-
action: acl-role-search
|
|
385
|
-
mode: rpc
|
|
386
|
-
# --- ACL grants (per-user; resourceId/companyId optional) ---
|
|
387
|
-
- name: acl-grant
|
|
388
|
-
method: POST
|
|
389
|
-
path: /acl/grants
|
|
390
|
-
dataSource: body
|
|
391
|
-
topic: rlb-acl
|
|
392
|
-
action: acl-grant
|
|
393
|
-
mode: rpc
|
|
394
|
-
- name: acl-revoke
|
|
395
|
-
method: DELETE
|
|
396
|
-
path: /acl/grants
|
|
397
|
-
dataSource: body
|
|
398
|
-
topic: rlb-acl
|
|
399
|
-
action: acl-revoke
|
|
400
|
-
mode: rpc
|
|
401
|
-
- name: acl-grant-search # access search; query: ?q=&limit=
|
|
402
|
-
method: GET
|
|
403
|
-
path: /acl/grants/search
|
|
404
|
-
dataSource: query
|
|
405
|
-
topic: rlb-acl
|
|
406
|
-
action: acl-grant-search
|
|
407
|
-
mode: rpc
|
|
408
|
-
# --- ACL check (GET → 200 with true/false) ---
|
|
409
|
-
# checkAction(userId, {companyId?, resourceId?}, action): true if the user holds the action
|
|
410
|
-
# via any role on the EXACT (companyId, resourceId). companyId/resourceId are optional;
|
|
411
|
-
# when both are absent the check matches resource-less grants only (no wildcard).
|
|
412
|
-
- name: acl-check-action
|
|
413
|
-
method: GET
|
|
414
|
-
path: /acl/check
|
|
415
|
-
dataSource: query
|
|
416
|
-
topic: rlb-acl
|
|
417
|
-
action: acl-check-action
|
|
418
|
-
mode: rpc
|
|
419
|
-
# Lists the caller's accessible resources. Add an 'auth: <provider>' line once you declare an auth-provider.
|
|
420
|
-
- name: acl-list-resources-by-user
|
|
421
|
-
method: GET
|
|
422
|
-
path: /acl/resources
|
|
423
|
-
dataSource: query
|
|
424
|
-
topic: rlb-acl
|
|
425
|
-
action: acl-list-resources-by-user
|
|
426
|
-
mode: rpc`;
|
|
427
|
-
|
|
428
|
-
function adminPaths(controlTopic: string): string {
|
|
429
|
-
return ` # --- gateway-admin: health + DB routes + auth-providers + metrics ---
|
|
430
|
-
- name: health
|
|
431
|
-
method: GET
|
|
432
|
-
path: /health
|
|
433
|
-
dataSource: query
|
|
434
|
-
topic: rlb-gateway-admin
|
|
435
|
-
action: gw-health
|
|
436
|
-
mode: rpc
|
|
437
|
-
- name: gw-path-create
|
|
438
|
-
method: POST
|
|
439
|
-
path: /admin/paths
|
|
440
|
-
dataSource: body
|
|
441
|
-
topic: rlb-gateway-admin
|
|
442
|
-
action: gw-path-create
|
|
443
|
-
mode: rpc
|
|
444
|
-
- name: gw-path-list
|
|
445
|
-
method: GET
|
|
446
|
-
path: /admin/paths
|
|
447
|
-
dataSource: query
|
|
448
|
-
topic: rlb-gateway-admin
|
|
449
|
-
action: gw-path-list
|
|
450
|
-
mode: rpc
|
|
451
|
-
- name: gw-path-search # query: ?q=&limit=
|
|
452
|
-
method: GET
|
|
453
|
-
path: /admin/paths/search
|
|
454
|
-
dataSource: query
|
|
455
|
-
topic: rlb-gateway-admin
|
|
456
|
-
action: gw-path-search
|
|
457
|
-
mode: rpc
|
|
458
|
-
- name: gw-path-export
|
|
459
|
-
method: GET
|
|
460
|
-
path: /admin/paths/export
|
|
461
|
-
dataSource: query
|
|
462
|
-
topic: rlb-gateway-admin
|
|
463
|
-
action: gw-path-export
|
|
464
|
-
mode: rpc
|
|
465
|
-
- name: gw-path-update
|
|
466
|
-
method: PUT
|
|
467
|
-
path: /admin/paths
|
|
468
|
-
dataSource: body
|
|
469
|
-
topic: rlb-gateway-admin
|
|
470
|
-
action: gw-path-update
|
|
471
|
-
mode: rpc
|
|
472
|
-
- name: gw-path-get
|
|
473
|
-
method: GET
|
|
474
|
-
path: /admin/paths/get
|
|
475
|
-
dataSource: query
|
|
476
|
-
topic: rlb-gateway-admin
|
|
477
|
-
action: gw-path-get
|
|
478
|
-
mode: rpc
|
|
479
|
-
- name: gw-path-delete
|
|
480
|
-
method: DELETE
|
|
481
|
-
path: /admin/paths
|
|
482
|
-
dataSource: body
|
|
483
|
-
topic: rlb-gateway-admin
|
|
484
|
-
action: gw-path-delete
|
|
485
|
-
mode: rpc
|
|
486
|
-
# Route-change journal (who changed what): actor=system for auto-discovery, userId for manual CRUD.
|
|
487
|
-
- name: gw-route-log-list
|
|
488
|
-
method: GET
|
|
489
|
-
path: /admin/route-log
|
|
490
|
-
dataSource: query
|
|
491
|
-
topic: rlb-gateway-admin
|
|
492
|
-
action: gw-route-log-list
|
|
493
|
-
mode: rpc
|
|
494
|
-
- name: gw-route-log-search # filtered+paginated; query: ?actor=&service=&event=&routeKey=&from=&to=&page=&limit=
|
|
495
|
-
method: GET
|
|
496
|
-
path: /admin/route-log/search
|
|
497
|
-
dataSource: query
|
|
498
|
-
topic: rlb-gateway-admin
|
|
499
|
-
action: gw-route-log-search
|
|
500
|
-
mode: rpc
|
|
501
|
-
- name: gw-auth-list
|
|
502
|
-
method: GET
|
|
503
|
-
path: /admin/auth
|
|
504
|
-
dataSource: query
|
|
505
|
-
topic: rlb-gateway-admin
|
|
506
|
-
action: gw-auth-list
|
|
507
|
-
mode: rpc
|
|
508
|
-
- name: gw-auth-search # query: ?q=&limit=
|
|
509
|
-
method: GET
|
|
510
|
-
path: /admin/auth/search
|
|
511
|
-
dataSource: query
|
|
512
|
-
topic: rlb-gateway-admin
|
|
513
|
-
action: gw-auth-search
|
|
514
|
-
mode: rpc
|
|
515
|
-
- name: gw-auth-upsert
|
|
516
|
-
method: PUT
|
|
517
|
-
path: /admin/auth
|
|
518
|
-
dataSource: body
|
|
519
|
-
topic: rlb-gateway-admin
|
|
520
|
-
action: gw-auth-update
|
|
521
|
-
mode: rpc
|
|
522
|
-
- name: gw-auth-get
|
|
523
|
-
method: GET
|
|
524
|
-
path: /admin/auth/get
|
|
525
|
-
dataSource: query
|
|
526
|
-
topic: rlb-gateway-admin
|
|
527
|
-
action: gw-auth-get
|
|
528
|
-
mode: rpc
|
|
529
|
-
- name: gw-auth-delete
|
|
530
|
-
method: DELETE
|
|
531
|
-
path: /admin/auth
|
|
532
|
-
dataSource: body
|
|
533
|
-
topic: rlb-gateway-admin
|
|
534
|
-
action: gw-auth-delete
|
|
535
|
-
mode: rpc
|
|
536
|
-
- name: gw-metrics-get
|
|
537
|
-
method: GET
|
|
538
|
-
path: /admin/metrics
|
|
539
|
-
dataSource: query
|
|
540
|
-
topic: rlb-gateway-admin
|
|
541
|
-
action: gw-metrics-get
|
|
542
|
-
mode: rpc
|
|
543
|
-
- name: gw-metrics-series
|
|
544
|
-
method: GET
|
|
545
|
-
path: /admin/metrics/series
|
|
546
|
-
dataSource: query
|
|
547
|
-
topic: rlb-gateway-admin
|
|
548
|
-
action: gw-metrics-series
|
|
549
|
-
mode: rpc
|
|
550
|
-
- name: gw-metrics-points
|
|
551
|
-
method: GET
|
|
552
|
-
path: /admin/metrics/points
|
|
553
|
-
dataSource: query
|
|
554
|
-
topic: rlb-gateway-admin
|
|
555
|
-
action: gw-metrics-points
|
|
556
|
-
mode: rpc
|
|
557
|
-
- name: gw-metrics-summary # dashboard overview; query: ?from=&to=&method=&route=&name=&topN=
|
|
558
|
-
method: GET
|
|
559
|
-
path: /admin/metrics/summary
|
|
560
|
-
dataSource: query
|
|
561
|
-
topic: rlb-gateway-admin
|
|
562
|
-
action: gw-metrics-summary
|
|
563
|
-
mode: rpc
|
|
564
|
-
- name: gw-metrics-prometheus # Prometheus text exposition (returned as text/plain)
|
|
565
|
-
method: GET
|
|
566
|
-
path: /admin/metrics/prometheus
|
|
567
|
-
dataSource: query
|
|
568
|
-
topic: rlb-gateway-admin
|
|
569
|
-
action: gw-metrics-prometheus
|
|
570
|
-
mode: rpc
|
|
571
|
-
headers:
|
|
572
|
-
Content-Type: text/plain; version=0.0.4
|
|
573
|
-
- name: gw-metrics-rollups # long-term downsampled aggregates; query: ?from=&to=&method=&route=
|
|
574
|
-
method: GET
|
|
575
|
-
path: /admin/metrics/rollups
|
|
576
|
-
dataSource: query
|
|
577
|
-
topic: rlb-gateway-admin
|
|
578
|
-
action: gw-metrics-rollups
|
|
579
|
-
mode: rpc
|
|
580
|
-
- name: gw-metrics-track
|
|
581
|
-
method: POST
|
|
582
|
-
path: /admin/metrics/track
|
|
583
|
-
dataSource: body
|
|
584
|
-
topic: rlb-gateway-admin
|
|
585
|
-
action: gw-metrics-track
|
|
586
|
-
mode: event
|
|
587
|
-
- name: gw-reload
|
|
588
|
-
method: POST
|
|
589
|
-
path: /admin/reload
|
|
590
|
-
dataSource: body
|
|
591
|
-
topic: ${controlTopic}
|
|
592
|
-
action: gw-reload
|
|
593
|
-
mode: event
|
|
594
|
-
- name: gw-auth-reload # deliberate: reload DB auth-providers into RAM (separate from gw-reload)
|
|
595
|
-
method: POST
|
|
596
|
-
path: /admin/auth/reload
|
|
597
|
-
dataSource: body
|
|
598
|
-
topic: ${controlTopic}
|
|
599
|
-
action: gw-auth-reload
|
|
600
|
-
mode: event`;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// ---------------------------------------------------------------------------
|
|
604
|
-
// AppModule wiring (import statements + @Module imports entries)
|
|
605
|
-
// ---------------------------------------------------------------------------
|
|
606
|
-
|
|
607
|
-
function buildImportStatements(sel: Resolved): string {
|
|
608
|
-
const libSymbols = ['AppConfig', 'BrokerModule', 'BrokerTopic', 'RabbitMQConfig'];
|
|
609
|
-
if (sel.gatewayConfig) libSymbols.push('GatewayConfig', 'HandlerAuthConfig', 'ProxyModule');
|
|
610
|
-
if (sel.acl) libSymbols.push('AclActionRepository', 'AclGrantRepository', 'AclModule', 'AclRoleRepository', 'AclService', 'RLB_ACL_CACHE_STORE', 'RLB_GTW_ACL_ROLE_SERVICE');
|
|
611
|
-
if (sel.admin || sel.routeReception) libSymbols.push('AuthProviderRepository', 'GatewayAdminModule', 'HttpMetricRepository', 'HttpPathRepository', 'RouteSyncLogRepository');
|
|
612
|
-
if (sel.admin && sel.gatewayConfig) libSymbols.push('RLB_GTW_AUTH_PROVIDER_SOURCE');
|
|
613
|
-
const lib = `import { ${[...new Set(libSymbols)].sort().join(', ')} } from '@open-rlb/nestjs-amqp';`;
|
|
614
|
-
|
|
615
|
-
const lines = [lib, `import { ConfigModule, ConfigService } from '@nestjs/config';`, `import yamlConfig from './config/config.loader';`];
|
|
616
|
-
if (sel.gatewayConfig) lines.push(`import { HttpModule } from '@nestjs/axios';`);
|
|
617
|
-
if (sel.acl) {
|
|
618
|
-
lines.push(`import { InMemoryAclActionRepository, InMemoryAclGrantRepository, InMemoryAclRoleRepository } from './modules/database/repository/acl.repository';`);
|
|
619
|
-
lines.push(`import { InMemoryAclStore } from './cache/in-memory-acl-store';`);
|
|
620
|
-
}
|
|
621
|
-
if (sel.admin || sel.routeReception) {
|
|
622
|
-
lines.push(`import { InMemoryAuthProviderRepository, InMemoryHttpMetricRepository, InMemoryHttpPathRepository } from './modules/database/repository/gateway.repository';`);
|
|
623
|
-
lines.push(`import { InMemoryRouteSyncLogRepository } from './modules/database/repository/route-sync.repository';`);
|
|
624
|
-
}
|
|
625
|
-
return lines.join('\n');
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function brokerForRootAsync(): string {
|
|
629
|
-
return `BrokerModule.forRootAsync({
|
|
630
|
-
imports: [ConfigModule],
|
|
631
|
-
inject: [ConfigService],
|
|
632
|
-
useFactory: async (configService: ConfigService) => ({
|
|
633
|
-
options: configService.get<RabbitMQConfig>('broker')!,
|
|
634
|
-
topics: configService.get<BrokerTopic[]>('topics')!,
|
|
635
|
-
appOptions: configService.get<AppConfig>('app'),
|
|
636
|
-
})
|
|
637
|
-
})`;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function proxyForRootAsync(sel: Resolved): string {
|
|
641
|
-
const provs: string[] = [];
|
|
642
|
-
if (sel.acl) provs.push(` // Action-gated paths resolve the caller's identity via AclService (in-process, no broker hop).
|
|
643
|
-
{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService },`);
|
|
644
|
-
if (sel.admin) provs.push(` // DB auth-provider source for the runtime registry; activated by a deliberate gw-auth-reload.
|
|
645
|
-
{ provide: RLB_GTW_AUTH_PROVIDER_SOURCE, useExisting: InMemoryAuthProviderRepository },`);
|
|
646
|
-
const providers = provs.length ? `[\n${provs.join('\n')}\n ]` : `[]`;
|
|
647
|
-
return `ProxyModule.forRootAsync({
|
|
648
|
-
imports: [ConfigModule],
|
|
649
|
-
inject: [ConfigService],
|
|
650
|
-
useFactory: (configService: ConfigService) => ({
|
|
651
|
-
authOptions: configService.get<HandlerAuthConfig[]>('auth-providers'),
|
|
652
|
-
gatewayOptions: configService.get<GatewayConfig>('gateway'),
|
|
653
|
-
}),
|
|
654
|
-
providers: ${providers},
|
|
655
|
-
})`;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function aclForRoot(): string {
|
|
659
|
-
return `AclModule.forRoot(
|
|
660
|
-
[
|
|
661
|
-
InMemoryAclActionRepository,
|
|
662
|
-
{ provide: AclActionRepository, useExisting: InMemoryAclActionRepository },
|
|
663
|
-
InMemoryAclRoleRepository,
|
|
664
|
-
{ provide: AclRoleRepository, useExisting: InMemoryAclRoleRepository },
|
|
665
|
-
InMemoryAclGrantRepository,
|
|
666
|
-
{ provide: AclGrantRepository, useExisting: InMemoryAclGrantRepository },
|
|
667
|
-
InMemoryAclStore,
|
|
668
|
-
{ provide: RLB_ACL_CACHE_STORE, useExisting: InMemoryAclStore },
|
|
669
|
-
],
|
|
670
|
-
{ cache: { ramTtlMs: 30000, l2TtlSec: 600 } },
|
|
671
|
-
)`;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function gatewayAdminForRoot(sel: Resolved): string {
|
|
675
|
-
const options = sel.routeReception
|
|
676
|
-
? `,
|
|
677
|
-
{
|
|
678
|
-
// Consumer-side route-discovery — names MUST match the publishers' broker.routeDiscovery.
|
|
679
|
-
routeDiscovery: { exchange: '${sel.names.routeExchange}', queue: '${sel.names.routeQueue}' },
|
|
680
|
-
}`
|
|
681
|
-
: '';
|
|
682
|
-
return `GatewayAdminModule.forRoot(
|
|
683
|
-
[
|
|
684
|
-
InMemoryHttpPathRepository,
|
|
685
|
-
{ provide: HttpPathRepository, useExisting: InMemoryHttpPathRepository },
|
|
686
|
-
InMemoryAuthProviderRepository,
|
|
687
|
-
{ provide: AuthProviderRepository, useExisting: InMemoryAuthProviderRepository },
|
|
688
|
-
InMemoryHttpMetricRepository,
|
|
689
|
-
{ provide: HttpMetricRepository, useExisting: InMemoryHttpMetricRepository },
|
|
690
|
-
InMemoryRouteSyncLogRepository,
|
|
691
|
-
{ provide: RouteSyncLogRepository, useExisting: InMemoryRouteSyncLogRepository },
|
|
692
|
-
]${options},
|
|
693
|
-
)`;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function buildModuleEntries(sel: Resolved): string[] {
|
|
697
|
-
const entries: string[] = [];
|
|
698
|
-
entries.push(`ConfigModule.forRoot({ isGlobal: true, load: [yamlConfig] })`);
|
|
699
|
-
entries.push(brokerForRootAsync());
|
|
700
|
-
if (sel.gatewayConfig) {
|
|
701
|
-
entries.push('HttpModule');
|
|
702
|
-
entries.push(proxyForRootAsync(sel));
|
|
703
|
-
}
|
|
704
|
-
if (sel.acl) entries.push(aclForRoot());
|
|
705
|
-
if (sel.admin || sel.routeReception) entries.push(gatewayAdminForRoot(sel));
|
|
706
|
-
return entries;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// ---------------------------------------------------------------------------
|
|
710
|
-
// Entry point
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
|
-
|
|
713
|
-
export function main(options: InitOptions): Rule {
|
|
714
|
-
const project = (options.project || 'my-service').toString();
|
|
715
|
-
options = transform(options);
|
|
716
|
-
return async (tree: Tree, context: SchematicContext) => {
|
|
717
|
-
const sel = await resolveSelections({ ...options, project } as InitOptions, context);
|
|
718
|
-
const anyRepo = sel.acl || sel.admin || sel.routeReception;
|
|
719
|
-
return branchAndMerge(
|
|
720
|
-
chain([
|
|
721
|
-
mergeSourceRoot(options),
|
|
722
|
-
addModulesToAppModule(sel),
|
|
723
|
-
createConfigLoader(),
|
|
724
|
-
updateConfigYaml(sel),
|
|
725
|
-
sel.gatewayConfig ? configureMainForGateway() : noop(),
|
|
726
|
-
anyRepo ? copyAsset('db-core') : noop(),
|
|
727
|
-
sel.acl ? copyAsset('acl') : noop(),
|
|
728
|
-
(sel.admin || sel.routeReception) ? copyAsset('gateway-admin') : noop(),
|
|
729
|
-
sel.skills ? copySkills() : noop(),
|
|
730
|
-
updatePackageJson(sel),
|
|
731
|
-
]),
|
|
732
|
-
);
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// ---------------------------------------------------------------------------
|
|
737
|
-
// Helpers
|
|
738
|
-
// ---------------------------------------------------------------------------
|
|
739
|
-
|
|
740
|
-
function transform(source: InitOptions): InitOptions {
|
|
741
|
-
const target: InitOptions = Object.assign({}, source);
|
|
742
|
-
target.metadata = 'providers';
|
|
743
|
-
target.type = 'module';
|
|
744
|
-
target.language = 'ts';
|
|
745
|
-
const location: Location = new NameParser().parse({ ...target, name: 'init' });
|
|
746
|
-
target.name = '';
|
|
747
|
-
target.path = normalizeToKebabOrSnakeCase(location.path);
|
|
748
|
-
target.specFileSuffix = normalizeToKebabOrSnakeCase(source.specFileSuffix || 'spec');
|
|
749
|
-
return target;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/** Copy a static asset tree under ./files/<name> to the project root (no templating). */
|
|
753
|
-
function copyAsset(name: string): Rule {
|
|
754
|
-
return mergeWith(apply(url(`./files/${name}`), [move(normalize('.'))]), MergeStrategy.Overwrite);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/** Copy the Claude skills tree to .claude/skills. */
|
|
109
|
+
/** Copy the Claude skills (the project's Claude-facing docs) into .claude/skills. */
|
|
758
110
|
function copySkills(): Rule {
|
|
759
111
|
return mergeWith(apply(url('./files/skills'), [move(normalize('.claude/skills'))]), MergeStrategy.Overwrite);
|
|
760
112
|
}
|
|
761
|
-
|
|
762
|
-
/** Create src/config/config.loader.ts (js-yaml loader) if it doesn't exist yet. */
|
|
763
|
-
function createConfigLoader(): Rule {
|
|
764
|
-
return (tree: Tree) => {
|
|
765
|
-
const path = 'src/config/config.loader.ts';
|
|
766
|
-
if (tree.exists(path)) return tree;
|
|
767
|
-
tree.create(path, `import { readFileSync } from 'fs';
|
|
768
|
-
import * as yaml from 'js-yaml';
|
|
769
|
-
import { join } from 'path';
|
|
770
|
-
|
|
771
|
-
const YAML_CONFIG_FILENAME = 'config/config.yaml';
|
|
772
|
-
|
|
773
|
-
export default () =>
|
|
774
|
-
yaml.load(readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8')) as Record<string, any>;
|
|
775
|
-
`);
|
|
776
|
-
return tree;
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function updateConfigYaml(sel: Resolved): Rule {
|
|
781
|
-
return (tree: Tree) => {
|
|
782
|
-
const CONFIG_PATH = 'config/config.yaml';
|
|
783
|
-
const block = buildConfigYaml(sel);
|
|
784
|
-
|
|
785
|
-
if (!tree.exists(CONFIG_PATH)) {
|
|
786
|
-
tree.create(CONFIG_PATH, block);
|
|
787
|
-
return tree;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Existing file: append only the still-missing top-level sections.
|
|
791
|
-
const existing = tree.read(CONFIG_PATH)!.toString('utf-8');
|
|
792
|
-
const SECTION_KEYS = ['app:', 'auth-providers:', 'broker:', 'topics:', ...(sel.gatewayConfig ? ['gateway:'] : [])] as const;
|
|
793
|
-
|
|
794
|
-
let toAppend = '';
|
|
795
|
-
for (const key of SECTION_KEYS) {
|
|
796
|
-
if (!existing.includes(key)) {
|
|
797
|
-
const section = extractYamlSection(block, key);
|
|
798
|
-
if (section) toAppend += '\n' + section + '\n';
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
if (toAppend.length > 0) {
|
|
802
|
-
tree.overwrite(CONFIG_PATH, existing.trimEnd() + '\n' + toAppend);
|
|
803
|
-
}
|
|
804
|
-
return tree;
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/** Extracts the YAML block starting at `sectionKey` up to the next top-level key. */
|
|
809
|
-
function extractYamlSection(yaml: string, sectionKey: string): string {
|
|
810
|
-
const lines = yaml.split('\n');
|
|
811
|
-
const startIdx = lines.findIndex((l) => l.startsWith(sectionKey));
|
|
812
|
-
if (startIdx === -1) return '';
|
|
813
|
-
const endIdx = lines.findIndex((l, i) => i > startIdx && l.length > 0 && !l.startsWith(' ') && !l.startsWith('#'));
|
|
814
|
-
const sectionLines = endIdx === -1 ? lines.slice(startIdx) : lines.slice(startIdx, endIdx);
|
|
815
|
-
return sectionLines.join('\n').trimEnd();
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function addModulesToAppModule(sel: Resolved): Rule {
|
|
819
|
-
return (tree: Tree) => {
|
|
820
|
-
const candidatePaths = ['/src/app.module.ts', '/app/app.module.ts', 'src/app.module.ts', 'app/app.module.ts'];
|
|
821
|
-
let modulePath: string | undefined = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'app.module.ts');
|
|
822
|
-
if (!modulePath) {
|
|
823
|
-
console.warn('[nest-add] app.module.ts not found: AppModule wiring skipped.');
|
|
824
|
-
return tree;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const raw = tree.read(modulePath);
|
|
828
|
-
if (!raw) return tree;
|
|
829
|
-
let content = raw.toString('utf-8');
|
|
830
|
-
|
|
831
|
-
// Sentinel: BrokerModule.forRootAsync means "already wired" → idempotent no-op.
|
|
832
|
-
if (content.includes('BrokerModule.forRootAsync')) return tree;
|
|
833
|
-
|
|
834
|
-
if (!content.includes('@open-rlb/nestjs-amqp')) {
|
|
835
|
-
const pos = findLastImportEndIndex(content);
|
|
836
|
-
content = content.slice(0, pos) + '\n' + buildImportStatements(sel) + content.slice(pos);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Insert the module entries (reverse so the final order matches buildModuleEntries()).
|
|
840
|
-
const entries = buildModuleEntries(sel);
|
|
841
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
842
|
-
content = insertIntoImportsArray(content, entries[i]);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
tree.overwrite(modulePath, content);
|
|
846
|
-
return tree;
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function configureMainForGateway(): Rule {
|
|
851
|
-
return (tree: Tree) => {
|
|
852
|
-
const candidatePaths = ['/src/main.ts', '/app/main.ts', 'src/main.ts', 'app/main.ts'];
|
|
853
|
-
const mainPath = candidatePaths.find((p) => tree.exists(p)) || findFileInTree(tree, 'main.ts');
|
|
854
|
-
if (!mainPath) {
|
|
855
|
-
console.warn('[nest-add] main.ts not found: enable rawBody + WsAdapter manually.');
|
|
856
|
-
return tree;
|
|
857
|
-
}
|
|
858
|
-
let content = tree.read(mainPath)!.toString('utf-8');
|
|
859
|
-
if (content.includes('WsAdapter')) return tree;
|
|
860
|
-
|
|
861
|
-
const pos = findLastImportEndIndex(content);
|
|
862
|
-
content = content.slice(0, pos) + "\nimport { WsAdapter } from '@nestjs/platform-ws';" + content.slice(pos);
|
|
863
|
-
|
|
864
|
-
const m = content.match(/const\s+(\w+)\s*=\s*await\s+NestFactory\.create\(\s*([A-Za-z0-9_]+)\s*(,\s*\{[^}]*\})?\s*\)\s*;?/);
|
|
865
|
-
if (m) {
|
|
866
|
-
const appVar = m[1];
|
|
867
|
-
const moduleArg = m[2];
|
|
868
|
-
const replacement = `const ${appVar} = await NestFactory.create(${moduleArg}, { rawBody: true });\n ${appVar}.useWebSocketAdapter(new WsAdapter(${appVar}));\n ${appVar}.enableShutdownHooks();`;
|
|
869
|
-
content = content.replace(m[0], replacement);
|
|
870
|
-
} else {
|
|
871
|
-
console.warn('[nest-add] NestFactory.create() not found in main.ts: add { rawBody: true } + useWebSocketAdapter manually.');
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
tree.overwrite(mainPath, content);
|
|
875
|
-
return tree;
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/** Inserts `moduleEntry` at the head of the first `imports: [...]` array in the @Module decorator. */
|
|
880
|
-
function insertIntoImportsArray(source: string, moduleEntry: string): string {
|
|
881
|
-
const match = /imports\s*:\s*\[/.exec(source);
|
|
882
|
-
if (!match) return source;
|
|
883
|
-
const openBracketPos = source.indexOf('[', match.index);
|
|
884
|
-
|
|
885
|
-
let depth = 0;
|
|
886
|
-
let closeBracketPos = -1;
|
|
887
|
-
for (let i = openBracketPos; i < source.length; i++) {
|
|
888
|
-
if (source[i] === '[') depth++;
|
|
889
|
-
else if (source[i] === ']') {
|
|
890
|
-
depth--;
|
|
891
|
-
if (depth === 0) { closeBracketPos = i; break; }
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (closeBracketPos === -1) return source;
|
|
895
|
-
|
|
896
|
-
const arrayContent = source.slice(openBracketPos + 1, closeBracketPos).trim();
|
|
897
|
-
const newArrayContent = arrayContent.length === 0
|
|
898
|
-
? `\n ${moduleEntry},\n `
|
|
899
|
-
: `\n ${moduleEntry},\n ${arrayContent}\n `;
|
|
900
|
-
return source.slice(0, openBracketPos + 1) + newArrayContent + source.slice(closeBracketPos);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function findLastImportEndIndex(source: string): number {
|
|
904
|
-
const importRegex = /^import\s+.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
905
|
-
let lastEnd = 0;
|
|
906
|
-
let match: RegExpExecArray | null;
|
|
907
|
-
while ((match = importRegex.exec(source)) !== null) {
|
|
908
|
-
lastEnd = match.index + match[0].length;
|
|
909
|
-
}
|
|
910
|
-
return lastEnd;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function findFileInTree(tree: Tree, fileName: string): string | undefined {
|
|
914
|
-
let found: string | undefined;
|
|
915
|
-
tree.visit((path) => {
|
|
916
|
-
if (!found && path.endsWith(`/${fileName}`)) found = path;
|
|
917
|
-
});
|
|
918
|
-
return found;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// ---------------------------------------------------------------------------
|
|
922
|
-
// package.json: add the runtime deps the generated code imports.
|
|
923
|
-
// ---------------------------------------------------------------------------
|
|
924
|
-
|
|
925
|
-
function updatePackageJson(sel: Resolved): Rule {
|
|
926
|
-
return (host: Tree) => {
|
|
927
|
-
if (!host.exists('package.json')) return host;
|
|
928
|
-
return updateJsonFile(host, 'package.json', (packageJson: Record<string, any>) => {
|
|
929
|
-
packageJson.dependencies = packageJson.dependencies || {};
|
|
930
|
-
const add = (name: string, version: string) => {
|
|
931
|
-
if (!packageJson.dependencies[name]) packageJson.dependencies[name] = version;
|
|
932
|
-
};
|
|
933
|
-
add('@nestjs/config', '^4.0.4');
|
|
934
|
-
add('js-yaml', '^4.1.0');
|
|
935
|
-
if (sel.gatewayConfig) {
|
|
936
|
-
add('@nestjs/axios', '^4.0.1');
|
|
937
|
-
add('@nestjs/platform-ws', '^11.0.1');
|
|
938
|
-
add('@nestjs/websockets', '^11.0.1');
|
|
939
|
-
add('ws', '^8.21.0');
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function updateJsonFile<T>(host: Tree, path: string, callback: UpdateJsonFn<T>): Tree {
|
|
946
|
-
const source = host.read(path);
|
|
947
|
-
if (source) {
|
|
948
|
-
const json = parse(source.toString('utf-8'));
|
|
949
|
-
callback(json as unknown as T);
|
|
950
|
-
host.overwrite(path, JSON.stringify(json, null, 2));
|
|
951
|
-
}
|
|
952
|
-
return host;
|
|
953
|
-
}
|