create-forgeon 0.3.15 → 0.3.16

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 (41) hide show
  1. package/package.json +4 -2
  2. package/src/core/docs.test.mjs +79 -40
  3. package/src/core/scaffold.test.mjs +99 -0
  4. package/src/modules/db-prisma.mjs +23 -55
  5. package/src/modules/executor.test.mjs +132 -36
  6. package/src/modules/files-access.mjs +27 -98
  7. package/src/modules/files-image.mjs +26 -100
  8. package/src/modules/files-quotas.mjs +67 -87
  9. package/src/modules/files.mjs +35 -104
  10. package/src/modules/i18n.mjs +17 -121
  11. package/src/modules/idempotency.test.mjs +174 -0
  12. package/src/modules/jwt-auth.mjs +90 -209
  13. package/src/modules/logger.mjs +0 -9
  14. package/src/modules/probes.test.mjs +202 -0
  15. package/src/modules/queue.mjs +325 -443
  16. package/src/modules/rate-limit.mjs +22 -66
  17. package/src/modules/rbac.mjs +27 -67
  18. package/src/modules/scheduler.mjs +44 -167
  19. package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
  20. package/src/modules/shared/probes.mjs +235 -0
  21. package/src/modules/sync-integrations.mjs +54 -21
  22. package/src/modules/sync-integrations.test.mjs +220 -0
  23. package/src/run-add-module.test.mjs +153 -0
  24. package/templates/base/README.md +7 -55
  25. package/templates/base/apps/web/src/App.tsx +70 -42
  26. package/templates/base/apps/web/src/probes.ts +61 -0
  27. package/templates/base/apps/web/src/styles.css +86 -25
  28. package/templates/base/package.json +21 -15
  29. package/templates/base/scripts/forgeon-sync-integrations.mjs +55 -11
  30. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
  31. package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
  32. package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
  33. package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
  34. package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
  35. package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
  36. package/templates/base/docs/AI/PROJECT.md +0 -43
  37. package/templates/base/docs/AI/ROADMAP.md +0 -171
  38. package/templates/base/docs/AI/TASKS.md +0 -60
  39. package/templates/base/docs/AI/VALIDATION.md +0 -31
  40. package/templates/base/docs/README.md +0 -18
  41. package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
@@ -12,6 +12,7 @@ import {
12
12
  ensureValidatorSchema,
13
13
  upsertEnvLines,
14
14
  } from './shared/patch-utils.mjs';
15
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
15
16
 
16
17
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
17
18
  const source = path.join(packageRoot, 'templates', 'module-presets', 'rate-limit', relativePath);
@@ -113,7 +114,11 @@ function patchAppModule(targetRoot) {
113
114
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
114
115
  }
115
116
 
116
- function patchHealthController(targetRoot) {
117
+ function patchHealthController(targetRoot, probeTargets) {
118
+ if (!probeTargets.allowApi) {
119
+ return;
120
+ }
121
+
117
122
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
118
123
  if (!fs.existsSync(filePath)) {
119
124
  return;
@@ -139,69 +144,18 @@ function patchHealthController(targetRoot) {
139
144
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
140
145
  }
141
146
 
142
- function patchWebApp(targetRoot) {
143
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
144
- if (!fs.existsSync(filePath)) {
145
- return;
146
- }
147
-
148
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
149
- content = content
150
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
151
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
152
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
153
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
154
-
155
- if (!content.includes('rateLimitProbeResult')) {
156
- const stateAnchors = [
157
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
158
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
159
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
160
- ];
161
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
162
- if (stateAnchor) {
163
- content = ensureLineAfter(
164
- content,
165
- stateAnchor,
166
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
167
- );
168
- }
169
- }
170
-
171
- if (!content.includes('Check rate limit (click repeatedly)')) {
172
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
173
- ? '/health/rate-limit'
174
- : '/api/health/rate-limit';
175
- const button = ` <button onClick={() => runProbe(setRateLimitProbeResult, '${probePath}')}>\n Check rate limit (click repeatedly)\n </button>`;
176
-
177
- const actionsStart = content.indexOf('<div className="actions">');
178
- if (actionsStart >= 0) {
179
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
180
- if (actionsEnd >= 0) {
181
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
182
- }
183
- }
184
- }
185
-
186
- if (!content.includes("{renderResult('Rate limit probe response', rateLimitProbeResult)}")) {
187
- const resultLine = " {renderResult('Rate limit probe response', rateLimitProbeResult)}";
188
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
189
- if (content.includes(networkLine)) {
190
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
191
- } else {
192
- const anchors = [
193
- "{renderResult('Auth probe response', authProbeResult)}",
194
- "{renderResult('DB probe response', dbProbeResult)}",
195
- "{renderResult('Validation probe response', validationProbeResult)}",
196
- ];
197
- const anchor = anchors.find((line) => content.includes(line));
198
- if (anchor) {
199
- content = ensureLineAfter(content, anchor, resultLine);
200
- }
201
- }
202
- }
203
-
204
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
147
+ function registerWebProbe(targetRoot, probeTargets) {
148
+ ensureWebProbeDefinition({
149
+ targetRoot,
150
+ probeTargets,
151
+ definition: {
152
+ id: 'rate-limit',
153
+ title: 'Rate Limit',
154
+ buttonLabel: 'Check rate limit (click repeatedly)',
155
+ resultTitle: 'Rate limit probe response',
156
+ path: '/health/rate-limit',
157
+ },
158
+ });
205
159
  }
206
160
 
207
161
  function patchApiDockerfile(targetRoot) {
@@ -326,10 +280,12 @@ Operational notes:
326
280
 
327
281
  export function applyRateLimitModule({ packageRoot, targetRoot }) {
328
282
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'rate-limit'));
283
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'rate-limit' });
284
+
329
285
  patchApiPackage(targetRoot);
330
286
  patchAppModule(targetRoot);
331
- patchHealthController(targetRoot);
332
- patchWebApp(targetRoot);
287
+ patchHealthController(targetRoot, probeTargets);
288
+ registerWebProbe(targetRoot, probeTargets);
333
289
  patchApiDockerfile(targetRoot);
334
290
  patchCompose(targetRoot);
335
291
  patchReadme(targetRoot);
@@ -10,6 +10,7 @@ import {
10
10
  ensureLineBefore,
11
11
  ensureNestCommonImport,
12
12
  } from './shared/patch-utils.mjs';
13
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
13
14
 
14
15
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
15
16
  const source = path.join(packageRoot, 'templates', 'module-presets', 'rbac', relativePath);
@@ -118,7 +119,11 @@ function patchAppModule(targetRoot) {
118
119
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
119
120
  }
120
121
 
121
- function patchHealthController(targetRoot) {
122
+ function patchHealthController(targetRoot, probeTargets) {
123
+ if (!probeTargets.allowApi) {
124
+ return;
125
+ }
126
+
122
127
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
123
128
  if (!fs.existsSync(filePath)) {
124
129
  return;
@@ -160,70 +165,23 @@ function patchHealthController(targetRoot) {
160
165
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
161
166
  }
162
167
 
163
- function patchWebApp(targetRoot) {
164
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
165
- if (!fs.existsSync(filePath)) {
166
- return;
167
- }
168
-
169
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
170
- content = content
171
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
172
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
173
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
174
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
175
-
176
- if (!content.includes('rbacProbeResult')) {
177
- const stateAnchors = [
178
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
179
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
180
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
181
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
182
- ];
183
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
184
- if (stateAnchor) {
185
- content = ensureLineAfter(
186
- content,
187
- stateAnchor,
188
- ' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
189
- );
190
- }
191
- }
192
-
193
- if (!content.includes('Check RBAC access')) {
194
- const useProxyPath = content.includes("runProbe(setHealthResult, '/health')");
195
- const probePath = useProxyPath ? '/health/rbac' : '/api/health/rbac';
196
- const button = ` <button\n onClick={() =>\n runProbe(setRbacProbeResult, '${probePath}', {\n headers: { 'x-forgeon-permissions': 'health.rbac' },\n })\n }\n >\n Check RBAC access\n </button>`;
197
-
198
- const actionsStart = content.indexOf('<div className="actions">');
199
- if (actionsStart >= 0) {
200
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
201
- if (actionsEnd >= 0) {
202
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
203
- }
204
- }
205
- }
206
-
207
- if (!content.includes("{renderResult('RBAC probe response', rbacProbeResult)}")) {
208
- const resultLine = " {renderResult('RBAC probe response', rbacProbeResult)}";
209
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
210
- if (content.includes(networkLine)) {
211
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
212
- } else {
213
- const anchors = [
214
- " {renderResult('Rate limit probe response', rateLimitProbeResult)}",
215
- " {renderResult('Auth probe response', authProbeResult)}",
216
- " {renderResult('DB probe response', dbProbeResult)}",
217
- " {renderResult('Validation probe response', validationProbeResult)}",
218
- ];
219
- const anchor = anchors.find((line) => content.includes(line));
220
- if (anchor) {
221
- content = ensureLineAfter(content, anchor, resultLine);
222
- }
223
- }
224
- }
225
-
226
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
168
+ function registerWebProbe(targetRoot, probeTargets) {
169
+ ensureWebProbeDefinition({
170
+ targetRoot,
171
+ probeTargets,
172
+ definition: {
173
+ id: 'rbac',
174
+ title: 'RBAC',
175
+ buttonLabel: 'Check RBAC access',
176
+ resultTitle: 'RBAC probe response',
177
+ path: '/health/rbac',
178
+ request: {
179
+ headers: {
180
+ 'x-forgeon-permissions': 'health.rbac',
181
+ },
182
+ },
183
+ },
184
+ });
227
185
  }
228
186
 
229
187
  function patchApiDockerfile(targetRoot) {
@@ -321,10 +279,12 @@ Current scope:
321
279
 
322
280
  export function applyRbacModule({ packageRoot, targetRoot }) {
323
281
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'rbac'));
282
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'rbac' });
283
+
324
284
  patchApiPackage(targetRoot);
325
285
  patchAppModule(targetRoot);
326
- patchHealthController(targetRoot);
327
- patchWebApp(targetRoot);
286
+ patchHealthController(targetRoot, probeTargets);
287
+ registerWebProbe(targetRoot, probeTargets);
328
288
  patchApiDockerfile(targetRoot);
329
289
  patchReadme(targetRoot);
330
290
  }
@@ -3,15 +3,13 @@ import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import {
5
5
  ensureBuildSteps,
6
- ensureClassMember,
7
6
  ensureDependency,
8
- ensureImportLine,
9
7
  ensureLineAfter,
10
8
  ensureLineBefore,
11
- ensureLoadItem,
12
- ensureValidatorSchema,
13
9
  upsertEnvLines,
14
10
  } from './shared/patch-utils.mjs';
11
+ import { patchAppModuleRegistration, patchHealthControllerServiceProbe } from './shared/nest-runtime-wiring.mjs';
12
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
15
13
 
16
14
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
17
15
  const source = path.join(packageRoot, 'templates', 'module-presets', 'scheduler', relativePath);
@@ -35,169 +33,47 @@ function patchApiPackage(targetRoot) {
35
33
  }
36
34
 
37
35
  function patchAppModule(targetRoot) {
38
- const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
39
- if (!fs.existsSync(filePath)) {
40
- return;
41
- }
42
-
43
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
44
- content = ensureImportLine(
45
- content,
46
- "import { ForgeonSchedulerModule, schedulerConfig, schedulerEnvSchema } from '@forgeon/scheduler';",
47
- );
48
- content = ensureLoadItem(content, 'schedulerConfig');
49
- content = ensureValidatorSchema(content, 'schedulerEnvSchema');
50
-
51
- if (!content.includes(' ForgeonSchedulerModule,')) {
52
- if (content.includes(' ForgeonQueueModule,')) {
53
- content = ensureLineAfter(content, ' ForgeonQueueModule,', ' ForgeonSchedulerModule,');
54
- } else if (content.includes(' ForgeonI18nModule.register({')) {
55
- content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonSchedulerModule,');
56
- } else if (content.includes(' ForgeonAuthModule.register({')) {
57
- content = ensureLineBefore(content, ' ForgeonAuthModule.register({', ' ForgeonSchedulerModule,');
58
- } else if (content.includes(' ForgeonAuthModule.register(),')) {
59
- content = ensureLineBefore(content, ' ForgeonAuthModule.register(),', ' ForgeonSchedulerModule,');
60
- } else if (content.includes(' DbPrismaModule,')) {
61
- content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonSchedulerModule,');
62
- } else if (content.includes(' ForgeonLoggerModule,')) {
63
- content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonSchedulerModule,');
64
- } else if (content.includes(' ForgeonSwaggerModule,')) {
65
- content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonSchedulerModule,');
66
- } else {
67
- content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonSchedulerModule,');
68
- }
69
- }
70
-
71
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
36
+ patchAppModuleRegistration(targetRoot, {
37
+ importLine: "import { ForgeonSchedulerModule, schedulerConfig, schedulerEnvSchema } from '@forgeon/scheduler';",
38
+ loadItem: 'schedulerConfig',
39
+ envSchema: 'schedulerEnvSchema',
40
+ moduleLine: ' ForgeonSchedulerModule,',
41
+ beforeAnchors: [
42
+ ' ForgeonI18nModule.register({',
43
+ ' ForgeonAuthModule.register({',
44
+ ' ForgeonAuthModule.register(),',
45
+ ],
46
+ afterAnchors: [
47
+ ' ForgeonQueueModule,',
48
+ ' DbPrismaModule,',
49
+ ' ForgeonLoggerModule,',
50
+ ' ForgeonSwaggerModule,',
51
+ ],
52
+ });
72
53
  }
73
54
 
74
- function patchHealthController(targetRoot) {
75
- const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
76
- if (!fs.existsSync(filePath)) {
77
- return;
78
- }
79
-
80
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
81
- content = ensureImportLine(content, "import { ForgeonSchedulerService } from '@forgeon/scheduler';");
82
-
83
- if (!content.includes('private readonly schedulerService: ForgeonSchedulerService')) {
84
- const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
85
- if (constructorMatch) {
86
- const original = constructorMatch[0];
87
- const inner = constructorMatch[1].trimEnd();
88
- const normalizedInner = inner.replace(/,\s*$/, '');
89
- const separator = normalizedInner.length > 0 ? ',' : '';
90
- const next = `constructor(${normalizedInner}${separator}
91
- private readonly schedulerService: ForgeonSchedulerService,
92
- ) {`;
93
- content = content.replace(original, next);
94
- } else {
95
- const classAnchor = 'export class HealthController {';
96
- if (content.includes(classAnchor)) {
97
- content = content.replace(
98
- classAnchor,
99
- `${classAnchor}
100
- constructor(private readonly schedulerService: ForgeonSchedulerService) {}
101
- `,
102
- );
103
- }
104
- }
105
- }
106
-
107
- if (!content.includes("@Get('scheduler')")) {
108
- const method = `
109
- @Get('scheduler')
110
- async getSchedulerProbe() {
111
- return this.schedulerService.getProbeStatus();
112
- }
113
- `;
114
- content = ensureClassMember(content, 'HealthController', method, { beforeNeedle: 'private translate(' });
115
- }
116
-
117
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
55
+ function patchHealthController(targetRoot, probeTargets) {
56
+ patchHealthControllerServiceProbe(targetRoot, probeTargets, {
57
+ importLine: "import { ForgeonSchedulerService } from '@forgeon/scheduler';",
58
+ constructorMember: 'private readonly schedulerService: ForgeonSchedulerService',
59
+ routePath: 'scheduler',
60
+ methodName: 'getSchedulerProbe',
61
+ serviceCall: 'this.schedulerService.getProbeStatus()',
62
+ });
118
63
  }
119
64
 
120
- function patchWebApp(targetRoot) {
121
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
122
- if (!fs.existsSync(filePath)) {
123
- return;
124
- }
125
-
126
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
127
- content = content
128
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
129
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
130
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
131
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
132
-
133
- if (!content.includes('schedulerProbeResult')) {
134
- const stateAnchors = [
135
- ' const [queueProbeResult, setQueueProbeResult] = useState<ProbeResult | null>(null);',
136
- ' const [filesImageProbeResult, setFilesImageProbeResult] = useState<ProbeResult | null>(null);',
137
- ' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
138
- ' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
139
- ' const [filesVariantsProbeResult, setFilesVariantsProbeResult] = useState<ProbeResult | null>(null);',
140
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
141
- ' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
142
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
143
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
144
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
145
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
146
- ];
147
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
148
- if (stateAnchor) {
149
- content = ensureLineAfter(
150
- content,
151
- stateAnchor,
152
- ' const [schedulerProbeResult, setSchedulerProbeResult] = useState<ProbeResult | null>(null);',
153
- );
154
- }
155
- }
156
-
157
- if (!content.includes('Check scheduler health')) {
158
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
159
- ? '/health/scheduler'
160
- : '/api/health/scheduler';
161
- const button = ` <button onClick={() => runProbe(setSchedulerProbeResult, '${probePath}')}>
162
- Check scheduler health
163
- </button>`;
164
-
165
- const actionsStart = content.indexOf('<div className="actions">');
166
- if (actionsStart >= 0) {
167
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
168
- if (actionsEnd >= 0) {
169
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
170
- }
171
- }
172
- }
173
-
174
- if (!content.includes("{renderResult('Scheduler probe response', schedulerProbeResult)}")) {
175
- const resultLine = " {renderResult('Scheduler probe response', schedulerProbeResult)}";
176
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
177
- if (content.includes(networkLine)) {
178
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
179
- } else {
180
- const anchors = [
181
- "{renderResult('Queue probe response', queueProbeResult)}",
182
- "{renderResult('Files image probe response', filesImageProbeResult)}",
183
- "{renderResult('Files quotas probe response', filesQuotasProbeResult)}",
184
- "{renderResult('Files access probe response', filesAccessProbeResult)}",
185
- "{renderResult('Files variants probe response', filesVariantsProbeResult)}",
186
- "{renderResult('Files probe response', filesProbeResult)}",
187
- "{renderResult('RBAC probe response', rbacProbeResult)}",
188
- "{renderResult('Rate limit probe response', rateLimitProbeResult)}",
189
- "{renderResult('Auth probe response', authProbeResult)}",
190
- "{renderResult('DB probe response', dbProbeResult)}",
191
- "{renderResult('Validation probe response', validationProbeResult)}",
192
- ];
193
- const anchor = anchors.find((line) => content.includes(line));
194
- if (anchor) {
195
- content = ensureLineAfter(content, anchor, resultLine);
196
- }
197
- }
198
- }
199
-
200
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
65
+ function registerWebProbe(targetRoot, probeTargets) {
66
+ ensureWebProbeDefinition({
67
+ targetRoot,
68
+ probeTargets,
69
+ definition: {
70
+ id: 'scheduler',
71
+ title: 'Scheduler',
72
+ buttonLabel: 'Check scheduler health',
73
+ resultTitle: 'Scheduler probe response',
74
+ path: '/health/scheduler',
75
+ },
76
+ });
201
77
  }
202
78
 
203
79
  function patchApiDockerfile(targetRoot) {
@@ -346,11 +222,12 @@ Operational notes:
346
222
 
347
223
  export function applySchedulerModule({ packageRoot, targetRoot }) {
348
224
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'scheduler'));
349
-
225
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'scheduler' });
226
+
350
227
  patchApiPackage(targetRoot);
351
228
  patchAppModule(targetRoot);
352
- patchHealthController(targetRoot);
353
- patchWebApp(targetRoot);
229
+ patchHealthController(targetRoot, probeTargets);
230
+ registerWebProbe(targetRoot, probeTargets);
354
231
  patchApiDockerfile(targetRoot);
355
232
  patchCompose(targetRoot);
356
233
  patchReadme(targetRoot);
@@ -365,4 +242,4 @@ export function applySchedulerModule({ packageRoot, targetRoot }) {
365
242
  'SCHEDULER_TIMEZONE=UTC',
366
243
  'SCHEDULER_HEARTBEAT_CRON=*/5 * * * *',
367
244
  ]);
368
- }
245
+ }
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ ensureClassMember,
5
+ ensureImportLine,
6
+ ensureLineAfter,
7
+ ensureLineBefore,
8
+ ensureLoadItem,
9
+ ensureValidatorSchema,
10
+ } from './patch-utils.mjs';
11
+
12
+ function normalize(content) {
13
+ return content.replace(/\r\n/g, '\n');
14
+ }
15
+
16
+ export function patchAppModuleRegistration(targetRoot, options) {
17
+ const {
18
+ importLine,
19
+ loadItem,
20
+ envSchema,
21
+ moduleLine,
22
+ afterAnchors = [],
23
+ beforeAnchors = [],
24
+ fallbackAnchor = ' CoreErrorsModule,',
25
+ } = options;
26
+
27
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
28
+ if (!fs.existsSync(filePath)) {
29
+ return;
30
+ }
31
+
32
+ let content = normalize(fs.readFileSync(filePath, 'utf8'));
33
+ content = ensureImportLine(content, importLine);
34
+ content = ensureLoadItem(content, loadItem);
35
+ content = ensureValidatorSchema(content, envSchema);
36
+
37
+ if (!content.includes(moduleLine)) {
38
+ const beforeAnchor = beforeAnchors.find((anchor) => content.includes(anchor));
39
+ if (beforeAnchor) {
40
+ content = ensureLineBefore(content, beforeAnchor, moduleLine);
41
+ } else {
42
+ const afterAnchor = afterAnchors.find((anchor) => content.includes(anchor)) ?? fallbackAnchor;
43
+ content = ensureLineAfter(content, afterAnchor, moduleLine);
44
+ }
45
+ }
46
+
47
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
48
+ }
49
+
50
+ export function patchHealthControllerServiceProbe(targetRoot, probeTargets, options) {
51
+ if (!probeTargets.allowApi) {
52
+ return;
53
+ }
54
+
55
+ const {
56
+ importLine,
57
+ constructorMember,
58
+ routePath,
59
+ methodName,
60
+ serviceCall,
61
+ className = 'HealthController',
62
+ classAnchor = 'export class HealthController {',
63
+ beforeNeedles = [],
64
+ beforeNeedle = 'private translate(',
65
+ } = options;
66
+
67
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
68
+ if (!fs.existsSync(filePath)) {
69
+ return;
70
+ }
71
+
72
+ let content = normalize(fs.readFileSync(filePath, 'utf8'));
73
+ content = ensureImportLine(content, importLine);
74
+
75
+ if (!content.includes(constructorMember)) {
76
+ const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
77
+ if (constructorMatch) {
78
+ const original = constructorMatch[0];
79
+ const inner = constructorMatch[1].trimEnd();
80
+ const normalizedInner = inner.replace(/,\s*$/, '');
81
+ const separator = normalizedInner.length > 0 ? ',' : '';
82
+ const next = `constructor(${normalizedInner}${separator}
83
+ ${constructorMember},
84
+ ) {`;
85
+ content = content.replace(original, next);
86
+ } else if (content.includes(classAnchor)) {
87
+ content = content.replace(
88
+ classAnchor,
89
+ `${classAnchor}
90
+ constructor(${constructorMember}) {}
91
+ `,
92
+ );
93
+ }
94
+ }
95
+
96
+ const routeDecorator = `@Get('${routePath}')`;
97
+ if (!content.includes(routeDecorator)) {
98
+ const method = `
99
+ ${routeDecorator}
100
+ async ${methodName}() {
101
+ return ${serviceCall};
102
+ }
103
+ `;
104
+ const resolvedBeforeNeedle =
105
+ beforeNeedles.find((needle) => content.includes(needle)) ?? beforeNeedle;
106
+ content = ensureClassMember(content, className, method, { beforeNeedle: resolvedBeforeNeedle });
107
+ }
108
+
109
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
110
+ }