@open-rlb/nestjs-amqp 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/modules/acl/const.d.ts +2 -0
  2. package/modules/acl/const.js +2 -0
  3. package/modules/acl/const.js.map +1 -1
  4. package/modules/acl/services/acl-management.service.d.ts +4 -0
  5. package/modules/acl/services/acl-management.service.js +36 -3
  6. package/modules/acl/services/acl-management.service.js.map +1 -1
  7. package/package.json +6 -5
  8. package/schematics/add-auth-provider/index.d.ts +3 -0
  9. package/schematics/add-auth-provider/index.js +60 -0
  10. package/schematics/add-auth-provider/index.js.map +1 -0
  11. package/schematics/add-auth-provider/index.spec.ts +83 -0
  12. package/schematics/add-auth-provider/index.ts +83 -0
  13. package/schematics/add-auth-provider/schema.d.ts +18 -0
  14. package/schematics/add-auth-provider/schema.js +3 -0
  15. package/schematics/add-auth-provider/schema.js.map +1 -0
  16. package/schematics/add-auth-provider/schema.json +59 -0
  17. package/schematics/add-auth-provider/schema.ts +35 -0
  18. package/schematics/add-exchange/index.d.ts +3 -0
  19. package/schematics/add-exchange/index.js +49 -0
  20. package/schematics/add-exchange/index.js.map +1 -0
  21. package/schematics/add-exchange/index.spec.ts +65 -0
  22. package/schematics/add-exchange/index.ts +65 -0
  23. package/schematics/add-exchange/schema.d.ts +14 -0
  24. package/schematics/add-exchange/schema.js +3 -0
  25. package/schematics/add-exchange/schema.js.map +1 -0
  26. package/schematics/add-exchange/schema.json +50 -0
  27. package/schematics/add-exchange/schema.ts +26 -0
  28. package/schematics/add-exchange-binding/index.d.ts +3 -0
  29. package/schematics/add-exchange-binding/index.js +53 -0
  30. package/schematics/add-exchange-binding/index.js.map +1 -0
  31. package/schematics/add-exchange-binding/index.spec.ts +73 -0
  32. package/schematics/add-exchange-binding/index.ts +64 -0
  33. package/schematics/add-exchange-binding/schema.d.ts +8 -0
  34. package/schematics/add-exchange-binding/schema.js +3 -0
  35. package/schematics/add-exchange-binding/schema.js.map +1 -0
  36. package/schematics/add-exchange-binding/schema.json +36 -0
  37. package/schematics/add-exchange-binding/schema.ts +14 -0
  38. package/schematics/add-gateway/index.d.ts +4 -0
  39. package/schematics/add-gateway/index.js +105 -0
  40. package/schematics/add-gateway/index.js.map +1 -0
  41. package/schematics/add-gateway/index.spec.ts +163 -0
  42. package/schematics/add-gateway/index.ts +141 -0
  43. package/schematics/add-gateway/paths.d.ts +12 -0
  44. package/schematics/add-gateway/paths.js +87 -0
  45. package/schematics/add-gateway/paths.js.map +1 -0
  46. package/schematics/add-gateway/paths.ts +103 -0
  47. package/schematics/add-gateway/schema.d.ts +13 -0
  48. package/schematics/add-gateway/schema.js +3 -0
  49. package/schematics/add-gateway/schema.js.map +1 -0
  50. package/schematics/add-gateway/schema.json +63 -0
  51. package/schematics/add-gateway/schema.ts +24 -0
  52. package/schematics/add-gateway/wiring.d.ts +14 -0
  53. package/schematics/add-gateway/wiring.js +102 -0
  54. package/schematics/add-gateway/wiring.js.map +1 -0
  55. package/schematics/add-gateway/wiring.ts +115 -0
  56. package/schematics/add-metrics-topic/index.d.ts +3 -0
  57. package/schematics/add-metrics-topic/index.js +31 -0
  58. package/schematics/add-metrics-topic/index.js.map +1 -0
  59. package/schematics/add-metrics-topic/index.spec.ts +79 -0
  60. package/schematics/add-metrics-topic/index.ts +49 -0
  61. package/schematics/add-metrics-topic/schema.d.ts +10 -0
  62. package/schematics/add-metrics-topic/schema.js +3 -0
  63. package/schematics/add-metrics-topic/schema.js.map +1 -0
  64. package/schematics/add-metrics-topic/schema.json +48 -0
  65. package/schematics/add-metrics-topic/schema.ts +18 -0
  66. package/schematics/add-queue/index.d.ts +3 -0
  67. package/schematics/add-queue/index.js +48 -0
  68. package/schematics/add-queue/index.js.map +1 -0
  69. package/schematics/add-queue/index.spec.ts +75 -0
  70. package/schematics/add-queue/index.ts +74 -0
  71. package/schematics/add-queue/schema.d.ts +17 -0
  72. package/schematics/add-queue/schema.js +3 -0
  73. package/schematics/add-queue/schema.js.map +1 -0
  74. package/schematics/add-queue/schema.json +54 -0
  75. package/schematics/add-queue/schema.ts +32 -0
  76. package/schematics/add-reply-queue/index.d.ts +3 -0
  77. package/schematics/add-reply-queue/index.js +26 -0
  78. package/schematics/add-reply-queue/index.js.map +1 -0
  79. package/schematics/add-reply-queue/index.spec.ts +69 -0
  80. package/schematics/add-reply-queue/index.ts +33 -0
  81. package/schematics/add-reply-queue/schema.d.ts +6 -0
  82. package/schematics/add-reply-queue/schema.js +3 -0
  83. package/schematics/add-reply-queue/schema.js.map +1 -0
  84. package/schematics/add-reply-queue/schema.json +28 -0
  85. package/schematics/add-reply-queue/schema.ts +10 -0
  86. package/schematics/add-route/index.d.ts +3 -0
  87. package/schematics/add-route/index.js +57 -0
  88. package/schematics/add-route/index.js.map +1 -0
  89. package/schematics/add-route/index.spec.ts +75 -0
  90. package/schematics/add-route/index.ts +74 -0
  91. package/schematics/add-route/schema.d.ts +22 -0
  92. package/schematics/add-route/schema.js +3 -0
  93. package/schematics/add-route/schema.js.map +1 -0
  94. package/schematics/add-route/schema.json +74 -0
  95. package/schematics/add-route/schema.ts +43 -0
  96. package/schematics/add-topic/index.d.ts +3 -0
  97. package/schematics/add-topic/index.js +69 -0
  98. package/schematics/add-topic/index.js.map +1 -0
  99. package/schematics/add-topic/index.spec.ts +100 -0
  100. package/schematics/add-topic/index.ts +104 -0
  101. package/schematics/add-topic/schema.d.ts +18 -0
  102. package/schematics/add-topic/schema.js +3 -0
  103. package/schematics/add-topic/schema.js.map +1 -0
  104. package/schematics/add-topic/schema.json +63 -0
  105. package/schematics/add-topic/schema.ts +34 -0
  106. package/schematics/add-ws-event/index.d.ts +3 -0
  107. package/schematics/add-ws-event/index.js +53 -0
  108. package/schematics/add-ws-event/index.js.map +1 -0
  109. package/schematics/add-ws-event/index.spec.ts +85 -0
  110. package/schematics/add-ws-event/index.ts +72 -0
  111. package/schematics/add-ws-event/schema.d.ts +17 -0
  112. package/schematics/add-ws-event/schema.js +3 -0
  113. package/schematics/add-ws-event/schema.js.map +1 -0
  114. package/schematics/add-ws-event/schema.json +53 -0
  115. package/schematics/add-ws-event/schema.ts +33 -0
  116. package/schematics/collection.json +81 -1
  117. package/schematics/configure-broker/index.d.ts +3 -0
  118. package/schematics/configure-broker/index.js +41 -0
  119. package/schematics/configure-broker/index.js.map +1 -0
  120. package/schematics/configure-broker/index.spec.ts +62 -0
  121. package/schematics/configure-broker/index.ts +51 -0
  122. package/schematics/configure-broker/schema.d.ts +11 -0
  123. package/schematics/configure-broker/schema.js +3 -0
  124. package/schematics/configure-broker/schema.js.map +1 -0
  125. package/schematics/configure-broker/schema.json +47 -0
  126. package/schematics/configure-broker/schema.ts +20 -0
  127. package/schematics/enable-load-config/index.d.ts +3 -0
  128. package/schematics/enable-load-config/index.js +32 -0
  129. package/schematics/enable-load-config/index.js.map +1 -0
  130. package/schematics/enable-load-config/index.spec.ts +61 -0
  131. package/schematics/enable-load-config/index.ts +39 -0
  132. package/schematics/enable-load-config/schema.d.ts +8 -0
  133. package/schematics/enable-load-config/schema.js +3 -0
  134. package/schematics/enable-load-config/schema.js.map +1 -0
  135. package/schematics/enable-load-config/schema.json +36 -0
  136. package/schematics/enable-load-config/schema.ts +14 -0
  137. package/schematics/enable-retry/index.d.ts +3 -0
  138. package/schematics/enable-retry/index.js +51 -0
  139. package/schematics/enable-retry/index.js.map +1 -0
  140. package/schematics/enable-retry/index.spec.ts +73 -0
  141. package/schematics/enable-retry/index.ts +62 -0
  142. package/schematics/enable-retry/schema.d.ts +13 -0
  143. package/schematics/enable-retry/schema.js +3 -0
  144. package/schematics/enable-retry/schema.js.map +1 -0
  145. package/schematics/enable-retry/schema.json +64 -0
  146. package/schematics/enable-retry/schema.ts +24 -0
  147. package/schematics/enable-route-discovery/index.d.ts +3 -0
  148. package/schematics/enable-route-discovery/index.js +36 -0
  149. package/schematics/enable-route-discovery/index.js.map +1 -0
  150. package/schematics/enable-route-discovery/index.spec.ts +55 -0
  151. package/schematics/enable-route-discovery/index.ts +48 -0
  152. package/schematics/enable-route-discovery/schema.d.ts +9 -0
  153. package/schematics/enable-route-discovery/schema.js +3 -0
  154. package/schematics/enable-route-discovery/schema.js.map +1 -0
  155. package/schematics/enable-route-discovery/schema.json +44 -0
  156. package/schematics/enable-route-discovery/schema.ts +16 -0
  157. package/schematics/harden-gateway/index.d.ts +3 -0
  158. package/schematics/harden-gateway/index.js +93 -0
  159. package/schematics/harden-gateway/index.js.map +1 -0
  160. package/schematics/harden-gateway/index.spec.ts +105 -0
  161. package/schematics/harden-gateway/index.ts +108 -0
  162. package/schematics/harden-gateway/schema.d.ts +11 -0
  163. package/schematics/harden-gateway/schema.js +3 -0
  164. package/schematics/harden-gateway/schema.js.map +1 -0
  165. package/schematics/harden-gateway/schema.json +47 -0
  166. package/schematics/harden-gateway/schema.ts +20 -0
  167. package/schematics/nest-add/index.js +55 -832
  168. package/schematics/nest-add/index.js.map +1 -1
  169. package/schematics/nest-add/index.spec.ts +73 -0
  170. package/schematics/nest-add/index.ts +76 -917
  171. package/schematics/nest-add/init.schema.d.ts +1 -0
  172. package/schematics/nest-add/init.schema.ts +3 -0
  173. package/schematics/nest-add/schema.json +4 -43
  174. package/schematics/set-connection-name/index.d.ts +3 -0
  175. package/schematics/set-connection-name/index.js +34 -0
  176. package/schematics/set-connection-name/index.js.map +1 -0
  177. package/schematics/set-connection-name/index.spec.ts +48 -0
  178. package/schematics/set-connection-name/index.ts +42 -0
  179. package/schematics/set-connection-name/schema.d.ts +5 -0
  180. package/schematics/set-connection-name/schema.js +3 -0
  181. package/schematics/set-connection-name/schema.js.map +1 -0
  182. package/schematics/set-connection-name/schema.json +24 -0
  183. package/schematics/set-connection-name/schema.ts +8 -0
  184. package/schematics/utils/broker-yaml.util.d.ts +53 -0
  185. package/schematics/utils/broker-yaml.util.js +91 -0
  186. package/schematics/utils/broker-yaml.util.js.map +1 -0
  187. package/schematics/utils/broker-yaml.util.ts +172 -0
  188. package/schematics/utils/nest-wiring.util.d.ts +30 -0
  189. package/schematics/utils/nest-wiring.util.js +170 -0
  190. package/schematics/utils/nest-wiring.util.js.map +1 -0
  191. package/schematics/utils/nest-wiring.util.ts +211 -0
  192. package/schematics/utils/schematic-prompt.util.d.ts +7 -0
  193. package/schematics/utils/schematic-prompt.util.js +57 -0
  194. package/schematics/utils/schematic-prompt.util.js.map +1 -0
  195. package/schematics/utils/schematic-prompt.util.ts +82 -0
  196. package/schematics/utils/yaml-config.util.d.ts +22 -0
  197. package/schematics/utils/yaml-config.util.js +119 -0
  198. package/schematics/utils/yaml-config.util.js.map +1 -0
  199. package/schematics/utils/yaml-config.util.ts +167 -0
  200. /package/schematics/{nest-add → add-gateway}/files/acl/src/cache/in-memory-acl-store.ts +0 -0
  201. /package/schematics/{nest-add → add-gateway}/files/acl/src/modules/database/repository/acl.repository.ts +0 -0
  202. /package/schematics/{nest-add → add-gateway}/files/db-core/src/modules/database/repository/in-memory-collection.ts +0 -0
  203. /package/schematics/{nest-add → add-gateway}/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +0 -0
  204. /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, branchAndMerge, chain, MergeStrategy, mergeWith, move, noop, Rule, SchematicContext, Tree, url } from '@angular-devkit/schematics';
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 { normalizeToKebabOrSnakeCase } from '../utils/formatting';
5
- import { Location, NameParser } from '../utils/name.parser';
6
- import { mergeSourceRoot } from '../utils/source-root.helpers';
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
- type UpdateJsonFn<T> = (obj: T) => T | void;
10
-
11
- // ---------------------------------------------------------------------------
12
- // Resolved selections (from interactive prompts or flags/defaults)
13
- // ---------------------------------------------------------------------------
14
-
15
- interface Names {
16
- exchange: string; // main AMQP exchange backing acl/admin queues (default rlb)
17
- aclQueue: string; // queue backing the fixed rlb-acl topic
18
- adminQueue: string; // queue backing the fixed rlb-gateway-admin topic
19
- controlTopic: string; // broadcast control/reload topic
20
- routeExchange: string; // route-discovery fanout exchange
21
- routeQueue: string; // route-sync durable queue
22
- serviceName: string; // route-publish ownership + connection_name
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
- interface Resolved {
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
- function defaultNames(project: string): Names {
36
- return {
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
- * Resolves the install selections. When stdin is a TTY and no flags were passed, it drives a
49
- * branching interactive flow with @inquirer/prompts:
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
- async function resolveSelections(o: InitOptions, context: SchematicContext): Promise<Resolved> {
55
- const project = (o.project || 'my-service').toString();
56
- const d = defaultNames(project);
57
- const flagsProvided = o.gatewayConfig !== undefined || (Array.isArray(o.features) && o.features.length > 0);
58
- const canPrompt = !!process.stdout && !!process.stdout.isTTY && !process.env.CI && !flagsProvided;
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
- const skills: boolean = await confirm({ message: 'Copy the Claude skills into .claude/skills?', default: true });
146
- return { gatewayConfig, acl, admin, routeReception, autoPublish, skills, names };
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:${clientProps}
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
- ${exchanges.join('\n')}
89
+ - name: example.fanout
90
+ type: "fanout"
91
+ createExchangeIfNotExists: true
92
+ options:
93
+ durable: true
261
94
  queues:
262
- ${queues.join('\n')}
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
- ${topics.join('\n')}
103
+ - name: example.topic
104
+ exchange: example.fanout
105
+ routingKey: "example.topic"
106
+ mode: event
266
107
  `;
267
108
 
268
- if (sel.gatewayConfig) {
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
- }