cabloy 5.1.98 → 5.1.99

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 (139) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/hooks/contract-loop-gate.ts +151 -44
  3. package/CHANGELOG.md +21 -0
  4. package/CLAUDE.md +2 -0
  5. package/cabloy-docs/.vitepress/config.mjs +4 -0
  6. package/cabloy-docs/backend/controller-aop-guide.md +0 -1
  7. package/cabloy-docs/frontend/a-openapi-under-the-hood.md +1 -1
  8. package/cabloy-docs/frontend/generated-contract-consumption-entry-branch.md +1 -1
  9. package/cabloy-docs/frontend/generated-contract-consumption-list-branch.md +1 -1
  10. package/cabloy-docs/frontend/generated-contract-consumption-verify-playbook.md +1 -1
  11. package/cabloy-docs/frontend/model-resource-owner-pattern.md +1 -1
  12. package/cabloy-docs/frontend/model-state-guide.md +3 -1
  13. package/cabloy-docs/frontend/resource-entry-page-deep-dive.md +2 -2
  14. package/cabloy-docs/frontend/resource-list-page-deep-dive.md +1 -1
  15. package/cabloy-docs/frontend/rest-resource-source-reading-map.md +2 -2
  16. package/cabloy-docs/frontend/rest-resource-under-the-hood.md +4 -4
  17. package/cabloy-docs/frontend/use-state-data-best-practices.md +384 -0
  18. package/package.json +1 -1
  19. package/scripts/upgrade.ts +2 -0
  20. package/vona/packages-cli/cli/package.json +1 -1
  21. package/vona/packages-cli/cli-set-api/package.json +1 -1
  22. package/vona/packages-vona/vona/package.json +1 -1
  23. package/vona/packages-vona/vona-core/package.json +1 -1
  24. package/vona/packages-vona/vona-core/src/lib/bean/beanBase.ts +9 -9
  25. package/vona/packages-vona/vona-mock/package.json +1 -1
  26. package/vona/pnpm-lock.yaml +4 -4
  27. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +22 -0
  28. package/vona/src/suite/a-training/modules/training-record/src/bean/meta.version.ts +1 -0
  29. package/vona/src/suite/a-training/modules/training-record/src/config/locale/en-us.ts +1 -0
  30. package/vona/src/suite/a-training/modules/training-record/src/config/locale/zh-cn.ts +1 -0
  31. package/vona/src/suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts +47 -0
  32. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +13 -0
  33. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +13 -0
  34. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +16 -5
  35. package/vona/src/suite/a-training/modules/training-record/src/index.ts +2 -0
  36. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +1 -0
  37. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +34 -0
  38. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +209 -123
  39. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  40. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  41. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +18 -1
  42. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -2
  43. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +1 -0
  44. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +5 -3
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +28 -0
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +8 -0
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyRequest.ts +15 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +30 -0
  49. package/vona/src/suite-vendor/a-file/package.json +2 -2
  50. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  51. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +27 -0
  52. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +8 -0
  53. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyRequest.ts +15 -0
  54. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +27 -0
  55. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +1 -1
  56. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +1 -1
  57. package/vona/src/suite-vendor/a-image/package.json +1 -1
  58. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  59. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  60. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/.metadata/index.ts +24 -9
  61. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +87 -73
  62. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionActionByRoles.ts +27 -0
  63. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionUser.ts +20 -0
  64. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +4 -3
  66. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  67. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +5 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-user/src/.metadata/index.ts +0 -13
  70. package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts +0 -7
  71. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-version/src/bean/meta.version.ts +2 -24
  73. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  74. package/zova/packages-utils/zova-jsx/package.json +2 -2
  75. package/zova/packages-zova/zova/package.json +3 -3
  76. package/zova/packages-zova/zova-core/package.json +1 -1
  77. package/zova/packages-zova/zova-core/src/bean/beanBase.ts +9 -13
  78. package/zova/pnpm-lock.yaml +29 -14
  79. package/zova/src/suite/a-demo/modules/demo-basic/src/page/toolOne/controller.tsx +2 -2
  80. package/zova/src/suite/a-demo/modules/demo-todo/src/page/todo/controller.tsx +2 -2
  81. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +2 -2
  82. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +2 -2
  83. package/zova/src/suite/a-home/modules/home-login/src/page/login/controller.tsx +2 -2
  84. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockDetails/controller.tsx +5 -1
  85. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockTable/controller.tsx +20 -15
  86. package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +6 -1
  87. package/zova/src/suite/cabloy-basic/modules/basic-file/cli/openapi.config.ts +9 -0
  88. package/zova/src/suite/cabloy-basic/modules/basic-file/package.json +58 -0
  89. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/component/formFieldFile.ts +31 -0
  90. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +222 -0
  91. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/locales.ts +7 -0
  92. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/this.ts +2 -0
  93. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +121 -0
  94. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/baseURL.ts +5 -0
  95. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/index.ts +3 -0
  96. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +306 -0
  97. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +5718 -0
  98. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +40 -0
  99. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +187 -0
  100. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +534 -0
  101. package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/en-us.ts +15 -0
  102. package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/zh-cn.ts +15 -0
  103. package/zova/src/suite/cabloy-basic/modules/basic-file/src/index.ts +4 -0
  104. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +74 -0
  105. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/index.ts +1 -0
  106. package/zova/src/suite/cabloy-basic/modules/basic-file/src/model/file.ts +23 -0
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/file.ts +12 -0
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/index.ts +1 -0
  109. package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.build.json +13 -0
  110. package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.json +5 -0
  111. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +61 -0
  112. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +38 -1
  113. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +31 -0
  114. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +52 -0
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +614 -1
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +10 -0
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +13 -0
  118. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +126 -53
  119. package/zova/src/suite/cabloy-basic/modules/basic-image/src/model/image.ts +23 -0
  120. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +44 -4
  121. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +17 -12
  122. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +2 -2
  123. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +2 -2
  124. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/controller.tsx +7 -2
  125. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +34 -33
  126. package/zova/src/suite/cabloy-basic/package.json +1 -0
  127. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  128. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +5 -3
  129. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +2 -2
  130. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resource/controller.tsx +2 -2
  131. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  132. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  133. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +17 -17
  134. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  135. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +8 -3
  136. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  137. package/zova/src/suite-vendor/a-zova/package.json +4 -4
  138. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permission.ts +0 -17
  139. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.userName.ts +0 -35
@@ -0,0 +1,384 @@
1
+ # `$useStateData` Best Practices
2
+
3
+ This guide explains how to use `$useStateData(...)` well in Zova, especially when you are deciding whether a piece of async frontend data should behave like render-time state or interaction-time ad hoc fetch logic.
4
+
5
+ Read [Model Architecture](/frontend/model-architecture) first if you want the broader role of Model.
6
+
7
+ Read [Model State Guide](/frontend/model-state-guide) first if you want the larger helper-family overview.
8
+
9
+ Use this page when your real design question is one of these:
10
+
11
+ - should this async value live in a model query or stay inside one controller?
12
+ - should the query be established during render or only during interaction?
13
+ - should I use `enabled: false` or let the query participate in the normal lifecycle?
14
+ - how should I think about `disableSuspenseOnInit: true`?
15
+ - if I remove a manual `ensure...()` helper, can interaction still wait for query readiness when needed?
16
+
17
+ ## Why this page exists
18
+
19
+ `$useStateData(...)` is easy to misuse if it is treated only as a request helper.
20
+
21
+ The larger value is architectural:
22
+
23
+ - model-owned async state gets one stable owner
24
+ - query identity, persistence, restore, and refetch behavior stay in one place
25
+ - render logic can consume a formal query state instead of rebuilding mini loading/cache systems in controllers
26
+
27
+ The common failure mode is not that the code stops working immediately.
28
+
29
+ The common failure mode is that the code keeps working while the state model becomes increasingly command-oriented, duplicated, and difficult to reason about.
30
+
31
+ ## The core mental model
32
+
33
+ The best starting point is simple:
34
+
35
+ > treat `$useStateData(...)` as a model-owned async state source, not only as a function that happens to fetch data.
36
+
37
+ That changes several downstream decisions.
38
+
39
+ If a value affects:
40
+
41
+ - whether a button can be clicked
42
+ - accepted file types
43
+ - maximum upload size
44
+ - whether a UI branch is shown
45
+ - whether a flow can continue safely
46
+
47
+ then that value is usually not just a click-time fetch result.
48
+ It is part of the page's real interaction state.
49
+
50
+ ## Practical rule 1: prefer model ownership for reusable async state
51
+
52
+ When a value has broader reuse, persistence, or lifecycle requirements, prefer model ownership.
53
+
54
+ Typical good fits are:
55
+
56
+ - upload policy by scene
57
+ - resource bootstrap
58
+ - schema metadata
59
+ - permission/capability state
60
+ - query-style domain state reused across pages or components
61
+
62
+ Typical weak fits are:
63
+
64
+ - a one-off page-local toggle
65
+ - a tiny local interaction detail with no reuse value
66
+ - temporary page-only state that does not benefit from caching or restore semantics
67
+
68
+ ### Review question
69
+
70
+ Ask:
71
+
72
+ > if another controller needed this same async value tomorrow, would I want the ownership and lifecycle policy to already exist in one model?
73
+
74
+ If the answer is yes, that state usually belongs in a model.
75
+
76
+ ## Practical rule 2: if the value affects render or interaction rules, establish the query during render
77
+
78
+ If a value determines runtime interaction conditions, prefer this shape:
79
+
80
+ 1. model exposes `getXxx(...)`
81
+ 2. controller/page establishes the query during render
82
+ 3. render consumes `data`, `pending`, and derived values
83
+ 4. interaction consumes that existing query state instead of inventing a second lifecycle
84
+
85
+ ### Why this is good
86
+
87
+ This keeps the UI honest.
88
+
89
+ The rendered UI can already know:
90
+
91
+ - whether interaction should wait
92
+ - which accept/multiple flags apply
93
+ - which validation path should be used
94
+
95
+ ### Smell
96
+
97
+ A controller renders as though the interaction is ready, but clicking the button is what really begins the important state lookup.
98
+
99
+ That is a sign that a query-state concern may have been pushed too far into imperative interaction code.
100
+
101
+ ## Practical rule 3: `disableSuspenseOnInit: true` disables the init-time `query.suspense()` kick, not the query itself
102
+
103
+ This point needs precise wording.
104
+
105
+ Current model runtime shows that `$useStateData(...)` creates the query wrapper first, then, only on first creation, optionally calls `query.suspense()`:
106
+
107
+ ```ts
108
+ if (!this[SymbolUseQueries][queryHash]) {
109
+ const useQuery = this.$useQuery(options, queryClient);
110
+ this[SymbolUseQueries][queryHash] = useQuery;
111
+ if (!options.meta?.disableSuspenseOnInit) {
112
+ useQuery.suspense();
113
+ }
114
+ }
115
+ ```
116
+
117
+ Two details matter:
118
+
119
+ - the query is already created either way
120
+ - the runtime does **not** use `await` there
121
+
122
+ So `disableSuspenseOnInit: true` does **not** mean “block vs do not block render” in a strict synchronous sense.
123
+
124
+ It more precisely means:
125
+
126
+ - with default behavior, the first consumer of that query immediately kicks one init-time `query.suspense()` call
127
+ - with `disableSuspenseOnInit: true`, that automatic init-time kick is skipped
128
+
129
+ This option is often a good fit when you want:
130
+
131
+ - query state to exist immediately
132
+ - persistence/restore/refetch behavior to remain normal
133
+ - no automatic init-time `query.suspense()` kick on first creation
134
+
135
+ Representative pattern:
136
+
137
+ ```ts
138
+ getUploadPolicy(imageScene?: string) {
139
+ if (!imageScene) return undefined;
140
+ return this.$useStateData({
141
+ queryKey: ['uploadPolicy', 'image', imageScene],
142
+ queryFn: async () => {
143
+ return this.scope.api.image.getUploadPolicy({ imageScene });
144
+ },
145
+ meta: {
146
+ disableSuspenseOnInit: true,
147
+ },
148
+ });
149
+ }
150
+ ```
151
+
152
+ ### What it does not mean
153
+
154
+ It does not mean:
155
+
156
+ - no query should be created
157
+ - no request should run
158
+ - no persisted value should be restored
159
+ - no refetch should happen later
160
+ - interaction can never explicitly wait for query readiness
161
+
162
+ That distinction matters a lot.
163
+
164
+ ## Practical rule 4: distinguish three different semantics
165
+
166
+ A useful review habit is to keep these three behaviors separate.
167
+
168
+ | Case | What happens | What it does **not** mean | Best fit |
169
+ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
170
+ | Default `$useStateData(...)` | On first query creation, runtime immediately calls `query.suspense()` once | It does **not** synchronously `await` readiness in that call site | Data where “first consumer should eagerly kick one refresh/update semantic” is a good default |
171
+ | `$useStateData(..., { meta: { disableSuspenseOnInit: true } })` | Query still exists, but the automatic init-time `query.suspense()` kick is skipped | It does **not** disable the query, persistence, restore, or later refetch behavior | Data that is relatively stable inside the current frontend process and does not need an automatic first-consumer kick every time |
172
+ | `await query.suspense()` | Current logic sequence explicitly waits until the query is ready | It is **not** merely a hint or passive kickoff | Interaction or orchestration boundaries where later logic must not continue until the value is ready |
173
+
174
+ Two practical consequences follow from this table:
175
+
176
+ 1. multiple synchronous `query.suspense()` calls can still deduplicate at the query runtime level
177
+ 2. only explicit `await query.suspense()` introduces a true sequencing guarantee for the current logic path
178
+
179
+ ## Practical rule 5: be cautious with `enabled: false` and `staleTime: Infinity`
180
+
181
+ These options are useful, but they are easy to over-apply.
182
+
183
+ ### `enabled: false`
184
+
185
+ Use it when the query should not exist yet because a real prerequisite is missing.
186
+
187
+ Good examples:
188
+
189
+ - required params are absent
190
+ - the page is in a branch where the query truly does not apply
191
+ - the query would be meaningless or invalid before some explicit condition
192
+
193
+ Be cautious when the real motivation is only:
194
+
195
+ - “I want to fetch later when the user clicks”
196
+
197
+ That often means you are taking a state concern and turning it back into a command concern.
198
+
199
+ ### `staleTime: Infinity`
200
+
201
+ Use it when the value is effectively static for the relevant session.
202
+
203
+ Be cautious when the underlying backend configuration may change.
204
+
205
+ If backend scene config can change, letting the query participate in normal freshness/refetch rules is usually healthier than freezing it forever.
206
+
207
+ ## Practical rule 6: removing a manual `ensure...()` helper does not mean interaction can never wait
208
+
209
+ A common over-correction is:
210
+
211
+ - first version: controller manually owns fetching through `ensure...()`
212
+ - second version: remove `ensure...()` and assume interaction must never wait for anything
213
+
214
+ The better middle ground is:
215
+
216
+ - query lifecycle remains model-owned
217
+ - query is established during render
218
+ - interaction consumes the existing query
219
+ - if an edge timing window appears, interaction may wait for the existing query to finish
220
+
221
+ That is very different from rebuilding a second fetch lifecycle.
222
+
223
+ ## Practical rule 7: derive render-time state once per render when possible
224
+
225
+ Even when the query object is reused, a controller can still become noisy if it repeatedly derives the same values in several helper calls.
226
+
227
+ A good cleanup pattern is to derive one local state object such as:
228
+
229
+ - `acceptAttr`
230
+ - `multiple`
231
+ - `pending`
232
+
233
+ and let input/button/render branches consume that shared result.
234
+
235
+ ### Why this helps
236
+
237
+ It makes one render pass easier to understand:
238
+
239
+ - what query-backed state is being consumed
240
+ - which UI props come from that state
241
+ - where the interaction gate really is
242
+
243
+ ### Smell
244
+
245
+ The controller repeatedly:
246
+
247
+ - grabs the same query
248
+ - re-reads the same policy data
249
+ - re-derives the same `multiple` or `accept` value in several places
250
+
251
+ That is often a sign that local render-time derivation should be consolidated.
252
+
253
+ ## Practical rule 8: design query keys so the ownership boundary is obvious
254
+
255
+ Prefer keys that make the domain explicit.
256
+
257
+ For example:
258
+
259
+ - `['uploadPolicy', 'image', imageScene]`
260
+ - `['uploadPolicy', 'file', fileScene]`
261
+
262
+ This is usually better than ambiguous cross-domain shapes such as:
263
+
264
+ - `['uploadPolicy', scene]`
265
+
266
+ ### Why this matters
267
+
268
+ Readable keys improve:
269
+
270
+ - debugging
271
+ - tracing cache behavior
272
+ - future refactors when similar query families grow
273
+
274
+ ## Anti-pattern: command-first controller state
275
+
276
+ A common anti-pattern looks like this:
277
+
278
+ - model query is disabled by default
279
+ - controller clicks trigger `ensure...()`
280
+ - controller maintains the important timing logic itself
281
+ - render does not fully know the interaction conditions yet
282
+
283
+ ### Why this is harmful
284
+
285
+ - the model no longer owns the full lifecycle cleanly
286
+ - restore/refetch semantics are weakened or bypassed
287
+ - controller responsibilities grow too large
288
+ - interaction rules become harder to inspect from the render path
289
+
290
+ ## Better pattern: model-owned query, render-owned derivation, interaction-owned orchestration
291
+
292
+ A healthier split is:
293
+
294
+ ### Model owns
295
+
296
+ - `queryKey`
297
+ - `queryFn`
298
+ - persistence/restore/refetch lifecycle
299
+ - the stable meaning of the async state
300
+
301
+ ### Controller/page owns
302
+
303
+ - how the query-backed state is rendered
304
+ - derived props needed by the current UI
305
+ - user interaction flow
306
+ - edge-timing waits on the already-existing query when necessary
307
+
308
+ This creates a much cleaner division of responsibilities.
309
+
310
+ ## Representative examples
311
+
312
+ ### Example A: resource entry pages usually prefer the default init-time kick
313
+
314
+ A good counterexample to overusing `disableSuspenseOnInit: true` is the resource entry-page branch.
315
+
316
+ Useful reading pages are:
317
+
318
+ - [Resource Entry Page Deep Dive](/frontend/resource-entry-page-deep-dive)
319
+ - [ModelResource Internals Deep Dive](/frontend/model-resource-internals-deep-dive)
320
+
321
+ In that branch, the practical expectation is often:
322
+
323
+ - opening a row page may show an already-known value first
324
+ - but entering that page should still eagerly kick one update semantic for the item-level state
325
+ - the page benefits from the default “first consumer kicks `query.suspense()` once” behavior
326
+
327
+ This makes entry pages a good mental model for queries where the default init-time kick is healthy:
328
+
329
+ - the query is a real page-state dependency
330
+ - the first consumer should actively nudge freshness
331
+ - you usually do not want every caller to remember to add that kickoff manually
332
+
333
+ This is not about synchronous blocking.
334
+ It is about preserving the default eager-init semantic when the first consumer of that page state appears.
335
+
336
+ ### Example B: upload policy prefers `disableSuspenseOnInit: true`
337
+
338
+ Representative files in the Cabloy Basic frontend:
339
+
340
+ - `zova/src/suite/cabloy-basic/modules/basic-image/src/model/image.ts`
341
+ - `zova/src/suite/cabloy-basic/modules/basic-file/src/model/file.ts`
342
+ - `zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx`
343
+ - `zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx`
344
+
345
+ ### What this example demonstrates
346
+
347
+ - `getUploadPolicy(...)` stays in the model
348
+ - the query is created as formal state instead of a one-off click helper
349
+ - `disableSuspenseOnInit: true` skips the automatic init-time `query.suspense()` kick while preserving the query as formal state
350
+ - render derives `acceptAttr`, `multiple`, and `pending`
351
+ - interaction reuses that state
352
+ - interaction may still `await query.suspense()` on the already-created query if an edge timing window requires it
353
+
354
+ Why this is a good fit:
355
+
356
+ - upload policy is a real query-backed state
357
+ - it is relatively stable within the current frontend process
358
+ - every first consumer does not need to auto-kick one eager init-time refresh semantic
359
+ - the strict-ready moment is the interaction boundary, not every initial render consumer
360
+
361
+ Taken together, Example A and Example B show the most important design contrast on this page:
362
+
363
+ - entry-page item state is a good example of why the default init-time kick can be valuable
364
+ - upload policy is a good example of why a stable state may still be a formal query while skipping that init-time kick
365
+
366
+ ## Checklist before you use `$useStateData(...)`
367
+
368
+ Ask these questions:
369
+
370
+ 1. Is this value really a reusable async state source rather than a one-off action result?
371
+ 2. Will this value affect render or interaction conditions?
372
+ 3. Should another page or controller be able to consume the same lifecycle and cache policy later?
373
+ 4. Is `disableSuspenseOnInit: true` enough, instead of disabling the query entirely?
374
+ 5. Would `enabled: false` or `staleTime: Infinity` accidentally cut off useful restore/refetch behavior?
375
+ 6. If interaction needs a safety wait, can it wait on the existing query rather than reintroducing a manual `ensure...()` lifecycle?
376
+ 7. Can I derive the render-time state once and reuse it across the UI branch?
377
+
378
+ ## Final takeaway
379
+
380
+ The most important usage insight is simple:
381
+
382
+ > `$useStateData(...)` is strongest when it models async state that the UI can render from, persist, restore, and refresh naturally.
383
+
384
+ When that mindset is clear, controllers become thinner, models become more authoritative, and query lifecycle decisions become easier to reason about.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.98",
3
+ "version": "5.1.99",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -50,6 +50,7 @@ const OVERWRITE_DIRS: string[] = [
50
50
  const MERGE_DIRS: string[] = [
51
51
  // Claude project assets
52
52
  '.claude/commands',
53
+ '.claude/hooks',
53
54
  '.claude/skills',
54
55
  // Vona Claude project assets
55
56
  'vona/.claude/commands',
@@ -67,6 +68,7 @@ const BLACKLIST_DIRS: string[] = [
67
68
  const WHITELIST_FILES: string[] = [
68
69
  // root
69
70
  'CLAUDE.md',
71
+ '.claude/settings.json',
70
72
  'tsconfig.json',
71
73
  'tsconfig.base.json',
72
74
  'tsconfig.base.esm.json',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.128",
3
+ "version": "1.1.129",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.126",
3
+ "version": "1.1.127",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.71",
3
+ "version": "5.1.72",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.31",
3
+ "version": "5.1.32",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -1,29 +1,29 @@
1
1
  import type winston from 'winston';
2
2
 
3
3
  import type { ILoggerChildRecord, ILoggerClientRecord } from '../../types/interface/logger.ts';
4
- import type { IModuleLocaleText } from './resource/locale/type.ts';
4
+ // import type { IModuleLocaleText } from './resource/locale/type.ts';
5
5
  import type { IBeanScopeContainer } from './scope/beanScopeContainer.ts';
6
6
 
7
7
  import { BeanBaseSimple, SymbolBeanInstanceKey, SymbolModuleBelong } from './beanBaseSimple.ts';
8
8
 
9
- const SymbolText = Symbol('SymbolText');
9
+ // const SymbolText = Symbol('SymbolText');
10
10
  const SymbolLogger = Symbol('SymbolLogger');
11
11
  const SymbolLoggerChildren = Symbol('SymbolLoggerChildren');
12
12
 
13
13
  export class BeanBase extends BeanBaseSimple {
14
- private [SymbolText]: IModuleLocaleText;
14
+ // private [SymbolText]: IModuleLocaleText;
15
15
  private [SymbolLogger]: Record<keyof ILoggerClientRecord, winston.Logger> = {} as any;
16
16
  private [SymbolLoggerChildren]: Record<
17
17
  keyof ILoggerClientRecord,
18
18
  Record<string, winston.Logger>
19
19
  > = {} as any;
20
20
 
21
- protected get $text(): IModuleLocaleText {
22
- if (!this[SymbolText]) {
23
- this[SymbolText] = this.app.meta.locale.createLocaleText(this[SymbolModuleBelong]);
24
- }
25
- return this[SymbolText];
26
- }
21
+ // protected get $text(): IModuleLocaleText {
22
+ // if (!this[SymbolText]) {
23
+ // this[SymbolText] = this.app.meta.locale.createLocaleText(this[SymbolModuleBelong]);
24
+ // }
25
+ // return this[SymbolText];
26
+ // }
27
27
 
28
28
  protected get $logger() {
29
29
  return this.$loggerClient('default');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.31",
3
+ "version": "6.1.32",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [
@@ -1517,13 +1517,13 @@ importers:
1517
1517
  src/suite-vendor/a-file:
1518
1518
  dependencies:
1519
1519
  vona-module-a-file:
1520
- specifier: ^5.0.1
1520
+ specifier: ^5.0.3
1521
1521
  version: link:modules/a-file
1522
1522
  vona-module-file-cloudflare:
1523
- specifier: ^5.0.1
1523
+ specifier: ^5.0.3
1524
1524
  version: link:modules/file-cloudflare
1525
1525
  vona-module-file-native:
1526
- specifier: ^5.0.1
1526
+ specifier: ^5.0.3
1527
1527
  version: link:modules/file-native
1528
1528
 
1529
1529
  src/suite-vendor/a-file/modules/a-file:
@@ -1625,7 +1625,7 @@ importers:
1625
1625
  src/suite-vendor/a-test:
1626
1626
  dependencies:
1627
1627
  vona-module-test-file:
1628
- specifier: ^5.0.1
1628
+ specifier: ^5.0.3
1629
1629
  version: link:modules/test-file
1630
1630
 
1631
1631
  src/suite-vendor/a-test/modules/test-auth:
@@ -0,0 +1,22 @@
1
+ import { BeanBase } from 'vona';
2
+ import { FileScene } from 'vona-module-a-file';
3
+
4
+ @FileScene({
5
+ upload: {
6
+ maxSize: 20 * 1024 * 1024,
7
+ mimeTypes: [
8
+ 'application/pdf',
9
+ 'application/msword',
10
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
11
+ 'application/vnd.ms-excel',
12
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
13
+ 'application/zip',
14
+ 'application/x-zip-compressed',
15
+ 'text/plain',
16
+ ],
17
+ extensions: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.zip', '.txt'],
18
+ multiple: true,
19
+ public: false,
20
+ },
21
+ })
22
+ export class FileSceneDossierFile extends BeanBase {}
@@ -18,6 +18,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
18
18
  table.decimal(entityRecord.averageScore, 8, 2).comment(entityRecord.$comment.averageScore);
19
19
  table.dateTime(entityRecord.trainingTime).comment(entityRecord.$comment.trainingTime);
20
20
  table.json(entityRecord.sceneImageIds).comment(entityRecord.$comment.sceneImageIds);
21
+ table.json(entityRecord.dossierFileIds).comment(entityRecord.$comment.dossierFileIds);
21
22
  table.string(entityRecord.description, 255).comment(entityRecord.$comment.description);
22
23
  });
23
24
  }
@@ -9,6 +9,7 @@ export default {
9
9
  TrainingRecordName: 'Training Record Name',
10
10
  TrainingTime: 'Training Time',
11
11
  ScenePhotos: 'Scene Photos',
12
+ DossierFiles: 'Dossier Files',
12
13
  AddTrainingRecord: 'Add Student Training Record',
13
14
  EditTrainingRecord: 'Edit Student Training Record',
14
15
  ViewTrainingRecord: 'View Student Training Record',
@@ -9,6 +9,7 @@ export default {
9
9
  TrainingRecordName: '培训记录名称',
10
10
  TrainingTime: '培训时间',
11
11
  ScenePhotos: '现场照片',
12
+ DossierFiles: '培训档案附件',
12
13
  AddTrainingRecord: '添加学生培训记录',
13
14
  EditTrainingRecord: '编辑学生培训记录',
14
15
  ViewTrainingRecord: '查看学生培训记录',
@@ -0,0 +1,47 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
3
+
4
+ import { Api, v } from 'vona-module-a-openapiutils';
5
+ import { Dto } from 'vona-module-a-web';
6
+ import z from 'zod';
7
+
8
+ export interface IDtoOptionsRecordDossierFileView extends IDecoratorDtoOptions {}
9
+
10
+ @Dto<IDtoOptionsRecordDossierFileView>()
11
+ export class DtoRecordDossierFileView {
12
+ @Api.field(v.tableIdentity())
13
+ id: TableIdentity;
14
+
15
+ @Api.field(z.string())
16
+ provider: string;
17
+
18
+ @Api.field(z.string())
19
+ clientName: string;
20
+
21
+ @Api.field(v.optional(), z.string())
22
+ fileScene?: string;
23
+
24
+ @Api.field(v.optional())
25
+ filename?: string;
26
+
27
+ @Api.field(v.optional())
28
+ contentType?: string;
29
+
30
+ @Api.field(v.optional())
31
+ size?: number;
32
+
33
+ @Api.field(v.optional())
34
+ public?: boolean;
35
+
36
+ @Api.field(v.optional())
37
+ uploadedAt?: Date;
38
+
39
+ @Api.field(v.optional(), z.record(z.string(), z.unknown()))
40
+ meta?: Record<string, unknown>;
41
+
42
+ @Api.field(z.string())
43
+ downloadUrl: string;
44
+
45
+ @Api.field(v.default(true))
46
+ signed: boolean;
47
+ }
@@ -7,7 +7,9 @@ import { Dto } from 'vona-module-a-web';
7
7
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
8
 
9
9
  import { $locale } from '../.metadata/locales.ts';
10
+ import { resolveDossierFiles } from '../lib/resolveDossierFiles.ts';
10
11
  import { ModelRecord } from '../model/record.ts';
12
+ import { DtoRecordDossierFileView } from './recordDossierFileView.ts';
11
13
 
12
14
  export interface IDtoOptionsRecordSelectResItem extends IDecoratorDtoOptions {}
13
15
 
@@ -37,6 +39,17 @@ export class DtoRecordSelectResItem extends $Dto.get(() => ModelRecord) {
37
39
  )
38
40
  sceneImages?: DtoImageView[];
39
41
 
42
+ @Api.field(
43
+ v.title($locale('DossierFiles')),
44
+ ZovaRender.visible(false),
45
+ v.optional(),
46
+ v.serializerTransform('a-serialization:custom', {
47
+ custom: resolveDossierFiles,
48
+ }),
49
+ v.array(DtoRecordDossierFileView),
50
+ )
51
+ dossierFiles?: DtoRecordDossierFileView[];
52
+
40
53
  @Api.field(
41
54
  v.title($locale('Operations')),
42
55
  ZovaRender.order(1, 'max'),
@@ -7,9 +7,11 @@ import { Dto } from 'vona-module-a-web';
7
7
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
8
 
9
9
  import { $locale } from '../.metadata/locales.ts';
10
+ import { resolveDossierFiles } from '../lib/resolveDossierFiles.ts';
10
11
  import { ModelRecord } from '../model/record.ts';
11
12
  import { DtoDetailRecordSubjectResItem } from './detailRecordSubjectResItem.tsx';
12
13
  import { DtoDetailRecordSubjectView } from './detailRecordSubjectView.tsx';
14
+ import { DtoRecordDossierFileView } from './recordDossierFileView.ts';
13
15
 
14
16
  export interface IDtoOptionsRecordView extends IDecoratorDtoOptions {}
15
17
 
@@ -49,6 +51,17 @@ export class DtoRecordView extends $Dto.get(() => ModelRecord, {
49
51
  )
50
52
  sceneImages?: DtoImageView[];
51
53
 
54
+ @Api.field(
55
+ v.title($locale('DossierFiles')),
56
+ ZovaRender.visible(false),
57
+ v.optional(),
58
+ v.serializerTransform('a-serialization:custom', {
59
+ custom: resolveDossierFiles,
60
+ }),
61
+ v.array(DtoRecordDossierFileView),
62
+ )
63
+ dossierFiles?: DtoRecordDossierFileView[];
64
+
52
65
  @Api.field(ZovaRender.visible(false), v.optional(), v.array(DtoDetailRecordSubjectResItem))
53
66
  _trainingRecordSubjects?: DtoDetailRecordSubjectResItem[];
54
67
  }