cabloy 5.1.102 → 5.1.104
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/.cabloy-version +1 -1
- package/CHANGELOG.md +47 -0
- package/CLAUDE.local.md +4 -0
- package/cabloy-docs/.vitepress/config.mjs +3 -0
- package/cabloy-docs/backend/file-guide.md +148 -0
- package/cabloy-docs/backend/image-guide.md +40 -40
- package/cabloy-docs/backend/jwt-guide.md +21 -0
- package/cabloy-docs/frontend/a-model-under-the-hood.md +26 -4
- package/cabloy-docs/frontend/file-guide.md +147 -0
- package/cabloy-docs/frontend/image-guide.md +18 -9
- package/cabloy-docs/frontend/model-architecture.md +1 -1
- package/cabloy-docs/frontend/model-state-guide.md +132 -16
- package/cabloy-docs/frontend/ssr-init-data.md +12 -1
- package/cabloy-docs/frontend/use-state-data-best-practices.md +54 -0
- package/cabloy-docs/fullstack/file-workflow.md +135 -0
- package/cabloy-docs/fullstack/image-workflow.md +12 -3
- package/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +23 -23
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +7 -7
- package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +30 -4
- package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +1 -1
- package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +8 -6
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +8 -6
- package/vona/src/suite/a-training/modules/training-record/src/index.ts +0 -1
- package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +0 -1
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +10 -12
- package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +9 -10
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +2 -2
- package/vona/src/suite-vendor/a-file/modules/a-file/package.json +3 -2
- package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +155 -35
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +283 -54
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +46 -63
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.index.ts +12 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +1 -1
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/schedule.fileDraftPrune.ts +29 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveView.ts +45 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveViews.ts +45 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +6 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +64 -56
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeRequest.ts +13 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeResponse.ts +10 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +0 -3
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +2 -14
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +0 -25
- package/vona/src/{suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts → suite-vendor/a-file/modules/a-file/src/dto/fileView.ts} +3 -15
- package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +10 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadUrl.ts +329 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +51 -10
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +8 -3
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +1 -1
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +12 -4
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +52 -18
- package/vona/src/suite-vendor/a-file/modules/file-native/package.json +1 -1
- package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +5 -5
- package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +7 -8
- package/vona/src/suite-vendor/a-file/package.json +4 -4
- package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +16 -19
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +99 -74
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +28 -68
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +4 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +78 -34
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +0 -3
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +1 -27
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +6 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +2 -36
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +0 -3
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +4 -27
- package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +36 -29
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +4 -4
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +1 -0
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +8 -11
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +4 -16
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +17 -16
- package/vona/src/suite-vendor/a-image/modules/image-native/package.json +2 -6
- package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +0 -37
- package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +13 -36
- package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +100 -113
- package/vona/src/suite-vendor/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/config/config.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +9 -4
- package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +113 -64
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +50 -12
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +610 -114
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +51 -2
- package/zova/src/suite/a-home/modules/home-passport/src/monkey.ts +17 -3
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +50 -12
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +610 -114
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +16 -12
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +25 -25
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +18 -24
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +228 -291
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +5 -5
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +23 -5
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +72 -43
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +22 -4
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +16 -42
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +5 -25
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +10 -24
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +216 -333
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +0 -5
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +38 -12
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +79 -32
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +22 -4
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +3 -4
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +6 -6
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -5
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +29 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +11 -2
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -3
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +4 -4
- package/zova/src/suite-vendor/a-zova/package.json +3 -3
- package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +0 -34
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +0 -24
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +0 -16
- package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +0 -57
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +0 -31
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +0 -13
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.104
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.104
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add update capabilities.
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- Initialize the passport model during application startup.
|
|
12
|
+
|
|
13
|
+
### Improvements
|
|
14
|
+
|
|
15
|
+
- Rename the generic async state type.
|
|
16
|
+
- Rename async local state APIs.
|
|
17
|
+
- Document the passport SSR state bridge.
|
|
18
|
+
- Clarify memory state SSR transfer behavior.
|
|
19
|
+
- Clarify how to select model state helpers.
|
|
20
|
+
|
|
21
|
+
## 5.1.103
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
- Add a direct image upload flow for the frontend.
|
|
26
|
+
- Add user-bound media delivery to restrict training media access to authorized users.
|
|
27
|
+
- Unify file and image visibility semantics across delivery flows.
|
|
28
|
+
- Move image signed URL policy configuration to scenes.
|
|
29
|
+
- Add shared serializer transforms for file responses.
|
|
30
|
+
- Align private delivery semantics for native images.
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
- Defer image rendering to the client.
|
|
35
|
+
- Refresh stale media authorization tokens.
|
|
36
|
+
- Resolve media URL type errors.
|
|
37
|
+
- Bind training media delivery to the requesting user.
|
|
38
|
+
- Restore file upload type validation.
|
|
39
|
+
|
|
40
|
+
### Improvements
|
|
41
|
+
|
|
42
|
+
- Use exact media delivery paths.
|
|
43
|
+
- Add fresh query helpers for retrieving current data.
|
|
44
|
+
- Simplify media delivery options and resolver view naming.
|
|
45
|
+
- Align the file upload lifecycle and simplify image upload handling.
|
|
46
|
+
- Reduce exposed public fields and contract surface for files and images.
|
|
47
|
+
- Clarify image transform key naming.
|
|
48
|
+
- Remove native image direct-upload support.
|
|
49
|
+
|
|
3
50
|
## 5.1.102
|
|
4
51
|
|
|
5
52
|
### Features
|
package/CLAUDE.local.md
ADDED
|
@@ -99,6 +99,7 @@ const fullstackGroups = [
|
|
|
99
99
|
link: '/fullstack/backend-metadata-to-frontend-table-actions-source-reading-map',
|
|
100
100
|
},
|
|
101
101
|
{ text: 'Fullstack Image Workflow', link: '/fullstack/image-workflow' },
|
|
102
|
+
{ text: 'Fullstack File Workflow', link: '/fullstack/file-workflow' },
|
|
102
103
|
{ text: 'Backend OpenAPI to Frontend SDK', link: '/fullstack/openapi-to-sdk' },
|
|
103
104
|
{
|
|
104
105
|
text: 'Frontend Metadata Back to Backend',
|
|
@@ -233,6 +234,7 @@ export default defineConfig({
|
|
|
233
234
|
{ text: 'Logger Guide', link: '/backend/logger-guide' },
|
|
234
235
|
{ text: 'Upload Guide', link: '/backend/upload-guide' },
|
|
235
236
|
{ text: 'Image Guide', link: '/backend/image-guide' },
|
|
237
|
+
{ text: 'File Guide', link: '/backend/file-guide' },
|
|
236
238
|
{ text: 'Mail Guide', link: '/backend/mail-guide' },
|
|
237
239
|
{ text: 'Serialization Guide', link: '/backend/serialization-guide' },
|
|
238
240
|
],
|
|
@@ -422,6 +424,7 @@ export default defineConfig({
|
|
|
422
424
|
{ text: 'Component Guide', link: '/frontend/component-guide' },
|
|
423
425
|
{ text: 'Form Guide', link: '/frontend/form-guide' },
|
|
424
426
|
{ text: 'Image Guide', link: '/frontend/image-guide' },
|
|
427
|
+
{ text: 'File Guide', link: '/frontend/file-guide' },
|
|
425
428
|
{
|
|
426
429
|
text: 'Zova Form Under the Hood',
|
|
427
430
|
link: '/frontend/zova-form-under-the-hood',
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# File Guide
|
|
2
|
+
|
|
3
|
+
This guide describes the Vona file contract in Cabloy Basic: scenes, providers, ingestion, lifecycle, delivery, and DTO-side views.
|
|
4
|
+
|
|
5
|
+
> Read this with [Frontend File Guide](/frontend/file-guide) and [Fullstack File Workflow](/fullstack/file-workflow). `a-file` is the core module; the built-in provider modules are Native (`file-native`) and Cloudflare R2 (`file-cloudflare`).
|
|
6
|
+
|
|
7
|
+
## Core model and trust boundary
|
|
8
|
+
|
|
9
|
+
A file use case is a named backend capability:
|
|
10
|
+
|
|
11
|
+
- `@FileScene(...)` defines the business policy;
|
|
12
|
+
- `app.bean.file` orchestrates provider storage and lifecycle;
|
|
13
|
+
- a provider owns storage-specific behavior and delivery;
|
|
14
|
+
- business entities persist file IDs, while serializer transforms resolve consumer-facing `DtoFileView` data.
|
|
15
|
+
|
|
16
|
+
The HTTP client supplies a `fileScene`; it never chooses the provider, client, visibility, bucket, object key, ETag, or storage path. Those values remain backend-owned.
|
|
17
|
+
|
|
18
|
+
The current defaults are:
|
|
19
|
+
|
|
20
|
+
| Setting | Default |
|
|
21
|
+
| ---------------------------- | -------------------- |
|
|
22
|
+
| Provider | `file-native:native` |
|
|
23
|
+
| Provider client | `default` |
|
|
24
|
+
| Visibility | private |
|
|
25
|
+
| Maximum upload size | 20 MiB |
|
|
26
|
+
| Direct-upload draft lifetime | 30 minutes |
|
|
27
|
+
|
|
28
|
+
Scene `upload` options override global `file.upload` options. Visibility resolves in this order: `FileScene.public`, then the selected provider client's `public`, then global `file.public`, then `false`.
|
|
29
|
+
|
|
30
|
+
## Define a file scene
|
|
31
|
+
|
|
32
|
+
Define a scene for every business capability. It owns provider selection, public/private policy, admission constraints, and optional metadata.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { BeanBase } from 'vona';
|
|
36
|
+
import { FileScene } from 'vona-module-a-file';
|
|
37
|
+
|
|
38
|
+
@FileScene({
|
|
39
|
+
public: false,
|
|
40
|
+
upload: {
|
|
41
|
+
maxSize: 20 * 1024 * 1024,
|
|
42
|
+
mimeTypes: ['application/pdf', 'application/zip', 'text/plain'],
|
|
43
|
+
extensions: ['.pdf', '.zip', '.txt'],
|
|
44
|
+
multiple: true,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
export class FileSceneDossierFile extends BeanBase {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The training-record suite implements this pattern in `FileSceneDossierFile`. Extensions are lowercase and include the leading dot. `provider` can be a fixed `{ providerName, clientName }` pair or an async resolver receiving the current Vona context; `meta` can likewise be static or context-derived. Use those resolvers for trusted tenant or user routing, not browser-provided storage choices.
|
|
51
|
+
|
|
52
|
+
## Provider capabilities
|
|
53
|
+
|
|
54
|
+
A provider implements `IFileProviderExecute`. Its capabilities determine what a selected scene can do.
|
|
55
|
+
|
|
56
|
+
| Capability | Requirement | Meaning |
|
|
57
|
+
| --------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------ |
|
|
58
|
+
| `upload`, `get`, `delete`, `getDownloadUrl` | Required | Normal storage, retrieval, deletion, and delivery |
|
|
59
|
+
| `uploadUrl` | Optional | Server-side remote URL import |
|
|
60
|
+
| `createDirectUpload` + `finalizeDirectUpload` | Optional pair | Third-party direct upload; both are required before policy advertises `directUpload: true` |
|
|
61
|
+
| `download` | Optional | Provider can stream/return a download result instead of only a URL |
|
|
62
|
+
|
|
63
|
+
The public upload-policy response exposes semantic constraints and `directUpload` only. It never exposes a provider name, client name, bucket, object key, ETag, or provider credentials.
|
|
64
|
+
|
|
65
|
+
Native supports ordinary multipart upload and proxy-style private delivery. It intentionally does **not** implement remote URL import or direct-upload creation/finalization. Cloudflare R2 is registered as `file-cloudflare:cloudflare`; it supports ordinary upload, secure URL import, and direct upload.
|
|
66
|
+
|
|
67
|
+
## Three ingestion models
|
|
68
|
+
|
|
69
|
+
### Authenticated multipart upload
|
|
70
|
+
|
|
71
|
+
`POST /file/upload` is the portable default:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
fileScene + file → backend validation/storage → ready response
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It accepts one multipart `fileScene` field and one file. Vona reads the received temporary file, resolves the scene, validates actual size, received multipart MIME type, and filename extension, then stores it with the selected provider. The returned action response is already `ready`.
|
|
78
|
+
|
|
79
|
+
MIME and extension checks are policy checks, not binary magic-byte inspection. Treat them as one layer of admission control rather than a content-safety scanner.
|
|
80
|
+
|
|
81
|
+
### Remote HTTPS URL import
|
|
82
|
+
|
|
83
|
+
`POST /file/upload-url` is an authorized, provider-capability operation. Native rejects it. Cloudflare R2 imports through the shared remote-fetch boundary:
|
|
84
|
+
|
|
85
|
+
- only HTTPS on port 443, with no URL credentials;
|
|
86
|
+
- at most three redirects, with URL and DNS/IP checks repeated for every hop;
|
|
87
|
+
- each hostname is resolved before connection, all answers must be public, and the validated address is pinned for that request;
|
|
88
|
+
- loopback, private, link-local, multicast, unspecified, reserved, shared, and documentation IPv4/IPv6 ranges are rejected;
|
|
89
|
+
- connection, response, read, and overall deadlines are enforced;
|
|
90
|
+
- bytes stream to a private `0600` temporary file and are limited by the scene's actual-byte policy;
|
|
91
|
+
- the filename is sanitized, while response content type and extension are checked against policy;
|
|
92
|
+
- temporary files are removed on success and failure.
|
|
93
|
+
|
|
94
|
+
Request-declared size, MIME type, content type, and object key are not persisted remote-file truth. The response headers, transferred bytes, and resolved filename drive validation and storage. A browser field must not fetch an arbitrary URL and forward it to Cabloy; build an explicitly authorized backend consumer when URL import is needed.
|
|
95
|
+
|
|
96
|
+
### Third-party direct upload
|
|
97
|
+
|
|
98
|
+
A direct-capable R2 scene uses an explicit lifecycle:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
create draft → browser raw provider transfer → finalize → ready
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
1. `POST /file/direct-upload` creates a draft and returns a presigned raw-body target.
|
|
105
|
+
2. The browser sends the raw file with the returned method and headers, without adding Cabloy credentials.
|
|
106
|
+
3. `POST /file/direct-upload/finalize` queries the persisted R2 object with `HeadObject`.
|
|
107
|
+
4. Vona verifies object existence, the reserved size, and content type; persists final metadata; and promotes the row to `ready`.
|
|
108
|
+
|
|
109
|
+
A draft cannot resolve to a file view, receive a download URL, be downloaded, or become a business relation. The scheduled prune job runs every 30 minutes, handles at most 100 expired drafts per execution, deletes provider content, and then marks the row `expired`. Finalization and expiration share a per-file Redlock. If provider deletion fails, the draft remains eligible for a later pruning retry.
|
|
110
|
+
|
|
111
|
+
Direct finalization validates provider metadata; it does not inspect file magic bytes or re-run filename-extension admission after provider transfer.
|
|
112
|
+
|
|
113
|
+
## Persist IDs; resolve safe views
|
|
114
|
+
|
|
115
|
+
Business data persists `TableIdentity` values. Consumer DTOs expose a safe projection using `a-file:resolveView` or `a-file:resolveViews`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
@Api.field(
|
|
119
|
+
v.optional(),
|
|
120
|
+
v.serializerTransform('a-file:resolveViews', {
|
|
121
|
+
fieldName: 'dossierFileIds',
|
|
122
|
+
fileScene: 'training-record:dossierFile',
|
|
123
|
+
}),
|
|
124
|
+
v.array(DtoFileView),
|
|
125
|
+
)
|
|
126
|
+
dossierFiles?: DtoFileView[];
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A resolved `DtoFileView` contains only completed-file presentation and delivery data: ID, filename, content type, size, visibility, upload time, `downloadUrl`, and signed state. Provider/client identities, bucket, object key, ETag, storage path, raw SDK output, metadata, status, and draft timestamps remain internal.
|
|
130
|
+
|
|
131
|
+
The serializer also checks the optional `fileScene`. A relation with the wrong scene is rejected rather than silently resolving a file from a different business capability.
|
|
132
|
+
|
|
133
|
+
## Delivery and trusted backend APIs
|
|
134
|
+
|
|
135
|
+
Files are private by default. Private delivery can be either a temporary Cabloy proxy URL or a provider-signed R2 URL. `GET /file/download?fileId=...` is route-public so a signed link can be opened. Proxy delivery still requires a temporary token bound to the delivery endpoint and a signed payload whose file ID must match the request query.
|
|
136
|
+
|
|
137
|
+
When Cabloy streams bytes it can set an attachment filename. When Cabloy redirects to a provider URL, download-versus-inline behavior is controlled by that provider response and the browser.
|
|
138
|
+
|
|
139
|
+
`app.bean.file` is a trusted backend API and can accept provider/client options, visibility, metadata, and object-key inputs. Do not treat that lower-level API as the browser contract or forward untrusted request values into those fields.
|
|
140
|
+
|
|
141
|
+
## Backend checklist
|
|
142
|
+
|
|
143
|
+
1. Define one `@FileScene(...)` per business capability.
|
|
144
|
+
2. Keep provider, visibility, and storage decisions on the server.
|
|
145
|
+
3. Use ordinary multipart upload unless the resolved policy advertises direct upload.
|
|
146
|
+
4. Use URL import only through a provider that implements the secure backend fetch boundary.
|
|
147
|
+
5. Persist IDs and resolve `DtoFileView` relations for consumers.
|
|
148
|
+
6. Write business relations only from ready/finalized responses and preserve signed delivery semantics when changing providers.
|
|
@@ -170,7 +170,7 @@ If neither is provided, Vona falls back to the configured default variant, which
|
|
|
170
170
|
|
|
171
171
|
Vona supports signed delivery in two forms:
|
|
172
172
|
|
|
173
|
-
- **proxy-signed delivery**: Vona creates a temporary delivery token and redirects through `/image/delivery
|
|
173
|
+
- **proxy-signed delivery**: Vona creates a temporary delivery token and redirects through `/image/delivery?imageId=...`
|
|
174
174
|
- **provider-signed delivery**: the provider generates the final signed URL directly
|
|
175
175
|
|
|
176
176
|
The built-in defaults differ:
|
|
@@ -196,7 +196,7 @@ Representative methods are:
|
|
|
196
196
|
|
|
197
197
|
A useful practical boundary is:
|
|
198
198
|
|
|
199
|
-
- use `BeanImageUploadPolicy` when you need scene resolution and upload
|
|
199
|
+
- use `BeanImageUploadPolicy` when you need scene resolution and upload policy
|
|
200
200
|
- use `app.bean.image` when you need the actual image lifecycle and delivery behavior
|
|
201
201
|
|
|
202
202
|
## Persistence model
|
|
@@ -231,43 +231,43 @@ If a provider-client record does not exist yet, `BeanImageProvider` can lazily r
|
|
|
231
231
|
|
|
232
232
|
## Operational flows
|
|
233
233
|
|
|
234
|
-
##
|
|
234
|
+
## Authenticated multipart upload
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
Ordinary image upload is one authenticated multipart request:
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
2. Vona resolves the image scene and validates the declared `size` and `mimeType`
|
|
242
|
-
3. Vona issues a temporary upload token that is bound to the upload path
|
|
243
|
-
4. submit `POST /image/upload` as multipart form data
|
|
244
|
-
5. send:
|
|
245
|
-
- `token`
|
|
238
|
+
1. submit `POST /image/upload` as multipart form data
|
|
239
|
+
2. send:
|
|
240
|
+
- `imageScene`
|
|
246
241
|
- `image`
|
|
247
|
-
|
|
242
|
+
3. Vona reads the received file's actual size and MIME type
|
|
243
|
+
4. Vona resolves the image scene, provider, visibility, and metadata server-side
|
|
244
|
+
5. Vona validates actual size, MIME type, and extension against the scene policy, then uploads through the resolved provider
|
|
248
245
|
|
|
249
246
|
Representative controller shape:
|
|
250
247
|
|
|
251
248
|
```typescript
|
|
252
|
-
@Web.post('upload-token')
|
|
253
|
-
async createUploadToken(@Arg.body() data: DtoImageUploadTokenRequest) {
|
|
254
|
-
return await this.bean.imageUploadPolicy.createUploadToken(data);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
249
|
@Web.post('upload')
|
|
258
|
-
@Core.fileUpload(
|
|
250
|
+
@Core.fileUpload({
|
|
251
|
+
busboy: {
|
|
252
|
+
limits: { fields: 1, files: 1, parts: 3 },
|
|
253
|
+
},
|
|
254
|
+
})
|
|
259
255
|
@Api.contentType('application/json')
|
|
260
|
-
async upload(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
256
|
+
async upload(
|
|
257
|
+
@Arg.field('imageScene', v.required(), z.string()) imageScene: string,
|
|
258
|
+
@Arg.file('image', v.required()) file: IUploadFile,
|
|
259
|
+
) {
|
|
260
|
+
const policy = await this.bean.imageUploadPolicy.resolveUploadPolicy({
|
|
261
|
+
imageScene,
|
|
262
|
+
size: (await fse.stat(file.file)).size,
|
|
263
|
+
mimeType: file.info.mimeType,
|
|
264
|
+
});
|
|
265
|
+
await this.bean.imageUploadPolicy.validateUploadFile(...);
|
|
266
266
|
return await this.bean.image.upload(...);
|
|
267
267
|
}
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
The normal passport guard remains the authorization boundary. Temporary tokens are still used for signed delivery where needed, but ordinary image upload does not issue or accept an upload token.
|
|
271
271
|
|
|
272
272
|
## Direct upload
|
|
273
273
|
|
|
@@ -279,14 +279,13 @@ The shared flow is:
|
|
|
279
279
|
2. call `app.bean.image.createDirectUpload(...)`
|
|
280
280
|
3. let the provider create the direct-upload resource
|
|
281
281
|
4. persist the draft image record
|
|
282
|
-
5. return
|
|
282
|
+
5. return the public direct-upload session: Cabloy image `id`, provider-hosted `uploadUrl`, and optional presentation fields
|
|
283
283
|
6. upload bytes to the returned `uploadUrl`
|
|
284
|
-
7. finalize through `POST /image/direct-upload/finalize`
|
|
284
|
+
7. finalize through `POST /image/direct-upload/finalize` using the Cabloy image `id`; the finalized response has the normal public image action shape
|
|
285
285
|
|
|
286
|
-
The built-in
|
|
286
|
+
The built-in Cloudflare provider supports this flow by returning a provider-hosted upload target.
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
- `image-native:native` returns an app-local upload target under `/image-native/direct-upload/:resourceId`, stores the uploaded bytes as a draft file, and promotes that draft into the final native storage path during finalize
|
|
288
|
+
The native provider does not support direct upload and should use the standard authenticated `/image/upload` flow instead.
|
|
290
289
|
|
|
291
290
|
That provider difference is exactly why the shared API keeps direct upload split into create-upload-target and finalize steps.
|
|
292
291
|
|
|
@@ -310,12 +309,12 @@ The native provider does not implement `uploadUrl`.
|
|
|
310
309
|
|
|
311
310
|
When delivery is signed and the provider uses proxy delivery, `BeanImage.getVariantUrl(...)` creates a temporary delivery token and returns a local delivery URL such as:
|
|
312
311
|
|
|
313
|
-
- `/image/delivery
|
|
312
|
+
- `/image/delivery?imageId=...&token=...`
|
|
314
313
|
|
|
315
314
|
The delivery controller then:
|
|
316
315
|
|
|
317
|
-
1. verifies the delivery token against the
|
|
318
|
-
2. confirms the token image ID matches the request
|
|
316
|
+
1. verifies the delivery token against the fixed delivery endpoint path
|
|
317
|
+
2. confirms the token image ID matches the request `imageId` query value
|
|
319
318
|
3. redirects to the final target URL
|
|
320
319
|
|
|
321
320
|
This is why native signed delivery behaves differently from Cloudflare signed delivery.
|
|
@@ -348,7 +347,7 @@ Practical behavior to know:
|
|
|
348
347
|
|
|
349
348
|
- it defaults to proxy-signed delivery
|
|
350
349
|
- it can return local static URLs under `/api/static/...`
|
|
351
|
-
- it
|
|
350
|
+
- it does not support direct upload and uses the standard authenticated multipart upload flow
|
|
352
351
|
- named variants produce stable variant files such as `__thumbnail`
|
|
353
352
|
- ad hoc transforms produce stable hashed files such as `__t_<hash>`
|
|
354
353
|
- repeated requests for the same custom transform reuse the same cached transformed output
|
|
@@ -489,7 +488,7 @@ A practical sequence is:
|
|
|
489
488
|
1. create the scene bean
|
|
490
489
|
2. define `upload` rules and optional provider/meta resolvers
|
|
491
490
|
3. point entity or DTO metadata at the new scene name
|
|
492
|
-
4. verify upload
|
|
491
|
+
4. verify authenticated multipart upload and serializer behavior end to end
|
|
493
492
|
|
|
494
493
|
## Add a new image provider
|
|
495
494
|
|
|
@@ -508,8 +507,9 @@ A practical provider checklist is:
|
|
|
508
507
|
|
|
509
508
|
1. implement the required methods
|
|
510
509
|
2. decide whether to support `uploadUrl`, `createDirectUpload`, and `download`
|
|
511
|
-
3.
|
|
512
|
-
4.
|
|
510
|
+
3. only implement `createDirectUpload` when the provider can hand the client a real external upload target instead of routing bytes back through the local app
|
|
511
|
+
4. define sensible base options in the decorator
|
|
512
|
+
5. verify variant URL behavior, signed delivery behavior, and delete behavior
|
|
513
513
|
|
|
514
514
|
## Test-backed reading map
|
|
515
515
|
|
|
@@ -519,8 +519,8 @@ The current repo already contains strong executable examples under:
|
|
|
519
519
|
|
|
520
520
|
The most useful tests are:
|
|
521
521
|
|
|
522
|
-
- `imageUpload.test.ts` for upload
|
|
523
|
-
- `imageNative.test.ts` for local upload,
|
|
522
|
+
- `imageUpload.test.ts` for authenticated multipart upload, native direct-upload rejection, Cloudflare direct-upload, and upload-url request flows
|
|
523
|
+
- `imageNative.test.ts` for local upload, variant URLs, signed delivery, and delete behavior
|
|
524
524
|
- `imageCloudflareMapping.test.ts` for Cloudflare mapping, custom transforms, signed delivery, direct upload, and upload-by-URL behavior
|
|
525
525
|
|
|
526
526
|
When the source and a prose explanation seem to disagree, prefer the current source and tests.
|
|
@@ -180,6 +180,27 @@ const accessToken = await this.bean.jwt.createTempAuthToken({ userId: '1' });
|
|
|
180
180
|
|
|
181
181
|
This is useful when a token should remain valid only briefly, for example for URL-bound handoff flows.
|
|
182
182
|
|
|
183
|
+
### Design rule: bind temporary auth to one exact endpoint
|
|
184
|
+
|
|
185
|
+
A temporary auth token can optionally bind to one exact API pathname. Keep that binding strict: do not introduce prefix or wildcard path matching merely because a browser-facing endpoint has a resource ID in its route path.
|
|
186
|
+
|
|
187
|
+
When one temporary passport token must authorize several resource URLs in the same delivery capability, define a fixed endpoint path and move the resource identity to a validated query parameter:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
GET /api/file/download?fileId=123
|
|
191
|
+
GET /api/image/delivery?imageId=456
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The temporary passport token can then bind exactly to `/api/file/download` or `/api/image/delivery`. It authorizes the caller at the delivery-endpoint level, not at the individual-resource level.
|
|
195
|
+
|
|
196
|
+
Resource authorization must remain separate and explicit. A signed delivery token should carry the file/image ID, and the controller must compare that signed ID with the validated `fileId` or `imageId` query value. For image transformations, continue taking authorization-relevant variant options from the signed payload rather than from mutable query input.
|
|
197
|
+
|
|
198
|
+
This separation keeps the token primitive generic and auditable:
|
|
199
|
+
|
|
200
|
+
- exact pathname binding scopes the temporary passport token to one endpoint
|
|
201
|
+
- signed payload claims scope the delivery token to its resource, audience, and any protected representation options
|
|
202
|
+
- query parameters carry request identity without expanding temporary-token path authority
|
|
203
|
+
|
|
183
204
|
## Verification
|
|
184
205
|
|
|
185
206
|
JWT verification can happen directly through a client:
|
|
@@ -85,9 +85,21 @@ Its main jobs are:
|
|
|
85
85
|
- create the shared `QueryClient`
|
|
86
86
|
- create the `QueryCache`
|
|
87
87
|
- install the Vue Query plugin into the app
|
|
88
|
-
- on the server, dehydrate query state after render
|
|
89
|
-
- on the client, hydrate
|
|
90
|
-
- clear the query client after
|
|
88
|
+
- on the server, dehydrate eligible query state after render into `stateDefer.query`
|
|
89
|
+
- on the client, hydrate that snapshot during SSR pre-hydration
|
|
90
|
+
- clear the server query client after the snapshot has been created
|
|
91
|
+
|
|
92
|
+
The server-to-client handoff is therefore:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
server QueryClient
|
|
96
|
+
→ dehydrate after render
|
|
97
|
+
→ stateDefer.query
|
|
98
|
+
→ deferred SSR bootstrap state
|
|
99
|
+
→ client QueryClient hydrate during SSR pre-hydration
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This is an initial SSR handoff, not browser persistence. The server cache is request-scoped for this flow and is cleared after its snapshot is taken.
|
|
91
103
|
|
|
92
104
|
This is the second important lower-level rule:
|
|
93
105
|
|
|
@@ -200,6 +212,10 @@ A practical reading rule is:
|
|
|
200
212
|
- different state families differ in persistence/storage and SSR behavior
|
|
201
213
|
- but they still participate in one model runtime vocabulary and query-key system
|
|
202
214
|
|
|
215
|
+
`$useStateMem(...)` makes the two policy axes especially clear. It returns an assignable custom-ref surface that reads and writes through the Model Query Cache, while setting `persister: false` so no browser storage adapter is used. No persistence backend does not imply no SSR transfer: a successful eligible memory query can still be included in the shared Query Cache snapshot.
|
|
216
|
+
|
|
217
|
+
A hydrated entry is reusable only when the later call resolves to the same effective key. The runtime prefixes each logical key with Model identity and, for selector-enabled Models, selector identity.
|
|
218
|
+
|
|
203
219
|
## Persister storage selection and restore/save/remove behavior
|
|
204
220
|
|
|
205
221
|
The persister layer lives in:
|
|
@@ -243,7 +259,13 @@ This file confirms default behavior such as:
|
|
|
243
259
|
- persistence max-age defaults
|
|
244
260
|
- dehydration rules for sync persister-backed queries
|
|
245
261
|
|
|
246
|
-
|
|
262
|
+
The current dehydration decision is ordered:
|
|
263
|
+
|
|
264
|
+
1. `meta.ssr.dehydrate === false` explicitly excludes a query.
|
|
265
|
+
2. A sync persister-backed query is excluded.
|
|
266
|
+
3. Remaining queries use TanStack Query's current default dehydration predicate, which accepts successful queries.
|
|
267
|
+
|
|
268
|
+
This is why `$useStateMem(...)` can participate in the initial SSR transfer despite `persister: false`, while `$useStateLocalAsync(...)` opts out explicitly. This is the place to check when the runtime feels surprising even though your page/model code looks correct.
|
|
247
269
|
|
|
248
270
|
## What this page does not re-explain
|
|
249
271
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# File Guide
|
|
2
|
+
|
|
3
|
+
This guide explains the Zova `basic-file` contract in Cabloy Basic. It is a metadata-driven, relation-backed field: the backend defines a file scene, the business field persists IDs, and DTO relations supply completed-file display and delivery data.
|
|
4
|
+
|
|
5
|
+
> Read this with [Backend File Guide](/backend/file-guide) and [Fullstack File Workflow](/fullstack/file-workflow). For the surrounding Zova architecture, see the [Frontend Source Reading Roadmap](/frontend/frontend-source-reading-roadmap) and [Zova Source Reading Map](/frontend/zova-source-reading-map).
|
|
6
|
+
|
|
7
|
+
## Persisted IDs and resolved relations
|
|
8
|
+
|
|
9
|
+
The primary form value is a file ID, never a storage URL:
|
|
10
|
+
|
|
11
|
+
| Shape | Contract |
|
|
12
|
+
| ---------------- | ----------------------------------------------------------------------- |
|
|
13
|
+
| Single file | `TableIdentity`; an empty field is `''` |
|
|
14
|
+
| Multiple files | `TableIdentity[]` |
|
|
15
|
+
| Display relation | DTO-resolved `DtoFileView` data, such as `dossierFiles?: DtoFileView[]` |
|
|
16
|
+
|
|
17
|
+
`IFilePreviewItem` is an internal `basic-file` display-normalization shape. It is not a public backend DTO contract. Persist IDs and expose a `DtoFileView` relation for rendering.
|
|
18
|
+
|
|
19
|
+
The training-record example aligns one scene, ID field, and relation name:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// EntityRecord: persisted field and resource renderers
|
|
23
|
+
@Api.field(
|
|
24
|
+
ZovaRender.field('basic-file:formFieldFile', {
|
|
25
|
+
fileScene: 'training-record:dossierFile',
|
|
26
|
+
relationName: 'dossierFiles',
|
|
27
|
+
maxCount: 3,
|
|
28
|
+
}),
|
|
29
|
+
ZovaRender.cell('basic-file:file', { relationName: 'dossierFiles' }),
|
|
30
|
+
v.array(v.tableIdentity()),
|
|
31
|
+
)
|
|
32
|
+
dossierFileIds?: TableIdentity[];
|
|
33
|
+
|
|
34
|
+
// DtoRecordView: consumer projection
|
|
35
|
+
@Api.field(
|
|
36
|
+
v.optional(),
|
|
37
|
+
v.serializerTransform('a-file:resolveViews', {
|
|
38
|
+
fieldName: 'dossierFileIds',
|
|
39
|
+
fileScene: 'training-record:dossierFile',
|
|
40
|
+
}),
|
|
41
|
+
v.array(DtoFileView),
|
|
42
|
+
)
|
|
43
|
+
dossierFiles?: DtoFileView[];
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The default naming convention maps `fooId` to `foo` and `fooIds` to `foos`. This inference is mechanical; provide `relationName` when the business name differs.
|
|
47
|
+
|
|
48
|
+
## Built-in resources
|
|
49
|
+
|
|
50
|
+
- `basic-file:formFieldFile` manages browser selection, local checks, transport selection, completed-file cards, and form/relation synchronization.
|
|
51
|
+
- `basic-file:file` renders a resolved relation in a table.
|
|
52
|
+
- `ModelFile` owns shared upload-policy query state. The field controller owns only interaction and upload state.
|
|
53
|
+
|
|
54
|
+
The table cell does not resolve metadata from IDs. With one resolved item it renders a download link; with several it displays the first filename and a `+N` selection surface. Without a resolved relation it can only fall back to the raw cell value.
|
|
55
|
+
|
|
56
|
+
## Configure `basic-file:formFieldFile`
|
|
57
|
+
|
|
58
|
+
`fileScene` has an optional TypeScript type but is operationally required before a user can select/upload a file.
|
|
59
|
+
|
|
60
|
+
| Option | Purpose and precedence |
|
|
61
|
+
| -------------- | -------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `fileScene` | Required backend scene for policy lookup and upload. |
|
|
63
|
+
| `relationName` | Explicit resolved-relation field; otherwise infer `fooId → foo` or `fooIds → foos`. |
|
|
64
|
+
| `multiple` | Local override; otherwise uses scene policy. |
|
|
65
|
+
| `maxCount` | Frontend-only retained-item limit. Defaults to `1`; single mode is always one. |
|
|
66
|
+
| `accept` | Browser accept string or tokens. When present, it overrides composed MIME/extension accept values. |
|
|
67
|
+
| `mimeTypes` | Local client restriction; falls back to scene policy when omitted. |
|
|
68
|
+
| `extensions` | Local client restriction; falls back to scene policy when omitted. |
|
|
69
|
+
| `maxSize` | Local client maximum; falls back to scene policy when omitted. |
|
|
70
|
+
| `minSize` | Frontend-only minimum size. |
|
|
71
|
+
| `placeholder` | Empty-field text. |
|
|
72
|
+
|
|
73
|
+
For composed browser acceptance, explicit `accept` wins. Otherwise local MIME types and extensions are used by category, falling back to policy values for omitted categories. The server always validates the real upload; browser validation is UX, not a security or storage boundary.
|
|
74
|
+
|
|
75
|
+
A scene can publish `multiple: true` while the field still retains only one item: set `maxCount` above `1` when the UI should allow multiple files.
|
|
76
|
+
|
|
77
|
+
## Model-owned policy state
|
|
78
|
+
|
|
79
|
+
`ModelFile.getUploadPolicy(fileScene)` creates scene-keyed `$useStateData(...)` state using `['uploadPolicy', 'file', fileScene]` and `disableSuspenseOnInit: true`.
|
|
80
|
+
|
|
81
|
+
Rendering establishes this query without suspending the initial form. The upload button is disabled while a real scene policy is pending. At the file-selection boundary, the controller calls `$QueryEnsureLoaded(...)` before it chooses cardinality, checks policy-backed constraints, or selects a transport. Fields with the same scene reuse the same model-owned policy state.
|
|
82
|
+
|
|
83
|
+
## Upload behavior
|
|
84
|
+
|
|
85
|
+
### Ordinary scenes
|
|
86
|
+
|
|
87
|
+
When policy does not advertise direct upload, the field uses the generated API action:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await scope.api.file.upload({ fileScene, file });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This is one authenticated multipart request. The completed response is ready, so the field updates its ID value and resolved relation together.
|
|
94
|
+
|
|
95
|
+
### Direct-capable scenes
|
|
96
|
+
|
|
97
|
+
When policy publishes `directUpload: true`, the field performs:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
create target
|
|
101
|
+
→ raw File fetch using returned method and headers
|
|
102
|
+
→ finalize through Cabloy
|
|
103
|
+
→ update form state from finalized response
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The browser branches only on `directUpload`, never on provider identity. It sends raw `File` bytes with the returned method and headers; current R2 targets use `PUT`. Do not add Cabloy credentials, cookies, or guessed headers to the provider request. Provider CORS must allow the frontend origin, method, and returned headers.
|
|
107
|
+
|
|
108
|
+
The component supports raw-body `PUT`/`POST` targets, not HTML-form/presigned-POST field protocols. It intentionally has no progress reporting, cancellation, retry, timeout, parallel transfer, or rollback UI. A direct draft ID never enters the ID field or relation; only the final ready response does.
|
|
109
|
+
|
|
110
|
+
`ApiFile.uploadUrl()` exists for authorized custom consumers. `basic-file:formFieldFile` has no remote URL-import UI and never fetches a remote URL itself.
|
|
111
|
+
|
|
112
|
+
## Relation synchronization and batch behavior
|
|
113
|
+
|
|
114
|
+
After a ready ordinary upload or ready direct finalization, the controller rebuilds the ID value and relation in the same form operation. It keeps resolved data already supplied by the DTO, merges newly returned upload data, and uses an ID-only fallback only when metadata is unavailable.
|
|
115
|
+
|
|
116
|
+
Removing an item changes form state only. It does not call a file-delete API or remove stored provider content.
|
|
117
|
+
|
|
118
|
+
Multiple selected files are processed sequentially and are not transactional:
|
|
119
|
+
|
|
120
|
+
- local validation failures can be skipped while later files continue;
|
|
121
|
+
- a thrown upload or finalization failure prevents the current batch from committing its new IDs to the form;
|
|
122
|
+
- an earlier successfully stored file is not automatically rolled back if a later file fails;
|
|
123
|
+
- failed direct transfers/finalizations can leave a backend draft or object for lifecycle cleanup.
|
|
124
|
+
|
|
125
|
+
Do not describe the form-state boundary as a storage rollback guarantee.
|
|
126
|
+
|
|
127
|
+
## Delivery and SSR
|
|
128
|
+
|
|
129
|
+
Render the resolved `downloadUrl`; never derive a provider URL from an ID, bucket, or storage key. `basic-file` prefixes a relative `/api/...` URL with configured `api.baseURL`; absolute CDN and provider-signed URLs pass through unchanged.
|
|
130
|
+
|
|
131
|
+
For a private Admin relation that the backend resolves with `deliveryOptions.audience: true`, the browser adds a short-lived, path-scoped passport code only when the user opens a download. The code is model-owned, shared by the current user for the file-download route family, and is not written back into the DTO URL. The delivery URL also remains resource-bound, so the code cannot authorize a different file. SSR never requests or serializes this passport code.
|
|
132
|
+
|
|
133
|
+
The hidden native file input is inside `ClientOnly`. Existing relation cards and raw download anchors can render during SSR, but file selection, direct provider transfer, and user-bound download URL decoration are browser-only. Policy lookup itself is model-owned rather than wrapped in `ClientOnly`, while the selection boundary waits for strict policy readiness.
|
|
134
|
+
|
|
135
|
+
## File UI remains file-specific
|
|
136
|
+
|
|
137
|
+
The field presents filename, content type, human-readable size, upload time, download, and removal. It intentionally does not inherit image crop, resize, thumbnail, gallery, variant, or transform behavior.
|
|
138
|
+
|
|
139
|
+
## Frontend checklist
|
|
140
|
+
|
|
141
|
+
1. Define the backend `fileScene` before configuring the field.
|
|
142
|
+
2. Persist ID values and expose matching `DtoFileView` relations.
|
|
143
|
+
3. Keep `relationName`, persisted field, serializer transform, and scene name aligned.
|
|
144
|
+
4. Let `ModelFile` own policy state and branch only on semantic policy capability.
|
|
145
|
+
5. Configure `maxCount` explicitly for multi-file UI.
|
|
146
|
+
6. Update IDs/relations only from ready/finalized responses.
|
|
147
|
+
7. Render resolved `downloadUrl` values rather than storage-derived paths.
|