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.
Files changed (138) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +47 -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/a-model-under-the-hood.md +26 -4
  9. package/cabloy-docs/frontend/file-guide.md +147 -0
  10. package/cabloy-docs/frontend/image-guide.md +18 -9
  11. package/cabloy-docs/frontend/model-architecture.md +1 -1
  12. package/cabloy-docs/frontend/model-state-guide.md +132 -16
  13. package/cabloy-docs/frontend/ssr-init-data.md +12 -1
  14. package/cabloy-docs/frontend/use-state-data-best-practices.md +54 -0
  15. package/cabloy-docs/fullstack/file-workflow.md +135 -0
  16. package/cabloy-docs/fullstack/image-workflow.md +12 -3
  17. package/package.json +1 -1
  18. package/vona/packages-vona/vona/package.json +1 -1
  19. package/vona/pnpm-lock.yaml +23 -23
  20. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +7 -7
  21. package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +30 -4
  22. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +1 -1
  23. package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +1 -0
  24. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +8 -6
  25. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +8 -6
  26. package/vona/src/suite/a-training/modules/training-record/src/index.ts +0 -1
  27. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +0 -1
  28. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +10 -12
  29. package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +1 -0
  30. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +9 -10
  31. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +2 -2
  32. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +3 -2
  33. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +155 -35
  34. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +283 -54
  35. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +46 -63
  36. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.index.ts +12 -0
  37. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +1 -1
  38. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +3 -0
  39. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/schedule.fileDraftPrune.ts +29 -0
  40. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveView.ts +45 -0
  41. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveViews.ts +45 -0
  42. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +6 -0
  43. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +64 -56
  44. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeRequest.ts +13 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeResponse.ts +10 -0
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +0 -3
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +2 -14
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +3 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +3 -0
  50. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +0 -25
  51. 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
  52. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +10 -0
  53. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadUrl.ts +329 -0
  54. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +1 -0
  55. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +51 -10
  56. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +8 -3
  57. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +1 -1
  58. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +1 -1
  59. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +12 -4
  60. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +52 -18
  61. package/vona/src/suite-vendor/a-file/modules/file-native/package.json +1 -1
  62. package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +5 -5
  63. package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +7 -8
  64. package/vona/src/suite-vendor/a-file/package.json +4 -4
  65. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  66. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +16 -19
  67. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +99 -74
  68. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +28 -68
  69. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +1 -1
  70. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +3 -0
  71. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +3 -0
  72. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +4 -0
  73. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +78 -34
  74. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +3 -0
  75. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +0 -3
  76. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +1 -27
  77. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +6 -0
  78. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +2 -36
  79. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +0 -3
  80. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +4 -27
  81. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +1 -1
  82. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +36 -29
  83. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +4 -4
  84. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +1 -0
  85. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  86. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +8 -11
  87. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +4 -16
  88. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +17 -16
  89. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +2 -6
  90. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +0 -37
  91. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +13 -36
  92. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +100 -113
  93. package/vona/src/suite-vendor/a-image/package.json +1 -1
  94. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  95. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/config/config.ts +3 -1
  96. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +9 -4
  97. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  98. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +113 -64
  99. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  100. package/zova/packages-zova/zova/package.json +2 -2
  101. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +50 -12
  102. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +610 -114
  103. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +51 -2
  104. package/zova/src/suite/a-home/modules/home-passport/src/monkey.ts +17 -3
  105. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +50 -12
  106. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +610 -114
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +16 -12
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +25 -25
  109. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +18 -24
  110. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +228 -291
  111. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +5 -5
  112. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +23 -5
  113. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +72 -43
  114. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +22 -4
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +16 -42
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +5 -25
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +10 -24
  118. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +216 -333
  119. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +0 -5
  120. package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +38 -12
  121. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +79 -32
  122. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +22 -4
  123. package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +3 -4
  124. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  125. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +6 -6
  126. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -5
  127. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +29 -1
  128. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +11 -2
  129. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -3
  130. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  131. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +4 -4
  132. package/zova/src/suite-vendor/a-zova/package.json +3 -3
  133. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +0 -34
  134. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +0 -24
  135. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +0 -16
  136. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +0 -57
  137. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +0 -31
  138. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +0 -13
@@ -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
- - upload-token based upload through the generated image API
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
- - upload-token based upload
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 backend upload-token verification remain the true policy boundary
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-token flow, and DTO-side image view resolution.
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 calls `scope.api.image.createUploadToken(...)`
320
- 6. the frontend uploads multipart data with `token` and `image`
321
- 7. the stored field value is updated to the uploaded image ID or image ID array
322
- 8. the resolved relation field is synchronized so the form can preview the uploaded image immediately
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
- That last step is easy to miss, but it is one of the most important behaviors in the current implementation.
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:
@@ -129,7 +129,7 @@ The `a-model` source exposes five main state families through `BeanModelBase` he
129
129
  - `mem` → in-memory state through `$useStateMem(...)`
130
130
  - `local` → local-storage state through `$useStateLocal(...)`
131
131
  - `cookie` → cookie-backed state through `$useStateCookie(...)`
132
- - `db` → async persisted state through `$useStateDb(...)`
132
+ - `db` → async persisted state through `$useStateLocalAsync(...)`
133
133
 
134
134
  This is why Zova Model is better understood as a **unified model-state layer** rather than only a remote-data wrapper.
135
135
 
@@ -86,7 +86,34 @@ The important point is architectural:
86
86
 
87
87
  ## The main helper families
88
88
 
89
- The current source organizes model state around these helper families.
89
+ The current source organizes model state around these helper families. They share model-owned query-key and cache infrastructure, but differ in the surface returned to callers and in their persistence and restore lifecycle.
90
+
91
+ ### Choose the state helper by ownership and lifecycle
92
+
93
+ Choose a helper by the state surface, persistence, restore, and SSR behavior it needs, not only by whether its value should survive a reload.
94
+
95
+ | Helper | Use it when | Caller-facing surface | Persistence and restore |
96
+ | -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
97
+ | `$useStateData(...)` | The model owns remote or query-style async state and consumers need query status | Full query wrapper, including `data`, `error`, and readiness/status state | Normal query lifecycle; may participate in query hydration subject to metadata |
98
+ | `$useStateMem(...)` | The model owns reusable runtime state without a browser persistence backend | Assignable state value | No browser persistence; eligible successful state can transfer through initial SSR hydration |
99
+ | `$useStateLocal(...)` | The model owns a small browser value that should restore synchronously | Assignable state value | Synchronous `localStorage` restore |
100
+ | `$useStateCookie(...)` | The state needs cookie persistence or request-aware handling | Assignable state value | Synchronous cookie restore |
101
+ | `$useStateLocalAsync(...)` | The model owns longer-lived browser-local state whose restore may be asynchronous | Assignable state value | Asynchronous `localforage` restore |
102
+ | `$useStateComputed(...)` | The model exposes derived state | Computed value | No query fetch or persistence |
103
+
104
+ ### Data, local, and async-local behavior
105
+
106
+ `$useStateData(...)`, `$useStateLocal(...)`, and `$useStateLocalAsync(...)` use the same model-owned cache foundation, but they are not interchangeable.
107
+
108
+ | Concern | `$useStateData(...)` | `$useStateLocal(...)` | `$useStateLocalAsync(...)` |
109
+ | -------------------- | --------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
110
+ | Return surface | A memoized query wrapper | An assignable state value | An assignable state value |
111
+ | Source or storage | `queryFn`, query cache, and query configuration | `localStorage` | `localforage` |
112
+ | Initial availability | Follows the query lifecycle | Restores synchronously, then falls back to `meta.defaultData` | Restores asynchronously, then falls back to `meta.defaultData` |
113
+ | Intended write flow | Mutation, cache update, and invalidation policy | Replace or assign the model field | Replace or assign the model field |
114
+ | SSR behavior | Can follow ordinary query dehydration and hydration rules | Browser storage is unavailable on the server | Browser storage is unavailable on the server and async-local state explicitly opts out of dehydration |
115
+
116
+ Use the detailed [`$useStateData` Best Practices](/frontend/use-state-data-best-practices) guide for render-versus-interaction placement, `disableSuspenseOnInit`, freshness utilities, and explicit readiness boundaries.
90
117
 
91
118
  ### 1. `$useStateData(...)`
92
119
 
@@ -112,6 +139,18 @@ findAll() {
112
139
  }
113
140
  ```
114
141
 
142
+ ### Query readiness utilities: loaded versus fresh
143
+
144
+ These utilities consume an existing model-owned `$useStateData(...)` query. They do not create another state category or a second fetch lifecycle:
145
+
146
+ - `$QueryEnsureLoaded(...)` is an availability utility. It waits only when the query has no `data`, then returns the query wrapper.
147
+ - `$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.
148
+ - `$QueryEnsureFresh(...)` is an awaited freshness gate. It waits for stale data to refresh, returns the result, and propagates query errors.
149
+
150
+ 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.
151
+
152
+ For render-versus-interaction placement guidance, see [`$useStateData` Best Practices](/frontend/use-state-data-best-practices).
153
+
115
154
  ## 2. `$useMutationData(...)`
116
155
 
117
156
  Use this for model-owned mutation state.
@@ -143,16 +182,62 @@ The pattern to notice is that the model owns both the mutation and the follow-up
143
182
 
144
183
  ## 3. `$useStateMem(...)`
145
184
 
146
- Use this when the state should stay only in memory.
185
+ Use this for reusable runtime state that belongs to a Model but does not need a browser persistence backend. It returns an assignable value, rather than a query wrapper, while retaining Model-owned Query Cache identity.
147
186
 
148
187
  Current-source characteristics:
149
188
 
150
- - no persistence
151
- - `enabled: false`
189
+ - `enabled: false`, so it does not define a normal remote-fetch lifecycle
152
190
  - `staleTime: Infinity`
153
- - still uses model-owned query cache semantics under the hood
191
+ - `meta.persister: false`, so it does not read or write `localStorage`, cookies, or `localforage`
192
+ - reads from existing Query Cache first, then `meta.defaultData`, then `undefined`
193
+ - assignments update the Query Cache without a persister save
194
+ - automatic key prefixing isolates the state by Model identity and, when enabled, selector identity
195
+
196
+ This is useful when several consumers or Model methods need the same business state during the current frontend runtime, but the browser should not restore it from storage. For structured values, replace or assign the Model field so its custom-ref setter updates the Query Cache:
197
+
198
+ ```typescript
199
+ this.selectedIds = nextSelectedIds;
200
+ ```
201
+
202
+ A plain Controller field is usually clearer when the state belongs only to that one Controller. Choose `$useStateLocal(...)`, `$useStateCookie(...)`, or `$useStateLocalAsync(...)` when browser persistence is part of the requirement. Choose `$useStateData(...)` when consumers need query status, errors, freshness, or a remote-fetch lifecycle.
203
+
204
+ ### SSR transfer is not persistence
205
+
206
+ `meta.persister: false` means no browser persistence backend. It does not, by itself, exclude the state from the initial SSR server-to-client Query Cache handoff.
207
+
208
+ After server rendering, a successful eligible memory-state query can be dehydrated with the shared Query Cache. During client SSR pre-hydration, the snapshot is hydrated into the client QueryClient. A later `$useStateMem(...)` read can therefore receive the transferred value directly from cache when its effective key matches: the same Model, the same selector when selector mode is enabled, and the same logical `queryKey`.
209
+
210
+ This transfer has limits:
211
+
212
+ - it applies only to entries accepted by the normal dehydration policy; `meta.ssr.dehydrate: false` opts a state out
213
+ - it is not a replacement for `localStorage`, cookies, or async-local persistence
214
+ - it does not make the value survive a later browser reload without a new SSR handoff
215
+ - it does not make an empty or unsuccessful cache entry transfer automatically
216
+
217
+ The router-tabs Model uses memory state for current navigation keys and for tabs when tab caching is disabled. The page-data Model uses memory slots keyed by page path. Read [SSR Init Data](/frontend/ssr-init-data) for the page-side flow, or [A-Model Under the Hood](/frontend/a-model-under-the-hood) for the Query Cache and dehydration mechanics.
218
+
219
+ ### SSR/CSR bridge: `ModelPassport`
154
220
 
155
- This is useful when you want model ownership and query-key identity without local persistence.
221
+ `ModelPassport` shows that the state helper selected on the server and the one selected on the client do not have to be the same. Its application initialization creates the same logical state differently by environment:
222
+
223
+ ```typescript
224
+ this.passport = process.env.CLIENT
225
+ ? this.$useStateLocal({ queryKey: ['passport'] })
226
+ : this.$useStateMem({ queryKey: ['passport'] });
227
+ ```
228
+
229
+ On the SSR server, authenticated work can assign the current passport to the memory-backed Query Cache entry. If that entry is successful and eligible, it is dehydrated with the server QueryClient. During client SSR pre-hydration, the snapshot restores the entry before the client Model consumes it.
230
+
231
+ The client then creates a `$useStateLocal(...)` wrapper for the same effective key. Its read order is Query Cache first, then `localStorage`, then `meta.defaultData`. The hydrated server passport therefore wins on the initial client read; synchronous local-storage restoration is a fallback only when the transferred cache entry is absent.
232
+
233
+ This gives authentication state two complementary properties:
234
+
235
+ - SSR uses the request-confirmed passport to keep the initial HTML and client hydration consistent.
236
+ - CSR-only entry and later browser sessions can restore the client-local passport value from `localStorage`.
237
+
238
+ The server `$useStateMem(...)` entry is the entry that is dehydrated. The client `$useStateLocal(...)` call is a new client-side wrapper that reuses hydrated cache because its effective key matches. Matching requires the same Model, the same selector when selector mode is enabled, and the same logical `queryKey`; the short logical key alone is not global identity.
239
+
240
+ Reading the hydrated value does not save it back to `localStorage`. A later assignment through the client local-state wrapper updates Query Cache and writes the local-storage record. This is an initial SSR cache handoff plus client persistence fallback, not automatic persistence of the SSR snapshot.
156
241
 
157
242
  ## 4. `$useStateLocal(...)`
158
243
 
@@ -183,9 +268,9 @@ Current-source characteristics:
183
268
 
184
269
  This is particularly relevant for state that must participate in request-aware or SSR-adjacent flows.
185
270
 
186
- ## 6. `$useStateDb(...)`
271
+ ## 6. `$useStateLocalAsync(...)`
187
272
 
188
- Use this when the model state should persist asynchronously in db-style client storage.
273
+ Use this when the model state should persist asynchronously in browser-local storage backed by `localforage`.
189
274
 
190
275
  Current-source characteristics:
191
276
 
@@ -193,10 +278,40 @@ Current-source characteristics:
193
278
  - `enabled: false`
194
279
  - `staleTime: Infinity`
195
280
  - explicit `ssr.dehydrate = false`
196
- - getters may need async restore flow before data is available
281
+ - the first unresolved read may need async restore before the state is available
197
282
 
198
283
  This is useful for larger persisted client state that should outlive a page session without being stored in cookies or plain local storage.
199
284
 
285
+ ### Initialize and ensure async-local state
286
+
287
+ Assign the state property before awaiting it. When later initialization depends on the restored value, establish an explicit restore boundary with `$ensureStateLocalAsync(...)`. It waits for the pending initial restore to settle; it does not force another load and does not guarantee a defined result:
288
+
289
+ ```typescript
290
+ protected async __init__() {
291
+ this.tabs = this.$useStateLocalAsync({
292
+ queryKey: ['tabs'],
293
+ meta: {
294
+ defaultData: [],
295
+ },
296
+ });
297
+
298
+ await this.$ensureStateLocalAsync(this.tabs);
299
+
300
+ // Continue only after persisted tabs, or the fallback default, is available.
301
+ this.initializeTabs();
302
+ }
303
+ ```
304
+
305
+ `meta.defaultData` is a fallback: it initializes the state only when no persisted value is restored. If neither provides a value, the ensured result can still be `undefined`. The router-tabs model follows this assign-then-ensure sequence before it continues with route initialization.
306
+
307
+ Persisted local and async-local state should be updated by replacing or assigning the model field so its custom-ref setter can update query state and persist the new value:
308
+
309
+ ```typescript
310
+ this.tabs = nextTabs;
311
+ ```
312
+
313
+ Do not rely on an in-place nested mutation alone to persist the change. Assigning `undefined` removes the persisted value.
314
+
200
315
  ## 7. `$useStateComputed(...)`
201
316
 
202
317
  Use this when the model should expose derived state that is still model-key-oriented but computed locally.
@@ -240,16 +355,17 @@ Model state choices can affect SSR behavior.
240
355
 
241
356
  Current-source behaviors to remember:
242
357
 
243
- - query state can be dehydrated on server and hydrated on client
358
+ - successful eligible Query Cache state can be dehydrated on the server and hydrated on the client
359
+ - `$useStateMem(...)` has no browser persister but can participate in that initial SSR handoff unless excluded by metadata
244
360
  - mutations are not dehydrated
245
- - `db` state is explicitly not dehydrated
246
- - server cannot use local-storage or db persistence backends
247
- - cookie state is special because cookie access can still exist through the app cookie surface
361
+ - `$useStateLocalAsync(...)` explicitly opts out of dehydration
362
+ - server-side sync local and cookie persister-backed Query Cache entries are excluded by the default dehydration policy; this does not prevent a client `$useStateLocal(...)` or `$useStateCookie(...)` wrapper from reusing a hydrated entry that the server created with a different eligible helper
363
+ - server cannot use local-storage or async-local persistence backends; cookie state is special because cookie access can still exist through the app cookie surface
248
364
 
249
365
  Practical implication:
250
366
 
251
- - do not assume every persisted model state behaves the same during SSR
252
- - choose `mem`, `local`, `cookie`, `db`, or `data` based on ownership and hydration requirements, not only convenience
367
+ - do not equate browser persistence with SSR transfer, or assume every state family behaves the same during SSR
368
+ - choose `mem`, `local`, `cookie`, `localAsync`, or `data` based on ownership, persistence, and hydration requirements, not only convenience
253
369
 
254
370
  ## Real examples to read
255
371
 
@@ -277,7 +393,7 @@ This file shows:
277
393
 
278
394
  - `@Model({ enableSelector: true, ... })`
279
395
  - richer model initialization in `__init__`
280
- - mixed use of `$useStateMem(...)` and `$useStateDb(...)`
396
+ - mixed use of `$useStateMem(...)` and `$useStateLocalAsync(...)`
281
397
  - model-owned tab state with cache and persistence decisions
282
398
 
283
399
  ### SSR-sensitive auth model
@@ -31,7 +31,18 @@ export class ControllerPageTodo {
31
31
  1. a model bean encapsulates the data access path
32
32
  2. the controller injects the model
33
33
  3. `__init__` prepares the data on the server
34
- 4. model-based SSR support synchronizes that data to the client and completes hydration automatically
34
+ 4. successful eligible Query Cache state is captured in the SSR handoff
35
+ 5. client SSR pre-hydration restores that cache before the client model consumes it
36
+
37
+ ## What hydration reuses
38
+
39
+ The initial SSR handoff reuses model Query Cache data, not only `$useStateData(...)` results. A successful eligible `$useStateMem(...)` value can also transfer from the server to the initial client runtime even though it has no browser persistence backend.
40
+
41
+ Client reuse requires the model call to resolve to the same effective query key: Zova prefixes the logical key with Model identity and, for selector-enabled models, selector identity. State marked with `meta.ssr.dehydrate: false` is excluded. This handoff does not make memory state durable across a later browser reload, and it does not guarantee that every client query avoids fetching.
42
+
43
+ `ModelPassport` is a concrete SSR/CSR bridge: the server creates `passport` with `$useStateMem({ queryKey: ['passport'] })`, while the client creates `$useStateLocal({ queryKey: ['passport'] })`. The successful eligible server memory entry is what SSR transfers. Because the client local-state wrapper reads Query Cache before `localStorage`, the hydrated passport supplies the initial client value when the effective key matches. If no transferred entry exists, `$useStateLocal(...)` can instead restore its browser-local value. Hydration alone does not save the transferred value to `localStorage`; later client-side assignments do.
44
+
45
+ For helper selection and the complete passport flow, read [Model State Guide](/frontend/model-state-guide). For the dehydration filter and QueryClient lifecycle, read [A-Model Under the Hood](/frontend/a-model-under-the-hood).
35
46
 
36
47
  ## Implementation checks for SSR data-loading changes
37
48
 
@@ -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.102",
3
+ "version": "5.1.104",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -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": [