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.
Files changed (128) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/CLAUDE.local.md +4 -0
  4. package/cabloy-docs/.vitepress/config.mjs +3 -0
  5. package/cabloy-docs/backend/file-guide.md +148 -0
  6. package/cabloy-docs/backend/image-guide.md +40 -40
  7. package/cabloy-docs/backend/jwt-guide.md +21 -0
  8. package/cabloy-docs/frontend/file-guide.md +147 -0
  9. package/cabloy-docs/frontend/image-guide.md +18 -9
  10. package/cabloy-docs/frontend/model-state-guide.md +12 -0
  11. package/cabloy-docs/frontend/use-state-data-best-practices.md +54 -0
  12. package/cabloy-docs/fullstack/file-workflow.md +135 -0
  13. package/cabloy-docs/fullstack/image-workflow.md +12 -3
  14. package/package.json +1 -1
  15. package/scripts/upgrade.ts +26 -10
  16. package/vona/packages-vona/vona/package.json +1 -1
  17. package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +30 -4
  18. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +1 -1
  19. package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +1 -0
  20. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +8 -6
  21. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +8 -6
  22. package/vona/src/suite/a-training/modules/training-record/src/index.ts +0 -1
  23. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +0 -1
  24. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +10 -12
  25. package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +1 -0
  26. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +9 -10
  27. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +2 -2
  28. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +3 -2
  29. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +155 -35
  30. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +283 -54
  31. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +46 -63
  32. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.index.ts +12 -0
  33. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +1 -1
  34. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +3 -0
  35. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/schedule.fileDraftPrune.ts +29 -0
  36. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveView.ts +45 -0
  37. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveViews.ts +45 -0
  38. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +6 -0
  39. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +64 -56
  40. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeRequest.ts +13 -0
  41. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeResponse.ts +10 -0
  42. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +0 -3
  43. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +2 -14
  44. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +3 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +3 -0
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +0 -25
  47. 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
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +10 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadUrl.ts +329 -0
  50. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +1 -0
  51. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +51 -10
  52. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +8 -3
  53. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +1 -1
  54. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +1 -1
  55. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +12 -4
  56. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +52 -18
  57. package/vona/src/suite-vendor/a-file/modules/file-native/package.json +1 -1
  58. package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +5 -5
  59. package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +7 -8
  60. package/vona/src/suite-vendor/a-file/package.json +4 -4
  61. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  62. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +16 -19
  63. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +99 -74
  64. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +28 -68
  65. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +1 -1
  66. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +3 -0
  67. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +3 -0
  68. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +4 -0
  69. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +78 -34
  70. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +3 -0
  71. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +0 -3
  72. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +1 -27
  73. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +6 -0
  74. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +2 -36
  75. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +0 -3
  76. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +4 -27
  77. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +1 -1
  78. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +36 -29
  79. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +4 -4
  80. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +1 -0
  81. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  82. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +8 -11
  83. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +4 -16
  84. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +17 -16
  85. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +2 -6
  86. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +0 -37
  87. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +13 -36
  88. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +100 -113
  89. package/vona/src/suite-vendor/a-image/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/config/config.ts +3 -1
  92. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +9 -4
  93. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  94. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +113 -64
  95. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  96. package/zova/packages-zova/zova/package.json +2 -2
  97. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +50 -12
  98. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +610 -114
  99. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +44 -1
  100. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +50 -12
  101. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +610 -114
  102. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +16 -12
  103. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +25 -25
  104. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +18 -24
  105. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +228 -291
  106. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +5 -5
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +23 -5
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +72 -43
  109. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +22 -4
  110. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +16 -42
  111. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +5 -25
  112. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +10 -24
  113. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +216 -333
  114. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +0 -5
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +38 -12
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +79 -32
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +22 -4
  118. package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +3 -4
  119. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  120. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +29 -1
  121. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -2
  122. package/zova/src/suite-vendor/a-zova/package.json +2 -2
  123. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +0 -34
  124. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +0 -24
  125. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +0 -16
  126. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +0 -57
  127. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +0 -31
  128. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +0 -13
@@ -220,6 +220,60 @@ The better middle ground is:
220
220
 
221
221
  That is very different from rebuilding a second fetch lifecycle.
222
222
 
223
+ ### Choose the wait semantic: loaded versus fresh
224
+
225
+ Waiting for an existing query can mean two different things:
226
+
227
+ - **Loaded** means a value exists.
228
+ `$QueryEnsureLoaded(...)` waits only when `query.data === undefined`, so it can continue with an older cached value.
229
+ - **Fresh** means the model's domain-specific validity rule accepts the value.
230
+ `$QueryGetFresh(...)` and `$QueryEnsureFresh(...)` receive an `isStale(query)` predicate, so the model decides whether loaded data is still safe to consume.
231
+
232
+ | Need | Helper | Missing or stale behavior | Return shape |
233
+ | ------------------------------------ | ------------------------------- | ------------------------------------------------------------------------ | --------------------------- |
234
+ | An interaction needs any loaded data | `await $QueryEnsureLoaded(...)` | Waits only while `query.data === undefined` | Query object or `undefined` |
235
+ | Render may consume only fresh data | `$QueryGetFresh(...)` | Starts `query.suspense()` and returns no data for the current render | `TData \| undefined` |
236
+ | An interaction requires fresh data | `await $QueryEnsureFresh(...)` | Waits for `query.suspense()` and throws a query error when refresh fails | `TData \| undefined` |
237
+
238
+ Loaded is an availability condition. Fresh is a validity condition defined by the model; it might use `dataUpdatedAt`, response expiry metadata, or another business rule. `a-model` accepts the predicate and does not prescribe that policy.
239
+
240
+ For example, `ModelPassport` keeps the temporary-token query and its 30-second reuse rule in the model, while the generic helpers provide the consumption mechanics:
241
+
242
+ ```ts
243
+ import { $QueryEnsureFresh, $QueryGetFresh, BeanModelBase, Model } from 'zova-module-a-model';
244
+
245
+ @Model()
246
+ export class ModelPassport extends BeanModelBase {
247
+ getFreshTempAuthToken(options: TempTokenOptions): string | undefined {
248
+ return $QueryGetFresh(
249
+ () => this.getTempAuthToken(options),
250
+ query => this._isTempAuthTokenExpired(query.data, query.dataUpdatedAt, options.staleTime),
251
+ );
252
+ }
253
+
254
+ async ensureFreshTempAuthToken(options: TempTokenOptions): Promise<string | undefined> {
255
+ return await $QueryEnsureFresh(
256
+ () => this.getTempAuthToken(options),
257
+ query => this._isTempAuthTokenExpired(query.data, query.dataUpdatedAt, options.staleTime),
258
+ );
259
+ }
260
+ }
261
+ ```
262
+
263
+ Use the non-blocking facade during render, where stale data should not build a protected URL:
264
+
265
+ ```ts
266
+ const passportCode = this.$passport.getFreshTempAuthToken(options);
267
+ ```
268
+
269
+ Use the awaited facade at an interaction boundary, where a download or preview must not continue with an expired token:
270
+
271
+ ```ts
272
+ const passportCode = await this.$passport.ensureFreshTempAuthToken(options);
273
+ ```
274
+
275
+ Both paths reuse the same model-owned query. When `$QueryGetFresh(...)` finds stale data, it starts `query.suspense()` and returns `undefined` for the current render; reactive query state provides the replacement value on a later render. `$QueryEnsureFresh(...)` instead waits for that refresh and propagates query errors to the interaction flow.
276
+
223
277
  ## Practical rule 7: derive render-time state once per render when possible
224
278
 
225
279
  Even when the query object is reused, a controller can still become noisy if it repeatedly derives the same values in several helper calls.
@@ -0,0 +1,135 @@
1
+ # Fullstack File Workflow
2
+
3
+ This page follows a Cabloy Basic file field from Vona contract truth to Zova interaction. It is the forward contract loop for `a-file` and `basic-file`.
4
+
5
+ > Read this with [Backend File Guide](/backend/file-guide), [Frontend File Guide](/frontend/file-guide), and [Backend OpenAPI to Frontend SDK](/fullstack/openapi-to-sdk).
6
+
7
+ ## One business field, two consumer surfaces
8
+
9
+ A file-backed business field persists IDs, while a DTO relation carries completed-file presentation data. The training-record module is the reference pattern:
10
+
11
+ ```ts
12
+ // Vona scene: one trusted business capability
13
+ @FileScene({
14
+ public: false,
15
+ upload: {
16
+ maxSize: 20 * 1024 * 1024,
17
+ mimeTypes: ['application/pdf', 'application/zip', 'text/plain'],
18
+ extensions: ['.pdf', '.zip', '.txt'],
19
+ multiple: true,
20
+ },
21
+ })
22
+ export class FileSceneDossierFile extends BeanBase {}
23
+
24
+ // Entity: persist only IDs and attach Zova resource renderers
25
+ @Api.field(
26
+ ZovaRender.field('basic-file:formFieldFile', {
27
+ fileScene: 'training-record:dossierFile',
28
+ relationName: 'dossierFiles',
29
+ maxCount: 3,
30
+ }),
31
+ ZovaRender.cell('basic-file:file', { relationName: 'dossierFiles' }),
32
+ v.array(v.tableIdentity()),
33
+ )
34
+ dossierFileIds?: TableIdentity[];
35
+
36
+ // Consumer DTO: resolve completed display/delivery data
37
+ @Api.field(
38
+ v.optional(),
39
+ v.serializerTransform('a-file:resolveViews', {
40
+ fieldName: 'dossierFileIds',
41
+ fileScene: 'training-record:dossierFile',
42
+ }),
43
+ v.array(DtoFileView),
44
+ )
45
+ dossierFiles?: DtoFileView[];
46
+ ```
47
+
48
+ Keep the persisted field, resolved relation, `relationName`, and scene name aligned. `DtoFileView` is a consumer projection; it does not replace ID persistence. The serializer verifies the optional scene while resolving IDs, preventing a relation from silently consuming a file from another business capability.
49
+
50
+ ## Three ingestion paths and readiness
51
+
52
+ ### 1. Ordinary local upload
53
+
54
+ ```text
55
+ Zova field
56
+ → authenticated POST /file/upload(fileScene, file)
57
+ → Vona validates received multipart file and resolves scene/provider
58
+ → provider storage
59
+ → ready action response
60
+ → ID field and relation update
61
+ ```
62
+
63
+ The backend owns validation and storage. The response is ready immediately after backend storage; this is the portable Native-compatible path and needs no file upload token.
64
+
65
+ ### 2. Remote HTTPS URL import
66
+
67
+ ```text
68
+ authorized server/API consumer
69
+ → POST /file/upload-url
70
+ → provider capability check
71
+ → secure HTTPS remote-fetch boundary
72
+ → provider storage
73
+ → ready action response
74
+ ```
75
+
76
+ Remote import is not built into the browser field. It is a server/provider operation: the selected provider validates remote URL safety and actual content before storage. Native does not support this path; Cloudflare R2 does. A browser must not implement its own unrestricted remote fetch.
77
+
78
+ ### 3. Third-party direct upload
79
+
80
+ ```text
81
+ policy.directUpload
82
+ → POST /file/direct-upload
83
+ → Cabloy draft + raw-body provider target
84
+ → browser transfers raw File without Cabloy credentials
85
+ → POST /file/direct-upload/finalize
86
+ → provider metadata verification + ready state
87
+ → ID field and relation update
88
+ ```
89
+
90
+ A direct target is draft-only, not a completed file. Finalization is the only promotion to `ready`; drafts cannot resolve views, be downloaded, or enter business IDs/relations. The frontend uses returned method/headers for raw file transfer and stores only the finalized response.
91
+
92
+ ## Forward contract loop
93
+
94
+ When a File contract changes, work from backend truth toward generated consumers:
95
+
96
+ 1. define or update the Vona `@FileScene`, persisted ID field, controller/DTO policy, and `a-file:resolveView(s)` relation;
97
+ 2. regenerate Vona metadata and inspect the emitted Swagger/OpenAPI contract;
98
+ 3. regenerate the filtered `basic-file` OpenAPI consumer;
99
+ 4. keep hand-authored Zova behavior in `ModelFile`, `ControllerFormFieldFile`, and `TableCellFile` thin and policy-driven;
100
+ 5. build the affected Admin and Web flavors, then run `npm run deps:vona` for the reverse dependency handoff.
101
+
102
+ Generated contract outputs include:
103
+
104
+ ```text
105
+ zova/.../basic-file/src/api/file.ts
106
+ zova/.../basic-file/src/apiSchema/file.ts
107
+ zova/.../basic-file/src/api/openapi/types.ts
108
+ zova/.../basic-file/src/api/openapi/schemas.ts
109
+ ```
110
+
111
+ Do not hand-edit these generated files. `ModelFile`, the form-field controller, the table cell, and Vona render metadata are hand-authored extension points.
112
+
113
+ ## Cross-layer invariants
114
+
115
+ | Layer | Owns | Must not own |
116
+ | ----------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
117
+ | Vona `a-file` | scene policy, provider/client selection, actual validation, lifecycle, storage, visibility, and delivery | browser-controlled provider or storage choices |
118
+ | DTO serializer | safe `DtoFileView` projection and scene matching | persistence replacement or storage metadata disclosure |
119
+ | Zova `basic-file` | picker UX, local UX checks, policy-driven transport, ID/relation synchronization | provider selection, storage URL construction, or draft persistence |
120
+ | Provider transfer | raw body transfer for a direct target | Cabloy session credential forwarding |
121
+
122
+ The frontend reads `downloadUrl` from a resolved view; it never constructs delivery URLs from IDs, buckets, or object keys. Upload-policy capability is semantic (`directUpload`), so frontend code never branches on provider identity.
123
+
124
+ ## Verification checklist
125
+
126
+ For a File contract or workflow change, verify:
127
+
128
+ 1. the scene policy and persisted ID field match the intended business capability;
129
+ 2. create/view/list DTO schemas expose the matching `DtoFileView` relation and serializer transform;
130
+ 3. Swagger/OpenAPI contains the expected policy, multipart, direct-finalize, and URL-import actions;
131
+ 4. regenerated `basic-file` API/schema output is used rather than hand-patched;
132
+ 5. Native reports no direct/URL-import capability while the configured Cloudflare R2 scene advertises direct upload only when creation and finalization are both available;
133
+ 6. a draft cannot resolve a view or download URL, and the form updates only after finalization;
134
+ 7. private/public delivery still uses resolved URLs without leaking provider/storage data;
135
+ 8. run the affected Admin and Web builds and then `npm run deps:vona` after a generated frontend handoff.
@@ -291,11 +291,20 @@ The form-side resource is responsible for:
291
291
  - selecting files
292
292
  - performing frontend validation
293
293
  - optionally cropping and resizing in the browser
294
- - requesting an upload token
295
- - uploading multipart data
296
- - binding the stored image ID field
294
+ - selecting the policy-supported ordinary or direct upload transport
295
+ - binding the finalized stored image ID field
296
+ - synchronizing the relation preview only after upload completion
297
297
  - synchronizing the resolved relation field for immediate UI refresh
298
298
 
299
+ ### Upload transport selection
300
+
301
+ The form field waits for the backend image-scene upload policy and selects one of two server-authorized paths:
302
+
303
+ - ordinary scenes submit authenticated multipart `imageScene` and `image` data to Cabloy;
304
+ - direct-capable scenes create a direct-upload session, send the image to the returned provider URL without Cabloy credentials, and finalize using the returned Cabloy image ID.
305
+
306
+ The browser does not branch on provider identity. In both paths, it updates the stored ID field and resolved relation preview only after it has a completed image response; a direct-upload draft is never used as the form value.
307
+
299
308
  ### `basic-image:image`
300
309
 
301
310
  The table-side resource is responsible for:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.101",
3
+ "version": "5.1.103",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -47,6 +47,14 @@ const OVERWRITE_DIRS: string[] = [
47
47
  'zova/scripts',
48
48
  ];
49
49
 
50
+ const OVERWRITE_DIRS_CABLOY_BASIC: string[] = [
51
+ 'vona/src/suite/cabloy-basic',
52
+ 'vona/src/suite/a-training',
53
+ 'zova/src/suite/cabloy-basic',
54
+ 'zova/src/suite/a-training',
55
+ 'zova/src/suite/a-devui',
56
+ ];
57
+
50
58
  const MERGE_DIRS: string[] = [
51
59
  // Claude project assets
52
60
  '.claude/commands',
@@ -242,8 +250,12 @@ async function downloadAndExtract(tarballUrl: string): Promise<void> {
242
250
  // --- Step 3: Selective overwrite ---
243
251
 
244
252
  function selectiveOverwrite(dryRun?: boolean): void {
253
+ const overwriteDirs = existsSync(resolve(ROOT_DIR, '__CABLOY_BASIC__'))
254
+ ? [...OVERWRITE_DIRS, ...OVERWRITE_DIRS_CABLOY_BASIC]
255
+ : OVERWRITE_DIRS;
256
+
245
257
  // Overwrite directories
246
- for (const dir of OVERWRITE_DIRS) {
258
+ for (const dir of overwriteDirs) {
247
259
  const src = resolve(TEMP_DIR, dir);
248
260
  const dest = resolve(ROOT_DIR, dir);
249
261
  if (!existsSync(src)) continue;
@@ -316,13 +328,12 @@ function runInitTestData(dryRun: boolean): void {
316
328
  // --- Step 6: Cleanup ---
317
329
 
318
330
  function cleanup(dryRun?: boolean): void {
319
- if (dryRun) {
320
- log(' [dry-run] Remove temp dir: node_modules/.cabloy-upgrade/');
321
- return;
322
- }
323
331
  if (existsSync(TEMP_DIR)) {
324
332
  rmSync(TEMP_DIR, { recursive: true, force: true });
325
333
  }
334
+ if (dryRun) {
335
+ log(' [dry-run] Removed temp dir: node_modules/.cabloy-upgrade/');
336
+ }
326
337
  }
327
338
 
328
339
  // --- Main ---
@@ -350,15 +361,20 @@ async function main(): Promise<void> {
350
361
  if (currentVersion) {
351
362
  const comparison = compareVersions(currentVersion, latestPackageInfo.version);
352
363
  if (comparison === 0) {
353
- log(`Cabloy is already up to date (current: ${currentVersion}). Skipping upgrade.`);
354
- return;
355
- }
356
- if (comparison > 0) {
364
+ if (!dryRun) {
365
+ log(`Cabloy is already up to date (current: ${currentVersion}). Skipping upgrade.`);
366
+ return;
367
+ }
368
+ log(
369
+ `Cabloy is already up to date (current: ${currentVersion}). Continuing dry-run to show overwrite plan.\n`,
370
+ );
371
+ } else if (comparison > 0) {
357
372
  throw new Error(
358
373
  `Project Cabloy version ${currentVersion} is newer than npm latest ${latestPackageInfo.version}. Aborting upgrade.`,
359
374
  );
375
+ } else {
376
+ log(`Upgrading Cabloy from ${currentVersion} to ${latestPackageInfo.version}...\n`);
360
377
  }
361
- log(`Upgrading Cabloy from ${currentVersion} to ${latestPackageInfo.version}...\n`);
362
378
  } else {
363
379
  log(`Upgrading Cabloy to ${latestPackageInfo.version}...\n`);
364
380
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.72",
3
+ "version": "5.1.73",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -388,20 +388,46 @@ declare module 'vona-module-training-record' {
388
388
  }
389
389
  }
390
390
  /** ssrMenu: end */
391
+ /** fileScene: begin */
392
+ export * from '../bean/fileScene.dossierFile.ts';
393
+
394
+ import { type IDecoratorFileSceneOptions } from 'vona-module-a-file';
395
+ declare module 'vona-module-a-file' {
396
+
397
+ export interface IFileSceneRecord {
398
+ 'training-record:dossierFile': IDecoratorFileSceneOptions;
399
+ }
400
+
401
+
402
+ }
403
+ declare module 'vona-module-training-record' {
404
+
405
+ export interface FileSceneDossierFile {
406
+ /** @internal */
407
+ get scope(): ScopeModuleTrainingRecord;
408
+ }
409
+
410
+ export interface FileSceneDossierFile {
411
+ get $beanFullName(): 'training-record.fileScene.dossierFile';
412
+ get $onionName(): 'training-record:dossierFile';
413
+ get $onionOptions(): IDecoratorFileSceneOptions;
414
+ }
415
+ }
416
+ /** fileScene: end */
391
417
  /** imageScene: begin */
392
418
  export * from '../bean/imageScene.sceneImage.ts';
393
419
 
394
420
  import { type IDecoratorImageSceneOptions } from 'vona-module-a-image';
395
421
  declare module 'vona-module-a-image' {
396
-
422
+
397
423
  export interface IImageSceneRecord {
398
424
  'training-record:sceneImage': IDecoratorImageSceneOptions;
399
425
  }
400
426
 
401
-
427
+
402
428
  }
403
429
  declare module 'vona-module-training-record' {
404
-
430
+
405
431
  export interface ImageSceneSceneImage {
406
432
  /** @internal */
407
433
  get scope(): ScopeModuleTrainingRecord;
@@ -411,7 +437,7 @@ declare module 'vona-module-training-record' {
411
437
  get $beanFullName(): 'training-record.imageScene.sceneImage';
412
438
  get $onionName(): 'training-record:sceneImage';
413
439
  get $onionOptions(): IDecoratorImageSceneOptions;
414
- }
440
+ }
415
441
  }
416
442
  /** imageScene: end */
417
443
  /** locale: begin */
@@ -2,6 +2,7 @@ import { BeanBase } from 'vona';
2
2
  import { FileScene } from 'vona-module-a-file';
3
3
 
4
4
  @FileScene({
5
+ public: false,
5
6
  upload: {
6
7
  maxSize: 20 * 1024 * 1024,
7
8
  mimeTypes: [
@@ -16,7 +17,6 @@ import { FileScene } from 'vona-module-a-file';
16
17
  ],
17
18
  extensions: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.zip', '.txt'],
18
19
  multiple: true,
19
- public: false,
20
20
  },
21
21
  })
22
22
  export class FileSceneDossierFile extends BeanBase {}
@@ -2,6 +2,7 @@ import { BeanBase } from 'vona';
2
2
  import { ImageScene } from 'vona-module-a-image';
3
3
 
4
4
  @ImageScene({
5
+ public: false,
5
6
  upload: {
6
7
  maxSize: 2 * 1024 * 1024,
7
8
  mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
@@ -1,5 +1,6 @@
1
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
2
 
3
+ import { DtoFileView } from 'vona-module-a-file';
3
4
  import { DtoImageView } from 'vona-module-a-image';
4
5
  import { Api, v } from 'vona-module-a-openapiutils';
5
6
  import { $Dto } from 'vona-module-a-orm';
@@ -7,9 +8,7 @@ import { Dto } from 'vona-module-a-web';
7
8
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
9
 
9
10
  import { $locale } from '../.metadata/locales.ts';
10
- import { resolveDossierFiles } from '../lib/resolveDossierFiles.ts';
11
11
  import { ModelRecord } from '../model/record.ts';
12
- import { DtoRecordDossierFileView } from './recordDossierFileView.ts';
13
12
 
14
13
  export interface IDtoOptionsRecordSelectResItem extends IDecoratorDtoOptions {}
15
14
 
@@ -34,6 +33,7 @@ export class DtoRecordSelectResItem extends $Dto.get(() => ModelRecord) {
34
33
  v.serializerTransform('a-image:resolveViews', {
35
34
  fieldName: 'sceneImageIds',
36
35
  imageScene: 'training-record:sceneImage',
36
+ deliveryOptions: { audience: true },
37
37
  }),
38
38
  v.array(DtoImageView),
39
39
  )
@@ -43,12 +43,14 @@ export class DtoRecordSelectResItem extends $Dto.get(() => ModelRecord) {
43
43
  v.title($locale('DossierFiles')),
44
44
  ZovaRender.visible(false),
45
45
  v.optional(),
46
- v.serializerTransform('a-serialization:custom', {
47
- custom: resolveDossierFiles,
46
+ v.serializerTransform('a-file:resolveViews', {
47
+ fieldName: 'dossierFileIds',
48
+ fileScene: 'training-record:dossierFile',
49
+ deliveryOptions: { audience: true },
48
50
  }),
49
- v.array(DtoRecordDossierFileView),
51
+ v.array(DtoFileView),
50
52
  )
51
- dossierFiles?: DtoRecordDossierFileView[];
53
+ dossierFiles?: DtoFileView[];
52
54
 
53
55
  @Api.field(
54
56
  v.title($locale('Operations')),
@@ -1,5 +1,6 @@
1
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
2
 
3
+ import { DtoFileView } from 'vona-module-a-file';
3
4
  import { DtoImageView } from 'vona-module-a-image';
4
5
  import { $makeMetadata, Api, v } from 'vona-module-a-openapiutils';
5
6
  import { $Dto } from 'vona-module-a-orm';
@@ -7,11 +8,9 @@ import { Dto } from 'vona-module-a-web';
7
8
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
9
 
9
10
  import { $locale } from '../.metadata/locales.ts';
10
- import { resolveDossierFiles } from '../lib/resolveDossierFiles.ts';
11
11
  import { ModelRecord } from '../model/record.ts';
12
12
  import { DtoDetailRecordSubjectResItem } from './detailRecordSubjectResItem.tsx';
13
13
  import { DtoDetailRecordSubjectView } from './detailRecordSubjectView.tsx';
14
- import { DtoRecordDossierFileView } from './recordDossierFileView.ts';
15
14
 
16
15
  export interface IDtoOptionsRecordView extends IDecoratorDtoOptions {}
17
16
 
@@ -46,6 +45,7 @@ export class DtoRecordView extends $Dto.get(() => ModelRecord, {
46
45
  v.serializerTransform('a-image:resolveViews', {
47
46
  fieldName: 'sceneImageIds',
48
47
  imageScene: 'training-record:sceneImage',
48
+ deliveryOptions: { audience: true },
49
49
  }),
50
50
  v.array(DtoImageView),
51
51
  )
@@ -55,12 +55,14 @@ export class DtoRecordView extends $Dto.get(() => ModelRecord, {
55
55
  v.title($locale('DossierFiles')),
56
56
  ZovaRender.visible(false),
57
57
  v.optional(),
58
- v.serializerTransform('a-serialization:custom', {
59
- custom: resolveDossierFiles,
58
+ v.serializerTransform('a-file:resolveViews', {
59
+ fieldName: 'dossierFileIds',
60
+ fileScene: 'training-record:dossierFile',
61
+ deliveryOptions: { audience: true },
60
62
  }),
61
- v.array(DtoRecordDossierFileView),
63
+ v.array(DtoFileView),
62
64
  )
63
- dossierFiles?: DtoRecordDossierFileView[];
65
+ dossierFiles?: DtoFileView[];
64
66
 
65
67
  @Api.field(ZovaRender.visible(false), v.optional(), v.array(DtoDetailRecordSubjectResItem))
66
68
  _trainingRecordSubjects?: DtoDetailRecordSubjectResItem[];
@@ -1,6 +1,5 @@
1
1
  export * from './bean/fileScene.dossierFile.ts';
2
2
  export * from './bean/imageScene.sceneImage.ts';
3
- export * from './dto/recordDossierFileView.ts';
4
3
  export * from './lib/index.ts';
5
4
  export * from './.metadata/locales.ts';
6
5
  export * from './.metadata/index.ts';
@@ -1,3 +1,2 @@
1
1
  export * from './onEffectForAverageScore.tsx';
2
2
  export * from './onEffectForTrainingRecordSubjects.tsx';
3
- export * from './resolveDossierFiles.ts';
@@ -43,10 +43,6 @@ describe('record.test.ts', () => {
43
43
  filename: 'attendance.txt',
44
44
  contentType: 'text/plain',
45
45
  public: false,
46
- meta: {
47
- category: 'attendanceSheet',
48
- source: 'upload',
49
- },
50
46
  },
51
47
  {
52
48
  clientName: 'default',
@@ -96,18 +92,24 @@ describe('record.test.ts', () => {
96
92
  assert.equal(String(record.studentId), String(studentId));
97
93
  assert.equal(record.subjectCount, recordData.subjectCount);
98
94
  assert.equal(record.totalScore, recordData.totalScore);
99
- assert.equal(record.averageScore, recordData.averageScore);
95
+ assert.equal(Number(record.averageScore), recordData.averageScore);
100
96
  assert.equal(new Date(record.trainingTime).toISOString(), trainingTime.toISOString());
101
97
  assert.equal(record.dossierFileIds?.length, 1);
102
98
  assert.equal(record.dossierFiles?.length, 1);
103
99
  assert.equal(String(recordDossierFile?.id), String(dossierFileAttendance.id));
104
100
  assert.equal(recordDossierFile?.filename, 'attendance.txt');
105
101
  assert.equal(recordDossierFile?.signed, true);
106
- assert.equal(recordDossierFile?.meta?.category, 'attendanceSheet');
102
+ assert.equal('meta' in (recordDossierFile ?? {}), false);
103
+ assert.equal('fileScene' in (recordDossierFile ?? {}), false);
107
104
  assert.equal(record.trainingRecordSubjects?.length, 1);
108
105
  assert.equal(recordSubject?.name, '__Math__');
109
106
  assert.equal(recordSubject?.score, 95);
110
- const downloadResponse = await fetch(recordDossierFile.downloadUrl);
107
+ const passportCode = await app.bean.passport.createTempAuthToken({
108
+ path: '/api/file/download',
109
+ });
110
+ const downloadUrl = new URL(recordDossierFile.downloadUrl);
111
+ downloadUrl.searchParams.set('x-vona-passport-code', passportCode);
112
+ const downloadResponse = await fetch(downloadUrl);
111
113
  assert.equal(downloadResponse.ok, true);
112
114
  assert.equal(await downloadResponse.text(), dossierTextAttendance.toString());
113
115
 
@@ -129,10 +131,6 @@ describe('record.test.ts', () => {
129
131
  filename: 'assessment.txt',
130
132
  contentType: 'text/plain',
131
133
  public: false,
132
- meta: {
133
- category: 'assessmentReport',
134
- source: 'upload',
135
- },
136
134
  },
137
135
  {
138
136
  clientName: 'default',
@@ -174,7 +172,7 @@ describe('record.test.ts', () => {
174
172
  assert.equal(record.name, dataUpdate.name);
175
173
  assert.equal(record.subjectCount, dataUpdate.subjectCount);
176
174
  assert.equal(record.totalScore, dataUpdate.totalScore);
177
- assert.equal(record.averageScore, dataUpdate.averageScore);
175
+ assert.equal(Number(record.averageScore), dataUpdate.averageScore);
178
176
  assert.equal(new Date(record.trainingTime).toISOString(), trainingTimeUpdate.toISOString());
179
177
  assert.equal(record.dossierFileIds?.length, 2);
180
178
  assert.equal(record.dossierFiles?.length, 2);
@@ -2,6 +2,7 @@ import { BeanBase } from 'vona';
2
2
  import { ImageScene } from 'vona-module-a-image';
3
3
 
4
4
  @ImageScene({
5
+ public: true,
5
6
  upload: {
6
7
  maxSize: 2 * 1024 * 1024,
7
8
  mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
@@ -1,16 +1,12 @@
1
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
2
 
3
3
  import { $Class } from 'vona';
4
+ import { DtoFileView } from 'vona-module-a-file';
4
5
  import { DtoImageView } from 'vona-module-a-image';
5
6
  import { $makeMetadata, Api, v } from 'vona-module-a-openapiutils';
6
7
  import { $Dto } from 'vona-module-a-orm';
7
8
  import { Dto } from 'vona-module-a-web';
8
- import {
9
- DtoRecordDossierFileView,
10
- ModelRecord,
11
- onEffectForTrainingRecordSubjects,
12
- resolveDossierFiles,
13
- } from 'vona-module-training-record';
9
+ import { ModelRecord, onEffectForTrainingRecordSubjects } from 'vona-module-training-record';
14
10
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
15
11
 
16
12
  import { $locale } from '../.metadata/locales.ts';
@@ -50,6 +46,7 @@ export class DtoDetailRecordBase extends $Dto.get(() => ModelRecord, {
50
46
  v.serializerTransform('a-image:resolveViews', {
51
47
  fieldName: 'sceneImageIds',
52
48
  imageScene: 'training-record:sceneImage',
49
+ deliveryOptions: { audience: true },
53
50
  }),
54
51
  v.array($Class.partial(DtoImageView)),
55
52
  )
@@ -59,10 +56,12 @@ export class DtoDetailRecordBase extends $Dto.get(() => ModelRecord, {
59
56
  v.title($locale('DossierFiles')),
60
57
  ZovaRender.visible(false),
61
58
  v.optional(),
62
- v.serializerTransform('a-serialization:custom', {
63
- custom: resolveDossierFiles,
59
+ v.serializerTransform('a-file:resolveViews', {
60
+ fieldName: 'dossierFileIds',
61
+ fileScene: 'training-record:dossierFile',
62
+ deliveryOptions: { audience: true },
64
63
  }),
65
- v.array($Class.partial(DtoRecordDossierFileView)),
64
+ v.array($Class.partial(DtoFileView)),
66
65
  )
67
- dossierFiles?: Partial<DtoRecordDossierFileView>[];
66
+ dossierFiles?: Partial<DtoFileView>[];
68
67
  }
@@ -87,7 +87,7 @@ describe('student.test.ts', () => {
87
87
  assert.equal(record?.name, '__Record__');
88
88
  assert.equal(record?.subjectCount, 1);
89
89
  assert.equal(record?.totalScore, 88);
90
- assert.equal(record?.averageScore, 88);
90
+ assert.equal(Number(record?.averageScore), 88);
91
91
  assert.equal(new Date(record?.trainingTime).toISOString(), trainingTime.toISOString());
92
92
  assert.equal(record?.trainingRecordSubjects?.length, 1);
93
93
  assert.equal(recordSubject?.name, '__Math__');
@@ -141,7 +141,7 @@ describe('student.test.ts', () => {
141
141
  assert.equal(updatedRecord?.name, '__RecordNew__');
142
142
  assert.equal(updatedRecord?.subjectCount, 2);
143
143
  assert.equal(updatedRecord?.totalScore, 183);
144
- assert.equal(updatedRecord?.averageScore, 91.5);
144
+ assert.equal(Number(updatedRecord?.averageScore), 91.5);
145
145
  assert.equal(
146
146
  new Date(updatedRecord?.trainingTime).toISOString(),
147
147
  trainingTimeUpdate.toISOString(),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-file",
3
- "version": "5.0.4",
3
+ "version": "5.0.5",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "assets",
11
11
  "dist",
12
- "src"
12
+ "src",
13
+ "cli"
13
14
  ],
14
15
  "type": "module",
15
16
  "exports": {