cabloy 5.1.101 → 5.1.103
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 +36 -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/file-guide.md +147 -0
- package/cabloy-docs/frontend/image-guide.md +18 -9
- package/cabloy-docs/frontend/model-state-guide.md +12 -0
- 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/scripts/upgrade.ts +26 -10
- package/vona/packages-vona/vona/package.json +1 -1
- 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 +44 -1
- 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/lib/utils.ts +29 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- 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.103
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.103
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add a direct image upload flow for the frontend.
|
|
8
|
+
- Add user-bound media delivery to restrict training media access to authorized users.
|
|
9
|
+
- Unify file and image visibility semantics across delivery flows.
|
|
10
|
+
- Move image signed URL policy configuration to scenes.
|
|
11
|
+
- Add shared serializer transforms for file responses.
|
|
12
|
+
- Align private delivery semantics for native images.
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- Defer image rendering to the client.
|
|
17
|
+
- Refresh stale media authorization tokens.
|
|
18
|
+
- Resolve media URL type errors.
|
|
19
|
+
- Bind training media delivery to the requesting user.
|
|
20
|
+
- Restore file upload type validation.
|
|
21
|
+
|
|
22
|
+
### Improvements
|
|
23
|
+
|
|
24
|
+
- Use exact media delivery paths.
|
|
25
|
+
- Add fresh query helpers for retrieving current data.
|
|
26
|
+
- Simplify media delivery options and resolver view naming.
|
|
27
|
+
- Align the file upload lifecycle and simplify image upload handling.
|
|
28
|
+
- Reduce exposed public fields and contract surface for files and images.
|
|
29
|
+
- Clarify image transform key naming.
|
|
30
|
+
- Remove native image direct-upload support.
|
|
31
|
+
|
|
32
|
+
## 5.1.102
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
- Keep the upgrade dry-run plan for the current version.
|
|
37
|
+
- Extend Cabloy Basic upgrade overwrite suites.
|
|
38
|
+
|
|
3
39
|
## 5.1.101
|
|
4
40
|
|
|
5
41
|
### 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:
|
|
@@ -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.
|
|
@@ -7,7 +7,7 @@ Zova image is not only a file input. It is a frontend image layer built around:
|
|
|
7
7
|
- the built-in form-field renderer `basic-image:formFieldImage`
|
|
8
8
|
- the built-in table-cell renderer `basic-image:image`
|
|
9
9
|
- resolved relation fields such as `image` and `sceneImages`
|
|
10
|
-
-
|
|
10
|
+
- policy-selected ordinary or provider-hosted direct upload through the generated image API
|
|
11
11
|
- optional crop and resize behavior in the browser
|
|
12
12
|
- shared preview dialogs for form and table usage
|
|
13
13
|
|
|
@@ -58,7 +58,7 @@ Use this as the standard image field renderer for forms.
|
|
|
58
58
|
It supports:
|
|
59
59
|
|
|
60
60
|
- single-image and multi-image fields
|
|
61
|
-
-
|
|
61
|
+
- policy-selected ordinary or direct upload
|
|
62
62
|
- frontend file validation
|
|
63
63
|
- optional crop
|
|
64
64
|
- optional resize and re-encode before upload
|
|
@@ -121,9 +121,9 @@ v.serializerTransform('a-image:resolveView', {
|
|
|
121
121
|
A practical rule is:
|
|
122
122
|
|
|
123
123
|
- frontend validation, crop, and resize improve UX
|
|
124
|
-
- backend image scenes and
|
|
124
|
+
- backend image scenes and authenticated multipart validation remain the true policy boundary
|
|
125
125
|
|
|
126
|
-
For the backend side of that contract, read [Backend Image Guide](/backend/image-guide), especially the sections on image scenes, upload
|
|
126
|
+
For the backend side of that contract, read [Backend Image Guide](/backend/image-guide), especially the sections on image scenes, authenticated multipart upload, and DTO-side image view resolution.
|
|
127
127
|
|
|
128
128
|
## One running example through this guide: Student image
|
|
129
129
|
|
|
@@ -316,12 +316,15 @@ The built-in form field follows a consistent frontend flow.
|
|
|
316
316
|
2. the frontend validates the file type and size
|
|
317
317
|
3. if enabled, the crop dialog opens
|
|
318
318
|
4. if configured, the browser resizes and re-encodes the image
|
|
319
|
-
5. the frontend
|
|
320
|
-
6.
|
|
321
|
-
7.
|
|
322
|
-
8. the
|
|
319
|
+
5. the frontend reads the server-provided upload-policy capability
|
|
320
|
+
6. ordinary scenes call `scope.api.image.upload(...)` with multipart `imageScene` and `image`
|
|
321
|
+
7. direct-capable scenes call `scope.api.image.createDirectUpload(...)`, upload multipart data to the returned provider URL without Cabloy credentials, then call `scope.api.image.finalizeDirectUpload(...)`
|
|
322
|
+
8. the stored field value is updated from the ordinary upload response or the finalized direct-upload response
|
|
323
|
+
9. the resolved relation field is synchronized so the form can preview the completed image immediately
|
|
323
324
|
|
|
324
|
-
|
|
325
|
+
The frontend does not select a provider by name. Vona resolves the scene and publishes only whether direct upload is available. A direct-upload draft ID is not written to the form until finalization succeeds.
|
|
326
|
+
|
|
327
|
+
That finalization boundary is easy to miss, but it is one of the most important behaviors in the current implementation.
|
|
325
328
|
|
|
326
329
|
The field value stores IDs.
|
|
327
330
|
|
|
@@ -348,6 +351,12 @@ In readonly mode, the field still renders preview cards.
|
|
|
348
351
|
|
|
349
352
|
If no images are available, it shows a lightweight empty-state message instead of a file input.
|
|
350
353
|
|
|
354
|
+
### Private Admin delivery
|
|
355
|
+
|
|
356
|
+
For a private Admin relation resolved with `deliveryOptions.audience: true`, the client obtains a short-lived passport code after hydration and appends it only to the Cabloy image delivery URL. The code is held in the passport model's in-memory, route-scoped cache and is never written into the resolved relation, SSR HTML, or dehydrated state. The backend still verifies the resource-bound delivery token and the issuing user, so another authenticated user cannot reuse a copied URL.
|
|
357
|
+
|
|
358
|
+
User-bound delivery forces the Cabloy proxy path. A provider redirect after Cabloy authorization can still expose a separate short-lived provider URL; use a streaming proxy when that remaining provider-link window is unacceptable.
|
|
359
|
+
|
|
351
360
|
### Preview dialog
|
|
352
361
|
|
|
353
362
|
When an image preview is available, the field opens a shared preview dialog that supports:
|
|
@@ -112,6 +112,18 @@ findAll() {
|
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
### Query readiness utilities: loaded versus fresh
|
|
116
|
+
|
|
117
|
+
These utilities consume an existing model-owned `$useStateData(...)` query. They do not create another state category or a second fetch lifecycle:
|
|
118
|
+
|
|
119
|
+
- `$QueryEnsureLoaded(...)` is an availability utility. It waits only when the query has no `data`, then returns the query wrapper.
|
|
120
|
+
- `$QueryGetFresh(...)` is a non-blocking freshness read. It returns data only when a model-supplied `isStale(query)` predicate accepts the cached value; otherwise it starts refresh work and returns `undefined` for the current render.
|
|
121
|
+
- `$QueryEnsureFresh(...)` is an awaited freshness gate. It waits for stale data to refresh, returns the result, and propagates query errors.
|
|
122
|
+
|
|
123
|
+
The freshness predicate belongs to the domain owner. For example, `ModelPassport` considers a temporary token stale when it is absent or when its `dataUpdatedAt` falls outside the model's short reuse window.
|
|
124
|
+
|
|
125
|
+
For render-versus-interaction placement guidance, see [`$useStateData` Best Practices](/frontend/use-state-data-best-practices).
|
|
126
|
+
|
|
115
127
|
## 2. `$useMutationData(...)`
|
|
116
128
|
|
|
117
129
|
Use this for model-owned mutation state.
|