cabloy 5.1.148 → 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.
Files changed (95) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -1
  3. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
  4. package/CHANGELOG.md +33 -0
  5. package/CLAUDE.md +1 -0
  6. package/package.json +1 -1
  7. package/repo-docs/.vitepress/config.mjs +4 -0
  8. package/repo-docs/backend/dto-guide.md +7 -6
  9. package/repo-docs/backend/dto-infer-generation.md +25 -5
  10. package/repo-docs/backend/markdown-guide.md +1 -0
  11. package/repo-docs/backend/relations-guide.md +1 -0
  12. package/repo-docs/frontend/a-model-under-the-hood.md +38 -0
  13. package/repo-docs/frontend/a-openapi-under-the-hood.md +13 -2
  14. package/repo-docs/frontend/form-layout-guide.md +39 -8
  15. package/repo-docs/frontend/markdown-guide.md +1 -0
  16. package/repo-docs/frontend/model-resource-best-practices.md +2 -0
  17. package/repo-docs/frontend/model-resource-cookbook.md +7 -1
  18. package/repo-docs/frontend/use-state-data-best-practices.md +44 -0
  19. package/repo-docs/frontend/zova-form-source-reading-map.md +26 -9
  20. package/repo-docs/frontend/zova-form-under-the-hood.md +11 -0
  21. package/repo-docs/fullstack/edition-collaboration-differences.md +1 -0
  22. package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +447 -0
  23. package/repo-e2e/specs/a-commerce.spec.ts +40 -0
  24. package/repo-e2e/specs/cabloy-basic.spec.ts +176 -1
  25. package/vona/packages-cli/cli/package.json +1 -1
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +2 -2
  27. package/vona/packages-cli/cli-set-api/package.json +1 -1
  28. package/vona/pnpm-lock.yaml +83 -75
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +108 -108
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +12 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +19 -1
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +53 -4
  33. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +148 -148
  34. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +37 -3
  35. package/vona/src/suite/a-training/modules/training-student/package.json +1 -0
  36. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +177 -103
  37. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.index.ts +1 -0
  38. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.version.ts +19 -1
  39. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  40. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  41. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +22 -2
  42. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +7 -1
  43. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +22 -2
  44. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +25 -3
  45. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -3
  46. package/vona/src/suite/a-training/modules/training-student/src/entity/studentContent.tsx +36 -0
  47. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +6 -0
  48. package/vona/src/suite/a-training/modules/training-student/src/model/studentContent.ts +22 -0
  49. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +43 -14
  50. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +137 -8
  51. package/zova/packages-zova/zova/package.json +2 -2
  52. package/zova/pnpm-lock.yaml +6 -2
  53. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  54. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +6 -1
  55. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionAdjustStock.ts +34 -0
  56. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +81 -6
  57. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeStockBalance.ts +32 -0
  58. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +44 -44
  59. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +356 -338
  60. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeStockBalance.ts +17 -0
  61. package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionAdjustStock.tsx +35 -0
  62. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +138 -0
  63. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +3 -0
  64. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +3 -0
  65. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +36 -0
  66. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +4 -4
  67. package/zova/src/suite/a-training/modules/training-student/package.json +3 -1
  68. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +44 -44
  69. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +360 -339
  70. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +19 -8
  71. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +2 -0
  72. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  73. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +2 -1
  74. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/model/resource.types.ts +11 -0
  75. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  76. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  77. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +69 -12
  78. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +157 -0
  79. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  80. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +3 -1
  81. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +11 -13
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +9 -7
  83. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -12
  84. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryPersister.ts +17 -0
  85. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryRefetch.ts +34 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +27 -3
  87. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryPersister.test.ts +115 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryRefetch.test.ts +84 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  90. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts +27 -5
  91. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +2 -2
  92. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +4 -0
  93. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +2 -5
  94. package/zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts +179 -0
  95. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -9,6 +9,7 @@ import type { ModelStudent } from 'zova-module-training-student';
9
9
 
10
10
  import { BeanBase } from 'zova';
11
11
  import { TableCell } from 'zova-module-a-table';
12
+ import { ZMarkdownHtml } from 'zova-module-basic-markdown';
12
13
 
13
14
  declare module 'zova-module-a-openapi' {
14
15
  export interface IResourceTableActionRowRecord {
@@ -39,14 +40,24 @@ export class TableCellActionSummary extends BeanBase implements ITableCellRender
39
40
  true,
40
41
  )) as ModelStudent;
41
42
  const querySummary = modelStudent.summary(id);
42
- const { data: summary } = await querySummary.refetch();
43
- const message = [
44
- `${this.scope.locale.Id()}: ${summary?.id ?? '-'}`,
45
- `${this.scope.locale.Name()}: ${summary?.name ?? '-'}`,
46
- `${this.scope.locale.Level()}: ${summary?.level ?? '-'}`,
47
- `${this.scope.locale.Description()}: ${summary?.description ?? '-'}`,
48
- ].join('\n');
49
- await $host.$performCommand('basic-commands:alert', { text: message }, renderContext);
43
+ await querySummary.refetch({ bypassPersister: true });
44
+ $host.$appModal.dialog(
45
+ {
46
+ title: this.scope.locale.Summary(),
47
+ slotDefault: () => (
48
+ <div class="student-summary-description">
49
+ <ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />
50
+ </div>
51
+ ),
52
+ },
53
+ {
54
+ maxWidth: 720,
55
+ maxHeight: 'calc(100vh - 2rem)',
56
+ closeOnBackdrop: true,
57
+ closeOnEscape: true,
58
+ showCloseButton: true,
59
+ },
60
+ );
50
61
  }}
51
62
  >
52
63
  {this.scope.locale.Summary()}
@@ -290,6 +290,8 @@ export class BehaviorAppModal extends BeanBehaviorBase<
290
290
  }}
291
291
  ></div>
292
292
  <div
293
+ role="dialog"
294
+ aria-modal="true"
293
295
  class="card bg-base-100 shadow-2xl relative w-full max-h-[calc(100vh-2rem)]"
294
296
  style={style}
295
297
  >
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-rest-resource",
3
- "version": "5.1.46",
3
+ "version": "5.1.47",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,5 +1,5 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { DataMutation, IDecoratorModelOptions } from 'zova-module-a-model';
2
+ import type { DataMutation, IDecoratorModelOptions, QueryMetaPersister } from 'zova-module-a-model';
3
3
  import type {
4
4
  IFormMeta,
5
5
  IFormProvider,
@@ -27,6 +27,7 @@ interface IModelResourceQueryItemOptions<TData> {
27
27
  meta?: {
28
28
  disableSuspenseOnInit?: boolean;
29
29
  disableErrorEffect?: boolean;
30
+ persister?: QueryMetaPersister | boolean;
30
31
  };
31
32
  }
32
33
 
@@ -17,6 +17,17 @@ void requiredBody.delete(id).mutateAsync({ reason: 'duplicate' });
17
17
  void optionalBody.delete(id).mutateAsync();
18
18
  void optionalBody.delete(id).mutateAsync({ reason: 'duplicate' });
19
19
 
20
+ const queryItem = standard.queryItem({
21
+ id,
22
+ action: 'summary',
23
+ queryFn: async () => ({ id: 1 }),
24
+ });
25
+ void queryItem.refetch({
26
+ bypassPersister: true,
27
+ cancelRefetch: false,
28
+ throwOnError: true,
29
+ });
30
+
20
31
  // @ts-expect-error required DELETE bodies cannot be omitted
21
32
  void requiredBody.delete(id).mutateAsync();
22
33
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-cabloy",
3
- "version": "5.1.47",
3
+ "version": "5.1.48",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "zova-module-a-rbac": "^5.0.1",
11
- "zova-module-rest-resource": "^5.1.46"
11
+ "zova-module-rest-resource": "^5.1.47"
12
12
  },
13
13
  "title": "a-cabloy"
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-form",
3
- "version": "5.1.48",
3
+ "version": "5.1.50",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -24,12 +24,21 @@ export function resolveFormLayout(
24
24
  layout: IFormLayout,
25
25
  properties: ISchemaObjectExtensionField[] | undefined,
26
26
  ): IResolvedFormLayout {
27
+ const visibleProperties = properties?.filter(item => item.rest?.visible !== false) ?? [];
27
28
  const propertyNames = new Set(
28
- properties
29
- ?.filter(item => item.rest?.visible !== false)
30
- .map(item => item.key)
31
- .filter(Boolean) as string[],
29
+ visibleProperties.map(item => item.key).filter(Boolean) as string[],
32
30
  );
31
+ const propertyAliases = new Map<string, Set<string>>();
32
+ for (const property of visibleProperties) {
33
+ if (!property.key) continue;
34
+ const schemaKeys = [property.schemaKey, ...(property.schemaKeys ?? [])];
35
+ for (const schemaKey of schemaKeys) {
36
+ if (!schemaKey || schemaKey === property.key) continue;
37
+ const aliases = propertyAliases.get(schemaKey) ?? new Set<string>();
38
+ aliases.add(property.key);
39
+ propertyAliases.set(schemaKey, aliases);
40
+ }
41
+ }
33
42
  const fieldNames = new Set<string>();
34
43
  const nodeIds = new Set<string>();
35
44
  const diagnostics: IFormLayoutDiagnostic[] = [];
@@ -41,6 +50,7 @@ export function resolveFormLayout(
41
50
  [index],
42
51
  [],
43
52
  propertyNames,
53
+ propertyAliases,
44
54
  fieldNames,
45
55
  nodeIds,
46
56
  diagnostics,
@@ -62,13 +72,22 @@ function resolveNode(
62
72
  indexPath: number[],
63
73
  tabPath: IResolvedFormLayout['fieldTabPaths'][string],
64
74
  propertyNames: Set<string>,
75
+ propertyAliases: Map<string, Set<string>>,
65
76
  fieldNames: Set<string>,
66
77
  nodeIds: Set<string>,
67
78
  diagnostics: IFormLayoutDiagnostic[],
68
79
  fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
69
80
  ): IResolvedFormLayoutNode | undefined {
70
81
  if (node.type === 'field') {
71
- return resolveField(node, tabPath, propertyNames, fieldNames, diagnostics, fieldTabPaths);
82
+ return resolveField(
83
+ node,
84
+ tabPath,
85
+ propertyNames,
86
+ propertyAliases,
87
+ fieldNames,
88
+ diagnostics,
89
+ fieldTabPaths,
90
+ );
72
91
  }
73
92
  if (node.type === 'block') {
74
93
  return node;
@@ -78,6 +97,7 @@ function resolveNode(
78
97
  node,
79
98
  indexPath,
80
99
  propertyNames,
100
+ propertyAliases,
81
101
  fieldNames,
82
102
  nodeIds,
83
103
  diagnostics,
@@ -89,7 +109,15 @@ function resolveNode(
89
109
  if (node.type === 'section') {
90
110
  const children = node.children
91
111
  .map(item =>
92
- resolveLeaf(item, tabPath, propertyNames, fieldNames, diagnostics, fieldTabPaths),
112
+ resolveLeaf(
113
+ item,
114
+ tabPath,
115
+ propertyNames,
116
+ propertyAliases,
117
+ fieldNames,
118
+ diagnostics,
119
+ fieldTabPaths,
120
+ ),
93
121
  )
94
122
  .filter(Boolean) as IResolvedFormLayoutLeaf[];
95
123
  return children.length ? { ...node, id, children } : undefined;
@@ -101,6 +129,7 @@ function resolveNode(
101
129
  [...indexPath, index],
102
130
  tabPath,
103
131
  propertyNames,
132
+ propertyAliases,
104
133
  fieldNames,
105
134
  nodeIds,
106
135
  diagnostics,
@@ -117,18 +146,28 @@ function resolveLeaf(
117
146
  node: IFormLayoutLeaf,
118
147
  tabPath: IResolvedFormLayout['fieldTabPaths'][string],
119
148
  propertyNames: Set<string>,
149
+ propertyAliases: Map<string, Set<string>>,
120
150
  fieldNames: Set<string>,
121
151
  diagnostics: IFormLayoutDiagnostic[],
122
152
  fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
123
153
  ): IResolvedFormLayoutLeaf | undefined {
124
154
  if (node.type === 'block') return node;
125
- return resolveField(node, tabPath, propertyNames, fieldNames, diagnostics, fieldTabPaths);
155
+ return resolveField(
156
+ node,
157
+ tabPath,
158
+ propertyNames,
159
+ propertyAliases,
160
+ fieldNames,
161
+ diagnostics,
162
+ fieldTabPaths,
163
+ );
126
164
  }
127
165
 
128
166
  function resolveTabs(
129
167
  node: IFormLayoutTabs,
130
168
  indexPath: number[],
131
169
  propertyNames: Set<string>,
170
+ propertyAliases: Map<string, Set<string>>,
132
171
  fieldNames: Set<string>,
133
172
  nodeIds: Set<string>,
134
173
  diagnostics: IFormLayoutDiagnostic[],
@@ -143,6 +182,7 @@ function resolveTabs(
143
182
  [...indexPath, index],
144
183
  id,
145
184
  propertyNames,
185
+ propertyAliases,
146
186
  fieldNames,
147
187
  nodeIds,
148
188
  diagnostics,
@@ -158,6 +198,7 @@ function resolveTab(
158
198
  indexPath: number[],
159
199
  tabsId: string,
160
200
  propertyNames: Set<string>,
201
+ propertyAliases: Map<string, Set<string>>,
161
202
  fieldNames: Set<string>,
162
203
  nodeIds: Set<string>,
163
204
  diagnostics: IFormLayoutDiagnostic[],
@@ -173,6 +214,7 @@ function resolveTab(
173
214
  [...indexPath, index],
174
215
  tabPath,
175
216
  propertyNames,
217
+ propertyAliases,
176
218
  fieldNames,
177
219
  nodeIds,
178
220
  diagnostics,
@@ -189,21 +231,36 @@ function resolveField(
189
231
  node: IFormLayoutField,
190
232
  tabPath: IResolvedFormLayout['fieldTabPaths'][string],
191
233
  propertyNames: Set<string>,
234
+ propertyAliases: Map<string, Set<string>>,
192
235
  fieldNames: Set<string>,
193
236
  diagnostics: IFormLayoutDiagnostic[],
194
237
  fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
195
238
  ): IResolvedFormLayoutField | undefined {
196
- if (!propertyNames.has(node.name)) {
239
+ const name = resolveFieldName(node.name, propertyNames, propertyAliases);
240
+ if (!name) {
197
241
  diagnostics.push({ type: 'unknownField', value: node.name });
198
242
  return;
199
243
  }
200
- if (fieldNames.has(node.name)) {
244
+ if (fieldNames.has(name)) {
201
245
  diagnostics.push({ type: 'duplicateField', value: node.name });
202
246
  return;
203
247
  }
204
- fieldNames.add(node.name);
205
- fieldTabPaths[node.name] = tabPath;
206
- return node;
248
+ fieldNames.add(name);
249
+ fieldTabPaths[name] = tabPath;
250
+ return name === node.name ? node : { ...node, name };
251
+ }
252
+
253
+ function resolveFieldName(
254
+ name: string,
255
+ propertyNames: Set<string>,
256
+ propertyAliases: Map<string, Set<string>>,
257
+ ) {
258
+ if (propertyNames.has(name)) return name;
259
+ const aliases = propertyAliases.get(name);
260
+ if (aliases?.size === 1) return aliases.values().next().value;
261
+ const fieldSourcePrefix = `${name}.`;
262
+ const fieldSources = [...propertyNames].filter(item => item.startsWith(fieldSourcePrefix));
263
+ return fieldSources.length === 1 ? fieldSources[0] : undefined;
207
264
  }
208
265
 
209
266
  function resolveId(node: { type: string; id?: string }, indexPath: number[]) {
@@ -0,0 +1,157 @@
1
+ import type { IFormLayout, ISchemaObjectExtensionField } from 'zova-module-a-openapi';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import test from 'node:test';
5
+
6
+ import { resolveFormLayout } from '../../src/lib/formLayout.js';
7
+
8
+ function createProperties(...properties: ISchemaObjectExtensionField[]) {
9
+ return properties;
10
+ }
11
+
12
+ function createTabsLayout(name: string): IFormLayout {
13
+ return {
14
+ children: [
15
+ {
16
+ type: 'tabs',
17
+ children: [
18
+ {
19
+ type: 'tab',
20
+ title: 'Content',
21
+ children: [{ type: 'section', children: [{ type: 'field', name }] }],
22
+ },
23
+ ],
24
+ },
25
+ ],
26
+ };
27
+ }
28
+
29
+ function getTabFieldNames(resolved: ReturnType<typeof resolveFormLayout>) {
30
+ const tabs = resolved.children[0] as any;
31
+ return tabs.children[0].children[0].children.map((item: any) => item.name);
32
+ }
33
+
34
+ test('Form Layout resolves a real relation name to its unique canonical source', () => {
35
+ const resolved = resolveFormLayout(
36
+ createTabsLayout('content'),
37
+ createProperties({ key: 'content.descriptionMarkdown', type: 'string' }),
38
+ );
39
+
40
+ assert.deepEqual(getTabFieldNames(resolved), ['content.descriptionMarkdown']);
41
+ assert.deepEqual(resolved.fieldTabPaths, {
42
+ 'content.descriptionMarkdown': [{ tabsId: 'tabs-0', tabId: 'tab-0-0' }],
43
+ });
44
+ assert.deepEqual(resolved.diagnostics, []);
45
+ });
46
+
47
+ test('Form Layout resolves a virtual schema alias to its canonical source', () => {
48
+ const resolved = resolveFormLayout(
49
+ createTabsLayout('_descriptionMarkdown'),
50
+ createProperties({
51
+ key: 'content.descriptionMarkdown',
52
+ schemaKey: '_descriptionMarkdown',
53
+ type: 'string',
54
+ }),
55
+ );
56
+
57
+ assert.deepEqual(getTabFieldNames(resolved), ['content.descriptionMarkdown']);
58
+ assert.deepEqual(resolved.fieldTabPaths, {
59
+ 'content.descriptionMarkdown': [{ tabsId: 'tabs-0', tabId: 'tab-0-0' }],
60
+ });
61
+ assert.deepEqual(resolved.diagnostics, []);
62
+ });
63
+
64
+ test('Form Layout accepts every preserved alias for the same canonical source', () => {
65
+ const resolved = resolveFormLayout(
66
+ createTabsLayout('studentContentForm'),
67
+ createProperties({
68
+ key: 'content.descriptionMarkdown',
69
+ schemaKey: '_descriptionMarkdown',
70
+ schemaKeys: ['studentContentForm'],
71
+ type: 'string',
72
+ }),
73
+ );
74
+
75
+ assert.deepEqual(getTabFieldNames(resolved), ['content.descriptionMarkdown']);
76
+ assert.deepEqual(resolved.diagnostics, []);
77
+ });
78
+
79
+ test('Form Layout prefers an exact canonical field over a colliding alias', () => {
80
+ const resolved = resolveFormLayout(
81
+ { children: [{ type: 'field', name: 'content' }] },
82
+ createProperties(
83
+ { key: 'content', type: 'object' },
84
+ {
85
+ key: 'content.descriptionMarkdown',
86
+ schemaKey: 'content',
87
+ type: 'string',
88
+ },
89
+ ),
90
+ );
91
+
92
+ assert.deepEqual(resolved.children, [
93
+ { type: 'field', name: 'content' },
94
+ { type: 'field', name: 'content.descriptionMarkdown' },
95
+ ]);
96
+ assert.deepEqual(resolved.diagnostics, []);
97
+ });
98
+
99
+ test('Form Layout detects duplicate alias and canonical declarations by canonical key', () => {
100
+ const resolved = resolveFormLayout(
101
+ {
102
+ children: [
103
+ { type: 'field', name: '_descriptionMarkdown' },
104
+ { type: 'field', name: 'content.descriptionMarkdown' },
105
+ ],
106
+ },
107
+ createProperties({
108
+ key: 'content.descriptionMarkdown',
109
+ schemaKey: '_descriptionMarkdown',
110
+ type: 'string',
111
+ }),
112
+ );
113
+
114
+ assert.deepEqual(resolved.children, [{ type: 'field', name: 'content.descriptionMarkdown' }]);
115
+ assert.deepEqual(resolved.diagnostics, [
116
+ { type: 'duplicateField', value: 'content.descriptionMarkdown' },
117
+ ]);
118
+ });
119
+
120
+ test('Form Layout keeps ambiguous aliases and relation prefixes unknown', () => {
121
+ const resolved = resolveFormLayout(
122
+ {
123
+ children: [
124
+ { type: 'field', name: '_content' },
125
+ { type: 'field', name: 'content' },
126
+ ],
127
+ },
128
+ createProperties(
129
+ { key: 'content.descriptionMarkdown', schemaKey: '_content', type: 'string' },
130
+ { key: 'content.descriptionHtml', schemaKey: '_content', type: 'string' },
131
+ ),
132
+ );
133
+
134
+ assert.deepEqual(resolved.children, [
135
+ { type: 'field', name: 'content.descriptionMarkdown' },
136
+ { type: 'field', name: 'content.descriptionHtml' },
137
+ ]);
138
+ assert.deepEqual(resolved.diagnostics, [
139
+ { type: 'unknownField', value: '_content' },
140
+ { type: 'unknownField', value: 'content' },
141
+ ]);
142
+ });
143
+
144
+ test('Form Layout does not resolve or append invisible aliases', () => {
145
+ const resolved = resolveFormLayout(
146
+ { children: [{ type: 'field', name: '_descriptionMarkdown' }] },
147
+ createProperties({
148
+ key: 'content.descriptionMarkdown',
149
+ schemaKey: '_descriptionMarkdown',
150
+ type: 'string',
151
+ rest: { visible: false },
152
+ }),
153
+ );
154
+
155
+ assert.deepEqual(resolved.children, []);
156
+ assert.deepEqual(resolved.diagnostics, [{ type: 'unknownField', value: '_descriptionMarkdown' }]);
157
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-model",
3
- "version": "5.1.35",
3
+ "version": "5.1.36",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "core",
6
6
  "keywords": [
@@ -7,6 +7,7 @@ import { SymbolBeanFullName } from 'zova';
7
7
  import type { QueryMetaPersister } from '../../types/index.js';
8
8
 
9
9
  import { CookieWrapper } from '../../common/cookieWrapper.js';
10
+ import { createQueryPersister } from '../../lib/queryPersister.js';
10
11
  import { resolveMaxAgeTime } from '../../types/index.js';
11
12
  import { BeanModelLast } from './bean.model.last.js';
12
13
 
@@ -145,13 +146,14 @@ export class BeanModelPersister extends BeanModelLast {
145
146
  protected _createPersister(options?: QueryMetaPersister | boolean): any {
146
147
  options = this._adjustPersisterOptions(options);
147
148
  if (!options) return undefined;
148
- return experimental_createQueryPersister({
149
+ const persisterFn = experimental_createQueryPersister({
149
150
  storage: this._getPersisterStorage(options) as any,
150
151
  maxAge: options.maxAge as number,
151
152
  refetchOnRestore: options.refetchOnRestore,
152
153
  prefix: options.prefix,
153
154
  buster: options.buster,
154
155
  }).persisterFn;
156
+ return createQueryPersister(persisterFn);
155
157
  }
156
158
 
157
159
  protected _adjustPersisterOptions(options?: QueryMetaPersister | boolean) {
@@ -1,12 +1,4 @@
1
- import type {
2
- DefaultError,
3
- QueryClient,
4
- QueryKey,
5
- UseQueryDefinedReturnType,
6
- UseQueryOptions,
7
- UseQueryReturnType,
8
- } from '@tanstack/vue-query';
9
- import type { UnwrapNestedRefs } from 'vue';
1
+ import type { DefaultError, QueryClient, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
10
2
 
11
3
  import { checkErrorJwtExpired } from '@cabloy/utils';
12
4
  import { useQuery } from '@tanstack/vue-query';
@@ -16,7 +8,9 @@ import type {
16
8
  DefinedInitialQueryOptions,
17
9
  UndefinedInitialQueryOptions,
18
10
  } from '../../common/types.js';
11
+ import type { ModelUseQueryDefinedReturnType, ModelUseQueryReturnType } from '../../types/query.js';
19
12
 
13
+ import { createQueryRefetch } from '../../lib/queryRefetch.js';
20
14
  import { resolveStaleTime } from '../../types/index.js';
21
15
  import { BeanModelQuery } from './bean.model.query.js';
22
16
 
@@ -29,7 +23,7 @@ export class BeanModelUseQuery extends BeanModelQuery {
29
23
  >(
30
24
  options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
31
25
  queryClient?: QueryClient,
32
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
26
+ ): ModelUseQueryReturnType<TData, TError>;
33
27
  $useQuery<
34
28
  TQueryFnData = unknown,
35
29
  TError = DefaultError,
@@ -38,7 +32,7 @@ export class BeanModelUseQuery extends BeanModelQuery {
38
32
  >(
39
33
  options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
40
34
  queryClient?: QueryClient,
41
- ): UnwrapNestedRefs<UseQueryDefinedReturnType<TData, TError>>;
35
+ ): ModelUseQueryDefinedReturnType<TData, TError>;
42
36
  $useQuery<
43
37
  TQueryFnData = unknown,
44
38
  TError = DefaultError,
@@ -47,7 +41,7 @@ export class BeanModelUseQuery extends BeanModelQuery {
47
41
  >(
48
42
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
49
43
  queryClient?: QueryClient,
50
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
44
+ ): ModelUseQueryReturnType<TData, TError>;
51
45
  $useQuery(options, queryClient) {
52
46
  const queryKey = this.self._forceQueryKeyPrefix(options.queryKey);
53
47
  const persister = this._createPersister(options.meta?.persister);
@@ -81,7 +75,11 @@ export class BeanModelUseQuery extends BeanModelQuery {
81
75
  };
82
76
  }
83
77
  return this.ctx.util.instanceScope(() => {
84
- return useQuery(options, queryClient);
78
+ const query = useQuery(options, queryClient) as unknown as ModelUseQueryReturnType<any, any>;
79
+ query.refetch = createQueryRefetch(query.refetch, () =>
80
+ this.$queryFind({ queryKey }),
81
+ ) as typeof query.refetch;
82
+ return query;
85
83
  });
86
84
  }
87
85
  }
@@ -4,11 +4,8 @@ import type {
4
4
  QueryClient,
5
5
  QueryKey,
6
6
  QueryMeta,
7
- UseQueryDefinedReturnType,
8
7
  UseQueryOptions,
9
- UseQueryReturnType,
10
8
  } from '@tanstack/vue-query';
11
- import type { UnwrapNestedRefs } from 'vue';
12
9
 
13
10
  import { hashKey } from '@tanstack/vue-query';
14
11
  import { deepExtend } from 'zova';
@@ -17,7 +14,12 @@ import type {
17
14
  DefinedInitialQueryOptions,
18
15
  UndefinedInitialQueryOptions,
19
16
  } from '../../common/types.js';
20
- import type { QueryMetaPersister, UseQueryComputedOptions } from '../../types/query.js';
17
+ import type {
18
+ ModelUseQueryDefinedReturnType,
19
+ ModelUseQueryReturnType,
20
+ QueryMetaPersister,
21
+ UseQueryComputedOptions,
22
+ } from '../../types/query.js';
21
23
 
22
24
  import { BeanModelUseQuery } from './bean.model.useQuery.js';
23
25
 
@@ -311,7 +313,7 @@ export class BeanModelUseState extends BeanModelUseQuery {
311
313
  >(
312
314
  options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
313
315
  queryClient?: QueryClient,
314
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
316
+ ): ModelUseQueryReturnType<TData, TError>;
315
317
  $useStateData<
316
318
  TQueryFnData = unknown,
317
319
  TError = DefaultError,
@@ -320,7 +322,7 @@ export class BeanModelUseState extends BeanModelUseQuery {
320
322
  >(
321
323
  options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
322
324
  queryClient?: QueryClient,
323
- ): UnwrapNestedRefs<UseQueryDefinedReturnType<TData, TError>>;
325
+ ): ModelUseQueryDefinedReturnType<TData, TError>;
324
326
  $useStateData<
325
327
  TQueryFnData = unknown,
326
328
  TError = DefaultError,
@@ -329,7 +331,7 @@ export class BeanModelUseState extends BeanModelUseQuery {
329
331
  >(
330
332
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
331
333
  queryClient?: QueryClient,
332
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
334
+ ): ModelUseQueryReturnType<TData, TError>;
333
335
  $useStateData(options, queryClient) {
334
336
  const queryKey = this.self._forceQueryKeyPrefix(options.queryKey);
335
337
  const queryHash = hashKey(queryKey);
@@ -1,17 +1,10 @@
1
- import type {
2
- DefaultError,
3
- QueryClient,
4
- QueryKey,
5
- UseQueryDefinedReturnType,
6
- UseQueryOptions,
7
- UseQueryReturnType,
8
- } from '@tanstack/vue-query';
9
- import type { UnwrapNestedRefs } from 'vue';
1
+ import type { DefaultError, QueryClient, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
10
2
 
11
3
  import type {
12
4
  DefinedInitialQueryOptions,
13
5
  UndefinedInitialQueryOptions,
14
6
  } from '../../common/types.js';
7
+ import type { ModelUseQueryDefinedReturnType, ModelUseQueryReturnType } from '../../types/query.js';
15
8
  import type { StateType } from '../../types/query.js';
16
9
 
17
10
  import { BeanModelUseState } from './bean.model.useState.js';
@@ -56,7 +49,7 @@ export class BeanModelUseStateGeneral extends BeanModelUseState {
56
49
  stateType: 'data',
57
50
  options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
58
51
  queryClient?: QueryClient,
59
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
52
+ ): ModelUseQueryReturnType<TData, TError>;
60
53
  $useState<
61
54
  TQueryFnData = unknown,
62
55
  TError = DefaultError,
@@ -66,7 +59,7 @@ export class BeanModelUseStateGeneral extends BeanModelUseState {
66
59
  stateType: 'data',
67
60
  options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
68
61
  queryClient?: QueryClient,
69
- ): UnwrapNestedRefs<UseQueryDefinedReturnType<TData, TError>>;
62
+ ): ModelUseQueryDefinedReturnType<TData, TError>;
70
63
  $useState<
71
64
  TQueryFnData = unknown,
72
65
  TError = DefaultError,
@@ -76,7 +69,7 @@ export class BeanModelUseStateGeneral extends BeanModelUseState {
76
69
  stateType: 'data',
77
70
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
78
71
  queryClient?: QueryClient,
79
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
72
+ ): ModelUseQueryReturnType<TData, TError>;
80
73
  $useState(stateType: StateType, options, queryClient) {
81
74
  switch (stateType) {
82
75
  case 'localAsync':
@@ -0,0 +1,17 @@
1
+ import type { QueryPersister } from '@tanstack/query-core';
2
+
3
+ // TanStack's FetchMeta is internal to the public QueryPersister call path.
4
+ type QueryFetchMeta = {
5
+ bypassPersister?: boolean;
6
+ };
7
+
8
+ export function createQueryPersister<T, TQueryKey extends readonly unknown[]>(
9
+ persisterFn: QueryPersister<T, TQueryKey>,
10
+ ): QueryPersister<T, TQueryKey> {
11
+ return (queryFn, context, query) => {
12
+ if ((query.state.fetchMeta as QueryFetchMeta | null)?.bypassPersister === true) {
13
+ return queryFn(context);
14
+ }
15
+ return persisterFn(queryFn, context, query);
16
+ };
17
+ }