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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ensureLineBefore,
|
|
12
12
|
ensureNestCommonImport,
|
|
13
13
|
} from './shared/patch-utils.mjs';
|
|
14
|
+
import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
|
|
14
15
|
|
|
15
16
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
16
17
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'files-access', relativePath);
|
|
@@ -159,24 +160,6 @@ function patchFilesController(targetRoot) {
|
|
|
159
160
|
}`,
|
|
160
161
|
);
|
|
161
162
|
|
|
162
|
-
content = content.replace(
|
|
163
|
-
` async download(@Param('publicId') publicId: string) {
|
|
164
|
-
const payload = await this.filesService.openDownload(publicId);
|
|
165
|
-
return new StreamableFile(payload.stream, {
|
|
166
|
-
disposition: \`inline; filename="\${payload.fileName}"\`,
|
|
167
|
-
type: payload.mimeType,
|
|
168
|
-
});
|
|
169
|
-
}`,
|
|
170
|
-
` async download(@Param('publicId') publicId: string, @Req() req: any) {
|
|
171
|
-
const file = await this.filesService.getByPublicId(publicId);
|
|
172
|
-
this.filesAccessService.assertCanRead(file, extractFilesAccessSubject(req));
|
|
173
|
-
const payload = await this.filesService.openDownload(publicId);
|
|
174
|
-
return new StreamableFile(payload.stream, {
|
|
175
|
-
disposition: \`inline; filename="\${payload.fileName}"\`,
|
|
176
|
-
type: payload.mimeType,
|
|
177
|
-
});
|
|
178
|
-
}`,
|
|
179
|
-
);
|
|
180
163
|
|
|
181
164
|
content = content.replace(
|
|
182
165
|
` async remove(@Param('publicId') publicId: string) {
|
|
@@ -193,7 +176,11 @@ function patchFilesController(targetRoot) {
|
|
|
193
176
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
194
177
|
}
|
|
195
178
|
|
|
196
|
-
function patchHealthController(targetRoot) {
|
|
179
|
+
function patchHealthController(targetRoot, probeTargets) {
|
|
180
|
+
if (!probeTargets.allowApi) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
197
184
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
|
|
198
185
|
if (!fs.existsSync(filePath)) {
|
|
199
186
|
return;
|
|
@@ -256,83 +243,23 @@ function patchHealthController(targetRoot) {
|
|
|
256
243
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
257
244
|
}
|
|
258
245
|
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
277
|
-
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
278
|
-
' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
|
|
279
|
-
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
280
|
-
];
|
|
281
|
-
const stateAnchor = stateAnchors.find((line) => content.includes(line));
|
|
282
|
-
if (stateAnchor) {
|
|
283
|
-
content = ensureLineAfter(
|
|
284
|
-
content,
|
|
285
|
-
stateAnchor,
|
|
286
|
-
' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (!content.includes('Check files access')) {
|
|
292
|
-
const probePath = content.includes("runProbe(setHealthResult, '/health')")
|
|
293
|
-
? '/health/files-access'
|
|
294
|
-
: '/api/health/files-access';
|
|
295
|
-
const button = ` <button
|
|
296
|
-
onClick={() =>
|
|
297
|
-
runProbe(setFilesAccessProbeResult, '${probePath}', {
|
|
298
|
-
headers: { 'x-forgeon-user-id': 'probe-owner' },
|
|
299
|
-
})
|
|
300
|
-
}
|
|
301
|
-
>
|
|
302
|
-
Check files access
|
|
303
|
-
</button>`;
|
|
304
|
-
|
|
305
|
-
const actionsStart = content.indexOf('<div className="actions">');
|
|
306
|
-
if (actionsStart >= 0) {
|
|
307
|
-
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
308
|
-
if (actionsEnd >= 0) {
|
|
309
|
-
content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (!content.includes("{renderResult('Files access probe response', filesAccessProbeResult)}")) {
|
|
315
|
-
const resultLine = " {renderResult('Files access probe response', filesAccessProbeResult)}";
|
|
316
|
-
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
317
|
-
if (content.includes(networkLine)) {
|
|
318
|
-
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
319
|
-
} else {
|
|
320
|
-
const anchors = [
|
|
321
|
-
" {renderResult('Files probe response', filesProbeResult)}",
|
|
322
|
-
" {renderResult('RBAC probe response', rbacProbeResult)}",
|
|
323
|
-
" {renderResult('Rate limit probe response', rateLimitProbeResult)}",
|
|
324
|
-
" {renderResult('Auth probe response', authProbeResult)}",
|
|
325
|
-
" {renderResult('DB probe response', dbProbeResult)}",
|
|
326
|
-
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
327
|
-
];
|
|
328
|
-
const anchor = anchors.find((line) => content.includes(line));
|
|
329
|
-
if (anchor) {
|
|
330
|
-
content = ensureLineAfter(content, anchor, resultLine);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
246
|
+
function registerWebProbe(targetRoot, probeTargets) {
|
|
247
|
+
ensureWebProbeDefinition({
|
|
248
|
+
targetRoot,
|
|
249
|
+
probeTargets,
|
|
250
|
+
definition: {
|
|
251
|
+
id: 'files-access',
|
|
252
|
+
title: 'Files Access',
|
|
253
|
+
buttonLabel: 'Check files access',
|
|
254
|
+
resultTitle: 'Files access probe response',
|
|
255
|
+
path: '/health/files-access',
|
|
256
|
+
request: {
|
|
257
|
+
headers: {
|
|
258
|
+
'x-forgeon-user-id': 'probe-owner',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
});
|
|
336
263
|
}
|
|
337
264
|
|
|
338
265
|
function patchApiDockerfile(targetRoot) {
|
|
@@ -435,12 +362,14 @@ Actor context for probe/testing:
|
|
|
435
362
|
|
|
436
363
|
export function applyFilesAccessModule({ packageRoot, targetRoot }) {
|
|
437
364
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-access'));
|
|
365
|
+
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-access' });
|
|
366
|
+
|
|
438
367
|
patchApiPackage(targetRoot);
|
|
439
368
|
patchFilesPackage(targetRoot);
|
|
440
369
|
patchAppModule(targetRoot);
|
|
441
370
|
patchFilesController(targetRoot);
|
|
442
|
-
patchHealthController(targetRoot);
|
|
443
|
-
|
|
371
|
+
patchHealthController(targetRoot, probeTargets);
|
|
372
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
444
373
|
patchApiDockerfile(targetRoot);
|
|
445
374
|
patchReadme(targetRoot);
|
|
446
375
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ensureValidatorSchema,
|
|
15
15
|
upsertEnvLines,
|
|
16
16
|
} from './shared/patch-utils.mjs';
|
|
17
|
+
import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
|
|
17
18
|
|
|
18
19
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
19
20
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'files-image', relativePath);
|
|
@@ -76,27 +77,6 @@ function patchRootPackage(targetRoot) {
|
|
|
76
77
|
writeJson(packagePath, packageJson);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function patchFilesTypes(targetRoot) {
|
|
80
|
-
const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'files.types.ts');
|
|
81
|
-
if (!fs.existsSync(filePath)) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
86
|
-
if (!content.includes('auditContext?:')) {
|
|
87
|
-
content = content.replace(
|
|
88
|
-
/(\s+createdById\?: string;\n)(\};)/m,
|
|
89
|
-
`$1 auditContext?: {
|
|
90
|
-
requestId?: string | null;
|
|
91
|
-
ip?: string | null;
|
|
92
|
-
userId?: string | null;
|
|
93
|
-
};
|
|
94
|
-
$2`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
99
|
-
}
|
|
100
80
|
|
|
101
81
|
function patchAppModule(targetRoot) {
|
|
102
82
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
@@ -143,14 +123,10 @@ function patchFilesModule(targetRoot) {
|
|
|
143
123
|
|
|
144
124
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
145
125
|
content = ensureImportLine(content, "import { ForgeonFilesImageModule } from '@forgeon/files-image';");
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'imports: [FilesConfigModule],',
|
|
151
|
-
'imports: [FilesConfigModule, ForgeonFilesImageModule],',
|
|
152
|
-
);
|
|
153
|
-
}
|
|
126
|
+
content = content.replace(
|
|
127
|
+
'imports: [FilesConfigModule],',
|
|
128
|
+
'imports: [FilesConfigModule, ForgeonFilesImageModule],',
|
|
129
|
+
);
|
|
154
130
|
|
|
155
131
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
156
132
|
}
|
|
@@ -298,7 +274,11 @@ function patchFilesService(targetRoot) {
|
|
|
298
274
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
299
275
|
}
|
|
300
276
|
|
|
301
|
-
function patchHealthController(targetRoot) {
|
|
277
|
+
function patchHealthController(targetRoot, probeTargets) {
|
|
278
|
+
if (!probeTargets.allowApi) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
302
282
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
|
|
303
283
|
if (!fs.existsSync(filePath)) {
|
|
304
284
|
return;
|
|
@@ -335,73 +315,18 @@ function patchHealthController(targetRoot) {
|
|
|
335
315
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
336
316
|
}
|
|
337
317
|
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (!content.includes('filesImageProbeResult')) {
|
|
352
|
-
const stateAnchors = [
|
|
353
|
-
' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
|
|
354
|
-
' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
|
|
355
|
-
' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
|
|
356
|
-
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
357
|
-
];
|
|
358
|
-
const stateAnchor = stateAnchors.find((line) => content.includes(line));
|
|
359
|
-
if (stateAnchor) {
|
|
360
|
-
content = ensureLineAfter(
|
|
361
|
-
content,
|
|
362
|
-
stateAnchor,
|
|
363
|
-
' const [filesImageProbeResult, setFilesImageProbeResult] = useState<ProbeResult | null>(null);',
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!content.includes('Check files image sanitize')) {
|
|
369
|
-
const probePath = content.includes("runProbe(setHealthResult, '/health')")
|
|
370
|
-
? '/health/files-image'
|
|
371
|
-
: '/api/health/files-image';
|
|
372
|
-
const button = ` <button onClick={() => runProbe(setFilesImageProbeResult, '${probePath}')}>
|
|
373
|
-
Check files image sanitize
|
|
374
|
-
</button>`;
|
|
375
|
-
|
|
376
|
-
const actionsStart = content.indexOf('<div className="actions">');
|
|
377
|
-
if (actionsStart >= 0) {
|
|
378
|
-
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
379
|
-
if (actionsEnd >= 0) {
|
|
380
|
-
content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (!content.includes("{renderResult('Files image probe response', filesImageProbeResult)}")) {
|
|
386
|
-
const resultLine = " {renderResult('Files image probe response', filesImageProbeResult)}";
|
|
387
|
-
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
388
|
-
if (content.includes(networkLine)) {
|
|
389
|
-
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
390
|
-
} else {
|
|
391
|
-
const anchors = [
|
|
392
|
-
" {renderResult('Files quotas probe response', filesQuotasProbeResult)}",
|
|
393
|
-
" {renderResult('Files access probe response', filesAccessProbeResult)}",
|
|
394
|
-
" {renderResult('Files probe response', filesProbeResult)}",
|
|
395
|
-
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
396
|
-
];
|
|
397
|
-
const anchor = anchors.find((line) => content.includes(line));
|
|
398
|
-
if (anchor) {
|
|
399
|
-
content = ensureLineAfter(content, anchor, resultLine);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
318
|
+
function registerWebProbe(targetRoot, probeTargets) {
|
|
319
|
+
ensureWebProbeDefinition({
|
|
320
|
+
targetRoot,
|
|
321
|
+
probeTargets,
|
|
322
|
+
definition: {
|
|
323
|
+
id: 'files-image',
|
|
324
|
+
title: 'Files Image',
|
|
325
|
+
buttonLabel: 'Check files image sanitize',
|
|
326
|
+
resultTitle: 'Files image probe response',
|
|
327
|
+
path: '/health/files-image',
|
|
328
|
+
},
|
|
329
|
+
});
|
|
405
330
|
}
|
|
406
331
|
|
|
407
332
|
function patchApiDockerfile(targetRoot) {
|
|
@@ -547,16 +472,17 @@ Key env:
|
|
|
547
472
|
|
|
548
473
|
export function applyFilesImageModule({ packageRoot, targetRoot }) {
|
|
549
474
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-image'));
|
|
475
|
+
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-image' });
|
|
476
|
+
|
|
550
477
|
patchApiPackage(targetRoot);
|
|
551
478
|
patchFilesPackage(targetRoot);
|
|
552
479
|
patchRootPackage(targetRoot);
|
|
553
|
-
patchFilesTypes(targetRoot);
|
|
554
480
|
patchAppModule(targetRoot);
|
|
555
481
|
patchFilesModule(targetRoot);
|
|
556
482
|
patchFilesController(targetRoot);
|
|
557
483
|
patchFilesService(targetRoot);
|
|
558
|
-
patchHealthController(targetRoot);
|
|
559
|
-
|
|
484
|
+
patchHealthController(targetRoot, probeTargets);
|
|
485
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
560
486
|
patchApiDockerfile(targetRoot);
|
|
561
487
|
patchCompose(targetRoot);
|
|
562
488
|
patchReadme(targetRoot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ensureValidatorSchema,
|
|
15
15
|
upsertEnvLines,
|
|
16
16
|
} from './shared/patch-utils.mjs';
|
|
17
|
+
import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
|
|
17
18
|
|
|
18
19
|
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
19
20
|
const source = path.join(packageRoot, 'templates', 'module-presets', 'files-quotas', relativePath);
|
|
@@ -49,7 +50,7 @@ function patchFilesPackage(targetRoot) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
52
|
-
ensureDependency(packageJson, '@
|
|
53
|
+
ensureDependency(packageJson, '@nestjs/core', '^11.0.1');
|
|
53
54
|
writeJson(packagePath, packageJson);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -99,9 +100,24 @@ function patchFilesController(targetRoot) {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
102
|
-
content = ensureImportLine(content, "import {
|
|
103
|
+
content = ensureImportLine(content, "import { ModuleRef } from '@nestjs/core';");
|
|
103
104
|
|
|
104
|
-
if (!content.includes(
|
|
105
|
+
if (!content.includes("const FORGEON_FILES_UPLOAD_QUOTA_SERVICE = 'FORGEON_FILES_UPLOAD_QUOTA_SERVICE';")) {
|
|
106
|
+
content = content.replace(
|
|
107
|
+
'};\n\n@Controller(\'files\')',
|
|
108
|
+
`};
|
|
109
|
+
|
|
110
|
+
const FORGEON_FILES_UPLOAD_QUOTA_SERVICE = 'FORGEON_FILES_UPLOAD_QUOTA_SERVICE';
|
|
111
|
+
|
|
112
|
+
type FilesUploadQuotaService = {
|
|
113
|
+
assertUploadAllowed(input: { ownerType: string; ownerId: string | null; fileSize: number }): Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
@Controller('files')`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!content.includes('private readonly moduleRef: ModuleRef')) {
|
|
105
121
|
const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
|
|
106
122
|
if (constructorMatch) {
|
|
107
123
|
const original = constructorMatch[0];
|
|
@@ -109,29 +125,51 @@ function patchFilesController(targetRoot) {
|
|
|
109
125
|
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
110
126
|
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
111
127
|
const next = `constructor(${normalizedInner}${separator}
|
|
112
|
-
private readonly
|
|
128
|
+
private readonly moduleRef: ModuleRef,
|
|
113
129
|
) {`;
|
|
114
130
|
content = content.replace(original, next);
|
|
115
131
|
}
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
if (!content.includes('filesQuotasService.
|
|
134
|
+
if (!content.includes('const filesQuotasService = this.getFilesUploadQuotaService();')) {
|
|
119
135
|
content = content.replace(
|
|
120
136
|
' return this.filesService.create({',
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
` const filesQuotasService = this.getFilesUploadQuotaService();
|
|
138
|
+
if (filesQuotasService) {
|
|
139
|
+
await filesQuotasService.assertUploadAllowed({
|
|
140
|
+
ownerType: body.ownerType ?? 'system',
|
|
141
|
+
ownerId: body.ownerId ?? null,
|
|
142
|
+
fileSize: file.size,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
126
145
|
|
|
127
146
|
return this.filesService.create({`,
|
|
128
147
|
);
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
if (!content.includes('private getFilesUploadQuotaService(): FilesUploadQuotaService | null {')) {
|
|
151
|
+
content = content.replace(
|
|
152
|
+
' private parseVariant(variantQuery?: string): FileVariantKey {',
|
|
153
|
+
` private getFilesUploadQuotaService(): FilesUploadQuotaService | null {
|
|
154
|
+
try {
|
|
155
|
+
return this.moduleRef.get(FORGEON_FILES_UPLOAD_QUOTA_SERVICE, { strict: false });
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private parseVariant(variantQuery?: string): FileVariantKey {`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
131
165
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
132
166
|
}
|
|
133
167
|
|
|
134
|
-
function patchHealthController(targetRoot) {
|
|
168
|
+
function patchHealthController(targetRoot, probeTargets) {
|
|
169
|
+
if (!probeTargets.allowApi) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
|
|
136
174
|
if (!fs.existsSync(filePath)) {
|
|
137
175
|
return;
|
|
@@ -177,79 +215,18 @@ function patchHealthController(targetRoot) {
|
|
|
177
215
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
178
216
|
}
|
|
179
217
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!content.includes('filesQuotasProbeResult')) {
|
|
194
|
-
const stateAnchors = [
|
|
195
|
-
' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
|
|
196
|
-
' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
|
|
197
|
-
' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
|
|
198
|
-
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
199
|
-
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
200
|
-
' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
|
|
201
|
-
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
202
|
-
];
|
|
203
|
-
const stateAnchor = stateAnchors.find((line) => content.includes(line));
|
|
204
|
-
if (stateAnchor) {
|
|
205
|
-
content = ensureLineAfter(
|
|
206
|
-
content,
|
|
207
|
-
stateAnchor,
|
|
208
|
-
' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (!content.includes('Check files quotas')) {
|
|
214
|
-
const probePath = content.includes("runProbe(setHealthResult, '/health')")
|
|
215
|
-
? '/health/files-quotas'
|
|
216
|
-
: '/api/health/files-quotas';
|
|
217
|
-
const button = ` <button onClick={() => runProbe(setFilesQuotasProbeResult, '${probePath}')}>
|
|
218
|
-
Check files quotas
|
|
219
|
-
</button>`;
|
|
220
|
-
|
|
221
|
-
const actionsStart = content.indexOf('<div className="actions">');
|
|
222
|
-
if (actionsStart >= 0) {
|
|
223
|
-
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
224
|
-
if (actionsEnd >= 0) {
|
|
225
|
-
content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!content.includes("{renderResult('Files quotas probe response', filesQuotasProbeResult)}")) {
|
|
231
|
-
const resultLine = " {renderResult('Files quotas probe response', filesQuotasProbeResult)}";
|
|
232
|
-
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
233
|
-
if (content.includes(networkLine)) {
|
|
234
|
-
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
235
|
-
} else {
|
|
236
|
-
const anchors = [
|
|
237
|
-
" {renderResult('Files access probe response', filesAccessProbeResult)}",
|
|
238
|
-
" {renderResult('Files probe response', filesProbeResult)}",
|
|
239
|
-
" {renderResult('RBAC probe response', rbacProbeResult)}",
|
|
240
|
-
" {renderResult('Rate limit probe response', rateLimitProbeResult)}",
|
|
241
|
-
" {renderResult('Auth probe response', authProbeResult)}",
|
|
242
|
-
" {renderResult('DB probe response', dbProbeResult)}",
|
|
243
|
-
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
244
|
-
];
|
|
245
|
-
const anchor = anchors.find((line) => content.includes(line));
|
|
246
|
-
if (anchor) {
|
|
247
|
-
content = ensureLineAfter(content, anchor, resultLine);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
218
|
+
function registerWebProbe(targetRoot, probeTargets) {
|
|
219
|
+
ensureWebProbeDefinition({
|
|
220
|
+
targetRoot,
|
|
221
|
+
probeTargets,
|
|
222
|
+
definition: {
|
|
223
|
+
id: 'files-quotas',
|
|
224
|
+
title: 'Files Quotas',
|
|
225
|
+
buttonLabel: 'Check files quotas',
|
|
226
|
+
resultTitle: 'Files quotas probe response',
|
|
227
|
+
path: '/health/files-quotas',
|
|
228
|
+
},
|
|
229
|
+
});
|
|
253
230
|
}
|
|
254
231
|
|
|
255
232
|
function patchApiDockerfile(targetRoot) {
|
|
@@ -379,12 +356,14 @@ Key env:
|
|
|
379
356
|
|
|
380
357
|
export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
|
|
381
358
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
|
|
359
|
+
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-quotas' });
|
|
360
|
+
|
|
382
361
|
patchApiPackage(targetRoot);
|
|
383
362
|
patchFilesPackage(targetRoot);
|
|
384
363
|
patchAppModule(targetRoot);
|
|
385
364
|
patchFilesController(targetRoot);
|
|
386
|
-
patchHealthController(targetRoot);
|
|
387
|
-
|
|
365
|
+
patchHealthController(targetRoot, probeTargets);
|
|
366
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
388
367
|
patchApiDockerfile(targetRoot);
|
|
389
368
|
patchCompose(targetRoot);
|
|
390
369
|
patchReadme(targetRoot);
|
|
@@ -400,3 +379,4 @@ export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
|
|
|
400
379
|
'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
|
|
401
380
|
]);
|
|
402
381
|
}
|
|
382
|
+
|