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.
- package/package.json +4 -2
- package/src/core/docs.test.mjs +79 -40
- package/src/core/scaffold.test.mjs +99 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/executor.test.mjs +132 -36
- package/src/modules/files-access.mjs +27 -98
- package/src/modules/files-image.mjs +26 -100
- package/src/modules/files-quotas.mjs +67 -87
- package/src/modules/files.mjs +35 -104
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +174 -0
- package/src/modules/jwt-auth.mjs +90 -209
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +202 -0
- package/src/modules/queue.mjs +325 -443
- package/src/modules/rate-limit.mjs +22 -66
- package/src/modules/rbac.mjs +27 -67
- package/src/modules/scheduler.mjs +44 -167
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +54 -21
- package/src/modules/sync-integrations.test.mjs +220 -0
- package/src/run-add-module.test.mjs +153 -0
- package/templates/base/README.md +7 -55
- package/templates/base/apps/web/src/App.tsx +70 -42
- package/templates/base/apps/web/src/probes.ts +61 -0
- package/templates/base/apps/web/src/styles.css +86 -25
- package/templates/base/package.json +21 -15
- package/templates/base/scripts/forgeon-sync-integrations.mjs +55 -11
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
- package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
- package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
- package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
- package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
- package/templates/base/docs/AI/PROJECT.md +0 -43
- package/templates/base/docs/AI/ROADMAP.md +0 -171
- package/templates/base/docs/AI/TASKS.md +0 -60
- package/templates/base/docs/AI/VALIDATION.md +0 -31
- package/templates/base/docs/README.md +0 -18
- 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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
287
|
+
patchHealthController(targetRoot, probeTargets);
|
|
288
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
333
289
|
patchApiDockerfile(targetRoot);
|
|
334
290
|
patchCompose(targetRoot);
|
|
335
291
|
patchReadme(targetRoot);
|
package/src/modules/rbac.mjs
CHANGED
|
@@ -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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
+
}
|