create-forgeon 0.3.19 → 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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add-help.mjs +3 -2
  3. package/src/core/docs.test.mjs +1 -0
  4. package/src/core/scaffold.test.mjs +1 -0
  5. package/src/modules/accounts.mjs +9 -18
  6. package/src/modules/dependencies.mjs +153 -4
  7. package/src/modules/dependencies.test.mjs +58 -0
  8. package/src/modules/executor.test.mjs +544 -515
  9. package/src/modules/files-access.mjs +375 -375
  10. package/src/modules/files-image.mjs +512 -510
  11. package/src/modules/files-quotas.mjs +365 -365
  12. package/src/modules/files.mjs +5 -6
  13. package/src/modules/idempotency.test.mjs +3 -2
  14. package/src/modules/registry.mjs +20 -0
  15. package/src/modules/shared/files-runtime-wiring.mjs +13 -10
  16. package/src/run-add-module.mjs +39 -26
  17. package/src/run-add-module.test.mjs +228 -152
  18. package/src/run-scan-integrations.mjs +1 -0
  19. package/templates/base/package.json +1 -0
  20. package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
  21. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
  22. package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
  23. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
  24. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
  25. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
  26. package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
  27. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
  28. package/templates/module-presets/files/packages/files/package.json +1 -0
  29. package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
  30. package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
  31. package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
  32. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
  33. package/templates/module-presets/files/packages/files/src/index.ts +1 -0
  34. package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
  35. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
  36. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
  37. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
  38. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
@@ -1,4 +1,4 @@
1
- import { describe, it } from 'node:test';
1
+ import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
@@ -234,241 +234,239 @@ function assertRbacWiring(projectRoot) {
234
234
  assert.match(readme, /accounts.*optional/i);
235
235
  }
236
236
 
237
- function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
238
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
239
- assert.match(appModule, /filesConfig/);
240
- assert.match(appModule, /filesEnvSchema/);
241
- assert.match(appModule, /ForgeonFilesModule\.register\(\{/);
242
- assert.match(appModule, /ForgeonFilesDbPrismaModule/);
243
- if (expectedStorageDriver === 's3') {
244
- assert.match(appModule, /ForgeonFilesS3StorageModule/);
245
- assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
246
- } else {
247
- assert.match(appModule, /ForgeonFilesLocalStorageModule/);
248
- }
249
-
250
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
251
- assert.match(apiPackage, /@forgeon\/files/);
252
- assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
253
-
254
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
255
- assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
256
- assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
257
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
258
-
259
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
260
- assert.match(apiEnv, /FILES_ENABLED=true/);
261
- assert.match(apiEnv, new RegExp('FILES_STORAGE_DRIVER=' + expectedStorageDriver));
262
- assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
263
- assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
264
- assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
265
-
266
- const healthController = fs.readFileSync(
267
- path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
268
- 'utf8',
269
- );
270
- assert.match(healthController, /@Post\('files'\)/);
271
- assert.match(healthController, /@Get\('files-variants'\)/);
272
- assert.match(healthController, /filesService\.createProbeRecord/);
273
- assert.match(healthController, /filesService\.getVariantsProbeStatus/);
274
- assert.match(healthController, /filesService\.deleteByPublicId/);
275
-
276
- const filesController = fs.readFileSync(
277
- path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
278
- 'utf8',
279
- );
280
- assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
281
- assert.match(filesController, /parseVariant\(variantQuery\)/);
282
- assert.match(filesController, /@Delete\(':publicId'\)/);
283
-
284
- const filesService = fs.readFileSync(
285
- path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
286
- 'utf8',
287
- );
288
- assert.match(filesService, /FILES_PERSISTENCE_PORT/);
289
- assert.match(filesService, /FILES_STORAGE_ADAPTER/);
290
- assert.match(filesService, /getOrCreateBlob/);
291
- assert.match(filesService, /cleanupReferencedBlobs/);
292
- assert.match(filesService, /isUniqueConstraintError/);
293
- assert.match(filesService, /storageAdapter\.put/);
294
- assert.match(filesService, /persistence\.createBlob/);
295
- assert.match(filesService, /persistence\.deleteBlobIfUnreferenced/);
296
- assert.doesNotMatch(filesService, /PrismaService/);
297
- assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
298
-
299
- const filesPorts = fs.readFileSync(
300
- path.join(projectRoot, 'packages', 'files', 'src', 'files.ports.ts'),
301
- 'utf8',
302
- );
303
- assert.match(filesPorts, /FILES_PERSISTENCE_PORT/);
304
- assert.match(filesPorts, /FILES_STORAGE_ADAPTER/);
305
- assert.match(filesPorts, /interface FilesPersistencePort/);
306
- assert.match(filesPorts, /interface FilesStorageAdapter/);
307
-
308
- const filesModule = fs.readFileSync(
309
- path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
310
- 'utf8',
311
- );
312
- assert.match(filesModule, /ForgeonFilesModuleOptions/);
313
- assert.match(filesModule, /static register\(options: ForgeonFilesModuleOptions = \{\}\)/);
314
- assert.match(filesModule, /FILES_PERSISTENCE_PORT/);
315
- assert.match(filesModule, /FILES_STORAGE_ADAPTER/);
316
-
317
- const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
318
- assert.doesNotMatch(filesPackage, /@forgeon\/db-prisma/);
319
-
320
- const prismaFilesStore = fs.readFileSync(
321
- path.join(projectRoot, 'apps', 'api', 'src', 'files', 'prisma-files-persistence.store.ts'),
322
- 'utf8',
323
- );
324
- assert.match(prismaFilesStore, /PrismaService/);
325
- assert.match(prismaFilesStore, /FILES_PERSISTENCE_PORT/);
326
- assert.match(prismaFilesStore, /fileBlob\.deleteMany/);
327
-
328
- const prismaFilesModule = fs.readFileSync(
329
- path.join(projectRoot, 'apps', 'api', 'src', 'files', 'forgeon-files-db-prisma.module.ts'),
330
- 'utf8',
331
- );
332
- assert.match(prismaFilesModule, /ForgeonFilesDbPrismaModule/);
333
- assert.match(prismaFilesModule, /DbPrismaModule/);
334
- assert.match(prismaFilesModule, /FILES_PERSISTENCE_PORT/);
335
-
336
- assertWebProbeShell(projectRoot);
337
- const probesTs = readWebProbes(projectRoot);
338
- assert.match(probesTs, /"id": "files"/);
339
- assert.match(probesTs, /"buttonLabel": "Check files probe \(create metadata\)"/);
340
- assert.match(probesTs, /"resultTitle": "Files probe response"/);
341
- assert.match(probesTs, /"id": "files-variants"/);
342
- assert.match(probesTs, /"buttonLabel": "Check files variants capability"/);
343
- assert.match(probesTs, /"resultTitle": "Files variants probe response"/);
344
-
345
- const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
346
- assert.match(schema, /model FileRecord \{/);
347
- assert.match(schema, /variants\s+FileVariant\[\]/);
348
- assert.match(schema, /model FileVariant \{/);
349
- assert.match(schema, /model FileBlob \{/);
350
- assert.match(schema, /blobId\s+String/);
351
- assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
352
- assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
353
- assert.match(schema, /publicId\s+String\s+@unique/);
354
- assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
355
-
356
- const migration = path.join(
357
- projectRoot,
358
- 'apps',
359
- 'api',
360
- 'prisma',
361
- 'migrations',
362
- '20260306_files_file_record',
363
- 'migration.sql',
364
- );
365
- assert.equal(fs.existsSync(migration), true);
366
-
367
- const variantMigration = path.join(
368
- projectRoot,
369
- 'apps',
370
- 'api',
371
- 'prisma',
372
- 'migrations',
373
- '20260306_files_file_variant',
374
- 'migration.sql',
375
- );
376
- assert.equal(fs.existsSync(variantMigration), true);
377
- }
378
-
379
- function assertFilesLocalWiring(projectRoot) {
380
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
381
- assert.match(appModule, /filesLocalConfig/);
382
- assert.match(appModule, /filesLocalEnvSchemaZod/);
383
- assert.match(appModule, /FilesLocalConfigModule/);
384
- assert.match(appModule, /ForgeonFilesLocalStorageModule/);
385
-
386
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
387
- assert.match(apiPackage, /@forgeon\/files-local/);
388
- assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
389
-
390
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
391
- assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
392
- assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
393
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
394
-
395
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
396
- assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
397
-
398
- const localModule = fs.readFileSync(
399
- path.join(projectRoot, 'packages', 'files-local', 'src', 'forgeon-files-local-storage.module.ts'),
400
- 'utf8',
401
- );
402
- assert.match(localModule, /ForgeonFilesLocalStorageModule/);
403
- assert.match(localModule, /FORGEON_FILES_STORAGE_ADAPTER/);
404
-
405
- const localAdapter = fs.readFileSync(
406
- path.join(projectRoot, 'packages', 'files-local', 'src', 'local-files-storage.adapter.ts'),
407
- 'utf8',
408
- );
409
- assert.match(localAdapter, /readonly driver = 'local'/);
410
- assert.match(localAdapter, /createReadStream/);
411
- assert.match(localAdapter, /writeFile/);
412
-
413
- const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
414
- assert.match(gitignore, /storage\//);
415
-
416
- const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
417
- assert.match(compose, /files_data:\/app\/storage/);
418
- assert.match(compose, /^\s{2}files_data:\s*$/m);
419
- }
420
-
421
- function assertFilesS3Wiring(projectRoot) {
422
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
423
- assert.match(appModule, /filesS3Config/);
424
- assert.match(appModule, /filesS3EnvSchemaZod/);
425
- assert.match(appModule, /FilesS3ConfigModule/);
426
- assert.match(appModule, /ForgeonFilesS3StorageModule/);
427
-
428
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
429
- assert.match(apiPackage, /@forgeon\/files-s3/);
430
- assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
431
-
432
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
433
- assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
434
- assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
435
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
436
-
437
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
438
- assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
439
- assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
440
- assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
441
- assert.match(apiEnv, /FILES_S3_REGION=/);
442
- assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
443
- assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
444
- assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
445
-
446
- const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
447
- assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
448
- assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
449
-
450
- const filesS3Package = fs.readFileSync(
451
- path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
452
- 'utf8',
453
- );
454
- assert.match(filesS3Package, /@aws-sdk\/client-s3/);
455
-
456
- const s3Module = fs.readFileSync(
457
- path.join(projectRoot, 'packages', 'files-s3', 'src', 'forgeon-files-s3-storage.module.ts'),
458
- 'utf8',
459
- );
460
- assert.match(s3Module, /ForgeonFilesS3StorageModule/);
461
- assert.match(s3Module, /FORGEON_FILES_STORAGE_ADAPTER/);
462
-
463
- const s3Adapter = fs.readFileSync(
464
- path.join(projectRoot, 'packages', 'files-s3', 'src', 's3-files-storage.adapter.ts'),
465
- 'utf8',
466
- );
467
- assert.match(s3Adapter, /readonly driver = 's3'/);
468
- assert.match(s3Adapter, /@aws-sdk\/client-s3/);
469
- assert.match(s3Adapter, /loadS3Module/);
470
- }
471
-
237
+ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
238
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
239
+ assert.match(appModule, /filesConfig/);
240
+ assert.match(appModule, /filesEnvSchema/);
241
+ assert.match(appModule, /ForgeonFilesModule\.register\(\{/);
242
+ assert.doesNotMatch(appModule, /ForgeonFilesDbPrismaModule/);
243
+ if (expectedStorageDriver === 's3') {
244
+ assert.match(appModule, /ForgeonFilesS3StorageModule/);
245
+ assert.doesNotMatch(appModule, /ForgeonFilesLocalStorageModule/);
246
+ } else {
247
+ assert.match(appModule, /ForgeonFilesLocalStorageModule/);
248
+ }
249
+
250
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
251
+ assert.match(apiPackage, /@forgeon\/files/);
252
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
253
+
254
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
255
+ assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
256
+ assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
257
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
258
+
259
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
260
+ assert.match(apiEnv, /FILES_ENABLED=true/);
261
+ assert.match(apiEnv, new RegExp('FILES_STORAGE_DRIVER=' + expectedStorageDriver));
262
+ assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
263
+ assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
264
+ assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
265
+
266
+ const healthController = fs.readFileSync(
267
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
268
+ 'utf8',
269
+ );
270
+ assert.match(healthController, /@Post\('files'\)/);
271
+ assert.match(healthController, /@Get\('files-variants'\)/);
272
+ assert.match(healthController, /filesService\.createProbeRecord/);
273
+ assert.match(healthController, /filesService\.getVariantsProbeStatus/);
274
+ assert.match(healthController, /filesService\.deleteByPublicId/);
275
+
276
+ const filesController = fs.readFileSync(
277
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
278
+ 'utf8',
279
+ );
280
+ assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
281
+ assert.match(filesController, /parseVariant\(variantQuery\)/);
282
+ assert.match(filesController, /@Delete\(':publicId'\)/);
283
+
284
+ const filesService = fs.readFileSync(
285
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
286
+ 'utf8',
287
+ );
288
+ assert.match(filesService, /FilesStore/);
289
+ assert.match(filesService, /FILES_STORAGE_ADAPTER/);
290
+ assert.match(filesService, /requireStorageAdapter/);
291
+ assert.match(filesService, /getOrCreateBlob/);
292
+ assert.match(filesService, /cleanupReferencedBlobs/);
293
+ assert.match(filesService, /isUniqueConstraintError/);
294
+ assert.match(filesService, /storageAdapter\.put/);
295
+ assert.match(filesService, /filesStore\.createBlob/);
296
+ assert.match(filesService, /filesStore\.deleteBlobIfUnreferenced/);
297
+ assert.doesNotMatch(filesService, /FILES_PERSISTENCE_PORT/);
298
+ assert.doesNotMatch(filesService, /PrismaService/);
299
+ assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
300
+
301
+ const filesPorts = fs.readFileSync(
302
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.ports.ts'),
303
+ 'utf8',
304
+ );
305
+ assert.doesNotMatch(filesPorts, /FILES_PERSISTENCE_PORT/);
306
+ assert.doesNotMatch(filesPorts, /interface FilesPersistencePort/);
307
+ assert.match(filesPorts, /FILES_STORAGE_ADAPTER/);
308
+ assert.match(filesPorts, /interface FilesStorageAdapter/);
309
+
310
+ const filesStore = fs.readFileSync(
311
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.store.ts'),
312
+ 'utf8',
313
+ );
314
+ assert.match(filesStore, /PrismaService/);
315
+ assert.match(filesStore, /fileBlob\.deleteMany/);
316
+
317
+ const filesModule = fs.readFileSync(
318
+ path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
319
+ 'utf8',
320
+ );
321
+ assert.match(filesModule, /ForgeonFilesModuleOptions/);
322
+ assert.match(filesModule, /static register\(options: ForgeonFilesModuleOptions = \{\}\)/);
323
+ assert.match(filesModule, /DbPrismaModule/);
324
+ assert.match(filesModule, /FilesStore/);
325
+ assert.doesNotMatch(filesModule, /FILES_PERSISTENCE_PORT/);
326
+
327
+ const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
328
+ assert.match(filesPackage, /@forgeon\/db-prisma/);
329
+
330
+ const prismaFilesDir = path.join(projectRoot, 'apps', 'api', 'src', 'files');
331
+ assert.equal(fs.existsSync(path.join(prismaFilesDir, 'prisma-files-persistence.store.ts')), false);
332
+ assert.equal(fs.existsSync(path.join(prismaFilesDir, 'forgeon-files-db-prisma.module.ts')), false);
333
+
334
+ assertWebProbeShell(projectRoot);
335
+ const probesTs = readWebProbes(projectRoot);
336
+ assert.match(probesTs, /"id": "files"/);
337
+ assert.match(probesTs, /"buttonLabel": "Check files probe \(create metadata\)"/);
338
+ assert.match(probesTs, /"resultTitle": "Files probe response"/);
339
+ assert.match(probesTs, /"id": "files-variants"/);
340
+ assert.match(probesTs, /"buttonLabel": "Check files variants capability"/);
341
+ assert.match(probesTs, /"resultTitle": "Files variants probe response"/);
342
+
343
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
344
+ assert.match(schema, /model FileRecord \{/);
345
+ assert.match(schema, /variants\s+FileVariant\[\]/);
346
+ assert.match(schema, /model FileVariant \{/);
347
+ assert.match(schema, /model FileBlob \{/);
348
+ assert.match(schema, /blobId\s+String/);
349
+ assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
350
+ assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
351
+ assert.match(schema, /publicId\s+String\s+@unique/);
352
+ assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
353
+
354
+ const migration = path.join(
355
+ projectRoot,
356
+ 'apps',
357
+ 'api',
358
+ 'prisma',
359
+ 'migrations',
360
+ '20260306_files_file_record',
361
+ 'migration.sql',
362
+ );
363
+ assert.equal(fs.existsSync(migration), true);
364
+
365
+ const variantMigration = path.join(
366
+ projectRoot,
367
+ 'apps',
368
+ 'api',
369
+ 'prisma',
370
+ 'migrations',
371
+ '20260306_files_file_variant',
372
+ 'migration.sql',
373
+ );
374
+ assert.equal(fs.existsSync(variantMigration), true);
375
+ }
376
+
377
+ function assertFilesLocalWiring(projectRoot) {
378
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
379
+ assert.match(appModule, /filesLocalConfig/);
380
+ assert.match(appModule, /filesLocalEnvSchemaZod/);
381
+ assert.match(appModule, /FilesLocalConfigModule/);
382
+ assert.match(appModule, /ForgeonFilesLocalStorageModule/);
383
+
384
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
385
+ assert.match(apiPackage, /@forgeon\/files-local/);
386
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
387
+
388
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
389
+ assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
390
+ assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
391
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
392
+
393
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
394
+ assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
395
+
396
+ const localModule = fs.readFileSync(
397
+ path.join(projectRoot, 'packages', 'files-local', 'src', 'forgeon-files-local-storage.module.ts'),
398
+ 'utf8',
399
+ );
400
+ assert.match(localModule, /ForgeonFilesLocalStorageModule/);
401
+ assert.match(localModule, /FORGEON_FILES_STORAGE_ADAPTER/);
402
+
403
+ const localAdapter = fs.readFileSync(
404
+ path.join(projectRoot, 'packages', 'files-local', 'src', 'local-files-storage.adapter.ts'),
405
+ 'utf8',
406
+ );
407
+ assert.match(localAdapter, /readonly driver = 'local'/);
408
+ assert.match(localAdapter, /createReadStream/);
409
+ assert.match(localAdapter, /writeFile/);
410
+
411
+ const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
412
+ assert.match(gitignore, /storage\//);
413
+
414
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
415
+ assert.match(compose, /files_data:\/app\/storage/);
416
+ assert.match(compose, /^\s{2}files_data:\s*$/m);
417
+ }
418
+
419
+ function assertFilesS3Wiring(projectRoot) {
420
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
421
+ assert.match(appModule, /filesS3Config/);
422
+ assert.match(appModule, /filesS3EnvSchemaZod/);
423
+ assert.match(appModule, /FilesS3ConfigModule/);
424
+ assert.match(appModule, /ForgeonFilesS3StorageModule/);
425
+
426
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
427
+ assert.match(apiPackage, /@forgeon\/files-s3/);
428
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
429
+
430
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
431
+ assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
432
+ assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
433
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
434
+
435
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
436
+ assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
437
+ assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
438
+ assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
439
+ assert.match(apiEnv, /FILES_S3_REGION=/);
440
+ assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
441
+ assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
442
+ assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
443
+
444
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
445
+ assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
446
+ assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
447
+
448
+ const filesS3Package = fs.readFileSync(
449
+ path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
450
+ 'utf8',
451
+ );
452
+ assert.match(filesS3Package, /@aws-sdk\/client-s3/);
453
+
454
+ const s3Module = fs.readFileSync(
455
+ path.join(projectRoot, 'packages', 'files-s3', 'src', 'forgeon-files-s3-storage.module.ts'),
456
+ 'utf8',
457
+ );
458
+ assert.match(s3Module, /ForgeonFilesS3StorageModule/);
459
+ assert.match(s3Module, /FORGEON_FILES_STORAGE_ADAPTER/);
460
+
461
+ const s3Adapter = fs.readFileSync(
462
+ path.join(projectRoot, 'packages', 'files-s3', 'src', 's3-files-storage.adapter.ts'),
463
+ 'utf8',
464
+ );
465
+ assert.match(s3Adapter, /readonly driver = 's3'/);
466
+ assert.match(s3Adapter, /@aws-sdk\/client-s3/);
467
+ assert.match(s3Adapter, /loadS3Module/);
468
+ }
469
+
472
470
  function assertFilesAccessWiring(projectRoot) {
473
471
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
474
472
  assert.match(appModule, /ForgeonFilesAccessModule/);
@@ -534,14 +532,14 @@ function assertFilesQuotasWiring(projectRoot) {
534
532
  assert.match(appModule, /filesQuotasEnvSchema/);
535
533
  assert.match(appModule, /ForgeonFilesQuotasModule/);
536
534
 
537
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
538
- assert.match(apiPackage, /@forgeon\/files-quotas/);
539
- assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
540
- assert.equal(
541
- apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') <
542
- apiPackage.indexOf('pnpm --filter @forgeon/files build'),
543
- true,
544
- );
535
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
536
+ assert.match(apiPackage, /@forgeon\/files-quotas/);
537
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
538
+ assert.equal(
539
+ apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') >
540
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
541
+ true,
542
+ );
545
543
 
546
544
  const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
547
545
  assert.doesNotMatch(filesPackage, /@forgeon\/files-quotas/);
@@ -552,13 +550,13 @@ function assertFilesQuotasWiring(projectRoot) {
552
550
  apiDockerfile,
553
551
  /COPY packages\/files-quotas\/package\.json packages\/files-quotas\/package\.json/,
554
552
  );
555
- assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
556
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
557
- assert.equal(
558
- apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') <
559
- apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
560
- true,
561
- );
553
+ assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
554
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
555
+ assert.equal(
556
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') >
557
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
558
+ true,
559
+ );
562
560
 
563
561
  const filesController = fs.readFileSync(
564
562
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
@@ -627,6 +625,11 @@ function assertFilesImageWiring(projectRoot) {
627
625
  assert.match(filesService, /filesImageService\.sanitizeForStorage/);
628
626
  assert.match(filesService, /sanitizeForStorage\({/);
629
627
  assert.match(filesService, /auditContext: input\.auditContext/);
628
+ assert.equal(
629
+ (filesService.match(/protected normalizeFileName\(originalName: string, extension: string, suffix\?: string\): string \{/g) ?? [])
630
+ .length,
631
+ 1,
632
+ );
630
633
 
631
634
  const filesController = fs.readFileSync(
632
635
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
@@ -706,83 +709,108 @@ function assertFilesImageWiring(projectRoot) {
706
709
  assert.match(readme, /metadata is stripped before storage/i);
707
710
  }
708
711
 
709
- function assertAccountsWiring(projectRoot) {
710
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
711
- assert.match(apiPackage, /@forgeon\/accounts-api/);
712
- assert.match(apiPackage, /@forgeon\/accounts-contracts/);
713
- assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-contracts build/);
714
- assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-api build/);
715
-
716
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
717
- assert.match(appModule, /authConfig/);
718
- assert.match(appModule, /authEnvSchema/);
719
- assert.match(appModule, /ForgeonAccountsModule\.register\(\{/);
720
- assert.match(appModule, /imports: \[DbPrismaModule\]/);
721
- assert.match(appModule, /PrismaAccountsPersistenceStore/);
722
- assert.match(appModule, /provide: ACCOUNTS_PERSISTENCE_PORT/);
723
- assert.match(appModule, /useExisting: PrismaAccountsPersistenceStore/);
724
- assert.match(appModule, /UsersModule\.register\(\{\}\)/);
725
- assert.doesNotMatch(appModule, /AUTH_REFRESH_TOKEN_STORE/);
726
-
727
- const healthController = fs.readFileSync(
728
- path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
729
- 'utf8',
730
- );
731
- assert.match(healthController, /@Get\('auth'\)/);
732
- assert.match(healthController, /authService\.getProbeStatus/);
733
- assert.match(healthController, /\$queryRaw/);
734
- assert.doesNotMatch(healthController, /data:\s*\{\s*email\s*\}/);
735
- assert.doesNotMatch(healthController, /,\s*,/);
736
-
737
- assertWebProbeShell(projectRoot);
738
- const probesTs = readWebProbes(projectRoot);
739
- assert.match(probesTs, /"id": "auth"/);
740
- assert.match(probesTs, /"buttonLabel": "Check accounts probe"/);
741
- assert.match(probesTs, /"resultTitle": "Accounts probe response"/);
742
-
743
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
744
- assert.match(
745
- apiDockerfile,
746
- /COPY packages\/accounts-contracts\/package\.json packages\/accounts-contracts\/package\.json/,
747
- );
748
- assert.match(apiDockerfile, /COPY packages\/accounts-api\/package\.json packages\/accounts-api\/package\.json/);
749
- assert.match(apiDockerfile, /COPY packages\/accounts-contracts packages\/accounts-contracts/);
750
- assert.match(apiDockerfile, /COPY packages\/accounts-api packages\/accounts-api/);
751
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-contracts build/);
752
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-api build/);
753
-
754
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
755
- assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
756
- assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
757
- assert.match(apiEnv, /AUTH_ARGON2_MEMORY_COST=/);
758
- assert.match(apiEnv, /AUTH_ARGON2_PARALLELISM=/);
759
-
760
- const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
761
- assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
762
- assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
763
- assert.match(compose, /AUTH_ARGON2_MEMORY_COST: \$\{AUTH_ARGON2_MEMORY_COST\}/);
764
-
765
- const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
766
- assert.match(readme, /## Accounts Module/);
767
- assert.match(readme, /owner-scoped user routes/);
768
- assert.match(readme, /AccountsEmailPort/);
769
-
770
- const authServiceSource = fs.readFileSync(
771
- path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
772
- 'utf8',
773
- );
774
- assert.match(authServiceSource, /import type \{ RegisterRequest \} from '@forgeon\/accounts-contracts';/);
775
-
776
- const prismaStorePath = path.join(
777
- projectRoot,
778
- 'apps',
779
- 'api',
780
- 'src',
781
- 'accounts',
782
- 'prisma-accounts-persistence.store.ts',
783
- );
784
- assert.equal(fs.existsSync(prismaStorePath), true);
785
- }
712
+ function assertAccountsWiring(projectRoot) {
713
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
714
+ assert.match(apiPackage, /@forgeon\/accounts-api/);
715
+ assert.match(apiPackage, /@forgeon\/accounts-contracts/);
716
+ assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-contracts build/);
717
+ assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-api build/);
718
+
719
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
720
+ assert.match(appModule, /authConfig/);
721
+ assert.match(appModule, /authEnvSchema/);
722
+ assert.match(appModule, /ForgeonAccountsModule\.register\(\{/);
723
+ assert.match(appModule, /UsersModule\.register\(\{\}\)/);
724
+ assert.doesNotMatch(appModule, /ACCOUNTS_PERSISTENCE_PORT/);
725
+ assert.doesNotMatch(appModule, /PrismaAccountsPersistenceStore/);
726
+ assert.doesNotMatch(appModule, /AUTH_REFRESH_TOKEN_STORE/);
727
+
728
+ const healthController = fs.readFileSync(
729
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
730
+ 'utf8',
731
+ );
732
+ assert.match(healthController, /@Get\('auth'\)/);
733
+ assert.match(healthController, /authService\.getProbeStatus/);
734
+ assert.match(healthController, /\$queryRaw/);
735
+ assert.doesNotMatch(healthController, /data:\s*\{\s*email\s*\}/);
736
+ assert.doesNotMatch(healthController, /,\s*,/);
737
+
738
+ assertWebProbeShell(projectRoot);
739
+ const probesTs = readWebProbes(projectRoot);
740
+ assert.match(probesTs, /"id": "auth"/);
741
+ assert.match(probesTs, /"buttonLabel": "Check accounts probe"/);
742
+ assert.match(probesTs, /"resultTitle": "Accounts probe response"/);
743
+
744
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
745
+ assert.match(
746
+ apiDockerfile,
747
+ /COPY packages\/accounts-contracts\/package\.json packages\/accounts-contracts\/package\.json/,
748
+ );
749
+ assert.match(apiDockerfile, /COPY packages\/accounts-api\/package\.json packages\/accounts-api\/package\.json/);
750
+ assert.match(apiDockerfile, /COPY packages\/accounts-contracts packages\/accounts-contracts/);
751
+ assert.match(apiDockerfile, /COPY packages\/accounts-api packages\/accounts-api/);
752
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-contracts build/);
753
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-api build/);
754
+
755
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
756
+ assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
757
+ assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
758
+ assert.match(apiEnv, /AUTH_ARGON2_MEMORY_COST=/);
759
+ assert.match(apiEnv, /AUTH_ARGON2_PARALLELISM=/);
760
+
761
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
762
+ assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
763
+ assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
764
+ assert.match(compose, /AUTH_ARGON2_MEMORY_COST: \$\{AUTH_ARGON2_MEMORY_COST\}/);
765
+
766
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
767
+ assert.match(readme, /## Accounts Module/);
768
+ assert.match(readme, /owner-scoped user routes/);
769
+ assert.match(readme, /AccountsEmailPort/);
770
+
771
+ const authServiceSource = fs.readFileSync(
772
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
773
+ 'utf8',
774
+ );
775
+ assert.match(authServiceSource, /import type \{ RegisterRequest \} from '@forgeon\/accounts-contracts';/);
776
+
777
+ const authCoreSource = fs.readFileSync(
778
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth-core.service.ts'),
779
+ 'utf8',
780
+ );
781
+ assert.match(authCoreSource, /AuthStore/);
782
+ assert.doesNotMatch(authCoreSource, /ACCOUNTS_PERSISTENCE_PORT/);
783
+
784
+ const accountsApiPackage = fs.readFileSync(
785
+ path.join(projectRoot, 'packages', 'accounts-api', 'package.json'),
786
+ 'utf8',
787
+ );
788
+ assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
789
+
790
+ const authStoreSource = fs.readFileSync(
791
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.store.ts'),
792
+ 'utf8',
793
+ );
794
+ assert.match(authStoreSource, /PrismaService/);
795
+ assert.match(authStoreSource, /authRefreshToken\.updateMany/);
796
+
797
+ const usersStoreSource = fs.readFileSync(
798
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'users.store.ts'),
799
+ 'utf8',
800
+ );
801
+ assert.match(usersStoreSource, /PrismaService/);
802
+ assert.match(usersStoreSource, /userProfile\.upsert/);
803
+
804
+ const prismaStorePath = path.join(
805
+ projectRoot,
806
+ 'apps',
807
+ 'api',
808
+ 'src',
809
+ 'accounts',
810
+ 'prisma-accounts-persistence.store.ts',
811
+ );
812
+ assert.equal(fs.existsSync(prismaStorePath), false);
813
+ }
786
814
  function stripDbPrismaArtifacts(projectRoot) {
787
815
  const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
788
816
  if (fs.existsSync(dbPackageDir)) {
@@ -1545,21 +1573,21 @@ describe('addModule', () => {
1545
1573
  packageRoot,
1546
1574
  });
1547
1575
 
1548
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1549
- assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
1550
-
1551
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
1552
- assert.match(appModule, /ForgeonFilesS3StorageModule/);
1553
- assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
1554
-
1555
- const filesService = fs.readFileSync(
1556
- path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
1557
- 'utf8',
1558
- );
1559
- assert.doesNotMatch(filesService, /storeS3/);
1560
- assert.doesNotMatch(filesService, /openS3/);
1561
- assert.doesNotMatch(filesService, /deleteS3/);
1562
- assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
1576
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1577
+ assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
1578
+
1579
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
1580
+ assert.match(appModule, /ForgeonFilesS3StorageModule/);
1581
+ assert.doesNotMatch(appModule, /ForgeonFilesLocalStorageModule/);
1582
+
1583
+ const filesService = fs.readFileSync(
1584
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
1585
+ 'utf8',
1586
+ );
1587
+ assert.doesNotMatch(filesService, /storeS3/);
1588
+ assert.doesNotMatch(filesService, /openS3/);
1589
+ assert.doesNotMatch(filesService, /deleteS3/);
1590
+ assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
1563
1591
  } finally {
1564
1592
  fs.rmSync(targetRoot, { recursive: true, force: true });
1565
1593
  }
@@ -2169,178 +2197,178 @@ describe('addModule', () => {
2169
2197
  }
2170
2198
  });
2171
2199
 
2172
- it('applies accounts with db-prisma and wires the DB-backed runtime immediately', () => {
2173
- const targetRoot = mkTmp('forgeon-module-accounts-db-');
2174
- const projectRoot = path.join(targetRoot, 'demo-accounts-db');
2175
- const templateRoot = path.join(packageRoot, 'templates', 'base');
2176
-
2177
- try {
2178
- scaffoldProject({
2179
- templateRoot,
2180
- packageRoot,
2181
- targetRoot: projectRoot,
2182
- projectName: 'demo-accounts-db',
2183
- frontend: 'react',
2184
- db: 'prisma',
2185
- dbPrismaEnabled: true,
2186
- i18nEnabled: true,
2187
- proxy: 'caddy',
2188
- });
2189
-
2190
- const result = addModule({
2191
- moduleId: 'accounts',
2192
- targetRoot: projectRoot,
2193
- packageRoot,
2194
- });
2195
-
2196
- assert.equal(result.applied, true);
2197
- assertAccountsWiring(projectRoot);
2198
-
2199
- const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
2200
- assert.match(schema, /model UserProfile/);
2201
- assert.match(schema, /model UserSettings/);
2202
- assert.match(schema, /model AuthIdentity/);
2203
- assert.match(schema, /model AuthCredential/);
2204
- assert.match(schema, /model AuthRefreshToken/);
2205
- assert.doesNotMatch(schema, /roles\s+/i);
2206
- assert.doesNotMatch(schema, /permissions\s+/i);
2207
-
2208
- const migrationPath = path.join(
2209
- projectRoot,
2210
- 'apps',
2211
- 'api',
2212
- 'prisma',
2213
- 'migrations',
2214
- '0002_accounts_core',
2215
- 'migration.sql',
2216
- );
2217
- assert.equal(fs.existsSync(migrationPath), true);
2218
-
2219
- const seedSource = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'seed.ts'), 'utf8');
2220
- assert.match(seedSource, /Prisma\.dmmf/);
2221
- assert.match(seedSource, /userFields\.has\('email'\)/);
2222
-
2223
- const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2224
- assert.match(readme, /POST \/api\/auth\/register/);
2225
- assert.match(readme, /POST \/api\/auth\/password-reset\/request/);
2226
- assert.match(readme, /\/api\/users\/:id\/settings/);
2227
-
2228
- const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
2229
- assert.match(moduleDoc, /Status: implemented/);
2230
- assert.match(moduleDoc, /db-adapter/);
2231
- assert.match(moduleDoc, /owner-scoped/);
2232
- } finally {
2233
- fs.rmSync(targetRoot, { recursive: true, force: true });
2234
- }
2235
- });
2236
-
2237
- it('detects and applies accounts-rbac compatibility sync explicitly', () => {
2238
- const targetRoot = mkTmp('forgeon-module-accounts-rbac-');
2239
- const projectRoot = path.join(targetRoot, 'demo-accounts-rbac');
2240
- const templateRoot = path.join(packageRoot, 'templates', 'base');
2241
-
2242
- try {
2243
- scaffoldProject({
2244
- templateRoot,
2245
- packageRoot,
2246
- targetRoot: projectRoot,
2247
- projectName: 'demo-accounts-rbac',
2248
- frontend: 'react',
2249
- db: 'prisma',
2250
- dbPrismaEnabled: true,
2251
- i18nEnabled: false,
2252
- proxy: 'caddy',
2253
- });
2254
-
2255
- addModule({
2256
- moduleId: 'rbac',
2257
- targetRoot: projectRoot,
2258
- packageRoot,
2259
- });
2260
- addModule({
2261
- moduleId: 'accounts',
2262
- targetRoot: projectRoot,
2263
- packageRoot,
2264
- });
2265
-
2266
- const scan = scanIntegrations({
2267
- targetRoot: projectRoot,
2268
- relatedModuleId: 'accounts',
2269
- });
2270
- assert.equal(scan.groups.some((group) => group.id === 'accounts-rbac'), true);
2271
-
2272
- const syncResult = syncIntegrations({
2273
- targetRoot: projectRoot,
2274
- packageRoot,
2275
- groupIds: ['accounts-rbac'],
2276
- });
2277
- const claimsPair = syncResult.summary.find((item) => item.id === 'accounts-rbac');
2278
- assert.ok(claimsPair);
2279
- assert.equal(claimsPair.result.applied, true);
2280
-
2281
- const contracts = fs.readFileSync(
2282
- path.join(projectRoot, 'packages', 'accounts-contracts', 'src', 'index.ts'),
2283
- 'utf8',
2284
- );
2285
- assert.match(contracts, /roles\?: string\[\];/);
2286
- assert.match(contracts, /permissions\?: string\[\];/);
2287
-
2288
- const authTypes = fs.readFileSync(
2289
- path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.types.ts'),
2290
- 'utf8',
2291
- );
2292
- assert.match(authTypes, /roles\?: string\[\];/);
2293
- assert.match(authTypes, /permissions\?: string\[\];/);
2294
-
2295
- const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2296
- assert.match(readme, /forgeon:accounts:rbac:start/);
2297
- assert.match(readme, /base accounts schema remains free of roles and permissions/);
2298
- } finally {
2299
- fs.rmSync(targetRoot, { recursive: true, force: true });
2300
- }
2301
- });
2302
-
2303
- it('scans accounts-rbac compatibility when accounts and rbac are both installed', () => {
2304
- const targetRoot = mkTmp('forgeon-module-accounts-rbac-scan-');
2305
- const projectRoot = path.join(targetRoot, 'demo-accounts-rbac-scan');
2306
- const templateRoot = path.join(packageRoot, 'templates', 'base');
2307
-
2308
- try {
2309
- scaffoldProject({
2310
- templateRoot,
2311
- packageRoot,
2312
- targetRoot: projectRoot,
2313
- projectName: 'demo-accounts-rbac-scan',
2314
- frontend: 'react',
2315
- db: 'prisma',
2316
- dbPrismaEnabled: true,
2317
- i18nEnabled: false,
2318
- proxy: 'caddy',
2319
- });
2320
-
2321
- addModule({
2322
- moduleId: 'accounts',
2323
- targetRoot: projectRoot,
2324
- packageRoot,
2325
- });
2326
- addModule({
2327
- moduleId: 'rbac',
2328
- targetRoot: projectRoot,
2329
- packageRoot,
2330
- });
2331
-
2332
- const scan = scanIntegrations({
2333
- targetRoot: projectRoot,
2334
- relatedModuleId: 'rbac',
2335
- });
2336
- const compatibilityGroup = scan.groups.find((group) => group.id === 'accounts-rbac');
2337
-
2338
- assert.ok(compatibilityGroup);
2339
- assert.deepEqual(compatibilityGroup.modules, ['accounts', 'rbac']);
2340
- } finally {
2341
- fs.rmSync(targetRoot, { recursive: true, force: true });
2342
- }
2343
- });
2200
+ it('applies accounts with db-prisma and wires the DB-backed runtime immediately', () => {
2201
+ const targetRoot = mkTmp('forgeon-module-accounts-db-');
2202
+ const projectRoot = path.join(targetRoot, 'demo-accounts-db');
2203
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2204
+
2205
+ try {
2206
+ scaffoldProject({
2207
+ templateRoot,
2208
+ packageRoot,
2209
+ targetRoot: projectRoot,
2210
+ projectName: 'demo-accounts-db',
2211
+ frontend: 'react',
2212
+ db: 'prisma',
2213
+ dbPrismaEnabled: true,
2214
+ i18nEnabled: true,
2215
+ proxy: 'caddy',
2216
+ });
2217
+
2218
+ const result = addModule({
2219
+ moduleId: 'accounts',
2220
+ targetRoot: projectRoot,
2221
+ packageRoot,
2222
+ });
2223
+
2224
+ assert.equal(result.applied, true);
2225
+ assertAccountsWiring(projectRoot);
2226
+
2227
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
2228
+ assert.match(schema, /model UserProfile/);
2229
+ assert.match(schema, /model UserSettings/);
2230
+ assert.match(schema, /model AuthIdentity/);
2231
+ assert.match(schema, /model AuthCredential/);
2232
+ assert.match(schema, /model AuthRefreshToken/);
2233
+ assert.doesNotMatch(schema, /roles\s+/i);
2234
+ assert.doesNotMatch(schema, /permissions\s+/i);
2235
+
2236
+ const migrationPath = path.join(
2237
+ projectRoot,
2238
+ 'apps',
2239
+ 'api',
2240
+ 'prisma',
2241
+ 'migrations',
2242
+ '0002_accounts_core',
2243
+ 'migration.sql',
2244
+ );
2245
+ assert.equal(fs.existsSync(migrationPath), true);
2246
+
2247
+ const seedSource = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'seed.ts'), 'utf8');
2248
+ assert.match(seedSource, /Prisma\.dmmf/);
2249
+ assert.match(seedSource, /userFields\.has\('email'\)/);
2250
+
2251
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2252
+ assert.match(readme, /POST \/api\/auth\/register/);
2253
+ assert.match(readme, /POST \/api\/auth\/password-reset\/request/);
2254
+ assert.match(readme, /\/api\/users\/:id\/settings/);
2255
+
2256
+ const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
2257
+ assert.match(moduleDoc, /Status: implemented/);
2258
+ assert.match(moduleDoc, /db-adapter/);
2259
+ assert.match(moduleDoc, /owner-scoped/);
2260
+ } finally {
2261
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2262
+ }
2263
+ });
2264
+
2265
+ it('detects and applies accounts-rbac compatibility sync explicitly', () => {
2266
+ const targetRoot = mkTmp('forgeon-module-accounts-rbac-');
2267
+ const projectRoot = path.join(targetRoot, 'demo-accounts-rbac');
2268
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2269
+
2270
+ try {
2271
+ scaffoldProject({
2272
+ templateRoot,
2273
+ packageRoot,
2274
+ targetRoot: projectRoot,
2275
+ projectName: 'demo-accounts-rbac',
2276
+ frontend: 'react',
2277
+ db: 'prisma',
2278
+ dbPrismaEnabled: true,
2279
+ i18nEnabled: false,
2280
+ proxy: 'caddy',
2281
+ });
2282
+
2283
+ addModule({
2284
+ moduleId: 'rbac',
2285
+ targetRoot: projectRoot,
2286
+ packageRoot,
2287
+ });
2288
+ addModule({
2289
+ moduleId: 'accounts',
2290
+ targetRoot: projectRoot,
2291
+ packageRoot,
2292
+ });
2293
+
2294
+ const scan = scanIntegrations({
2295
+ targetRoot: projectRoot,
2296
+ relatedModuleId: 'accounts',
2297
+ });
2298
+ assert.equal(scan.groups.some((group) => group.id === 'accounts-rbac'), true);
2299
+
2300
+ const syncResult = syncIntegrations({
2301
+ targetRoot: projectRoot,
2302
+ packageRoot,
2303
+ groupIds: ['accounts-rbac'],
2304
+ });
2305
+ const claimsPair = syncResult.summary.find((item) => item.id === 'accounts-rbac');
2306
+ assert.ok(claimsPair);
2307
+ assert.equal(claimsPair.result.applied, true);
2308
+
2309
+ const contracts = fs.readFileSync(
2310
+ path.join(projectRoot, 'packages', 'accounts-contracts', 'src', 'index.ts'),
2311
+ 'utf8',
2312
+ );
2313
+ assert.match(contracts, /roles\?: string\[\];/);
2314
+ assert.match(contracts, /permissions\?: string\[\];/);
2315
+
2316
+ const authTypes = fs.readFileSync(
2317
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.types.ts'),
2318
+ 'utf8',
2319
+ );
2320
+ assert.match(authTypes, /roles\?: string\[\];/);
2321
+ assert.match(authTypes, /permissions\?: string\[\];/);
2322
+
2323
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2324
+ assert.match(readme, /forgeon:accounts:rbac:start/);
2325
+ assert.match(readme, /base accounts schema remains free of roles and permissions/);
2326
+ } finally {
2327
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2328
+ }
2329
+ });
2330
+
2331
+ it('scans accounts-rbac compatibility when accounts and rbac are both installed', () => {
2332
+ const targetRoot = mkTmp('forgeon-module-accounts-rbac-scan-');
2333
+ const projectRoot = path.join(targetRoot, 'demo-accounts-rbac-scan');
2334
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2335
+
2336
+ try {
2337
+ scaffoldProject({
2338
+ templateRoot,
2339
+ packageRoot,
2340
+ targetRoot: projectRoot,
2341
+ projectName: 'demo-accounts-rbac-scan',
2342
+ frontend: 'react',
2343
+ db: 'prisma',
2344
+ dbPrismaEnabled: true,
2345
+ i18nEnabled: false,
2346
+ proxy: 'caddy',
2347
+ });
2348
+
2349
+ addModule({
2350
+ moduleId: 'accounts',
2351
+ targetRoot: projectRoot,
2352
+ packageRoot,
2353
+ });
2354
+ addModule({
2355
+ moduleId: 'rbac',
2356
+ targetRoot: projectRoot,
2357
+ packageRoot,
2358
+ });
2359
+
2360
+ const scan = scanIntegrations({
2361
+ targetRoot: projectRoot,
2362
+ relatedModuleId: 'rbac',
2363
+ });
2364
+ const compatibilityGroup = scan.groups.find((group) => group.id === 'accounts-rbac');
2365
+
2366
+ assert.ok(compatibilityGroup);
2367
+ assert.deepEqual(compatibilityGroup.modules, ['accounts', 'rbac']);
2368
+ } finally {
2369
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2370
+ }
2371
+ });
2344
2372
  it('applies logger then accounts on db/i18n-disabled scaffold without breaking health controller syntax', () => {
2345
2373
  const targetRoot = mkTmp('forgeon-module-jwt-nodb-noi18n-');
2346
2374
  const projectRoot = path.join(targetRoot, 'demo-jwt-nodb-noi18n');
@@ -2650,3 +2678,4 @@ describe('addModule', () => {
2650
2678
 
2651
2679
 
2652
2680
 
2681
+