create-forgeon 0.3.5 → 0.3.7
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 +1 -1
- package/src/cli/add-help.mjs +1 -0
- package/src/cli/add-options.mjs +6 -0
- package/src/cli/add-options.test.mjs +2 -0
- package/src/modules/dependencies.mjs +31 -0
- package/src/modules/dependencies.test.mjs +207 -5
- package/src/modules/executor.mjs +14 -0
- package/src/modules/executor.test.mjs +752 -14
- package/src/modules/files-access.mjs +446 -0
- package/src/modules/files-image.mjs +540 -0
- package/src/modules/files-local.mjs +221 -0
- package/src/modules/files-quotas.mjs +402 -0
- package/src/modules/files-s3.mjs +266 -0
- package/src/modules/files.mjs +527 -0
- package/src/modules/queue.mjs +410 -0
- package/src/modules/registry.mjs +93 -3
- package/src/modules/shared/patch-utils.mjs +25 -0
- package/src/run-add-module.mjs +89 -2
- package/templates/module-fragments/files/00_title.md +1 -0
- package/templates/module-fragments/files/10_overview.md +17 -0
- package/templates/module-fragments/files/20_scope.md +13 -0
- package/templates/module-fragments/files/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-access/00_title.md +1 -0
- package/templates/module-fragments/files-access/10_overview.md +9 -0
- package/templates/module-fragments/files-access/20_scope.md +20 -0
- package/templates/module-fragments/files-access/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-image/00_title.md +1 -0
- package/templates/module-fragments/files-image/10_overview.md +10 -0
- package/templates/module-fragments/files-image/20_scope.md +20 -0
- package/templates/module-fragments/files-image/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-local/00_title.md +1 -0
- package/templates/module-fragments/files-local/10_overview.md +9 -0
- package/templates/module-fragments/files-local/20_scope.md +10 -0
- package/templates/module-fragments/files-local/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-quotas/00_title.md +1 -0
- package/templates/module-fragments/files-quotas/10_overview.md +9 -0
- package/templates/module-fragments/files-quotas/20_scope.md +20 -0
- package/templates/module-fragments/files-quotas/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-s3/00_title.md +1 -0
- package/templates/module-fragments/files-s3/10_overview.md +17 -0
- package/templates/module-fragments/files-s3/20_scope.md +11 -0
- package/templates/module-fragments/files-s3/90_status_implemented.md +5 -0
- package/templates/module-fragments/queue/20_scope.md +8 -7
- package/templates/module-fragments/queue/90_status_implemented.md +3 -0
- package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_record/migration.sql +30 -0
- package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_variant/migration.sql +55 -0
- package/templates/module-presets/files/packages/files/package.json +24 -0
- package/templates/module-presets/files/packages/files/src/dto/create-file.dto.ts +30 -0
- package/templates/module-presets/files/packages/files/src/files-config.loader.ts +21 -0
- package/templates/module-presets/files/packages/files/src/files-config.module.ts +12 -0
- package/templates/module-presets/files/packages/files/src/files-config.service.ts +32 -0
- package/templates/module-presets/files/packages/files/src/files-env.schema.ts +30 -0
- package/templates/module-presets/files/packages/files/src/files.controller.ts +90 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +762 -0
- package/templates/module-presets/files/packages/files/src/files.types.ts +35 -0
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +12 -0
- package/templates/module-presets/files/packages/files/src/index.ts +9 -0
- package/templates/module-presets/files/packages/files/tsconfig.json +9 -0
- package/templates/module-presets/files-access/packages/files-access/package.json +17 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.service.ts +59 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.subject.ts +45 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.types.ts +14 -0
- package/templates/module-presets/files-access/packages/files-access/src/forgeon-files-access.module.ts +8 -0
- package/templates/module-presets/files-access/packages/files-access/src/index.ts +4 -0
- package/templates/module-presets/files-access/packages/files-access/tsconfig.json +9 -0
- package/templates/module-presets/files-image/packages/files-image/package.json +21 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.loader.ts +32 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.module.ts +11 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.service.ts +55 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-env.schema.ts +28 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image.service.ts +420 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image.types.ts +18 -0
- package/templates/module-presets/files-image/packages/files-image/src/forgeon-files-image.module.ts +10 -0
- package/templates/module-presets/files-image/packages/files-image/src/index.ts +7 -0
- package/templates/module-presets/files-image/packages/files-image/tsconfig.json +9 -0
- package/templates/module-presets/files-local/packages/files-local/package.json +19 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.loader.ts +13 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.module.ts +12 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.service.ts +11 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-env.schema.ts +13 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +4 -0
- package/templates/module-presets/files-local/packages/files-local/tsconfig.json +9 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.loader.ts +22 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.module.ts +11 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.service.ts +27 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-env.schema.ts +15 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.types.ts +22 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +11 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/index.ts +7 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/tsconfig.json +9 -0
- package/templates/module-presets/files-s3/packages/files-s3/package.json +20 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.loader.ts +57 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.module.ts +12 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.service.ts +44 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-env.schema.ts +51 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +4 -0
- package/templates/module-presets/files-s3/packages/files-s3/tsconfig.json +9 -0
- package/templates/module-presets/queue/packages/queue/package.json +21 -0
- package/templates/module-presets/queue/packages/queue/src/forgeon-queue.module.ts +10 -0
- package/templates/module-presets/queue/packages/queue/src/index.ts +6 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.loader.ts +24 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.module.ts +10 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.service.ts +69 -0
- package/templates/module-presets/queue/packages/queue/src/queue-env.schema.ts +17 -0
- package/templates/module-presets/queue/packages/queue/src/queue.service.ts +88 -0
- package/templates/module-presets/queue/packages/queue/tsconfig.json +9 -0
- package/templates/module-fragments/queue/90_status_planned.md +0 -3
|
@@ -86,6 +86,52 @@ function assertRateLimitWiring(projectRoot) {
|
|
|
86
86
|
assert.match(readme, /no optional integration sync is required/i);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function assertQueueWiring(projectRoot) {
|
|
90
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
91
|
+
assert.match(appModule, /queueConfig/);
|
|
92
|
+
assert.match(appModule, /queueEnvSchema/);
|
|
93
|
+
assert.match(appModule, /ForgeonQueueModule/);
|
|
94
|
+
|
|
95
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
96
|
+
assert.match(apiPackage, /@forgeon\/queue/);
|
|
97
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/queue build/);
|
|
98
|
+
|
|
99
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
100
|
+
assert.match(apiDockerfile, /COPY packages\/queue\/package\.json packages\/queue\/package\.json/);
|
|
101
|
+
assert.match(apiDockerfile, /COPY packages\/queue packages\/queue/);
|
|
102
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/queue build/);
|
|
103
|
+
|
|
104
|
+
const healthController = fs.readFileSync(
|
|
105
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
106
|
+
'utf8',
|
|
107
|
+
);
|
|
108
|
+
assert.match(healthController, /QueueService/);
|
|
109
|
+
assert.match(healthController, /@Get\('queue'\)/);
|
|
110
|
+
assert.match(healthController, /queueService\.getProbeStatus/);
|
|
111
|
+
|
|
112
|
+
const webApp = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
113
|
+
assert.match(webApp, /Check queue health/);
|
|
114
|
+
assert.match(webApp, /Queue probe response/);
|
|
115
|
+
|
|
116
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
117
|
+
assert.match(apiEnv, /QUEUE_ENABLED=true/);
|
|
118
|
+
assert.match(apiEnv, /QUEUE_REDIS_URL=redis:\/\/localhost:6379/);
|
|
119
|
+
assert.match(apiEnv, /QUEUE_DEFAULT_ATTEMPTS=3/);
|
|
120
|
+
assert.match(apiEnv, /QUEUE_DEFAULT_BACKOFF_MS=1000/);
|
|
121
|
+
|
|
122
|
+
const dockerEnv = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', '.env.example'), 'utf8');
|
|
123
|
+
assert.match(dockerEnv, /QUEUE_REDIS_URL=redis:\/\/redis:6379/);
|
|
124
|
+
|
|
125
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
126
|
+
assert.match(compose, /^\s{2}redis:\s*$/m);
|
|
127
|
+
assert.match(compose, /QUEUE_ENABLED: \$\{QUEUE_ENABLED\}/);
|
|
128
|
+
assert.match(compose, /depends_on:\n\s+redis:\n\s+condition: service_healthy/);
|
|
129
|
+
|
|
130
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
131
|
+
assert.match(readme, /## Queue Module/);
|
|
132
|
+
assert.match(readme, /runtime baseline backed by Redis/i);
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
function assertRbacWiring(projectRoot) {
|
|
90
136
|
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
91
137
|
assert.match(appModule, /ForgeonRbacModule/);
|
|
@@ -119,6 +165,374 @@ function assertRbacWiring(projectRoot) {
|
|
|
119
165
|
assert.match(readme, /jwt-auth.*optional/i);
|
|
120
166
|
}
|
|
121
167
|
|
|
168
|
+
function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
169
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
170
|
+
assert.match(appModule, /filesConfig/);
|
|
171
|
+
assert.match(appModule, /filesEnvSchema/);
|
|
172
|
+
assert.match(appModule, /ForgeonFilesModule/);
|
|
173
|
+
|
|
174
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
175
|
+
assert.match(apiPackage, /@forgeon\/files/);
|
|
176
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
|
|
177
|
+
|
|
178
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
179
|
+
assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
|
|
180
|
+
assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
|
|
181
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
|
|
182
|
+
|
|
183
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
184
|
+
assert.match(apiEnv, /FILES_ENABLED=true/);
|
|
185
|
+
assert.match(apiEnv, new RegExp(`FILES_STORAGE_DRIVER=${expectedStorageDriver}`));
|
|
186
|
+
assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
|
|
187
|
+
assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
|
|
188
|
+
assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
|
|
189
|
+
|
|
190
|
+
const healthController = fs.readFileSync(
|
|
191
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
192
|
+
'utf8',
|
|
193
|
+
);
|
|
194
|
+
assert.match(healthController, /@Post\('files'\)/);
|
|
195
|
+
assert.match(healthController, /@Get\('files-variants'\)/);
|
|
196
|
+
assert.match(healthController, /filesService\.createProbeRecord/);
|
|
197
|
+
assert.match(healthController, /filesService\.getVariantsProbeStatus/);
|
|
198
|
+
assert.match(healthController, /filesService\.deleteByPublicId/);
|
|
199
|
+
|
|
200
|
+
const filesController = fs.readFileSync(
|
|
201
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
202
|
+
'utf8',
|
|
203
|
+
);
|
|
204
|
+
assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
|
|
205
|
+
assert.match(filesController, /parseVariant\(variantQuery\)/);
|
|
206
|
+
assert.match(filesController, /@Delete\(':publicId'\)/);
|
|
207
|
+
|
|
208
|
+
const filesService = fs.readFileSync(
|
|
209
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
210
|
+
'utf8',
|
|
211
|
+
);
|
|
212
|
+
assert.match(filesService, /getOrCreateBlob/);
|
|
213
|
+
assert.match(filesService, /cleanupReferencedBlobs/);
|
|
214
|
+
assert.match(filesService, /isUniqueConstraintError/);
|
|
215
|
+
assert.match(filesService, /fileBlob\.deleteMany/);
|
|
216
|
+
assert.match(filesService, /variants:\s*\{[\s\S]*?none:\s*\{[\s\S]*?\}/);
|
|
217
|
+
assert.match(filesService, /prisma\.fileBlob/);
|
|
218
|
+
|
|
219
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
220
|
+
assert.match(appTsx, /Check files probe \(create metadata\)/);
|
|
221
|
+
assert.match(appTsx, /Check files variants capability/);
|
|
222
|
+
assert.match(appTsx, /Files probe response/);
|
|
223
|
+
assert.match(appTsx, /Files variants probe response/);
|
|
224
|
+
|
|
225
|
+
const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
|
|
226
|
+
assert.match(schema, /model FileRecord \{/);
|
|
227
|
+
assert.match(schema, /variants\s+FileVariant\[\]/);
|
|
228
|
+
assert.match(schema, /model FileVariant \{/);
|
|
229
|
+
assert.match(schema, /model FileBlob \{/);
|
|
230
|
+
assert.match(schema, /blobId\s+String/);
|
|
231
|
+
assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
|
|
232
|
+
assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
|
|
233
|
+
assert.match(schema, /publicId\s+String\s+@unique/);
|
|
234
|
+
assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
|
|
235
|
+
|
|
236
|
+
const migration = path.join(
|
|
237
|
+
projectRoot,
|
|
238
|
+
'apps',
|
|
239
|
+
'api',
|
|
240
|
+
'prisma',
|
|
241
|
+
'migrations',
|
|
242
|
+
'20260306_files_file_record',
|
|
243
|
+
'migration.sql',
|
|
244
|
+
);
|
|
245
|
+
assert.equal(fs.existsSync(migration), true);
|
|
246
|
+
|
|
247
|
+
const variantMigration = path.join(
|
|
248
|
+
projectRoot,
|
|
249
|
+
'apps',
|
|
250
|
+
'api',
|
|
251
|
+
'prisma',
|
|
252
|
+
'migrations',
|
|
253
|
+
'20260306_files_file_variant',
|
|
254
|
+
'migration.sql',
|
|
255
|
+
);
|
|
256
|
+
assert.equal(fs.existsSync(variantMigration), true);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function assertFilesLocalWiring(projectRoot) {
|
|
260
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
261
|
+
assert.match(appModule, /filesLocalConfig/);
|
|
262
|
+
assert.match(appModule, /filesLocalEnvSchemaZod/);
|
|
263
|
+
assert.match(appModule, /FilesLocalConfigModule/);
|
|
264
|
+
|
|
265
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
266
|
+
assert.match(apiPackage, /@forgeon\/files-local/);
|
|
267
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
|
|
268
|
+
|
|
269
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
270
|
+
assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
|
|
271
|
+
assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
|
|
272
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
|
|
273
|
+
|
|
274
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
275
|
+
assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
|
|
276
|
+
|
|
277
|
+
const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
|
|
278
|
+
assert.match(gitignore, /storage\//);
|
|
279
|
+
|
|
280
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
281
|
+
assert.match(compose, /files_data:\/app\/storage/);
|
|
282
|
+
assert.match(compose, /^\s{2}files_data:\s*$/m);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function assertFilesS3Wiring(projectRoot) {
|
|
286
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
287
|
+
assert.match(appModule, /filesS3Config/);
|
|
288
|
+
assert.match(appModule, /filesS3EnvSchemaZod/);
|
|
289
|
+
assert.match(appModule, /FilesS3ConfigModule/);
|
|
290
|
+
|
|
291
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
292
|
+
assert.match(apiPackage, /@forgeon\/files-s3/);
|
|
293
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
|
|
294
|
+
|
|
295
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
296
|
+
assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
|
|
297
|
+
assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
|
|
298
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
|
|
299
|
+
|
|
300
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
301
|
+
assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
|
|
302
|
+
assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
|
|
303
|
+
assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
|
|
304
|
+
assert.match(apiEnv, /FILES_S3_REGION=/);
|
|
305
|
+
assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
|
|
306
|
+
assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
|
|
307
|
+
assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
|
|
308
|
+
|
|
309
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
310
|
+
assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
|
|
311
|
+
assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
|
|
312
|
+
|
|
313
|
+
const filesS3Package = fs.readFileSync(
|
|
314
|
+
path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
|
|
315
|
+
'utf8',
|
|
316
|
+
);
|
|
317
|
+
assert.match(filesS3Package, /@aws-sdk\/client-s3/);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function assertFilesAccessWiring(projectRoot) {
|
|
321
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
322
|
+
assert.match(appModule, /ForgeonFilesAccessModule/);
|
|
323
|
+
|
|
324
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
325
|
+
assert.match(apiPackage, /@forgeon\/files-access/);
|
|
326
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-access build/);
|
|
327
|
+
assert.equal(
|
|
328
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-access build') <
|
|
329
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
330
|
+
true,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
334
|
+
assert.match(filesPackage, /@forgeon\/files-access/);
|
|
335
|
+
|
|
336
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
337
|
+
assert.match(
|
|
338
|
+
apiDockerfile,
|
|
339
|
+
/COPY packages\/files-access\/package\.json packages\/files-access\/package\.json/,
|
|
340
|
+
);
|
|
341
|
+
assert.match(apiDockerfile, /COPY packages\/files-access packages\/files-access/);
|
|
342
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-access build/);
|
|
343
|
+
assert.equal(
|
|
344
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-access build') <
|
|
345
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
346
|
+
true,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const filesController = fs.readFileSync(
|
|
350
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
351
|
+
'utf8',
|
|
352
|
+
);
|
|
353
|
+
assert.match(filesController, /extractFilesAccessSubject/);
|
|
354
|
+
assert.match(filesController, /filesAccessService\.assertCanRead/);
|
|
355
|
+
assert.match(filesController, /filesAccessService\.assertCanDelete/);
|
|
356
|
+
assert.match(filesController, /@Req\(\) req: any/);
|
|
357
|
+
assert.match(filesController, /openDownload\(publicId,\s*variant\)/);
|
|
358
|
+
|
|
359
|
+
const healthController = fs.readFileSync(
|
|
360
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
361
|
+
'utf8',
|
|
362
|
+
);
|
|
363
|
+
assert.match(healthController, /@Get\('files-access'\)/);
|
|
364
|
+
assert.match(healthController, /extractFilesAccessSubject/);
|
|
365
|
+
assert.match(healthController, /filesAccessService\.canRead/);
|
|
366
|
+
|
|
367
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
368
|
+
assert.match(appTsx, /Check files access/);
|
|
369
|
+
assert.match(appTsx, /Files access probe response/);
|
|
370
|
+
assert.match(appTsx, /x-forgeon-user-id/);
|
|
371
|
+
|
|
372
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
373
|
+
assert.match(readme, /## Files Access Module/);
|
|
374
|
+
assert.match(readme, /resource-level authorization/i);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function assertFilesQuotasWiring(projectRoot) {
|
|
378
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
379
|
+
assert.match(appModule, /filesQuotasConfig/);
|
|
380
|
+
assert.match(appModule, /filesQuotasEnvSchema/);
|
|
381
|
+
assert.match(appModule, /ForgeonFilesQuotasModule/);
|
|
382
|
+
|
|
383
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
384
|
+
assert.match(apiPackage, /@forgeon\/files-quotas/);
|
|
385
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
|
|
386
|
+
assert.equal(
|
|
387
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') <
|
|
388
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
389
|
+
true,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
393
|
+
assert.match(filesPackage, /@forgeon\/files-quotas/);
|
|
394
|
+
|
|
395
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
396
|
+
assert.match(
|
|
397
|
+
apiDockerfile,
|
|
398
|
+
/COPY packages\/files-quotas\/package\.json packages\/files-quotas\/package\.json/,
|
|
399
|
+
);
|
|
400
|
+
assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
|
|
401
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
|
|
402
|
+
assert.equal(
|
|
403
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') <
|
|
404
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
405
|
+
true,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const filesController = fs.readFileSync(
|
|
409
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
410
|
+
'utf8',
|
|
411
|
+
);
|
|
412
|
+
assert.match(filesController, /FilesQuotasService/);
|
|
413
|
+
assert.match(filesController, /filesQuotasService\.assertUploadAllowed/);
|
|
414
|
+
|
|
415
|
+
const healthController = fs.readFileSync(
|
|
416
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
417
|
+
'utf8',
|
|
418
|
+
);
|
|
419
|
+
assert.match(healthController, /@Get\('files-quotas'\)/);
|
|
420
|
+
assert.match(healthController, /filesQuotasService\.getProbeStatus/);
|
|
421
|
+
|
|
422
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
423
|
+
assert.match(appTsx, /Check files quotas/);
|
|
424
|
+
assert.match(appTsx, /Files quotas probe response/);
|
|
425
|
+
|
|
426
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
427
|
+
assert.match(apiEnv, /FILES_QUOTAS_ENABLED=true/);
|
|
428
|
+
assert.match(apiEnv, /FILES_QUOTA_MAX_FILES_PER_OWNER=100/);
|
|
429
|
+
assert.match(apiEnv, /FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600/);
|
|
430
|
+
|
|
431
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
432
|
+
assert.match(compose, /FILES_QUOTAS_ENABLED: \$\{FILES_QUOTAS_ENABLED\}/);
|
|
433
|
+
|
|
434
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
435
|
+
assert.match(readme, /## Files Quotas Module/);
|
|
436
|
+
assert.match(readme, /owner-based limits/i);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function assertFilesImageWiring(projectRoot) {
|
|
440
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
441
|
+
assert.match(appModule, /filesImageConfig/);
|
|
442
|
+
assert.match(appModule, /filesImageEnvSchema/);
|
|
443
|
+
|
|
444
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
445
|
+
assert.match(apiPackage, /@forgeon\/files-image/);
|
|
446
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-image build/);
|
|
447
|
+
assert.equal(
|
|
448
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-image build') <
|
|
449
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
450
|
+
true,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
454
|
+
assert.match(filesPackage, /@forgeon\/files-image/);
|
|
455
|
+
|
|
456
|
+
const filesModule = fs.readFileSync(
|
|
457
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
|
|
458
|
+
'utf8',
|
|
459
|
+
);
|
|
460
|
+
assert.match(filesModule, /ForgeonFilesImageModule/);
|
|
461
|
+
|
|
462
|
+
const filesService = fs.readFileSync(
|
|
463
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
464
|
+
'utf8',
|
|
465
|
+
);
|
|
466
|
+
assert.match(filesService, /FilesImageService/);
|
|
467
|
+
assert.match(filesService, /filesImageService\.sanitizeForStorage/);
|
|
468
|
+
assert.match(filesService, /sanitizeForStorage\({/);
|
|
469
|
+
assert.match(filesService, /auditContext: input\.auditContext/);
|
|
470
|
+
|
|
471
|
+
const filesController = fs.readFileSync(
|
|
472
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
473
|
+
'utf8',
|
|
474
|
+
);
|
|
475
|
+
assert.match(filesController, /@Req\(\) req: any/);
|
|
476
|
+
assert.match(filesController, /requestId:/);
|
|
477
|
+
|
|
478
|
+
const filesTypes = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'src', 'files.types.ts'), 'utf8');
|
|
479
|
+
assert.match(filesTypes, /auditContext\?: \{/);
|
|
480
|
+
|
|
481
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
482
|
+
assert.match(
|
|
483
|
+
apiDockerfile,
|
|
484
|
+
/COPY packages\/files-image\/package\.json packages\/files-image\/package\.json/,
|
|
485
|
+
);
|
|
486
|
+
assert.match(apiDockerfile, /COPY packages\/files-image packages\/files-image/);
|
|
487
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-image build/);
|
|
488
|
+
assert.equal(
|
|
489
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-image build') <
|
|
490
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
491
|
+
true,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const healthController = fs.readFileSync(
|
|
495
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
496
|
+
'utf8',
|
|
497
|
+
);
|
|
498
|
+
assert.match(healthController, /@Get\('files-image'\)/);
|
|
499
|
+
assert.match(healthController, /filesImageService\.getProbeStatus/);
|
|
500
|
+
|
|
501
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
502
|
+
assert.match(appTsx, /Check files image sanitize/);
|
|
503
|
+
assert.match(appTsx, /Files image probe response/);
|
|
504
|
+
|
|
505
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
506
|
+
assert.match(apiEnv, /FILES_IMAGE_ENABLED=true/);
|
|
507
|
+
assert.match(apiEnv, /FILES_IMAGE_STRIP_METADATA=true/);
|
|
508
|
+
assert.match(apiEnv, /FILES_IMAGE_MAX_WIDTH=4096/);
|
|
509
|
+
assert.match(apiEnv, /FILES_IMAGE_MAX_HEIGHT=4096/);
|
|
510
|
+
assert.match(apiEnv, /FILES_IMAGE_MAX_PIXELS=16777216/);
|
|
511
|
+
assert.match(apiEnv, /FILES_IMAGE_MAX_FRAMES=1/);
|
|
512
|
+
assert.match(apiEnv, /FILES_IMAGE_PROCESS_TIMEOUT_MS=5000/);
|
|
513
|
+
assert.match(apiEnv, /FILES_IMAGE_ALLOWED_MIME_TYPES=image\/jpeg,image\/png,image\/webp/);
|
|
514
|
+
|
|
515
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
516
|
+
assert.match(compose, /FILES_IMAGE_ENABLED: \$\{FILES_IMAGE_ENABLED\}/);
|
|
517
|
+
assert.match(compose, /FILES_IMAGE_STRIP_METADATA: \$\{FILES_IMAGE_STRIP_METADATA\}/);
|
|
518
|
+
assert.match(compose, /FILES_IMAGE_MAX_WIDTH: \$\{FILES_IMAGE_MAX_WIDTH\}/);
|
|
519
|
+
assert.match(compose, /FILES_IMAGE_MAX_HEIGHT: \$\{FILES_IMAGE_MAX_HEIGHT\}/);
|
|
520
|
+
assert.match(compose, /FILES_IMAGE_MAX_PIXELS: \$\{FILES_IMAGE_MAX_PIXELS\}/);
|
|
521
|
+
assert.match(compose, /FILES_IMAGE_MAX_FRAMES: \$\{FILES_IMAGE_MAX_FRAMES\}/);
|
|
522
|
+
assert.match(compose, /FILES_IMAGE_PROCESS_TIMEOUT_MS: \$\{FILES_IMAGE_PROCESS_TIMEOUT_MS\}/);
|
|
523
|
+
|
|
524
|
+
const filesImagePackage = fs.readFileSync(
|
|
525
|
+
path.join(projectRoot, 'packages', 'files-image', 'package.json'),
|
|
526
|
+
'utf8',
|
|
527
|
+
);
|
|
528
|
+
assert.match(filesImagePackage, /"sharp":/);
|
|
529
|
+
assert.match(filesImagePackage, /"file-type":/);
|
|
530
|
+
|
|
531
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
532
|
+
assert.match(readme, /## Files Image Module/);
|
|
533
|
+
assert.match(readme, /metadata is stripped before storage/i);
|
|
534
|
+
}
|
|
535
|
+
|
|
122
536
|
function assertJwtAuthWiring(projectRoot, withPrismaStore) {
|
|
123
537
|
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
124
538
|
assert.match(apiPackage, /@forgeon\/auth-api/);
|
|
@@ -282,32 +696,48 @@ function stripDbPrismaArtifacts(projectRoot) {
|
|
|
282
696
|
fs.writeFileSync(dockerEnvExamplePath, dockerEnv, 'utf8');
|
|
283
697
|
}
|
|
284
698
|
|
|
285
|
-
describe('addModule', () => {
|
|
699
|
+
describe('addModule', () => {
|
|
286
700
|
const modulesDir = path.dirname(fileURLToPath(import.meta.url));
|
|
287
701
|
const packageRoot = path.resolve(modulesDir, '..', '..');
|
|
288
702
|
|
|
289
|
-
it('
|
|
290
|
-
const targetRoot = mkTmp('forgeon-module-');
|
|
291
|
-
|
|
292
|
-
|
|
703
|
+
it('applies queue module on top of scaffold without db and i18n', () => {
|
|
704
|
+
const targetRoot = mkTmp('forgeon-module-queue-');
|
|
705
|
+
const projectRoot = path.join(targetRoot, 'demo-queue');
|
|
706
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
scaffoldProject({
|
|
710
|
+
templateRoot,
|
|
711
|
+
packageRoot,
|
|
712
|
+
targetRoot: projectRoot,
|
|
713
|
+
projectName: 'demo-queue',
|
|
714
|
+
frontend: 'react',
|
|
715
|
+
db: 'prisma',
|
|
716
|
+
dbPrismaEnabled: false,
|
|
717
|
+
i18nEnabled: false,
|
|
718
|
+
proxy: 'caddy',
|
|
719
|
+
});
|
|
720
|
+
|
|
293
721
|
const result = addModule({
|
|
294
722
|
moduleId: 'queue',
|
|
295
|
-
targetRoot,
|
|
723
|
+
targetRoot: projectRoot,
|
|
296
724
|
packageRoot,
|
|
297
725
|
});
|
|
298
|
-
|
|
299
|
-
assert.equal(result.applied,
|
|
300
|
-
assert.match(result.message, /
|
|
726
|
+
|
|
727
|
+
assert.equal(result.applied, true);
|
|
728
|
+
assert.match(result.message, /applied/);
|
|
301
729
|
assert.equal(fs.existsSync(result.docsPath), true);
|
|
302
730
|
assert.match(result.docsPath, /modules[\\/].+[\\/]README\.md$/);
|
|
303
|
-
assert.equal(fs.existsSync(path.join(
|
|
731
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'modules', 'README.md')), true);
|
|
732
|
+
|
|
733
|
+
assertQueueWiring(projectRoot);
|
|
304
734
|
|
|
305
735
|
const note = fs.readFileSync(result.docsPath, 'utf8');
|
|
306
736
|
assert.match(note, /Queue Worker/);
|
|
307
|
-
assert.match(note, /Status:
|
|
308
|
-
} finally {
|
|
309
|
-
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
310
|
-
}
|
|
737
|
+
assert.match(note, /Status: implemented/);
|
|
738
|
+
} finally {
|
|
739
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
740
|
+
}
|
|
311
741
|
});
|
|
312
742
|
|
|
313
743
|
it('throws for unknown module id', () => {
|
|
@@ -713,6 +1143,314 @@ describe('addModule', () => {
|
|
|
713
1143
|
}
|
|
714
1144
|
});
|
|
715
1145
|
|
|
1146
|
+
it('applies files-local then files foundation modules without breaking api wiring', () => {
|
|
1147
|
+
const targetRoot = mkTmp('forgeon-module-files-local-');
|
|
1148
|
+
const projectRoot = path.join(targetRoot, 'demo-files-local');
|
|
1149
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1150
|
+
|
|
1151
|
+
try {
|
|
1152
|
+
scaffoldProject({
|
|
1153
|
+
templateRoot,
|
|
1154
|
+
packageRoot,
|
|
1155
|
+
targetRoot: projectRoot,
|
|
1156
|
+
projectName: 'demo-files-local',
|
|
1157
|
+
frontend: 'react',
|
|
1158
|
+
db: 'prisma',
|
|
1159
|
+
dbPrismaEnabled: true,
|
|
1160
|
+
i18nEnabled: false,
|
|
1161
|
+
proxy: 'caddy',
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
const localResult = addModule({
|
|
1165
|
+
moduleId: 'files-local',
|
|
1166
|
+
targetRoot: projectRoot,
|
|
1167
|
+
packageRoot,
|
|
1168
|
+
});
|
|
1169
|
+
assert.equal(localResult.applied, true);
|
|
1170
|
+
assertFilesLocalWiring(projectRoot);
|
|
1171
|
+
|
|
1172
|
+
const filesResult = addModule({
|
|
1173
|
+
moduleId: 'files',
|
|
1174
|
+
targetRoot: projectRoot,
|
|
1175
|
+
packageRoot,
|
|
1176
|
+
});
|
|
1177
|
+
assert.equal(filesResult.applied, true);
|
|
1178
|
+
assertFilesWiring(projectRoot);
|
|
1179
|
+
|
|
1180
|
+
const moduleDoc = fs.readFileSync(filesResult.docsPath, 'utf8');
|
|
1181
|
+
assert.match(moduleDoc, /requires `db-adapter`/i);
|
|
1182
|
+
assert.match(moduleDoc, /requires `files-storage-adapter`/i);
|
|
1183
|
+
} finally {
|
|
1184
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it('applies files-s3 foundation module with env and docker wiring', () => {
|
|
1189
|
+
const targetRoot = mkTmp('forgeon-module-files-s3-');
|
|
1190
|
+
const projectRoot = path.join(targetRoot, 'demo-files-s3');
|
|
1191
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1192
|
+
|
|
1193
|
+
try {
|
|
1194
|
+
scaffoldProject({
|
|
1195
|
+
templateRoot,
|
|
1196
|
+
packageRoot,
|
|
1197
|
+
targetRoot: projectRoot,
|
|
1198
|
+
projectName: 'demo-files-s3',
|
|
1199
|
+
frontend: 'react',
|
|
1200
|
+
db: 'prisma',
|
|
1201
|
+
dbPrismaEnabled: true,
|
|
1202
|
+
i18nEnabled: false,
|
|
1203
|
+
proxy: 'caddy',
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
const result = addModule({
|
|
1207
|
+
moduleId: 'files-s3',
|
|
1208
|
+
targetRoot: projectRoot,
|
|
1209
|
+
packageRoot,
|
|
1210
|
+
});
|
|
1211
|
+
assert.equal(result.applied, true);
|
|
1212
|
+
assertFilesS3Wiring(projectRoot);
|
|
1213
|
+
} finally {
|
|
1214
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
it('applies files-s3 then files and keeps s3 driver default without requiring files-local', () => {
|
|
1219
|
+
const targetRoot = mkTmp('forgeon-module-files-s3-runtime-');
|
|
1220
|
+
const projectRoot = path.join(targetRoot, 'demo-files-s3-runtime');
|
|
1221
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1222
|
+
|
|
1223
|
+
try {
|
|
1224
|
+
scaffoldProject({
|
|
1225
|
+
templateRoot,
|
|
1226
|
+
packageRoot,
|
|
1227
|
+
targetRoot: projectRoot,
|
|
1228
|
+
projectName: 'demo-files-s3-runtime',
|
|
1229
|
+
frontend: 'react',
|
|
1230
|
+
db: 'prisma',
|
|
1231
|
+
dbPrismaEnabled: true,
|
|
1232
|
+
i18nEnabled: false,
|
|
1233
|
+
proxy: 'caddy',
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
addModule({
|
|
1237
|
+
moduleId: 'files-s3',
|
|
1238
|
+
targetRoot: projectRoot,
|
|
1239
|
+
packageRoot,
|
|
1240
|
+
});
|
|
1241
|
+
addModule({
|
|
1242
|
+
moduleId: 'files',
|
|
1243
|
+
targetRoot: projectRoot,
|
|
1244
|
+
packageRoot,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
1248
|
+
assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
|
|
1249
|
+
|
|
1250
|
+
const filesService = fs.readFileSync(
|
|
1251
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
1252
|
+
'utf8',
|
|
1253
|
+
);
|
|
1254
|
+
assert.match(filesService, /storeS3/);
|
|
1255
|
+
assert.match(filesService, /openS3/);
|
|
1256
|
+
assert.match(filesService, /deleteS3/);
|
|
1257
|
+
assert.match(filesService, /@aws-sdk\/client-s3/);
|
|
1258
|
+
} finally {
|
|
1259
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it('applies files-access after files and wires file route checks and probe UI', () => {
|
|
1264
|
+
const targetRoot = mkTmp('forgeon-module-files-access-');
|
|
1265
|
+
const projectRoot = path.join(targetRoot, 'demo-files-access');
|
|
1266
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
scaffoldProject({
|
|
1270
|
+
templateRoot,
|
|
1271
|
+
packageRoot,
|
|
1272
|
+
targetRoot: projectRoot,
|
|
1273
|
+
projectName: 'demo-files-access',
|
|
1274
|
+
frontend: 'react',
|
|
1275
|
+
db: 'prisma',
|
|
1276
|
+
dbPrismaEnabled: true,
|
|
1277
|
+
i18nEnabled: false,
|
|
1278
|
+
proxy: 'caddy',
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
addModule({
|
|
1282
|
+
moduleId: 'files-local',
|
|
1283
|
+
targetRoot: projectRoot,
|
|
1284
|
+
packageRoot,
|
|
1285
|
+
});
|
|
1286
|
+
addModule({
|
|
1287
|
+
moduleId: 'files',
|
|
1288
|
+
targetRoot: projectRoot,
|
|
1289
|
+
packageRoot,
|
|
1290
|
+
});
|
|
1291
|
+
const result = addModule({
|
|
1292
|
+
moduleId: 'files-access',
|
|
1293
|
+
targetRoot: projectRoot,
|
|
1294
|
+
packageRoot,
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
assert.equal(result.applied, true);
|
|
1298
|
+
assertFilesAccessWiring(projectRoot);
|
|
1299
|
+
} finally {
|
|
1300
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('applies files-quotas after files and wires upload quota checks and probe UI', () => {
|
|
1305
|
+
const targetRoot = mkTmp('forgeon-module-files-quotas-');
|
|
1306
|
+
const projectRoot = path.join(targetRoot, 'demo-files-quotas');
|
|
1307
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1308
|
+
|
|
1309
|
+
try {
|
|
1310
|
+
scaffoldProject({
|
|
1311
|
+
templateRoot,
|
|
1312
|
+
packageRoot,
|
|
1313
|
+
targetRoot: projectRoot,
|
|
1314
|
+
projectName: 'demo-files-quotas',
|
|
1315
|
+
frontend: 'react',
|
|
1316
|
+
db: 'prisma',
|
|
1317
|
+
dbPrismaEnabled: true,
|
|
1318
|
+
i18nEnabled: false,
|
|
1319
|
+
proxy: 'caddy',
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
addModule({
|
|
1323
|
+
moduleId: 'files-local',
|
|
1324
|
+
targetRoot: projectRoot,
|
|
1325
|
+
packageRoot,
|
|
1326
|
+
});
|
|
1327
|
+
addModule({
|
|
1328
|
+
moduleId: 'files',
|
|
1329
|
+
targetRoot: projectRoot,
|
|
1330
|
+
packageRoot,
|
|
1331
|
+
});
|
|
1332
|
+
const result = addModule({
|
|
1333
|
+
moduleId: 'files-quotas',
|
|
1334
|
+
targetRoot: projectRoot,
|
|
1335
|
+
packageRoot,
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
assert.equal(result.applied, true);
|
|
1339
|
+
assertFilesQuotasWiring(projectRoot);
|
|
1340
|
+
} finally {
|
|
1341
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it('applies files-image after files and wires sanitize pipeline with default metadata stripping', () => {
|
|
1346
|
+
const targetRoot = mkTmp('forgeon-module-files-image-');
|
|
1347
|
+
const projectRoot = path.join(targetRoot, 'demo-files-image');
|
|
1348
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1349
|
+
|
|
1350
|
+
try {
|
|
1351
|
+
scaffoldProject({
|
|
1352
|
+
templateRoot,
|
|
1353
|
+
packageRoot,
|
|
1354
|
+
targetRoot: projectRoot,
|
|
1355
|
+
projectName: 'demo-files-image',
|
|
1356
|
+
frontend: 'react',
|
|
1357
|
+
db: 'prisma',
|
|
1358
|
+
dbPrismaEnabled: true,
|
|
1359
|
+
i18nEnabled: false,
|
|
1360
|
+
proxy: 'caddy',
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
addModule({
|
|
1364
|
+
moduleId: 'files-local',
|
|
1365
|
+
targetRoot: projectRoot,
|
|
1366
|
+
packageRoot,
|
|
1367
|
+
});
|
|
1368
|
+
addModule({
|
|
1369
|
+
moduleId: 'files',
|
|
1370
|
+
targetRoot: projectRoot,
|
|
1371
|
+
packageRoot,
|
|
1372
|
+
});
|
|
1373
|
+
const result = addModule({
|
|
1374
|
+
moduleId: 'files-image',
|
|
1375
|
+
targetRoot: projectRoot,
|
|
1376
|
+
packageRoot,
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
assert.equal(result.applied, true);
|
|
1380
|
+
assertFilesImageWiring(projectRoot);
|
|
1381
|
+
} finally {
|
|
1382
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it('applies full files stack in mixed order and keeps runtime probes consistent', () => {
|
|
1387
|
+
const targetRoot = mkTmp('forgeon-module-files-stack-smoke-');
|
|
1388
|
+
const projectRoot = path.join(targetRoot, 'demo-files-stack-smoke');
|
|
1389
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1390
|
+
|
|
1391
|
+
try {
|
|
1392
|
+
scaffoldProject({
|
|
1393
|
+
templateRoot,
|
|
1394
|
+
packageRoot,
|
|
1395
|
+
targetRoot: projectRoot,
|
|
1396
|
+
projectName: 'demo-files-stack-smoke',
|
|
1397
|
+
frontend: 'react',
|
|
1398
|
+
db: 'prisma',
|
|
1399
|
+
dbPrismaEnabled: true,
|
|
1400
|
+
i18nEnabled: false,
|
|
1401
|
+
proxy: 'caddy',
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
addModule({
|
|
1405
|
+
moduleId: 'files-s3',
|
|
1406
|
+
targetRoot: projectRoot,
|
|
1407
|
+
packageRoot,
|
|
1408
|
+
});
|
|
1409
|
+
addModule({
|
|
1410
|
+
moduleId: 'files',
|
|
1411
|
+
targetRoot: projectRoot,
|
|
1412
|
+
packageRoot,
|
|
1413
|
+
});
|
|
1414
|
+
addModule({
|
|
1415
|
+
moduleId: 'files-image',
|
|
1416
|
+
targetRoot: projectRoot,
|
|
1417
|
+
packageRoot,
|
|
1418
|
+
});
|
|
1419
|
+
addModule({
|
|
1420
|
+
moduleId: 'files-access',
|
|
1421
|
+
targetRoot: projectRoot,
|
|
1422
|
+
packageRoot,
|
|
1423
|
+
});
|
|
1424
|
+
addModule({
|
|
1425
|
+
moduleId: 'files-quotas',
|
|
1426
|
+
targetRoot: projectRoot,
|
|
1427
|
+
packageRoot,
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
assertFilesS3Wiring(projectRoot);
|
|
1431
|
+
assertFilesWiring(projectRoot, 's3');
|
|
1432
|
+
assertFilesImageWiring(projectRoot);
|
|
1433
|
+
assertFilesAccessWiring(projectRoot);
|
|
1434
|
+
assertFilesQuotasWiring(projectRoot);
|
|
1435
|
+
|
|
1436
|
+
const healthController = fs.readFileSync(
|
|
1437
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
1438
|
+
'utf8',
|
|
1439
|
+
);
|
|
1440
|
+
assert.match(healthController, /@Post\('files'\)/);
|
|
1441
|
+
assert.match(healthController, /@Get\('files-variants'\)/);
|
|
1442
|
+
assert.match(healthController, /@Get\('files-image'\)/);
|
|
1443
|
+
assert.match(healthController, /@Get\('files-access'\)/);
|
|
1444
|
+
assert.match(healthController, /@Get\('files-quotas'\)/);
|
|
1445
|
+
|
|
1446
|
+
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
1447
|
+
const filesChecks = appTsx.match(/Check files /g) ?? [];
|
|
1448
|
+
assert.equal(filesChecks.length, 5);
|
|
1449
|
+
} finally {
|
|
1450
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
716
1454
|
it('applies swagger module on top of scaffold without i18n', () => {
|
|
717
1455
|
const targetRoot = mkTmp('forgeon-module-swagger-');
|
|
718
1456
|
const projectRoot = path.join(targetRoot, 'demo-swagger');
|