create-forgeon 0.3.20 → 0.3.21

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.
@@ -1,384 +1,384 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
- import {
5
- ensureBuildStepBefore,
6
- ensureBuildSteps,
7
- ensureClassMember,
8
- ensureDependency,
9
- ensureImportLine,
10
- ensureLineAfter,
11
- ensureLineBefore,
12
- ensureLoadItem,
13
- ensureNestCommonImport,
14
- ensureValidatorSchema,
15
- upsertEnvLines,
16
- } from './shared/patch-utils.mjs';
17
- import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
18
-
19
- function copyFromPreset(packageRoot, targetRoot, relativePath) {
20
- const source = path.join(packageRoot, 'templates', 'module-presets', 'files-quotas', relativePath);
21
- if (!fs.existsSync(source)) {
22
- throw new Error(`Missing files-quotas preset template: ${source}`);
23
- }
24
- const destination = path.join(targetRoot, relativePath);
25
- copyRecursive(source, destination);
26
- }
27
-
28
- function patchApiPackage(targetRoot) {
29
- const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
30
- if (!fs.existsSync(packagePath)) {
31
- return;
32
- }
33
-
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
+ import {
5
+ ensureBuildStepBefore,
6
+ ensureBuildSteps,
7
+ ensureClassMember,
8
+ ensureDependency,
9
+ ensureImportLine,
10
+ ensureLineAfter,
11
+ ensureLineBefore,
12
+ ensureLoadItem,
13
+ ensureNestCommonImport,
14
+ ensureValidatorSchema,
15
+ upsertEnvLines,
16
+ } from './shared/patch-utils.mjs';
17
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
18
+
19
+ function copyFromPreset(packageRoot, targetRoot, relativePath) {
20
+ const source = path.join(packageRoot, 'templates', 'module-presets', 'files-quotas', relativePath);
21
+ if (!fs.existsSync(source)) {
22
+ throw new Error(`Missing files-quotas preset template: ${source}`);
23
+ }
24
+ const destination = path.join(targetRoot, relativePath);
25
+ copyRecursive(source, destination);
26
+ }
27
+
28
+ function patchApiPackage(targetRoot) {
29
+ const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
30
+ if (!fs.existsSync(packagePath)) {
31
+ return;
32
+ }
33
+
34
34
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
35
35
  ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
36
36
  ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-quotas build']);
37
37
  ensureBuildStepBefore(
38
38
  packageJson,
39
39
  'predev',
40
- 'pnpm --filter @forgeon/files-quotas build',
41
40
  'pnpm --filter @forgeon/files build',
41
+ 'pnpm --filter @forgeon/files-quotas build',
42
42
  );
43
43
  writeJson(packagePath, packageJson);
44
44
  }
45
-
46
- function patchFilesPackage(targetRoot) {
47
- const packagePath = path.join(targetRoot, 'packages', 'files', 'package.json');
48
- if (!fs.existsSync(packagePath)) {
49
- return;
50
- }
51
-
52
- const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
53
- ensureDependency(packageJson, '@nestjs/core', '^11.0.1');
54
- writeJson(packagePath, packageJson);
55
- }
56
-
57
- function patchAppModule(targetRoot) {
58
- const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
59
- if (!fs.existsSync(filePath)) {
60
- return;
61
- }
62
-
63
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
64
- content = ensureImportLine(
65
- content,
66
- "import { filesQuotasConfig, filesQuotasEnvSchema, ForgeonFilesQuotasModule } from '@forgeon/files-quotas';",
67
- );
68
- content = ensureLoadItem(content, 'filesQuotasConfig');
69
- content = ensureValidatorSchema(content, 'filesQuotasEnvSchema');
70
-
71
- if (!content.includes(' ForgeonFilesQuotasModule,')) {
72
- if (content.includes(' ForgeonFilesAccessModule,')) {
73
- content = ensureLineAfter(content, ' ForgeonFilesAccessModule,', ' ForgeonFilesQuotasModule,');
74
- } else if (content.includes(' ForgeonFilesModule,')) {
75
- content = ensureLineAfter(content, ' ForgeonFilesModule,', ' ForgeonFilesQuotasModule,');
76
- } else if (content.includes(' ForgeonI18nModule.register({')) {
77
- content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonFilesQuotasModule,');
78
- } else if (content.includes(' ForgeonAccountsModule.register({')) {
79
- content = ensureLineBefore(content, ' ForgeonAccountsModule.register({', ' ForgeonFilesQuotasModule,');
80
- } else if (content.includes(' ForgeonAccountsModule.register(),')) {
81
- content = ensureLineBefore(content, ' ForgeonAccountsModule.register(),', ' ForgeonFilesQuotasModule,');
82
- } else if (content.includes(' DbPrismaModule,')) {
83
- content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonFilesQuotasModule,');
84
- } else if (content.includes(' ForgeonLoggerModule,')) {
85
- content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonFilesQuotasModule,');
86
- } else if (content.includes(' ForgeonSwaggerModule,')) {
87
- content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonFilesQuotasModule,');
88
- } else {
89
- content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonFilesQuotasModule,');
90
- }
91
- }
92
-
93
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
94
- }
95
-
96
- function patchFilesController(targetRoot) {
97
- const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'files.controller.ts');
98
- if (!fs.existsSync(filePath)) {
99
- return;
100
- }
101
-
102
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
103
- content = ensureImportLine(content, "import { ModuleRef } from '@nestjs/core';");
104
-
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')) {
121
- const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
122
- if (constructorMatch) {
123
- const original = constructorMatch[0];
124
- const inner = constructorMatch[1].trimEnd();
125
- const normalizedInner = inner.replace(/,\s*$/, '');
126
- const separator = normalizedInner.length > 0 ? ',' : '';
127
- const next = `constructor(${normalizedInner}${separator}
128
- private readonly moduleRef: ModuleRef,
129
- ) {`;
130
- content = content.replace(original, next);
131
- }
132
- }
133
-
134
- if (!content.includes('const filesQuotasService = this.getFilesUploadQuotaService();')) {
135
- content = content.replace(
136
- ' return this.filesService.create({',
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
- }
145
-
146
- return this.filesService.create({`,
147
- );
148
- }
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
-
165
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
166
- }
167
-
168
- function patchHealthController(targetRoot, probeTargets) {
169
- if (!probeTargets.allowApi) {
170
- return;
171
- }
172
-
173
- const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
174
- if (!fs.existsSync(filePath)) {
175
- return;
176
- }
177
-
178
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
179
- content = ensureNestCommonImport(content, 'Query');
180
- content = ensureImportLine(content, "import { FilesQuotasService } from '@forgeon/files-quotas';");
181
-
182
- if (!content.includes('private readonly filesQuotasService: FilesQuotasService')) {
183
- const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
184
- if (constructorMatch) {
185
- const original = constructorMatch[0];
186
- const inner = constructorMatch[1].trimEnd();
187
- const normalizedInner = inner.replace(/,\s*$/, '');
188
- const separator = normalizedInner.length > 0 ? ',' : '';
189
- const next = `constructor(${normalizedInner}${separator}
190
- private readonly filesQuotasService: FilesQuotasService,
191
- ) {`;
192
- content = content.replace(original, next);
193
- }
194
- }
195
-
196
- if (!content.includes("@Get('files-quotas')")) {
197
- const method = `
198
- @Get('files-quotas')
199
- async getFilesQuotasProbe(
200
- @Query('ownerType') ownerType = 'user',
201
- @Query('ownerId') ownerId = 'probe-owner',
202
- @Query('size') size = '1024',
203
- ) {
204
- const parsedSize = Number.isFinite(Number(size)) ? Math.max(1, Number(size)) : 1024;
205
- return this.filesQuotasService.getProbeStatus({
206
- ownerType,
207
- ownerId: ownerId || null,
208
- fileSize: parsedSize,
209
- });
210
- }
211
- `;
212
- content = ensureClassMember(content, 'HealthController', method, { beforeNeedle: 'private translate(' });
213
- }
214
-
215
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
216
- }
217
-
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
- });
230
- }
231
-
232
- function patchApiDockerfile(targetRoot) {
233
- const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
234
- if (!fs.existsSync(dockerfilePath)) {
235
- return;
236
- }
237
-
238
- let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
239
- const packageAnchors = [
240
- 'COPY packages/files-access/package.json packages/files-access/package.json',
241
- 'COPY packages/files/package.json packages/files/package.json',
242
- 'COPY packages/files-local/package.json packages/files-local/package.json',
243
- 'COPY packages/files-s3/package.json packages/files-s3/package.json',
244
- 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
245
- 'COPY packages/rbac/package.json packages/rbac/package.json',
246
- 'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
247
- 'COPY packages/logger/package.json packages/logger/package.json',
248
- 'COPY packages/swagger/package.json packages/swagger/package.json',
249
- 'COPY packages/i18n/package.json packages/i18n/package.json',
250
- 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
251
- 'COPY packages/core/package.json packages/core/package.json',
252
- ];
253
- const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
254
- content = ensureLineAfter(
255
- content,
256
- packageAnchor,
257
- 'COPY packages/files-quotas/package.json packages/files-quotas/package.json',
258
- );
259
-
260
- const sourceAnchors = [
261
- 'COPY packages/files-access packages/files-access',
262
- 'COPY packages/files packages/files',
263
- 'COPY packages/files-local packages/files-local',
264
- 'COPY packages/files-s3 packages/files-s3',
265
- 'COPY packages/accounts-api packages/accounts-api',
266
- 'COPY packages/rbac packages/rbac',
267
- 'COPY packages/rate-limit packages/rate-limit',
268
- 'COPY packages/logger packages/logger',
269
- 'COPY packages/swagger packages/swagger',
270
- 'COPY packages/i18n packages/i18n',
271
- 'COPY packages/db-prisma packages/db-prisma',
272
- 'COPY packages/core packages/core',
273
- ];
274
- const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
275
- content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-quotas packages/files-quotas');
276
-
45
+
46
+ function patchFilesPackage(targetRoot) {
47
+ const packagePath = path.join(targetRoot, 'packages', 'files', 'package.json');
48
+ if (!fs.existsSync(packagePath)) {
49
+ return;
50
+ }
51
+
52
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
53
+ ensureDependency(packageJson, '@nestjs/core', '^11.0.1');
54
+ writeJson(packagePath, packageJson);
55
+ }
56
+
57
+ function patchAppModule(targetRoot) {
58
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
59
+ if (!fs.existsSync(filePath)) {
60
+ return;
61
+ }
62
+
63
+ let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
64
+ content = ensureImportLine(
65
+ content,
66
+ "import { filesQuotasConfig, filesQuotasEnvSchema, ForgeonFilesQuotasModule } from '@forgeon/files-quotas';",
67
+ );
68
+ content = ensureLoadItem(content, 'filesQuotasConfig');
69
+ content = ensureValidatorSchema(content, 'filesQuotasEnvSchema');
70
+
71
+ if (!content.includes(' ForgeonFilesQuotasModule,')) {
72
+ if (content.includes(' ForgeonFilesAccessModule,')) {
73
+ content = ensureLineAfter(content, ' ForgeonFilesAccessModule,', ' ForgeonFilesQuotasModule,');
74
+ } else if (content.includes(' ForgeonFilesModule,')) {
75
+ content = ensureLineAfter(content, ' ForgeonFilesModule,', ' ForgeonFilesQuotasModule,');
76
+ } else if (content.includes(' ForgeonI18nModule.register({')) {
77
+ content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonFilesQuotasModule,');
78
+ } else if (content.includes(' ForgeonAccountsModule.register({')) {
79
+ content = ensureLineBefore(content, ' ForgeonAccountsModule.register({', ' ForgeonFilesQuotasModule,');
80
+ } else if (content.includes(' ForgeonAccountsModule.register(),')) {
81
+ content = ensureLineBefore(content, ' ForgeonAccountsModule.register(),', ' ForgeonFilesQuotasModule,');
82
+ } else if (content.includes(' DbPrismaModule,')) {
83
+ content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonFilesQuotasModule,');
84
+ } else if (content.includes(' ForgeonLoggerModule,')) {
85
+ content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonFilesQuotasModule,');
86
+ } else if (content.includes(' ForgeonSwaggerModule,')) {
87
+ content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonFilesQuotasModule,');
88
+ } else {
89
+ content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonFilesQuotasModule,');
90
+ }
91
+ }
92
+
93
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
94
+ }
95
+
96
+ function patchFilesController(targetRoot) {
97
+ const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'files.controller.ts');
98
+ if (!fs.existsSync(filePath)) {
99
+ return;
100
+ }
101
+
102
+ let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
103
+ content = ensureImportLine(content, "import { ModuleRef } from '@nestjs/core';");
104
+
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')) {
121
+ const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
122
+ if (constructorMatch) {
123
+ const original = constructorMatch[0];
124
+ const inner = constructorMatch[1].trimEnd();
125
+ const normalizedInner = inner.replace(/,\s*$/, '');
126
+ const separator = normalizedInner.length > 0 ? ',' : '';
127
+ const next = `constructor(${normalizedInner}${separator}
128
+ private readonly moduleRef: ModuleRef,
129
+ ) {`;
130
+ content = content.replace(original, next);
131
+ }
132
+ }
133
+
134
+ if (!content.includes('const filesQuotasService = this.getFilesUploadQuotaService();')) {
135
+ content = content.replace(
136
+ ' return this.filesService.create({',
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
+ }
145
+
146
+ return this.filesService.create({`,
147
+ );
148
+ }
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
+
165
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
166
+ }
167
+
168
+ function patchHealthController(targetRoot, probeTargets) {
169
+ if (!probeTargets.allowApi) {
170
+ return;
171
+ }
172
+
173
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
174
+ if (!fs.existsSync(filePath)) {
175
+ return;
176
+ }
177
+
178
+ let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
179
+ content = ensureNestCommonImport(content, 'Query');
180
+ content = ensureImportLine(content, "import { FilesQuotasService } from '@forgeon/files-quotas';");
181
+
182
+ if (!content.includes('private readonly filesQuotasService: FilesQuotasService')) {
183
+ const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
184
+ if (constructorMatch) {
185
+ const original = constructorMatch[0];
186
+ const inner = constructorMatch[1].trimEnd();
187
+ const normalizedInner = inner.replace(/,\s*$/, '');
188
+ const separator = normalizedInner.length > 0 ? ',' : '';
189
+ const next = `constructor(${normalizedInner}${separator}
190
+ private readonly filesQuotasService: FilesQuotasService,
191
+ ) {`;
192
+ content = content.replace(original, next);
193
+ }
194
+ }
195
+
196
+ if (!content.includes("@Get('files-quotas')")) {
197
+ const method = `
198
+ @Get('files-quotas')
199
+ async getFilesQuotasProbe(
200
+ @Query('ownerType') ownerType = 'user',
201
+ @Query('ownerId') ownerId = 'probe-owner',
202
+ @Query('size') size = '1024',
203
+ ) {
204
+ const parsedSize = Number.isFinite(Number(size)) ? Math.max(1, Number(size)) : 1024;
205
+ return this.filesQuotasService.getProbeStatus({
206
+ ownerType,
207
+ ownerId: ownerId || null,
208
+ fileSize: parsedSize,
209
+ });
210
+ }
211
+ `;
212
+ content = ensureClassMember(content, 'HealthController', method, { beforeNeedle: 'private translate(' });
213
+ }
214
+
215
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
216
+ }
217
+
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
+ });
230
+ }
231
+
232
+ function patchApiDockerfile(targetRoot) {
233
+ const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
234
+ if (!fs.existsSync(dockerfilePath)) {
235
+ return;
236
+ }
237
+
238
+ let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
239
+ const packageAnchors = [
240
+ 'COPY packages/files-access/package.json packages/files-access/package.json',
241
+ 'COPY packages/files/package.json packages/files/package.json',
242
+ 'COPY packages/files-local/package.json packages/files-local/package.json',
243
+ 'COPY packages/files-s3/package.json packages/files-s3/package.json',
244
+ 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
245
+ 'COPY packages/rbac/package.json packages/rbac/package.json',
246
+ 'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
247
+ 'COPY packages/logger/package.json packages/logger/package.json',
248
+ 'COPY packages/swagger/package.json packages/swagger/package.json',
249
+ 'COPY packages/i18n/package.json packages/i18n/package.json',
250
+ 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
251
+ 'COPY packages/core/package.json packages/core/package.json',
252
+ ];
253
+ const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
254
+ content = ensureLineAfter(
255
+ content,
256
+ packageAnchor,
257
+ 'COPY packages/files-quotas/package.json packages/files-quotas/package.json',
258
+ );
259
+
260
+ const sourceAnchors = [
261
+ 'COPY packages/files-access packages/files-access',
262
+ 'COPY packages/files packages/files',
263
+ 'COPY packages/files-local packages/files-local',
264
+ 'COPY packages/files-s3 packages/files-s3',
265
+ 'COPY packages/accounts-api packages/accounts-api',
266
+ 'COPY packages/rbac packages/rbac',
267
+ 'COPY packages/rate-limit packages/rate-limit',
268
+ 'COPY packages/logger packages/logger',
269
+ 'COPY packages/swagger packages/swagger',
270
+ 'COPY packages/i18n packages/i18n',
271
+ 'COPY packages/db-prisma packages/db-prisma',
272
+ 'COPY packages/core packages/core',
273
+ ];
274
+ const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
275
+ content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-quotas packages/files-quotas');
276
+
277
277
  content = content.replace(/^RUN pnpm --filter @forgeon\/files-quotas build\r?\n?/gm, '');
278
278
  const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
279
279
  ? 'RUN pnpm --filter @forgeon/files build'
280
280
  : content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
281
281
  ? 'RUN pnpm --filter @forgeon/api prisma:generate'
282
282
  : 'RUN pnpm --filter @forgeon/api build';
283
- content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-quotas build');
284
-
285
- fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
286
- }
287
-
288
- function patchCompose(targetRoot) {
289
- const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
290
- if (!fs.existsSync(composePath)) {
291
- return;
292
- }
293
-
294
- let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
295
- if (!content.includes('FILES_QUOTAS_ENABLED: ${FILES_QUOTAS_ENABLED}')) {
296
- const anchors = [
297
- /^(\s+FILES_ALLOWED_MIME_PREFIXES:.*)$/m,
298
- /^(\s+FILES_MAX_FILE_SIZE_BYTES:.*)$/m,
299
- /^(\s+FILES_PUBLIC_BASE_PATH:.*)$/m,
300
- /^(\s+FILES_STORAGE_DRIVER:.*)$/m,
301
- /^(\s+FILES_ENABLED:.*)$/m,
302
- /^(\s+API_PREFIX:.*)$/m,
303
- ];
304
- const anchorPattern = anchors.find((pattern) => pattern.test(content)) ?? anchors.at(-1);
305
- content = content.replace(
306
- anchorPattern,
307
- `$1
308
- FILES_QUOTAS_ENABLED: \${FILES_QUOTAS_ENABLED}
309
- FILES_QUOTA_MAX_FILES_PER_OWNER: \${FILES_QUOTA_MAX_FILES_PER_OWNER}
310
- FILES_QUOTA_MAX_BYTES_PER_OWNER: \${FILES_QUOTA_MAX_BYTES_PER_OWNER}`,
311
- );
312
- }
313
-
314
- fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
315
- }
316
-
317
- function patchReadme(targetRoot) {
318
- const readmePath = path.join(targetRoot, 'README.md');
319
- if (!fs.existsSync(readmePath)) {
320
- return;
321
- }
322
-
323
- const marker = '## Files Quotas Module';
324
- let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
325
- if (content.includes(marker)) {
326
- return;
327
- }
328
-
329
- const section = `## Files Quotas Module
330
-
331
- The files-quotas module adds owner-based limits for file upload attempts.
332
-
333
- What it adds:
334
- - \`@forgeon/files-quotas\` package
335
- - upload pre-check in files controller
336
- - probe endpoint: \`GET /api/health/files-quotas\`
337
-
338
- Current quota model:
339
- - max files per owner
340
- - max total bytes per owner
341
- - owner identity from file payload (\`ownerType\`, \`ownerId\`)
342
-
343
- Key env:
344
- - \`FILES_QUOTAS_ENABLED=true\`
345
- - \`FILES_QUOTA_MAX_FILES_PER_OWNER=100\`
346
- - \`FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600\``;
347
-
348
- if (content.includes('## Prisma In Docker Start')) {
349
- content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
350
- } else {
351
- content = `${content.trimEnd()}\n\n${section}\n`;
352
- }
353
-
354
- fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
355
- }
356
-
357
- export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
358
- copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
359
- const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-quotas' });
360
-
361
- patchApiPackage(targetRoot);
362
- patchFilesPackage(targetRoot);
363
- patchAppModule(targetRoot);
364
- patchFilesController(targetRoot);
365
- patchHealthController(targetRoot, probeTargets);
366
- registerWebProbe(targetRoot, probeTargets);
367
- patchApiDockerfile(targetRoot);
368
- patchCompose(targetRoot);
369
- patchReadme(targetRoot);
370
-
371
- upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
372
- 'FILES_QUOTAS_ENABLED=true',
373
- 'FILES_QUOTA_MAX_FILES_PER_OWNER=100',
374
- 'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
375
- ]);
376
- upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
377
- 'FILES_QUOTAS_ENABLED=true',
378
- 'FILES_QUOTA_MAX_FILES_PER_OWNER=100',
379
- 'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
380
- ]);
381
- }
283
+ content = ensureLineAfter(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-quotas build');
284
+
285
+ fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
286
+ }
287
+
288
+ function patchCompose(targetRoot) {
289
+ const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
290
+ if (!fs.existsSync(composePath)) {
291
+ return;
292
+ }
293
+
294
+ let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
295
+ if (!content.includes('FILES_QUOTAS_ENABLED: ${FILES_QUOTAS_ENABLED}')) {
296
+ const anchors = [
297
+ /^(\s+FILES_ALLOWED_MIME_PREFIXES:.*)$/m,
298
+ /^(\s+FILES_MAX_FILE_SIZE_BYTES:.*)$/m,
299
+ /^(\s+FILES_PUBLIC_BASE_PATH:.*)$/m,
300
+ /^(\s+FILES_STORAGE_DRIVER:.*)$/m,
301
+ /^(\s+FILES_ENABLED:.*)$/m,
302
+ /^(\s+API_PREFIX:.*)$/m,
303
+ ];
304
+ const anchorPattern = anchors.find((pattern) => pattern.test(content)) ?? anchors.at(-1);
305
+ content = content.replace(
306
+ anchorPattern,
307
+ `$1
308
+ FILES_QUOTAS_ENABLED: \${FILES_QUOTAS_ENABLED}
309
+ FILES_QUOTA_MAX_FILES_PER_OWNER: \${FILES_QUOTA_MAX_FILES_PER_OWNER}
310
+ FILES_QUOTA_MAX_BYTES_PER_OWNER: \${FILES_QUOTA_MAX_BYTES_PER_OWNER}`,
311
+ );
312
+ }
313
+
314
+ fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
315
+ }
316
+
317
+ function patchReadme(targetRoot) {
318
+ const readmePath = path.join(targetRoot, 'README.md');
319
+ if (!fs.existsSync(readmePath)) {
320
+ return;
321
+ }
322
+
323
+ const marker = '## Files Quotas Module';
324
+ let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
325
+ if (content.includes(marker)) {
326
+ return;
327
+ }
328
+
329
+ const section = `## Files Quotas Module
330
+
331
+ The files-quotas module adds owner-based limits for file upload attempts.
332
+
333
+ What it adds:
334
+ - \`@forgeon/files-quotas\` package
335
+ - upload pre-check in files controller
336
+ - probe endpoint: \`GET /api/health/files-quotas\`
337
+
338
+ Current quota model:
339
+ - max files per owner
340
+ - max total bytes per owner
341
+ - owner identity from file payload (\`ownerType\`, \`ownerId\`)
342
+
343
+ Key env:
344
+ - \`FILES_QUOTAS_ENABLED=true\`
345
+ - \`FILES_QUOTA_MAX_FILES_PER_OWNER=100\`
346
+ - \`FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600\``;
347
+
348
+ if (content.includes('## Prisma In Docker Start')) {
349
+ content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
350
+ } else {
351
+ content = `${content.trimEnd()}\n\n${section}\n`;
352
+ }
353
+
354
+ fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
355
+ }
356
+
357
+ export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
358
+ copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
359
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-quotas' });
360
+
361
+ patchApiPackage(targetRoot);
362
+ patchFilesPackage(targetRoot);
363
+ patchAppModule(targetRoot);
364
+ patchFilesController(targetRoot);
365
+ patchHealthController(targetRoot, probeTargets);
366
+ registerWebProbe(targetRoot, probeTargets);
367
+ patchApiDockerfile(targetRoot);
368
+ patchCompose(targetRoot);
369
+ patchReadme(targetRoot);
370
+
371
+ upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
372
+ 'FILES_QUOTAS_ENABLED=true',
373
+ 'FILES_QUOTA_MAX_FILES_PER_OWNER=100',
374
+ 'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
375
+ ]);
376
+ upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
377
+ 'FILES_QUOTAS_ENABLED=true',
378
+ 'FILES_QUOTA_MAX_FILES_PER_OWNER=100',
379
+ 'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
380
+ ]);
381
+ }
382
382
 
383
383
 
384
384