cabloy 5.1.149 → 5.1.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -1
- package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
- package/CHANGELOG.md +22 -0
- package/CLAUDE.md +1 -0
- package/package.json +1 -1
- package/repo-docs/backend/dto-guide.md +7 -6
- package/repo-docs/backend/dto-infer-generation.md +25 -5
- package/repo-docs/frontend/a-model-under-the-hood.md +38 -0
- package/repo-docs/frontend/a-openapi-under-the-hood.md +13 -2
- package/repo-docs/frontend/form-layout-guide.md +26 -9
- package/repo-docs/frontend/model-resource-best-practices.md +2 -0
- package/repo-docs/frontend/model-resource-cookbook.md +7 -1
- package/repo-docs/frontend/use-state-data-best-practices.md +44 -0
- package/repo-docs/frontend/zova-form-source-reading-map.md +26 -9
- package/repo-docs/frontend/zova-form-under-the-hood.md +11 -0
- package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +1 -1
- package/repo-e2e/specs/a-commerce.spec.ts +40 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +2 -2
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/pnpm-lock.yaml +83 -75
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +108 -108
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +12 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +19 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +53 -4
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +148 -148
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +37 -3
- package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +2 -2
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +7 -6
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +1 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +7 -6
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +10 -7
- package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +4 -4
- package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +39 -67
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +56 -37
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +2 -2
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +6 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionAdjustStock.ts +34 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +81 -6
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeStockBalance.ts +32 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +44 -44
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +356 -338
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeStockBalance.ts +17 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionAdjustStock.tsx +35 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +138 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +36 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +4 -4
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +8 -8
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +11 -11
- package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +2 -2
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +2 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +2 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/model/resource.types.ts +11 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +58 -9
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +157 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +3 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +11 -13
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +9 -7
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -12
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryPersister.ts +17 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryRefetch.ts +34 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +27 -3
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryPersister.test.ts +115 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryRefetch.test.ts +84 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts +27 -5
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +4 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +2 -5
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts +179 -0
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.150
|
|
@@ -18,7 +18,8 @@ After generating or extending a backend thread, check which follow-up layers app
|
|
|
18
18
|
- treat the top-level `$Dto.create(...)` / `$Dto.update(...)` default omissions as a write-input authorization boundary, not as a reason to mechanically narrow ordinary `$Dto.get(...)` contracts
|
|
19
19
|
- when a narrowed DTO or OpenAPI schema is expected to remove physical response fields, separately shape and verify the action response; declaration narrowing alone does not guarantee runtime field stripping
|
|
20
20
|
- when stable Entity, Model, relation, or query truth exists, define the DTO projection first with `$Dto.*` and, where needed, `columns`, `include`, or `dtoClass`
|
|
21
|
-
- for an inferred field, use `$makeMetadata(...)` for metadata-only refinement and `$makeSchema(...)` for schema or validation refinement; use a class-body `@Api.field(...)` member only for a genuinely new field
|
|
21
|
+
- for an inferred/projected field, use `$makeMetadata(...)` for metadata-only refinement and `$makeSchema(...)` for schema or validation refinement; use a class-body `@Api.field(...)` member only for a genuinely new declared field
|
|
22
|
+
- before using `$makeMetadata(...)`, confirm the `fields` key already has an inferred/projected schema; a true virtual key added only through `@Dto({ fields })` must use `$makeSchema(...)` with a final concrete `z.<type>()` schema, because `fieldSource(...)` maps a source path but supplies no missing type; see [Virtual fields in the DTO fields map](../../../../repo-docs/backend/dto-infer-generation.md#virtual-fields-in-the-dto-fields-map)
|
|
22
23
|
- when inference cannot express the contract clearly, choose an explicit DTO deliberately; see [Default-first three-layer DTO authoring](../../../../repo-docs/backend/dto-infer-generation.md#default-first-three-layer-dto-authoring)
|
|
23
24
|
- frontend contract impact
|
|
24
25
|
- `@Api.field(...)` / `$makeSchema(...)` ordering: framework guards now preserve previously attached OpenAPI metadata across schema rebuilds, but structure-shaping schemaLike is still order-sensitive
|
|
@@ -180,6 +180,7 @@ Check whether the feature needs:
|
|
|
180
180
|
- schema-driven UI or `$apiSchema` review
|
|
181
181
|
- SSR hydration-equivalence review: classify state as SSR-required or intentionally deferred; keep server HTML and the hydration-time client render equivalent; defer private, cookie-unavailable, or browser-only query/load/render branches to an explicit post-hydration, admission, mounted, or interaction boundary
|
|
182
182
|
- distinguish `$useStateData(...)` query ownership from readiness waits: `disableSuspenseOnInit` only skips its init-time suspense kick and does not prevent query creation or fetches; choose `$QueryEnsureLoaded(...)` or freshness helpers only at the later boundary that needs them
|
|
183
|
+
- verify that render-driving UI reads model/query-owned reactive state (`query.data` or a model-derived surface); keep awaited `refetch()` results local to one-shot interaction/orchestration and never as a parallel ongoing controller/render state copy
|
|
183
184
|
- reverse fullstack handoff when newly added frontend resources will later be consumed by backend metadata or backend tooling
|
|
184
185
|
|
|
185
186
|
If the frontend change introduces resources such as a custom form-field renderer, table-cell renderer, or other generated metadata that backend `ZovaRender.field(...)` / `ZovaRender.cell(...)` will consume, do not treat the task as frontend-only cleanup.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.150
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add `bypassPersister` support.
|
|
8
|
+
- Update query functionality.
|
|
9
|
+
- Add runbook updates.
|
|
10
|
+
- Add schema alias support.
|
|
11
|
+
- Add SKU stock availability support.
|
|
12
|
+
- Update related functionality and resources.
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- Fix end-to-end tests.
|
|
17
|
+
- Correct `bypassPersister` behavior.
|
|
18
|
+
- Update schema tests.
|
|
19
|
+
|
|
20
|
+
### Improvements
|
|
21
|
+
|
|
22
|
+
- Refactor the student model and service.
|
|
23
|
+
- Improve documentation for `bypassPersister`.
|
|
24
|
+
|
|
3
25
|
## 5.1.149
|
|
4
26
|
|
|
5
27
|
### Features
|
package/CLAUDE.md
CHANGED
|
@@ -65,6 +65,7 @@ Before inventing a custom implementation path:
|
|
|
65
65
|
- In Zova page routes, any route with dynamic `params` must define `route.name`; do not rely on unnamed path-keyed routes for typed `$params`. Static routes should omit `route.name` unless a documented named-route requirement exists; use `$router.getPagePath(...)` for canonical static URL generation rather than adding a name or alias for convenience. Ordinary business routes without `locale` params should omit app-config aliases unless a documented system, compatibility, or user-facing URL exception requires one. Choose `ssrProfile` from the page's rendering contract: Web remains `public` by default, while `session` is an explicit choice for cookie-backed state, protected admission, personalized first paint, or private SSR data; the absence of a locale parameter alone does not select a profile. `requiresAuth` is independent, so anonymous routes must explicitly use `requiresAuth: false`. Regenerate page metadata after route changes.
|
|
66
66
|
- For frontend async state that affects rendering or interaction across consumers, prefer model-owned `$useStateData(...)` over controller-managed fetch/cache state.
|
|
67
67
|
- Default to establishing such query state during render. Use `disableSuspenseOnInit: true` only for relatively stable query-backed state when you want to skip the init-time `query.suspense()` kick; it does not prevent query creation, fetches, or hydration-time rendering. If strict readiness is needed later, wait explicitly at the interaction boundary.
|
|
68
|
+
- For render-driving async state, read the current query-owned reactive surface (`query.data` or a model-derived projection); an awaited `query.refetch()` result is valid only for one-shot interaction or orchestration and must not become a parallel ongoing controller/render state owner.
|
|
68
69
|
- In SSR, keep server HTML and the client's hydration-time initial render equivalent. When server rendering intentionally omits private, cookie-unavailable, or browser-only state, keep the same neutral shell or placeholder through hydration and defer its query/load/render branch to an explicit post-hydration, admission, mounted, or interaction boundary.
|
|
69
70
|
- Keep repo-wide AI rules in `CLAUDE.md` short and durable; put branching Zova analysis workflows in `.claude/skills/`.
|
|
70
71
|
- Do not modify shared environment identity or ports merely to bypass a busy resource. For intentional parallel worktree setup, follow `repo-docs/fullstack/parallel-worktree-environment.md`; otherwise wait for the shared resource or ask the user. Create or change worktree-local environment overrides only through the explicitly invoked `cabloy-worktree-environment` skill and its confirmation phase, and only in `vona/env/.env.local` and `zova/env/.env.local`; never modify flavor-, mode-, app-mode-, or runtime-specific `.env.*.local` files. The skill derives the standard `APP_NAME`, `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, `DEV_SERVER_HMR_PORT`, and API-derived `API_BASE_URL` tuple only from Git worktree metadata and fixed port baselines; never read or expose `.env*` content while recommending values. Admin and Web are alternative commands using this shared tuple and must not run concurrently in one worktree; use another linked worktree for concurrent use. Detect the active edition before choosing scripts, and never run `npm run init` as an automatic follow-up.
|
package/package.json
CHANGED
|
@@ -183,7 +183,7 @@ A practical split is:
|
|
|
183
183
|
- use inferred DTOs when the contract closely follows model structure or query shape
|
|
184
184
|
- wrap inferred DTOs in a named DTO class when reuse or discoverability becomes more important
|
|
185
185
|
|
|
186
|
-
When stable Entity, Model, relation, or query truth already exists, prefer inference first: use `$Dto.*` with `columns`, `include`, or `dtoClass` to define the projection; use `@Dto({ fields })` with `$makeMetadata(...)` for metadata-only differences or `$makeSchema(...)` for schema and validation differences;
|
|
186
|
+
When stable Entity, Model, relation, or query truth already exists, prefer inference first: use `$Dto.*` with `columns`, `include`, or `dtoClass` to define the projection; use `@Dto({ fields })` with `$makeMetadata(...)` only for metadata-only differences on an existing inferred/projected field, or `$makeSchema(...)` for schema and validation differences. A true virtual `fields` key absent from that projection must use `$makeSchema(...)` with a final concrete `z.<type>()` schema; `fieldSource(...)` maps its binding but supplies no type. Add an `@Api.field(...)` member only for a genuinely new declared DTO property. See [Virtual fields in the DTO fields map](/backend/dto-infer-generation#virtual-fields-in-the-dto-fields-map) for the full decision rule. Keep an explicit DTO as a deliberate fallback when no suitable upstream truth exists or the inferred composition is less clear.
|
|
187
187
|
|
|
188
188
|
Advanced inferred DTO shaping can also stay named and reusable through helper options such as `dtoClass`, especially for relation-aware contracts and nested DTO surfaces. `$Dto.get(...)` otherwise keeps the complete model-aware read shape by default; use `columns` or `dtoClass` for a genuine business projection, not merely to remove `iid` or `deleted`. For the canonical authoring sequence, see [Default-first three-layer DTO authoring](/backend/dto-infer-generation#default-first-three-layer-dto-authoring) and [Default read shape versus a public projection](/backend/dto-infer-generation#default-read-shape-versus-a-public-projection).
|
|
189
189
|
|
|
@@ -205,11 +205,12 @@ When creating DTOs:
|
|
|
205
205
|
|
|
206
206
|
1. check whether an Entity, Model, relation, or query shape already provides suitable upstream contract truth
|
|
207
207
|
2. define the inferred projection before redeclaring fields: choose the `$Dto.*` helper and, where needed, `columns`, `include`, or `dtoClass`
|
|
208
|
-
3. use `$makeMetadata(...)` for metadata-only refinement and `$makeSchema(...)` for schema or validation refinement of an inferred field
|
|
209
|
-
4.
|
|
210
|
-
5.
|
|
211
|
-
6.
|
|
212
|
-
7.
|
|
208
|
+
3. use `$makeMetadata(...)` for metadata-only refinement and `$makeSchema(...)` for schema or validation refinement of an inferred/projected field
|
|
209
|
+
4. for a true virtual key added only through `@Dto({ fields })`, use `$makeSchema(...)` with a final concrete `z.<type>()` schema; `fieldSource(...)` supplies no missing type
|
|
210
|
+
5. use `@Api.field(...)` for a genuinely new declared DTO property, not as a second declaration of an inferred field
|
|
211
|
+
6. keep DTO validation and OpenAPI concerns aligned, and keep the final structure-defining schema last when using schema-like composition
|
|
212
|
+
7. decide whether the contract should remain inferred, be wrapped in a named inferred DTO, or deliberately fall back to an explicit DTO
|
|
213
|
+
8. treat DTO design as part of the contract between backend handlers, models, and frontend integration
|
|
213
214
|
|
|
214
215
|
## Where to read next
|
|
215
216
|
|
|
@@ -193,6 +193,25 @@ export class DtoStudentSelectReq extends $Dto.queryPage(EntityStudent, [
|
|
|
193
193
|
|
|
194
194
|
Framework DTO composition preserves inherited OpenAPI metadata when a field schema is refined. That includes metadata such as titles, render information, and scene identity. It does **not** make structure-shaping schema-like arguments order-independent: keep the final structure-defining argument last and verify the effective schema rather than assuming a metadata merge can repair a reordered schema.
|
|
195
195
|
|
|
196
|
+
#### Virtual fields in the DTO fields map
|
|
197
|
+
|
|
198
|
+
The Layer 2 `$makeMetadata(...)` rule has a precondition: the field key must already be supplied by the inferred projection, whether through the `$Dto.*` base, `columns`, `include`, or `dtoClass`. It overlays metadata on that existing runtime schema.
|
|
199
|
+
|
|
200
|
+
A **true virtual DTO field** is a key added only through `@Dto({ fields })` and absent from that inherited projection. Define such a key with `$makeSchema(...)` and a final concrete Zod schema, even when its purpose is only to map a renderer to a nested source:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const descriptionMarkdownField = $makeSchema(
|
|
204
|
+
ZovaRender.fieldSource('content.descriptionMarkdown'),
|
|
205
|
+
ZovaRender.field('basic-markdown:formFieldMarkdown'),
|
|
206
|
+
v.optional(),
|
|
207
|
+
z.string(),
|
|
208
|
+
);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`z.string()` establishes the virtual key's validation and emitted OpenAPI type. `fieldSource(...)` only maps the DTO-facing key to the canonical source path used by downstream schema-driven UI; it does not supply a type. `$makeMetadata(...)` alone has no inherited schema to overlay for a true virtual key, so its metadata is attached to an unconstrained fallback rather than a meaningful field schema.
|
|
212
|
+
|
|
213
|
+
Use a class-body `@Api.field(...)` member instead when the new field should be a declared, statically typed DTO property rather than a contract-only `fields`-map key.
|
|
214
|
+
|
|
196
215
|
`@Dto({ fields })` changes the runtime contract and metadata. It does not rewrite the TypeScript property type inferred from the `$Dto.*` base class. Do not add a duplicate `declare` field or a second field decorator solely to mirror a runtime schema restriction unless a separate static contract is genuinely required and is type-compatible with the inferred base.
|
|
197
216
|
|
|
198
217
|
### Layer 3: add contract-only fields
|
|
@@ -507,10 +526,11 @@ When evaluating a return shape or input contract that closely follows model stru
|
|
|
507
526
|
2. is the contract get/list/query/create/update/aggregate/group oriented, and which `$Dto.*` helper matches it?
|
|
508
527
|
3. should `columns`, `include`, `with`, or `dtoClass` define the projection boundary?
|
|
509
528
|
4. should the inferred DTO stay inline or be wrapped in a named DTO class?
|
|
510
|
-
5. for every local difference, is it
|
|
511
|
-
6.
|
|
512
|
-
7.
|
|
513
|
-
8.
|
|
514
|
-
9.
|
|
529
|
+
5. for every local difference, does the `fields` key already have an inferred/projected schema, or is it a true virtual key?
|
|
530
|
+
6. use `$makeMetadata(...)` only for metadata-only refinement of an inferred/projected field; define every true virtual key with `$makeSchema(...)` and a final concrete `z.<type>()` schema
|
|
531
|
+
7. is every `@Api.field(...)` member genuinely new instead of a redeclared inferred field?
|
|
532
|
+
8. if `$makeSchema(...)` is used, is the structure-defining schema last and is emitted schema/OpenAPI verification planned?
|
|
533
|
+
9. does the resulting DTO also affect OpenAPI and frontend generation paths?
|
|
534
|
+
10. is CRUD generation already giving enough contract structure that another handwritten DTO would be redundant, or is an explicit DTO clearer?
|
|
515
535
|
|
|
516
536
|
That helps reduce redundant type work and keeps contracts closer to the model truth.
|
|
@@ -187,6 +187,44 @@ The important SSR-aware rule is:
|
|
|
187
187
|
|
|
188
188
|
That means stale-time behavior is part of the model runtime contract, not only a local query option.
|
|
189
189
|
|
|
190
|
+
### Per-fetch persistence bypass
|
|
191
|
+
|
|
192
|
+
The returned query's `refetch()` is extended by the model runtime with an opt-in per-fetch persistence bypass:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
await query.refetch({ bypassPersister: true });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The source-level execution path is:
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
query.refetch({ bypassPersister: true })
|
|
202
|
+
-> BeanModelUseQuery refetch wrapper
|
|
203
|
+
-> remove bypassPersister from the public options
|
|
204
|
+
-> attach a per-fetch internal marker
|
|
205
|
+
-> TanStack Query fetch
|
|
206
|
+
-> BeanModelPersister wrapper
|
|
207
|
+
-> call queryFn(context) directly
|
|
208
|
+
-> TanStack Query updates in-memory query state
|
|
209
|
+
-> observers and query.data update
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
An ordinary refetch follows the existing persister path instead:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
query.refetch()
|
|
216
|
+
-> TanStack Query fetch
|
|
217
|
+
-> experimental query persister
|
|
218
|
+
-> possible persisted restore or restore-triggered refresh
|
|
219
|
+
-> normal persistence behavior
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For a bypassed fetch, the marker makes the model persister skip persisted restore and the persister's save path for that fetch. The result still goes through TanStack Query's normal success/error handling, so the query's in-memory data, timestamps, observers, and reactive result continue to behave normally. The marker is scoped to the fetch and does not modify static query options or `meta.persister`; `meta.persister: false` remains the query-wide switch.
|
|
223
|
+
|
|
224
|
+
This option is not a force-new-request primitive. The model wrapper preserves TanStack Query cancellation and in-flight deduplication semantics, including the meaning of `cancelRefetch`. A fetch that has already been deduplicated cannot be retroactively converted into a bypassed fetch. The bypass call also does not automatically cancel an ordinary persistence callback that was already queued before it.
|
|
225
|
+
|
|
226
|
+
The current implementation transports the marker through the installed TanStack Query observer's internal fetch-options forwarding. `bypassPersister` is therefore a Zova model option, not a native public TanStack `RefetchOptions` field; the model wrapper keeps that internal detail out of the normal authoring surface.
|
|
227
|
+
|
|
190
228
|
## State helper families as one runtime family
|
|
191
229
|
|
|
192
230
|
The state helper layer lives mainly in:
|
|
@@ -31,7 +31,7 @@ A practical mental model is:
|
|
|
31
31
|
2. `$sdk` resolves a locale-scoped `ModelSdk`
|
|
32
32
|
3. `ModelSdk` exposes bootstrap, permissions, sdk, schema, Zod, and default-value helpers
|
|
33
33
|
4. `SysSdk` owns the lower cache/fetch layer for bootstrap/docs/schemas
|
|
34
|
-
5. `schema.ts` extracts request/query/filter/body/row/paged schema surfaces
|
|
34
|
+
5. `schema.ts` extracts request/query/filter/body/row/paged schema surfaces, applies scene-aware property selection, and normalizes `fieldSource` fields into canonical runtime keys
|
|
35
35
|
6. downstream resource/model/table consumers reuse those lower-level surfaces rather than rebuilding them independently
|
|
36
36
|
|
|
37
37
|
That means `a-openapi` is not only about generated SDK usage. It is also the lower-level schema/runtime bridge beneath the higher-level frontend runtime.
|
|
@@ -204,12 +204,21 @@ This file owns the main lower-level helpers for:
|
|
|
204
204
|
- scene-aware property loading
|
|
205
205
|
- JSON-schema-to-Zod conversion
|
|
206
206
|
|
|
207
|
+
### Scene overlays, `fieldSource`, and preserved aliases
|
|
208
|
+
|
|
207
209
|
The most important scene-aware rule is in `loadSchemaProperties(...)`:
|
|
208
210
|
|
|
209
211
|
- property metadata can be extended by `rest.*`
|
|
210
|
-
-
|
|
212
|
+
- for `form-view`, `form-create`, and `filter`, it merges base `rest`, the shared `rest.form` overlay, and then the exact scene overlay before reading `fieldSource`
|
|
213
|
+
- other scenes apply their exact overlay
|
|
211
214
|
- field ordering is resolved through `rest.order`
|
|
212
215
|
|
|
216
|
+
When that effective metadata supplies `fieldSource`, its nested source path becomes the canonical runtime `key`. This is the field identity that downstream form and structural consumers bind and render. The first differing original schema property name that resolves to this key is retained as `schemaKey`; later coalesced names are retained in `schemaKeys`. If a schema property already has the canonical name, `key` itself remains that original identity.
|
|
217
|
+
|
|
218
|
+
Several schema properties can therefore describe one canonical field without producing several runtime field records. Coalescing is by canonical key, so its result does not depend on declaration order. Ordinary properties that do not use `fieldSource` do not acquire alias metadata.
|
|
219
|
+
|
|
220
|
+
The preserved names are metadata aliases, not additional bindings. They let a structural consumer accept a DTO-facing field name while still handing the canonical key to the form runtime. See [Zova Form Under the Hood](/frontend/zova-form-under-the-hood#fieldsource-canonicalization-and-preserved-schema-aliases) for the binding consequence and [Form Layout Guide](/frontend/form-layout-guide#how-the-resolver-handles-the-declared-tree) for author-facing declaration rules.
|
|
221
|
+
|
|
213
222
|
That means `a-openapi` is not only a transport/schema lookup layer.
|
|
214
223
|
|
|
215
224
|
It is also the lower-level metadata shaping layer for schema-driven UI behavior.
|
|
@@ -243,6 +252,8 @@ Use these next steps depending on your question:
|
|
|
243
252
|
- if you want OpenAPI generation/config usage, read [OpenAPI SDK Guide](/frontend/openapi-sdk-guide)
|
|
244
253
|
- if you want schema-driven UI positioning, read [API Schema Guide](/frontend/api-schema-guide)
|
|
245
254
|
- if you want the resource-owner consumer side, read [ModelResource Internals Deep Dive](/frontend/model-resource-internals-deep-dive)
|
|
255
|
+
- if you want canonical schema keys to become form bindings, read [Zova Form Under the Hood](/frontend/zova-form-under-the-hood)
|
|
256
|
+
- if you want Form Layout field-declaration matching, read [Form Layout Guide](/frontend/form-layout-guide)
|
|
246
257
|
- if you want the table/resource consumer side, read [Zova Table Under the Hood](/frontend/zova-table-under-the-hood) and the resource deep dives
|
|
247
258
|
|
|
248
259
|
## Final takeaway
|
|
@@ -219,13 +219,25 @@ Set `layout: 'flow'` when compact fields should appear from left to right withou
|
|
|
219
219
|
|
|
220
220
|
## How the resolver handles the declared tree
|
|
221
221
|
|
|
222
|
-
Before rendering, `resolveFormLayout(...)` reconciles `formLayout` with the current scene's resolved schema properties. This makes the declaration a **placement overlay**, not an allow-list.
|
|
222
|
+
Before rendering, `resolveFormLayout(...)` reconciles `formLayout` with the current scene's resolved schema properties. This makes the declaration a **placement overlay**, not an allow-list. The preceding OpenAPI normalization step is explained in [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood#scene-overlays-fieldsource-and-preserved-aliases).
|
|
223
223
|
|
|
224
224
|
### Eligible and omitted fields
|
|
225
225
|
|
|
226
226
|
Only schema properties with `rest.visible !== false` are eligible. When an eligible visible field is absent from `formLayout`, the resolver appends it as a root-level field after the declared nodes, in schema-property order.
|
|
227
227
|
|
|
228
|
-
A
|
|
228
|
+
A field that uses `fieldSource` is represented at runtime by its nested canonical source key. The loader stores that source path in `key`, retains the first differing original schema name in `schemaKey`, and retains further coalesced names in `schemaKeys`. A property already named by its canonical key retains that identity in `key`. These preserved names are aliases for declaration matching, not separate fields or bindings.
|
|
229
|
+
|
|
230
|
+
Form Layout accepts three declaration forms in precedence order:
|
|
231
|
+
|
|
232
|
+
1. the exact eligible canonical key;
|
|
233
|
+
2. any uniquely mapped preserved schema alias from `schemaKey` or `schemaKeys`;
|
|
234
|
+
3. a unique relation-prefix shorthand.
|
|
235
|
+
|
|
236
|
+
For example, a real relation declaration `studentContentForm` can resolve to the sole `studentContentForm.descriptionMarkdown` source, while an intentional `fieldSource` schema alias such as `_descriptionMarkdown` can resolve to `content.descriptionMarkdown` even though it is not that source path's prefix. The resolved plan always renders and binds the canonical nested key. `fieldSource(...)` is a binding and canonical-key mapping mechanism; it neither infers nor declares the backend DTO field type. A projected field already has a schema, while a true virtual DTO key absent from the inferred projection must be defined with `$makeSchema(...)` and a concrete schema such as `z.string()`; see [Virtual fields in the DTO fields map](/backend/dto-infer-generation#virtual-fields-in-the-dto-fields-map).
|
|
237
|
+
|
|
238
|
+
An alias or relation prefix must resolve to exactly one eligible canonical source. If multiple visible source keys match, it is unresolved and receives `unknownField`; declare the exact canonical source key instead. Exact canonical matches win over colliding aliases. Invisible properties contribute neither eligible fields nor usable aliases.
|
|
239
|
+
|
|
240
|
+
Duplicate declarations are detected by canonical key, so an alias and `content.descriptionMarkdown` cannot render the same field twice. Resolved field names, duplicate identity, and tab paths all use the canonical key. If a declaration is removed because it is unknown, invisible, ambiguous, or duplicate, otherwise unplaced eligible canonical fields are still appended at the root.
|
|
229
241
|
|
|
230
242
|
If a field must not render, make it invisible in schema metadata. Leaving it out of `formLayout.children` is not enough.
|
|
231
243
|
|
|
@@ -324,7 +336,7 @@ ZovaRender.block('basic-pageentry:blockForm', {
|
|
|
324
336
|
});
|
|
325
337
|
```
|
|
326
338
|
|
|
327
|
-
`studentContentForm` is one
|
|
339
|
+
`studentContentForm` is one projected, `fieldSource`-mapped nested-relation field in the structural tree. It remains the DTO-facing declaration name, while the resolved plan rewrites it to `studentContentForm.descriptionMarkdown` before field-state lookup and rendering. Its Markdown renderer owns the nested source-field UI, while the separate `StudentContent` group expresses that it is a distinct content area rather than part of the responsive profile Grid.
|
|
328
340
|
|
|
329
341
|
`trainingRecords` is one field in the structural tree. Its `basic-details:formFieldDetails` renderer owns the nested details UI; Form Layout does not recursively arrange the properties inside each detail record.
|
|
330
342
|
|
|
@@ -371,8 +383,9 @@ Here `formFieldLayout.inline: true` controls how each field wrapper is presented
|
|
|
371
383
|
4. Use `formLayout` when the requirement is field placement, Grid or flow structure, groups, or tabs.
|
|
372
384
|
5. Use `layout`, `formFieldLayout`, `options`, or provider behaviors when the requirement is one field's wrapper or renderer.
|
|
373
385
|
6. Keep entry actions in page-entry toolbar blocks. Keep filter action semantics in `basic-page:blockFilterActions`; place that block inside Form Layout when the actions must share structural Grid or flow placement with fields.
|
|
374
|
-
7. For maintained Cabloy Basic list filters, prefer one inline flow section that explicitly lists every real filter-schema field in schema order and ends with one embedded `basic-page:blockFilterActions` block. Do not add
|
|
386
|
+
7. For maintained Cabloy Basic list filters, prefer one inline flow section that explicitly lists every real filter-schema field in schema order and ends with one embedded `basic-page:blockFilterActions` block. Do not add synthetic request-only filter fields, alter filter transforms, or combine it with a sibling action block.
|
|
375
387
|
8. Review field names against the scene-specific schema. Unlisted visible fields are appended; unknown and duplicate declarations are silently pruned from the rendered plan.
|
|
388
|
+
9. Prefer an exact canonical source path when an alias or relation prefix could match multiple visible fields. Do not list both an alias and its canonical key; canonical duplicate detection keeps only the first declaration.
|
|
376
389
|
|
|
377
390
|
## Source-reading and verification path
|
|
378
391
|
|
|
@@ -380,10 +393,14 @@ For source-level investigation, follow this order:
|
|
|
380
393
|
|
|
381
394
|
1. `vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx` or `studentSelectResItem.tsx`
|
|
382
395
|
2. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formLayout.ts`
|
|
383
|
-
3. `zova/src/suite-vendor/a-zova/modules/a-
|
|
384
|
-
4. `zova/src/suite/
|
|
385
|
-
5. `
|
|
386
|
-
|
|
387
|
-
|
|
396
|
+
3. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts`
|
|
397
|
+
4. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts`
|
|
398
|
+
5. `zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts`
|
|
399
|
+
6. `zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx`
|
|
400
|
+
7. `zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts`
|
|
401
|
+
8. `zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts`
|
|
402
|
+
9. `vona/src/suite/a-training/modules/training-student/test/student.test.ts`
|
|
403
|
+
|
|
404
|
+
The OpenAPI loader test verifies canonical `fieldSource` keys, preserved aliases, scene overlays, coalescing, and declaration-order independence. The Form Layout test verifies canonical rewriting, alias and prefix resolution, ambiguity, duplicate detection, visibility, and tab paths. The Student test verifies that entry and filter DTO metadata preserves the current block nesting, optional IDs, Grid columns/spans, flow layout selection, and field order through OpenAPI generation. It is a contract-metadata test, not a browser end-to-end assertion for tabs, layout behavior, or error badges.
|
|
388
405
|
|
|
389
406
|
For the broader form runtime, continue with [Zova Form Under the Hood](/frontend/zova-form-under-the-hood) and [Zova Form Source Reading Map](/frontend/zova-form-source-reading-map).
|
|
@@ -360,6 +360,8 @@ Use this checklist before merging resource-model changes:
|
|
|
360
360
|
8. Does bootstrap/readiness stay inside the model boundary?
|
|
361
361
|
9. Are `$fetch`, `$sdk`, and model state composed coherently?
|
|
362
362
|
10. Would another screen reuse this model surface confidently?
|
|
363
|
+
11. Does ongoing render output read `query.data` or a model-derived reactive surface rather than an awaited `refetch()` snapshot?
|
|
364
|
+
12. Are awaited `refetch()` results limited to one-shot interaction/orchestration without creating a parallel ongoing render-state owner?
|
|
363
365
|
|
|
364
366
|
## When to use this page
|
|
365
367
|
|
|
@@ -121,9 +121,15 @@ const modelStudent = (await ctx.bean._getBean(
|
|
|
121
121
|
true,
|
|
122
122
|
)) as ModelStudent;
|
|
123
123
|
const querySummary = modelStudent.summary(id);
|
|
124
|
-
|
|
124
|
+
|
|
125
|
+
await querySummary.refetch({ bypassPersister: true });
|
|
126
|
+
$host.$appModal.dialog({
|
|
127
|
+
slotDefault: () => <ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />,
|
|
128
|
+
});
|
|
125
129
|
```
|
|
126
130
|
|
|
131
|
+
`refetch({ bypassPersister: true })` is useful when this interaction needs an API-fresh result without restoring or scheduling a persistence save through the persister for that fetch. The successful result still updates the model-owned in-memory query, and the dialog remains bound to `querySummary.data`, so the query remains the source of its ongoing render state rather than transferring ownership to an awaited-result snapshot. The option affects only this fetch; normal query cancellation and in-flight deduplication rules still apply. It is not a force-new-request option: when an existing fetch is reused by TanStack Query, that fetch's semantics remain in effect. The bypassed fetch itself does not intentionally replace an existing persisted value; an already queued ordinary persistence callback is a separate operation and is not automatically cancelled. Use static `meta.persister: false` only when persistence should be disabled for the query generally.
|
|
132
|
+
|
|
127
133
|
### Avoid
|
|
128
134
|
|
|
129
135
|
Do not create a second model that independently caches the same student resource list/item state just because a custom action was added.
|
|
@@ -278,6 +278,50 @@ const passportCode = await this.$passport.ensureFreshTempAuthToken(options);
|
|
|
278
278
|
|
|
279
279
|
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.
|
|
280
280
|
|
|
281
|
+
## Render-driving state versus one-shot refetch results
|
|
282
|
+
|
|
283
|
+
A query established during render remains the owner of its ongoing state. Render paths should read the current reactive surface—typically `query.data`, `query.pending`, `query.error`, or a model-derived projection—and let query updates drive a later render.
|
|
284
|
+
|
|
285
|
+
An interaction or orchestration boundary may still await `query.refetch()` when it needs one result to decide whether to continue a command, navigate, show a notification, or open a dialog. That result is local to the current sequence; it does not transfer query ownership to the controller or render path.
|
|
286
|
+
|
|
287
|
+
Do not copy an awaited `refetch()` result into a second long-lived controller/render state that drives an open dialog or persistent component. If the UI remains mounted and displays query-backed data, bind it to `query.data` or a model-derived reactive surface so later refetches and model updates remain visible.
|
|
288
|
+
|
|
289
|
+
### Per-fetch persistence bypass
|
|
290
|
+
|
|
291
|
+
When one interaction needs an API-fresh result but should not restore persisted data or schedule a persistence save for that fetch, use the model query's per-fetch option:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
await query.refetch({ bypassPersister: true });
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`bypassPersister: true`:
|
|
298
|
+
|
|
299
|
+
- affects only the current fetch
|
|
300
|
+
- does not change the query's static options or `meta.persister`
|
|
301
|
+
- skips persisted-query restore for that fetch
|
|
302
|
+
- does not schedule that fetch through the persister's save path
|
|
303
|
+
- still lets TanStack Query update the in-memory query, timestamps, observers, and reactive `query.data`
|
|
304
|
+
- does not create a second query or cache owner
|
|
305
|
+
|
|
306
|
+
This option is not a force-new-request flag. Cancellation, `cancelRefetch`, and in-flight deduplication continue to follow TanStack Query semantics. If an existing fetch is reused, its already-established fetch semantics remain in effect. The bypassed fetch itself does not intentionally replace an existing persisted value; an already queued ordinary persistence callback is a separate operation and is not automatically cancelled.
|
|
307
|
+
|
|
308
|
+
This is different from static `meta.persister: false`: the static option disables persistence for the query generally, while `bypassPersister: true` opts out only for one fetch.
|
|
309
|
+
|
|
310
|
+
For example, an interaction can request a fresh summary and then open a dialog while the dialog remains bound to the query-owned state:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const querySummary = modelStudent.summary(id);
|
|
314
|
+
await querySummary.refetch({ bypassPersister: true });
|
|
315
|
+
|
|
316
|
+
$host.$appModal.dialog({
|
|
317
|
+
slotDefault: () => (
|
|
318
|
+
<ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />
|
|
319
|
+
),
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The awaited result may coordinate the current interaction, but ongoing rendering should continue to read `querySummary.data`.
|
|
324
|
+
|
|
281
325
|
## Practical rule 7: derive render-time state once per render when possible
|
|
282
326
|
|
|
283
327
|
Even when the query object is reused, a controller can still become noisy if it repeatedly derives the same values in several helper calls.
|
|
@@ -147,21 +147,29 @@ Use this path when you are asking questions like:
|
|
|
147
147
|
|
|
148
148
|
- how does a schema become rendered fields?
|
|
149
149
|
- where do field props come from?
|
|
150
|
+
- where does `fieldSource` become the canonical form key, and where are original schema aliases preserved?
|
|
150
151
|
- how are metadata expressions or render providers resolved?
|
|
151
152
|
|
|
152
153
|
### Read the docs first
|
|
153
154
|
|
|
154
155
|
- [API Schema Guide](/frontend/api-schema-guide)
|
|
156
|
+
- [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood)
|
|
155
157
|
- [Form Guide](/frontend/form-guide)
|
|
156
158
|
|
|
157
159
|
### Then read source in this order
|
|
158
160
|
|
|
159
|
-
1. `zova/src/suite-vendor/a-zova/modules/a-
|
|
160
|
-
2. `zova/src/suite-vendor/a-zova/modules/a-
|
|
161
|
-
3. `zova/src/suite-vendor/a-zova/modules/a-
|
|
161
|
+
1. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts`
|
|
162
|
+
2. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts`
|
|
163
|
+
3. `zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts`
|
|
164
|
+
4. `zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx`
|
|
165
|
+
5. `zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/render.tsx`
|
|
166
|
+
6. `zova/src/suite-vendor/a-zova/modules/a-form/src/types/formField.ts`
|
|
162
167
|
|
|
163
168
|
### What each file clarifies
|
|
164
169
|
|
|
170
|
+
- `schema.ts` shows scene-aware `fieldSource` canonicalization and coalescing by canonical key
|
|
171
|
+
- `types/rest.ts` defines the preserved `schemaKey` and `schemaKeys` aliases
|
|
172
|
+
- `schema.test.ts` is the compact behavior matrix for canonical keys, aliases, scene overlays, declaration-order independence, and ordinary fields
|
|
165
173
|
- `form/controller.tsx` shows schema property loading, field CEL scope creation, and top-level field prop extraction
|
|
166
174
|
- `form/render.tsx` shows how schema properties become children when the form body is not manually overridden
|
|
167
175
|
- `types/formField.ts` shows the field render-context shapes that the runtime passes through to renderers and behaviors
|
|
@@ -224,12 +232,14 @@ Use this path when you are asking questions like:
|
|
|
224
232
|
|
|
225
233
|
- where does `formLayout` come from in a resource DTO?
|
|
226
234
|
- how are fields, embedded blocks, sections, groups, and tabs normalized before rendering?
|
|
235
|
+
- how do canonical keys, preserved schema aliases, and unique relation-prefix shorthand resolve to one field?
|
|
227
236
|
- why are omitted visible fields appended or duplicate fields removed?
|
|
228
237
|
- where does Cabloy Basic render responsive grids and tab error badges?
|
|
229
238
|
|
|
230
239
|
### Read the docs first
|
|
231
240
|
|
|
232
241
|
- [Form Layout Guide](/frontend/form-layout-guide)
|
|
242
|
+
- [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood)
|
|
233
243
|
- [Table + Resource CRUD Cookbook](/frontend/table-resource-crud-cookbook)
|
|
234
244
|
- [Resource Entry Page Deep Dive](/frontend/resource-entry-page-deep-dive)
|
|
235
245
|
|
|
@@ -238,18 +248,25 @@ Use this path when you are asking questions like:
|
|
|
238
248
|
1. `vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx`
|
|
239
249
|
2. `vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx`
|
|
240
250
|
3. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formLayout.ts`
|
|
241
|
-
4. `zova/src/suite-vendor/a-zova/modules/a-
|
|
242
|
-
5. `zova/src/suite/
|
|
243
|
-
6. `zova/src/suite/
|
|
244
|
-
7. `
|
|
251
|
+
4. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts`
|
|
252
|
+
5. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts`
|
|
253
|
+
6. `zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts`
|
|
254
|
+
7. `zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx`
|
|
255
|
+
8. `zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockFilterActions/controller.tsx`
|
|
256
|
+
9. `zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts`
|
|
257
|
+
10. `zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts`
|
|
258
|
+
11. `vona/src/suite/a-training/modules/training-student/test/student.test.ts`
|
|
245
259
|
|
|
246
260
|
### What each file clarifies
|
|
247
261
|
|
|
248
262
|
- the Student DTOs show the entry and filter block composition that supplies layout metadata
|
|
249
263
|
- the OpenAPI type contract defines the legal node grammar and responsive values
|
|
250
|
-
-
|
|
251
|
-
-
|
|
264
|
+
- `schema.ts` resolves scene overlays, canonicalizes `fieldSource`, and coalesces aliases that target one canonical key
|
|
265
|
+
- `types/rest.ts` defines the preserved alias metadata
|
|
266
|
+
- the resolver filters visible fields, resolves exact canonical keys before unique aliases and unique prefixes, and records duplicate identity and tab paths by canonical key
|
|
267
|
+
- the Basic block controller renders sections, groups, tabs, field spans, and embedded blocks while delegating canonical field names to `$$form.renderField(...)`
|
|
252
268
|
- `blockFilterActions` shows how a block rendered inside Form Layout reuses the inherited form CEL scope to invoke `$$filter`
|
|
269
|
+
- the OpenAPI and Form Layout unit tests verify canonicalization, alias precedence, ambiguity, duplicates, visibility, root append, and tab paths
|
|
253
270
|
- the Student test verifies emitted metadata nesting, columns, spans, embedded action blocks, and optional IDs; it is not a browser rendering test
|
|
254
271
|
|
|
255
272
|
## 8. Resource-driven CRUD page integration
|
|
@@ -11,6 +11,7 @@ Use this page together with:
|
|
|
11
11
|
- [Zova Reactivity Under the Hood](/frontend/zova-reactivity-under-the-hood)
|
|
12
12
|
- [Behavior Guide](/frontend/behavior-guide)
|
|
13
13
|
- [API Schema Guide](/frontend/api-schema-guide)
|
|
14
|
+
- [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood)
|
|
14
15
|
|
|
15
16
|
Use this page after [Form Guide](/frontend/form-guide) when you want to move from the public authoring surface to the internal cooperation among form controllers, field controllers, schema metadata, provider config, behaviors, and CRUD integration.
|
|
16
17
|
|
|
@@ -233,6 +234,14 @@ A practical reading takeaway is:
|
|
|
233
234
|
- **schema is not only validation truth**
|
|
234
235
|
- **schema also drives ordering, render metadata, and scene-specific field behavior**
|
|
235
236
|
|
|
237
|
+
### `fieldSource` canonicalization and preserved schema aliases
|
|
238
|
+
|
|
239
|
+
`loadSchemaProperties(...)` constructs the effective property `rest` metadata before inspecting `fieldSource`: base metadata, the applicable shared `form` overlay, and then the exact scene overlay. When that metadata supplies `fieldSource`, its nested source path becomes the property's canonical `key`.
|
|
240
|
+
|
|
241
|
+
The form uses this canonical key for field-property lookup, CEL field scope, values, validation, and rendering. When an original schema property name differs, `schemaKey` preserves the first such name that reached the canonical key, while `schemaKeys` preserves later coalesced names. A property already named by its canonical key retains that identity in `key`. These preserved names are aliases for metadata consumers such as Form Layout, not independent form bindings.
|
|
242
|
+
|
|
243
|
+
Consequently, several DTO-facing schema names can represent one nested field without creating duplicate form state. The OpenAPI loader owns this normalization and coalescing; see [OpenAPI Runtime Under the Hood](/frontend/a-openapi-under-the-hood#scene-overlays-fieldsource-and-preserved-aliases) for its lower-level contract.
|
|
244
|
+
|
|
236
245
|
### The form owner supplies the schema lifetime
|
|
237
246
|
|
|
238
247
|
`ZForm` derives `properties` and `zodSchema` from the schema it receives. Its runtime does not own `$apiSchema` locale selection or refresh a facade retained by the page/controller. If the owner supplies a schema object created under an earlier locale, the form will faithfully render that object's titles and metadata.
|
|
@@ -445,6 +454,8 @@ That means automatic schema-driven rendering is not happening magically in the w
|
|
|
445
454
|
|
|
446
455
|
When `ZForm` receives a nonempty block list, the render bean delegates body rendering to those blocks instead of iterating schema fields directly. For Cabloy Basic structural forms, `basic-form:blockFormLayout` resolves `formLayout` against the form's current schema properties and calls `$$form.renderField(...)` for each surviving layout field.
|
|
447
456
|
|
|
457
|
+
For a field declaration, the shared resolver first accepts an exact canonical key, then a uniquely mapped preserved schema alias, and then a unique relation-prefix shorthand. It rewrites an alias or shorthand to the canonical key before calling `$$form.renderField(...)`. Exact canonical keys win over colliding aliases; ambiguous aliases or prefixes are reported as `unknownField`. Duplicate tracking and tab-path bookkeeping use the canonical key, while visible canonical fields not placed by a surviving declaration are appended afterward. See [Form Layout Guide](/frontend/form-layout-guide#how-the-resolver-handles-the-declared-tree) for the full DTO authoring rules.
|
|
458
|
+
|
|
448
459
|
Form Layout also supports a leaf `block` node. It wraps an existing resource block descriptor and the Basic renderer invokes it with the inherited `IJsxRenderContextForm`, including the same JSX runtime and CEL scope. The node has no schema property or field value; for example, a filter can place `basic-page:blockFilterActions` inside a flow section while that action block continues to read `$$filter` from the filter-owned form scope.
|
|
449
460
|
|
|
450
461
|
This keeps ownership separate:
|
|
@@ -318,7 +318,7 @@ const contentField = $makeMetadata(
|
|
|
318
318
|
);
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
-
The exact renderer is edition-specific; the nested source contract is not.
|
|
321
|
+
The exact renderer is edition-specific; the nested source contract is not. This is a valid `$makeMetadata(...)` overlay because `parentContentForm` is already supplied by the inferred relation projection through the DTO relation option type, `include`, and `dtoClass`; its schema exists before the renderer and `fieldSource(...)` metadata is added. `fieldSource(...)` maps the projected field to the nested source path but does not declare `sourceMarkdown`'s type. If a field-map key is instead a true virtual DTO key absent from the inferred projection, define it with `$makeSchema(...)` and an appropriate final concrete schema such as `z.string()`; see [Virtual fields in the DTO fields map](/backend/dto-infer-generation#virtual-fields-in-the-dto-fields-map). Do not add derived HTML to a mutation DTO merely because it exists in the table.
|
|
322
322
|
|
|
323
323
|
A DTO or OpenAPI declaration is not a substitute for runtime response review. If a service returns an already-built object with extra properties, narrowing the declaration alone may not remove those properties from JSON. Verify the actual action response as well as emitted schema metadata.
|
|
324
324
|
|
|
@@ -1737,6 +1737,7 @@ test(
|
|
|
1737
1737
|
const categoryName = `E2E SKU Category ${suffix}`;
|
|
1738
1738
|
const productTitle = `E2E SKU Product ${suffix}`;
|
|
1739
1739
|
const skuCode = `E2E-SKU-${suffix}`;
|
|
1740
|
+
const stockActionPath = '/api/commerce/trade/stockBalance/adjustStock';
|
|
1740
1741
|
const adminContext = await browser.newContext();
|
|
1741
1742
|
const adminPage = await adminContext.newPage();
|
|
1742
1743
|
const adminPageErrors = collectPageErrors(adminPage);
|
|
@@ -1805,8 +1806,46 @@ test(
|
|
|
1805
1806
|
const skuRow = adminPage.getByRole('row', { name: new RegExp(skuCode) });
|
|
1806
1807
|
await expect(skuRow).toBeVisible();
|
|
1807
1808
|
await expect(skuRow.getByText('12.34', { exact: true })).toBeVisible();
|
|
1809
|
+
await expect(skuRow.getByText('0', { exact: true })).toBeVisible();
|
|
1808
1810
|
await expect(skuRow.getByText('Draft', { exact: true })).toBeVisible();
|
|
1809
1811
|
await expect(skuRow.getByRole('link', { name: skuCode, exact: true })).toBeVisible();
|
|
1812
|
+
const adjustStockButton = skuRow.getByRole('button', { name: 'Adjust stock', exact: true });
|
|
1813
|
+
await expect(adjustStockButton).toBeVisible();
|
|
1814
|
+
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
1815
|
+
'data-zova-hydrated',
|
|
1816
|
+
'commerceAdmin',
|
|
1817
|
+
);
|
|
1818
|
+
await adjustStockButton.click();
|
|
1819
|
+
await expect(adminPage.getByRole('dialog')).toBeVisible();
|
|
1820
|
+
await expect(adminPage.getByRole('dialog').getByText(skuCode, { exact: true })).toBeVisible();
|
|
1821
|
+
await expect(adminPage.getByRole('dialog').getByText('0', { exact: true })).toBeVisible();
|
|
1822
|
+
const deltaInput = adminPage.getByRole('group', { name: 'Delta' }).getByRole('spinbutton');
|
|
1823
|
+
const reasonInput = adminPage.getByRole('group', { name: 'Reason' }).getByRole('textbox');
|
|
1824
|
+
await deltaInput.fill('7');
|
|
1825
|
+
await reasonInput.fill('Initial E2E stock');
|
|
1826
|
+
const stockRequest = adminPage.waitForRequest(request => {
|
|
1827
|
+
return request.method() === 'POST' && new URL(request.url()).pathname === stockActionPath;
|
|
1828
|
+
});
|
|
1829
|
+
const stockResponse = adminPage.waitForResponse(response => {
|
|
1830
|
+
return (
|
|
1831
|
+
response.request().method() === 'POST' &&
|
|
1832
|
+
new URL(response.url()).pathname === stockActionPath
|
|
1833
|
+
);
|
|
1834
|
+
});
|
|
1835
|
+
await adminPage
|
|
1836
|
+
.getByRole('dialog')
|
|
1837
|
+
.getByRole('button', { name: 'Adjust stock', exact: true })
|
|
1838
|
+
.click();
|
|
1839
|
+
const stockRequestValue = await stockRequest;
|
|
1840
|
+
expect(stockRequestValue.postDataJSON()).toMatchObject({
|
|
1841
|
+
skuId,
|
|
1842
|
+
delta: 7,
|
|
1843
|
+
reason: 'Initial E2E stock',
|
|
1844
|
+
correlationId: expect.any(String),
|
|
1845
|
+
});
|
|
1846
|
+
expect((await stockResponse).ok()).toBeTruthy();
|
|
1847
|
+
await expect(adminPage.getByRole('dialog')).toHaveCount(0);
|
|
1848
|
+
await expect(skuRow.getByText('7', { exact: true })).toBeVisible();
|
|
1810
1849
|
await expect(
|
|
1811
1850
|
adminPage.locator('section').getByText('SKU code', { exact: true }),
|
|
1812
1851
|
).toBeVisible();
|
|
@@ -1822,6 +1861,7 @@ test(
|
|
|
1822
1861
|
expect(adminPageErrors).toEqual([]);
|
|
1823
1862
|
} finally {
|
|
1824
1863
|
if (skuId && headers) {
|
|
1864
|
+
await adminPage.request.delete(`${stockActionPath}/${skuId}`, { headers }).catch(() => {});
|
|
1825
1865
|
const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
|
|
1826
1866
|
expect(response.ok()).toBeTruthy();
|
|
1827
1867
|
}
|