create-forgeon 0.3.5 → 0.3.6

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 (108) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add-help.mjs +1 -0
  3. package/src/cli/add-options.mjs +6 -0
  4. package/src/cli/add-options.test.mjs +2 -0
  5. package/src/modules/dependencies.mjs +31 -0
  6. package/src/modules/dependencies.test.mjs +207 -5
  7. package/src/modules/executor.mjs +14 -0
  8. package/src/modules/executor.test.mjs +719 -14
  9. package/src/modules/files-access.mjs +437 -0
  10. package/src/modules/files-image.mjs +531 -0
  11. package/src/modules/files-local.mjs +221 -0
  12. package/src/modules/files-quotas.mjs +381 -0
  13. package/src/modules/files-s3.mjs +266 -0
  14. package/src/modules/files.mjs +527 -0
  15. package/src/modules/queue.mjs +410 -0
  16. package/src/modules/registry.mjs +93 -3
  17. package/src/run-add-module.mjs +89 -2
  18. package/templates/module-fragments/files/00_title.md +1 -0
  19. package/templates/module-fragments/files/10_overview.md +17 -0
  20. package/templates/module-fragments/files/20_scope.md +13 -0
  21. package/templates/module-fragments/files/90_status_implemented.md +3 -0
  22. package/templates/module-fragments/files-access/00_title.md +1 -0
  23. package/templates/module-fragments/files-access/10_overview.md +9 -0
  24. package/templates/module-fragments/files-access/20_scope.md +20 -0
  25. package/templates/module-fragments/files-access/90_status_implemented.md +3 -0
  26. package/templates/module-fragments/files-image/00_title.md +1 -0
  27. package/templates/module-fragments/files-image/10_overview.md +10 -0
  28. package/templates/module-fragments/files-image/20_scope.md +20 -0
  29. package/templates/module-fragments/files-image/90_status_implemented.md +3 -0
  30. package/templates/module-fragments/files-local/00_title.md +1 -0
  31. package/templates/module-fragments/files-local/10_overview.md +9 -0
  32. package/templates/module-fragments/files-local/20_scope.md +10 -0
  33. package/templates/module-fragments/files-local/90_status_implemented.md +3 -0
  34. package/templates/module-fragments/files-quotas/00_title.md +1 -0
  35. package/templates/module-fragments/files-quotas/10_overview.md +9 -0
  36. package/templates/module-fragments/files-quotas/20_scope.md +20 -0
  37. package/templates/module-fragments/files-quotas/90_status_implemented.md +3 -0
  38. package/templates/module-fragments/files-s3/00_title.md +1 -0
  39. package/templates/module-fragments/files-s3/10_overview.md +17 -0
  40. package/templates/module-fragments/files-s3/20_scope.md +11 -0
  41. package/templates/module-fragments/files-s3/90_status_implemented.md +5 -0
  42. package/templates/module-fragments/queue/20_scope.md +8 -7
  43. package/templates/module-fragments/queue/90_status_implemented.md +3 -0
  44. package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_record/migration.sql +30 -0
  45. package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_variant/migration.sql +55 -0
  46. package/templates/module-presets/files/packages/files/package.json +24 -0
  47. package/templates/module-presets/files/packages/files/src/dto/create-file.dto.ts +30 -0
  48. package/templates/module-presets/files/packages/files/src/files-config.loader.ts +21 -0
  49. package/templates/module-presets/files/packages/files/src/files-config.module.ts +12 -0
  50. package/templates/module-presets/files/packages/files/src/files-config.service.ts +32 -0
  51. package/templates/module-presets/files/packages/files/src/files-env.schema.ts +30 -0
  52. package/templates/module-presets/files/packages/files/src/files.controller.ts +89 -0
  53. package/templates/module-presets/files/packages/files/src/files.service.ts +744 -0
  54. package/templates/module-presets/files/packages/files/src/files.types.ts +35 -0
  55. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +12 -0
  56. package/templates/module-presets/files/packages/files/src/index.ts +9 -0
  57. package/templates/module-presets/files/packages/files/tsconfig.json +9 -0
  58. package/templates/module-presets/files-access/packages/files-access/package.json +17 -0
  59. package/templates/module-presets/files-access/packages/files-access/src/files-access.service.ts +59 -0
  60. package/templates/module-presets/files-access/packages/files-access/src/files-access.subject.ts +45 -0
  61. package/templates/module-presets/files-access/packages/files-access/src/files-access.types.ts +14 -0
  62. package/templates/module-presets/files-access/packages/files-access/src/forgeon-files-access.module.ts +8 -0
  63. package/templates/module-presets/files-access/packages/files-access/src/index.ts +4 -0
  64. package/templates/module-presets/files-access/packages/files-access/tsconfig.json +9 -0
  65. package/templates/module-presets/files-image/packages/files-image/package.json +21 -0
  66. package/templates/module-presets/files-image/packages/files-image/src/files-image-config.loader.ts +32 -0
  67. package/templates/module-presets/files-image/packages/files-image/src/files-image-config.module.ts +11 -0
  68. package/templates/module-presets/files-image/packages/files-image/src/files-image-config.service.ts +55 -0
  69. package/templates/module-presets/files-image/packages/files-image/src/files-image-env.schema.ts +28 -0
  70. package/templates/module-presets/files-image/packages/files-image/src/files-image.service.ts +420 -0
  71. package/templates/module-presets/files-image/packages/files-image/src/files-image.types.ts +18 -0
  72. package/templates/module-presets/files-image/packages/files-image/src/forgeon-files-image.module.ts +10 -0
  73. package/templates/module-presets/files-image/packages/files-image/src/index.ts +7 -0
  74. package/templates/module-presets/files-image/packages/files-image/tsconfig.json +9 -0
  75. package/templates/module-presets/files-local/packages/files-local/package.json +19 -0
  76. package/templates/module-presets/files-local/packages/files-local/src/files-local-config.loader.ts +13 -0
  77. package/templates/module-presets/files-local/packages/files-local/src/files-local-config.module.ts +12 -0
  78. package/templates/module-presets/files-local/packages/files-local/src/files-local-config.service.ts +11 -0
  79. package/templates/module-presets/files-local/packages/files-local/src/files-local-env.schema.ts +13 -0
  80. package/templates/module-presets/files-local/packages/files-local/src/index.ts +4 -0
  81. package/templates/module-presets/files-local/packages/files-local/tsconfig.json +9 -0
  82. package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -0
  83. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.loader.ts +22 -0
  84. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.module.ts +11 -0
  85. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.service.ts +27 -0
  86. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-env.schema.ts +15 -0
  87. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -0
  88. package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.types.ts +22 -0
  89. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +11 -0
  90. package/templates/module-presets/files-quotas/packages/files-quotas/src/index.ts +7 -0
  91. package/templates/module-presets/files-quotas/packages/files-quotas/tsconfig.json +9 -0
  92. package/templates/module-presets/files-s3/packages/files-s3/package.json +20 -0
  93. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.loader.ts +57 -0
  94. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.module.ts +12 -0
  95. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.service.ts +44 -0
  96. package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-env.schema.ts +51 -0
  97. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +4 -0
  98. package/templates/module-presets/files-s3/packages/files-s3/tsconfig.json +9 -0
  99. package/templates/module-presets/queue/packages/queue/package.json +21 -0
  100. package/templates/module-presets/queue/packages/queue/src/forgeon-queue.module.ts +10 -0
  101. package/templates/module-presets/queue/packages/queue/src/index.ts +6 -0
  102. package/templates/module-presets/queue/packages/queue/src/queue-config.loader.ts +24 -0
  103. package/templates/module-presets/queue/packages/queue/src/queue-config.module.ts +10 -0
  104. package/templates/module-presets/queue/packages/queue/src/queue-config.service.ts +69 -0
  105. package/templates/module-presets/queue/packages/queue/src/queue-env.schema.ts +17 -0
  106. package/templates/module-presets/queue/packages/queue/src/queue.service.ts +88 -0
  107. package/templates/module-presets/queue/packages/queue/tsconfig.json +9 -0
  108. package/templates/module-fragments/queue/90_status_planned.md +0 -3
@@ -0,0 +1,9 @@
1
+ ## Overview
2
+
3
+ `files-access` adds resource-level authorization checks on top of `files`.
4
+
5
+ It provides:
6
+ - `@forgeon/files-access` package
7
+ - actor extraction helpers for request/user context
8
+ - policy service with owner/public/permission checks
9
+ - enforcement hooks in files controller routes
@@ -0,0 +1,20 @@
1
+ ## Scope
2
+
3
+ Current stage:
4
+ - requires `files-runtime` capability (`files` module)
5
+ - protects:
6
+ - `GET /api/files/:publicId`
7
+ - `GET /api/files/:publicId/download`
8
+ - `DELETE /api/files/:publicId`
9
+ - adds probe endpoint:
10
+ - `GET /api/health/files-access`
11
+
12
+ Current policy:
13
+ - allow when `files.manage` permission is present
14
+ - allow owner (`ownerType=user` and `ownerId===actorId`)
15
+ - allow read for `visibility=public`
16
+
17
+ Future work:
18
+ - integrate richer actor context from auth/rbac modules
19
+ - extend policy for group/tenant membership
20
+ - move toward dedicated `files-access` integration strategy for domain rules
@@ -0,0 +1,3 @@
1
+ ## Status
2
+
3
+ Implemented (runtime access-policy stage) and applied by `create-forgeon add files-access`.
@@ -0,0 +1 @@
1
+ # {{MODULE_LABEL}}
@@ -0,0 +1,10 @@
1
+ ## Overview
2
+
3
+ `files-image` adds an image sanitation pipeline on top of `files`.
4
+
5
+ It provides:
6
+ - magic-bytes detection for actual file type
7
+ - declared/detected MIME mismatch rejection
8
+ - decode -> sanitize -> re-encode flow via `sharp`
9
+ - default metadata stripping before storage
10
+ - optional `preview` variant generation for files runtime
@@ -0,0 +1,20 @@
1
+ ## Scope
2
+
3
+ Current stage:
4
+ - requires `files-runtime` capability (`files` module)
5
+ - supports image sanitization for:
6
+ - `image/jpeg`
7
+ - `image/png`
8
+ - `image/webp`
9
+ - integrates into files upload flow before storage write
10
+ - adds probe endpoint:
11
+ - `GET /api/health/files-image`
12
+
13
+ Default policy:
14
+ - `FILES_IMAGE_STRIP_METADATA=true`
15
+ - metadata is removed unless explicitly disabled by config
16
+
17
+ Future work:
18
+ - richer resize/thumbnail presets
19
+ - optional async processing mode
20
+ - extended format support based on project needs
@@ -0,0 +1,3 @@
1
+ ## Status
2
+
3
+ Implemented (runtime image-sanitize stage) and applied by `create-forgeon add files-image`.
@@ -0,0 +1 @@
1
+ # {{MODULE_LABEL}}
@@ -0,0 +1,9 @@
1
+ ## Overview
2
+
3
+ `files-local` is the local-disk provider for the `files-storage-adapter` capability.
4
+
5
+ It provides:
6
+ - `@forgeon/files-local` package
7
+ - local storage config/env wiring in API runtime
8
+
9
+ Use this provider as the default foundation for local development and simple deployments.
@@ -0,0 +1,10 @@
1
+ ## Scope
2
+
3
+ Current stage:
4
+ - used by `files` runtime when `FILES_STORAGE_DRIVER=local`
5
+ - local root configuration via `FILES_LOCAL_ROOT`
6
+ - Docker compose named volume `files_data` is mounted to `/app/storage`
7
+
8
+ Future work:
9
+ - local storage operational hardening
10
+ - optional Docker volume presets via dedicated follow-up task/module
@@ -0,0 +1,3 @@
1
+ ## Status
2
+
3
+ Implemented (runtime local-provider stage) and applied by `create-forgeon add files-local`.
@@ -0,0 +1 @@
1
+ # {{MODULE_LABEL}}
@@ -0,0 +1,9 @@
1
+ ## Overview
2
+
3
+ `files-quotas` adds owner-level upload limits on top of `files`.
4
+
5
+ It provides:
6
+ - `@forgeon/files-quotas` package
7
+ - quota config/env module
8
+ - quota evaluation service
9
+ - upload pre-check hook in files controller
@@ -0,0 +1,20 @@
1
+ ## Scope
2
+
3
+ Current stage:
4
+ - requires `files-runtime` capability (`files` module)
5
+ - owner-based limits:
6
+ - max files per owner
7
+ - max total bytes per owner
8
+ - upload check runs before file write
9
+ - probe endpoint:
10
+ - `GET /api/health/files-quotas`
11
+
12
+ Current limitations:
13
+ - per-owner only
14
+ - no tenant-wide or group-wide aggregation yet
15
+ - no async reconciliation job yet
16
+
17
+ Future work:
18
+ - richer quota subjects (`tenant`, `group`)
19
+ - reconciliation support for drift correction
20
+ - integration with future `files-quotas` accounting enhancements
@@ -0,0 +1,3 @@
1
+ ## Status
2
+
3
+ Implemented (runtime owner-quota stage) and applied by `create-forgeon add files-quotas`.
@@ -0,0 +1 @@
1
+ # {{MODULE_LABEL}}
@@ -0,0 +1,17 @@
1
+ ## Overview
2
+
3
+ `files-s3` is the S3-compatible provider for the `files-storage-adapter` capability.
4
+
5
+ It is intended for:
6
+ - AWS S3
7
+ - Cloudflare R2
8
+ - MinIO
9
+ - other S3-compatible endpoints
10
+
11
+ This module provides runtime S3 storage integration used by `files` when `FILES_STORAGE_DRIVER=s3`.
12
+
13
+ It includes provider tuning presets:
14
+ - `minio` (default local/dev preset)
15
+ - `aws`
16
+ - `r2`
17
+ - `custom`
@@ -0,0 +1,11 @@
1
+ ## Scope
2
+
3
+ Current stage:
4
+ - provider config and env surface
5
+ - runtime S3 storage is wired through the files service
6
+ - provider presets (`minio | r2 | aws | custom`) with override-friendly env keys
7
+ - retry tuning via `FILES_S3_MAX_ATTEMPTS`
8
+ - empty `FILES_S3_REGION` / `FILES_S3_ENDPOINT` / `FILES_S3_FORCE_PATH_STYLE` use preset defaults
9
+
10
+ Future work:
11
+ - upload/download and signed URL strategy integration in `files`
@@ -0,0 +1,5 @@
1
+ ## Status
2
+
3
+ Implemented and applied by `create-forgeon add files-s3`:
4
+ - runtime baseline
5
+ - provider presets (`minio | r2 | aws | custom`)
@@ -1,7 +1,8 @@
1
- ## Scope (Planned)
2
-
3
- Planned implementation target:
4
-
5
- 1. Add queue package preset (worker + producer abstractions).
6
- 2. Wire module imports and config in `apps/api`.
7
- 3. Document runtime and deployment notes.
1
+ ## Scope
2
+
3
+ Current implementation includes:
4
+
5
+ 1. Queue runtime package preset (`@forgeon/queue`) with Redis-backed BullMQ queue service.
6
+ 2. API wiring in `AppModule` (config loader + env schema + queue module import).
7
+ 3. Queue probe endpoint (`GET /api/health/queue`) and web probe button wiring.
8
+ 4. Docker Compose Redis service + API queue env wiring.
@@ -0,0 +1,3 @@
1
+ ## Status
2
+
3
+ Implemented in the current scaffold.
@@ -0,0 +1,30 @@
1
+ -- CreateTable
2
+ CREATE TABLE "FileRecord" (
3
+ "id" TEXT NOT NULL,
4
+ "publicId" TEXT NOT NULL,
5
+ "storageKey" TEXT NOT NULL,
6
+ "originalName" TEXT NOT NULL,
7
+ "mimeType" TEXT NOT NULL,
8
+ "size" INTEGER NOT NULL,
9
+ "storageDriver" TEXT NOT NULL,
10
+ "ownerType" TEXT NOT NULL DEFAULT 'system',
11
+ "ownerId" TEXT,
12
+ "visibility" TEXT NOT NULL DEFAULT 'private',
13
+ "createdById" TEXT,
14
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
15
+ "updatedAt" TIMESTAMP(3) NOT NULL,
16
+
17
+ CONSTRAINT "FileRecord_pkey" PRIMARY KEY ("id")
18
+ );
19
+
20
+ -- CreateIndex
21
+ CREATE UNIQUE INDEX "FileRecord_publicId_key" ON "FileRecord"("publicId");
22
+
23
+ -- CreateIndex
24
+ CREATE INDEX "FileRecord_ownerType_ownerId_createdAt_idx" ON "FileRecord"("ownerType", "ownerId", "createdAt");
25
+
26
+ -- CreateIndex
27
+ CREATE INDEX "FileRecord_createdById_createdAt_idx" ON "FileRecord"("createdById", "createdAt");
28
+
29
+ -- CreateIndex
30
+ CREATE INDEX "FileRecord_visibility_createdAt_idx" ON "FileRecord"("visibility", "createdAt");
@@ -0,0 +1,55 @@
1
+ -- CreateTable
2
+ CREATE TABLE "FileBlob" (
3
+ "id" TEXT NOT NULL,
4
+ "hash" TEXT NOT NULL,
5
+ "size" INTEGER NOT NULL,
6
+ "mimeType" TEXT NOT NULL,
7
+ "storageDriver" TEXT NOT NULL,
8
+ "storageKey" TEXT NOT NULL,
9
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ "updatedAt" TIMESTAMP(3) NOT NULL,
11
+
12
+ CONSTRAINT "FileBlob_pkey" PRIMARY KEY ("id")
13
+ );
14
+
15
+ -- CreateTable
16
+ CREATE TABLE "FileVariant" (
17
+ "id" TEXT NOT NULL,
18
+ "fileId" TEXT NOT NULL,
19
+ "variantKey" TEXT NOT NULL,
20
+ "blobId" TEXT NOT NULL,
21
+ "mimeType" TEXT NOT NULL,
22
+ "size" INTEGER NOT NULL,
23
+ "status" TEXT NOT NULL DEFAULT 'ready',
24
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+ "updatedAt" TIMESTAMP(3) NOT NULL,
26
+
27
+ CONSTRAINT "FileVariant_pkey" PRIMARY KEY ("id")
28
+ );
29
+
30
+ -- CreateIndex
31
+ CREATE UNIQUE INDEX "FileBlob_hash_size_mimeType_storageDriver_key" ON "FileBlob"("hash", "size", "mimeType", "storageDriver");
32
+
33
+ -- CreateIndex
34
+ CREATE INDEX "FileBlob_storageDriver_createdAt_idx" ON "FileBlob"("storageDriver", "createdAt");
35
+
36
+ -- CreateIndex
37
+ CREATE UNIQUE INDEX "FileVariant_fileId_variantKey_key" ON "FileVariant"("fileId", "variantKey");
38
+
39
+ -- CreateIndex
40
+ CREATE INDEX "FileVariant_blobId_idx" ON "FileVariant"("blobId");
41
+
42
+ -- CreateIndex
43
+ CREATE INDEX "FileVariant_variantKey_status_idx" ON "FileVariant"("variantKey", "status");
44
+
45
+ -- AddForeignKey
46
+ ALTER TABLE "FileVariant"
47
+ ADD CONSTRAINT "FileVariant_fileId_fkey"
48
+ FOREIGN KEY ("fileId") REFERENCES "FileRecord"("id")
49
+ ON DELETE CASCADE ON UPDATE CASCADE;
50
+
51
+ -- AddForeignKey
52
+ ALTER TABLE "FileVariant"
53
+ ADD CONSTRAINT "FileVariant_blobId_fkey"
54
+ FOREIGN KEY ("blobId") REFERENCES "FileBlob"("id")
55
+ ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@forgeon/files",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json"
9
+ },
10
+ "dependencies": {
11
+ "@forgeon/db-prisma": "workspace:*",
12
+ "@nestjs/common": "^11.0.1",
13
+ "@nestjs/config": "^4.0.0",
14
+ "@nestjs/platform-express": "^11.0.1",
15
+ "class-transformer": "^0.5.1",
16
+ "class-validator": "^0.14.1",
17
+ "zod": "^3.24.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/express": "^5.0.0",
21
+ "@types/node": "^22.10.7",
22
+ "typescript": "^5.7.3"
23
+ }
24
+ }
@@ -0,0 +1,30 @@
1
+ import { Transform } from 'class-transformer';
2
+ import { IsIn, IsOptional, IsString, MaxLength } from 'class-validator';
3
+
4
+ const visibilityValues = ['private', 'members', 'public'] as const;
5
+ type FileVisibility = (typeof visibilityValues)[number];
6
+
7
+ export class CreateFileDto {
8
+ @IsOptional()
9
+ @IsString()
10
+ @MaxLength(32)
11
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
12
+ ownerType?: string;
13
+
14
+ @IsOptional()
15
+ @IsString()
16
+ @MaxLength(191)
17
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
18
+ ownerId?: string;
19
+
20
+ @IsOptional()
21
+ @IsString()
22
+ @IsIn(visibilityValues)
23
+ visibility?: FileVisibility;
24
+
25
+ @IsOptional()
26
+ @IsString()
27
+ @MaxLength(191)
28
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
29
+ createdById?: string;
30
+ }
@@ -0,0 +1,21 @@
1
+ import { registerAs } from '@nestjs/config';
2
+ import { parseFilesEnv } from './files-env.schema';
3
+
4
+ export type FilesConfigValue = {
5
+ enabled: boolean;
6
+ storageDriver: 'local' | 's3';
7
+ publicBasePath: string;
8
+ maxFileSizeBytes: number;
9
+ allowedMimePrefixes: string[];
10
+ };
11
+
12
+ export const filesConfig = registerAs('files', (): FilesConfigValue => {
13
+ const env = parseFilesEnv(process.env);
14
+ return {
15
+ enabled: env.FILES_ENABLED,
16
+ storageDriver: env.FILES_STORAGE_DRIVER,
17
+ publicBasePath: env.FILES_PUBLIC_BASE_PATH,
18
+ maxFileSizeBytes: env.FILES_MAX_FILE_SIZE_BYTES,
19
+ allowedMimePrefixes: env.FILES_ALLOWED_MIME_PREFIXES,
20
+ };
21
+ });
@@ -0,0 +1,12 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { filesConfig } from './files-config.loader';
4
+ import { FilesConfigService } from './files-config.service';
5
+
6
+ @Global()
7
+ @Module({
8
+ imports: [ConfigModule.forFeature(filesConfig)],
9
+ providers: [FilesConfigService],
10
+ exports: [FilesConfigService],
11
+ })
12
+ export class FilesConfigModule {}
@@ -0,0 +1,32 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import type { FilesConfigValue } from './files-config.loader';
4
+
5
+ @Injectable()
6
+ export class FilesConfigService {
7
+ constructor(private readonly configService: ConfigService) {}
8
+
9
+ get enabled(): boolean {
10
+ return this.configService.getOrThrow<FilesConfigValue['enabled']>('files.enabled');
11
+ }
12
+
13
+ get storageDriver(): FilesConfigValue['storageDriver'] {
14
+ return this.configService.getOrThrow<FilesConfigValue['storageDriver']>('files.storageDriver');
15
+ }
16
+
17
+ get publicBasePath(): string {
18
+ return this.configService.getOrThrow<FilesConfigValue['publicBasePath']>('files.publicBasePath');
19
+ }
20
+
21
+ get maxFileSizeBytes(): number {
22
+ return this.configService.getOrThrow<FilesConfigValue['maxFileSizeBytes']>(
23
+ 'files.maxFileSizeBytes',
24
+ );
25
+ }
26
+
27
+ get allowedMimePrefixes(): string[] {
28
+ return this.configService.getOrThrow<FilesConfigValue['allowedMimePrefixes']>(
29
+ 'files.allowedMimePrefixes',
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+
3
+ const filesEnvSchema = z.object({
4
+ FILES_ENABLED: z
5
+ .string()
6
+ .optional()
7
+ .default('true')
8
+ .transform((value) => value !== 'false'),
9
+ FILES_STORAGE_DRIVER: z.enum(['local', 's3']).optional().default('local'),
10
+ FILES_PUBLIC_BASE_PATH: z.string().optional().default('/files'),
11
+ FILES_MAX_FILE_SIZE_BYTES: z.coerce.number().int().positive().optional().default(10 * 1024 * 1024),
12
+ FILES_ALLOWED_MIME_PREFIXES: z
13
+ .string()
14
+ .optional()
15
+ .default('image/,application/pdf,text/')
16
+ .transform((value) =>
17
+ value
18
+ .split(',')
19
+ .map((item) => item.trim())
20
+ .filter(Boolean),
21
+ ),
22
+ });
23
+
24
+ export type FilesEnvSchema = z.infer<typeof filesEnvSchema>;
25
+
26
+ export function parseFilesEnv(input: Record<string, string | undefined>): FilesEnvSchema {
27
+ return filesEnvSchema.parse(input);
28
+ }
29
+
30
+ export const filesEnvSchemaZod = filesEnvSchema;
@@ -0,0 +1,89 @@
1
+ import {
2
+ BadRequestException,
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ Get,
7
+ Param,
8
+ Post,
9
+ Query,
10
+ StreamableFile,
11
+ UploadedFile,
12
+ UseInterceptors,
13
+ } from '@nestjs/common';
14
+ import { FileInterceptor } from '@nestjs/platform-express';
15
+ import { CreateFileDto } from './dto/create-file.dto';
16
+ import { FilesService } from './files.service';
17
+ import type { FileVariantKey } from './files.types';
18
+
19
+ type UploadedFileShape = {
20
+ originalname: string;
21
+ mimetype: string;
22
+ size: number;
23
+ buffer: Buffer;
24
+ };
25
+
26
+ @Controller('files')
27
+ export class FilesController {
28
+ constructor(private readonly filesService: FilesService) {}
29
+
30
+ @Post('upload')
31
+ @UseInterceptors(FileInterceptor('file'))
32
+ async uploadFile(
33
+ @UploadedFile() file: UploadedFileShape | undefined,
34
+ @Body() body: CreateFileDto,
35
+ ) {
36
+ if (!file || !file.buffer) {
37
+ throw new BadRequestException('File is required. Use multipart/form-data with field "file".');
38
+ }
39
+
40
+ return this.filesService.create({
41
+ originalName: file.originalname,
42
+ mimeType: file.mimetype,
43
+ size: file.size,
44
+ buffer: file.buffer,
45
+ ownerType: body.ownerType,
46
+ ownerId: body.ownerId,
47
+ visibility: body.visibility,
48
+ createdById: body.createdById,
49
+ });
50
+ }
51
+
52
+ @Get(':publicId')
53
+ async getMetadata(@Param('publicId') publicId: string) {
54
+ return this.filesService.getByPublicId(publicId);
55
+ }
56
+
57
+ @Get(':publicId/download')
58
+ async download(@Param('publicId') publicId: string, @Query('variant') variantQuery?: string) {
59
+ const variant = this.parseVariant(variantQuery);
60
+ const payload = await this.filesService.openDownload(publicId, variant);
61
+ return new StreamableFile(payload.stream, {
62
+ disposition: `inline; filename="${payload.fileName}"`,
63
+ type: payload.mimeType,
64
+ });
65
+ }
66
+
67
+ @Delete(':publicId')
68
+ async remove(@Param('publicId') publicId: string) {
69
+ return this.filesService.deleteByPublicId(publicId);
70
+ }
71
+
72
+ private parseVariant(variantQuery?: string): FileVariantKey {
73
+ if (!variantQuery || variantQuery.length === 0) {
74
+ return 'original';
75
+ }
76
+
77
+ if (variantQuery === 'original' || variantQuery === 'preview') {
78
+ return variantQuery;
79
+ }
80
+
81
+ throw new BadRequestException({
82
+ message: 'Unsupported file variant',
83
+ details: {
84
+ variant: variantQuery,
85
+ allowedVariants: ['original', 'preview'],
86
+ },
87
+ });
88
+ }
89
+ }