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.
- 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 +719 -14
- package/src/modules/files-access.mjs +437 -0
- package/src/modules/files-image.mjs +531 -0
- package/src/modules/files-local.mjs +221 -0
- package/src/modules/files-quotas.mjs +381 -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/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 +89 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +744 -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
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
# {{MODULE_LABEL}}
|
|
@@ -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 @@
|
|
|
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`
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
## Scope
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
1.
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
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,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
|
+
}
|