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
@@ -0,0 +1,34 @@
1
+ import type { Query, RefetchOptions } from '@tanstack/query-core';
2
+
3
+ import type { QueryRefetchOptions } from '../types/query.js';
4
+
5
+ type QueryRefetch<TResult> = (options?: RefetchOptions) => Promise<TResult>;
6
+ type QueryRefetchWithFetchOptions<TResult> = (
7
+ options?: RefetchOptions & { meta?: { bypassPersister?: boolean } },
8
+ ) => Promise<TResult>;
9
+
10
+ export function createQueryRefetch<TResult>(
11
+ refetch: QueryRefetch<TResult>,
12
+ getQuery: () => Query | undefined,
13
+ ): (options?: QueryRefetchOptions) => Promise<TResult> {
14
+ return async (refetchOptions?: QueryRefetchOptions) => {
15
+ const { bypassPersister, ...options } = refetchOptions ?? {};
16
+ if (!bypassPersister) return refetch(options);
17
+
18
+ const query = getQuery();
19
+ // A cold query may already be fetching before its result is returned. Query.fetch
20
+ // deduplicates that request before applying the new fetch metadata, so wait for it
21
+ // before issuing the marked fetch.
22
+ if (query && query.state.fetchStatus !== 'idle' && query.state.data === undefined) {
23
+ await refetch({ cancelRefetch: false });
24
+ }
25
+
26
+ // TanStack's public RefetchOptions does not expose the internal fetch metadata
27
+ // forwarded by the installed QueryObserver implementation.
28
+ const refetchWithFetchOptions = refetch as QueryRefetchWithFetchOptions<TResult>;
29
+ return await refetchWithFetchOptions({
30
+ ...options,
31
+ meta: { bypassPersister: true },
32
+ });
33
+ };
34
+ }
@@ -3,8 +3,11 @@ import type {
3
3
  DehydratedState,
4
4
  Query,
5
5
  QueryKey,
6
+ QueryObserverResult,
7
+ RefetchOptions,
8
+ UseQueryDefinedReturnType as TanStackUseQueryDefinedReturnType,
9
+ UseQueryReturnType as TanStackUseQueryReturnType,
6
10
  useMutation,
7
- useQuery,
8
11
  useQueryClient,
9
12
  } from '@tanstack/vue-query';
10
13
  import type { DebuggerOptions, UnwrapNestedRefs } from 'vue';
@@ -32,6 +35,26 @@ declare module '@tanstack/vue-query' {
32
35
 
33
36
  export type { UseQueryOptions } from '@tanstack/vue-query';
34
37
 
38
+ export type QueryRefetchOptions = RefetchOptions & {
39
+ bypassPersister?: boolean;
40
+ };
41
+
42
+ type QueryRefetch<TResult> = (options?: QueryRefetchOptions) => Promise<TResult>;
43
+
44
+ export type ModelUseQueryReturnType<TData, TError> = Omit<
45
+ UnwrapNestedRefs<TanStackUseQueryReturnType<TData, TError>>,
46
+ 'refetch'
47
+ > & {
48
+ refetch: QueryRefetch<QueryObserverResult<TData, TError>>;
49
+ };
50
+
51
+ export type ModelUseQueryDefinedReturnType<TData, TError> = Omit<
52
+ UnwrapNestedRefs<TanStackUseQueryDefinedReturnType<TData, TError>>,
53
+ 'refetch'
54
+ > & {
55
+ refetch: QueryRefetch<QueryObserverResult<TData, TError>>;
56
+ };
57
+
35
58
  export interface MyQueryMetaBase extends Record<string, unknown> {
36
59
  defaultData?: (() => any) | any;
37
60
  ssr?: QueryMetaSSR;
@@ -108,8 +131,9 @@ export interface QueryMetaPersister {
108
131
  refetchOnRestore?: boolean | 'always';
109
132
  }
110
133
 
111
- export type DataQuery<TData, TError = Error | null> = UnwrapNestedRefs<
112
- ReturnType<typeof useQuery<TData | undefined, TError>>
134
+ export type DataQuery<TData, TError = Error | null> = ModelUseQueryReturnType<
135
+ TData | undefined,
136
+ TError
113
137
  >;
114
138
 
115
139
  export type DataMutation<TData = unknown, TVariables = void, TContext = unknown> = UnwrapNestedRefs<
@@ -0,0 +1,115 @@
1
+ import { QueryClient, QueryObserver } from '@tanstack/query-core';
2
+ import { experimental_createQueryPersister } from '@tanstack/query-persist-client-core';
3
+ import assert from 'node:assert/strict';
4
+ import test from 'node:test';
5
+
6
+ import { createQueryPersister } from '../../src/lib/queryPersister.js';
7
+
8
+ type Value = { value: string };
9
+
10
+ function createStorage(initialValue?: Value) {
11
+ const stored = initialValue
12
+ ? JSON.stringify({
13
+ state: { data: initialValue, dataUpdatedAt: Date.now(), errorUpdatedAt: 0 },
14
+ queryKey: ['summary'],
15
+ queryHash: '["summary"]',
16
+ buster: '',
17
+ })
18
+ : undefined;
19
+ let value = stored;
20
+ let reads = 0;
21
+ let writes = 0;
22
+
23
+ return {
24
+ storage: {
25
+ getItem() {
26
+ reads++;
27
+ return value;
28
+ },
29
+ setItem(_key: string, nextValue: string) {
30
+ writes++;
31
+ value = nextValue;
32
+ },
33
+ removeItem() {
34
+ value = undefined;
35
+ },
36
+ },
37
+ get reads() {
38
+ return reads;
39
+ },
40
+ get writes() {
41
+ return writes;
42
+ },
43
+ };
44
+ }
45
+
46
+ function createObserver(storage: ReturnType<typeof createStorage>, queryFn: () => Promise<Value>) {
47
+ const client = new QueryClient();
48
+ const persisterFn = experimental_createQueryPersister({ storage: storage.storage }).persisterFn;
49
+ const observer = new QueryObserver(client, {
50
+ queryKey: ['summary'],
51
+ queryFn,
52
+ enabled: false,
53
+ persister: createQueryPersister(persisterFn),
54
+ });
55
+
56
+ return { client, observer };
57
+ }
58
+
59
+ test('bypassPersister fetches fresh data without restoring or saving persisted data', async () => {
60
+ const storage = createStorage({ value: 'old' });
61
+ let calls = 0;
62
+ const { client, observer } = createObserver(storage, async () => ({ value: `fresh-${++calls}` }));
63
+
64
+ try {
65
+ const result = await observer
66
+ .getCurrentQuery()
67
+ .fetch(undefined, { meta: { bypassPersister: true } as any });
68
+
69
+ assert.deepEqual(result, { value: 'fresh-1' });
70
+ assert.deepEqual(observer.getCurrentQuery().state.data, { value: 'fresh-1' });
71
+ assert.equal(calls, 1);
72
+ assert.equal(storage.reads, 0);
73
+ assert.equal(storage.writes, 0);
74
+ } finally {
75
+ client.clear();
76
+ }
77
+ });
78
+
79
+ test('ordinary refetch still restores persisted data after a bypassed fetch', async () => {
80
+ const storage = createStorage({ value: 'old' });
81
+ let calls = 0;
82
+ const { client, observer } = createObserver(storage, async () => ({ value: `fresh-${++calls}` }));
83
+
84
+ try {
85
+ const bypassed = await observer
86
+ .getCurrentQuery()
87
+ .fetch(undefined, { meta: { bypassPersister: true } as any });
88
+ assert.deepEqual(bypassed, { value: 'fresh-1' });
89
+
90
+ client.removeQueries({ queryKey: ['summary'] });
91
+ const restored = await observer.refetch();
92
+
93
+ assert.deepEqual(restored.data, { value: 'old' });
94
+ assert.equal(calls, 1);
95
+ assert.equal(storage.reads, 1);
96
+ } finally {
97
+ client.clear();
98
+ }
99
+ });
100
+
101
+ test('ordinary refetch restores persisted data without invoking the query function', async () => {
102
+ const storage = createStorage({ value: 'old' });
103
+ let calls = 0;
104
+ const { client, observer } = createObserver(storage, async () => ({ value: `fresh-${++calls}` }));
105
+
106
+ try {
107
+ const result = await observer.refetch();
108
+
109
+ assert.deepEqual(result.data, { value: 'old' });
110
+ assert.equal(calls, 0);
111
+ assert.equal(storage.reads, 1);
112
+ } finally {
113
+ client.clear();
114
+ }
115
+ });
@@ -0,0 +1,84 @@
1
+ import type { Query, RefetchOptions } from '@tanstack/query-core';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import test from 'node:test';
5
+
6
+ import { createQueryRefetch } from '../../src/lib/queryRefetch.js';
7
+
8
+ type Result = { data: string };
9
+
10
+ function createQuery(state: Query['state']) {
11
+ return { state } as Query;
12
+ }
13
+
14
+ test('bypassPersister waits for a cold initial fetch before starting its own marked fetch', async () => {
15
+ let resolveInitial: (() => void) | undefined;
16
+ const initial = new Promise<void>(resolve => {
17
+ resolveInitial = resolve;
18
+ });
19
+ const calls: Array<RefetchOptions | undefined> = [];
20
+ const query = createQuery({
21
+ fetchStatus: 'fetching',
22
+ data: undefined,
23
+ fetchMeta: null,
24
+ } as Query['state']);
25
+ const refetch = async (options?: RefetchOptions): Promise<Result> => {
26
+ calls.push(options);
27
+ if (calls.length === 1) {
28
+ await initial;
29
+ query.state.fetchStatus = 'idle';
30
+ return { data: 'initial' };
31
+ }
32
+ return { data: 'fresh' };
33
+ };
34
+ const wrapped = createQueryRefetch(refetch, () => query);
35
+
36
+ const result = wrapped({ bypassPersister: true, cancelRefetch: false, throwOnError: true });
37
+ assert.deepEqual(calls, [{ cancelRefetch: false }]);
38
+
39
+ resolveInitial!();
40
+ assert.deepEqual(await result, { data: 'fresh' });
41
+ assert.deepEqual(calls, [
42
+ { cancelRefetch: false },
43
+ { cancelRefetch: false, throwOnError: true, meta: { bypassPersister: true } },
44
+ ]);
45
+ });
46
+
47
+ test('ordinary refetch forwards native options unchanged', async () => {
48
+ const calls: Array<RefetchOptions | undefined> = [];
49
+ const refetch = async (options?: RefetchOptions): Promise<Result> => {
50
+ calls.push(options);
51
+ return { data: 'result' };
52
+ };
53
+ const wrapped = createQueryRefetch(refetch, () => undefined);
54
+ const options = { cancelRefetch: false, throwOnError: true };
55
+
56
+ assert.deepEqual(await wrapped(options), { data: 'result' });
57
+ assert.deepEqual(calls, [options]);
58
+ });
59
+
60
+ test('false bypassPersister retains native refetch behavior', async () => {
61
+ const calls: Array<RefetchOptions | undefined> = [];
62
+ const refetch = async (options?: RefetchOptions): Promise<Result> => {
63
+ calls.push(options);
64
+ return { data: 'result' };
65
+ };
66
+ const wrapped = createQueryRefetch(refetch, () => undefined);
67
+
68
+ assert.deepEqual(await wrapped({ bypassPersister: false }), { data: 'result' });
69
+ assert.deepEqual(calls, [{}]);
70
+ });
71
+
72
+ test('bypassPersister leaves the caller options object unchanged', async () => {
73
+ const calls: Array<RefetchOptions | undefined> = [];
74
+ const refetch = async (options?: RefetchOptions): Promise<Result> => {
75
+ calls.push(options);
76
+ return { data: 'result' };
77
+ };
78
+ const wrapped = createQueryRefetch(refetch, () => undefined);
79
+ const options = { bypassPersister: true, cancelRefetch: false };
80
+
81
+ await wrapped(options);
82
+ assert.deepEqual(options, { bypassPersister: true, cancelRefetch: false });
83
+ assert.deepEqual(calls, [{ cancelRefetch: false, meta: { bypassPersister: true } }]);
84
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-openapi",
3
- "version": "5.1.50",
3
+ "version": "5.1.51",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -21,7 +21,7 @@ export function loadSchemaProperties(
21
21
  schema: SchemaObject | undefined,
22
22
  onGetSchema: (schemaName: string) => SchemaObject | undefined,
23
23
  schemaScene?: TypeSchemaScene,
24
- ): SchemaObject[] | undefined {
24
+ ): ISchemaObjectExtensionField[] | undefined {
25
25
  if (!schema) return;
26
26
  const properties = schema.properties!;
27
27
  const result: ISchemaObjectExtensionField[] = [];
@@ -32,12 +32,26 @@ export function loadSchemaProperties(
32
32
  property = onGetSchema(property.$ref)!;
33
33
  }
34
34
  if (!property) continue;
35
- const fieldSource = property.rest?.fieldSource;
35
+ const schemaKey = key;
36
+ const propertyRest = deepExtend(
37
+ {},
38
+ property.rest,
39
+ schemaScene && ['form-view', 'form-create', 'filter'].includes(schemaScene)
40
+ ? property.rest?.form
41
+ : undefined,
42
+ schemaScene ? property.rest?.[schemaScene] : undefined,
43
+ );
44
+ const fieldSource = propertyRest?.fieldSource;
36
45
  if (fieldSource) {
37
46
  const parts = fieldSource.split('.');
38
- const propertyParent: any =
39
- parts[0] === key ? property : result.find(item => item.key === parts[0]);
40
- property = propertyParent?.properties[parts[1]];
47
+ let propertyParent =
48
+ parts[0] === schemaKey
49
+ ? property
50
+ : (properties[parts[0]] as ISchemaObjectExtensionField | undefined);
51
+ if (propertyParent?.$ref) {
52
+ propertyParent = onGetSchema(propertyParent.$ref)!;
53
+ }
54
+ property = propertyParent?.properties?.[parts[1]] as ISchemaObjectExtensionField;
41
55
  key = fieldSource;
42
56
  }
43
57
  if (!property) continue;
@@ -49,6 +63,14 @@ export function loadSchemaProperties(
49
63
  : undefined,
50
64
  schemaScene ? { rest: property.rest?.[schemaScene] ?? {} } : undefined,
51
65
  );
66
+ if (schemaKey !== key) {
67
+ property.schemaKey = schemaKey;
68
+ }
69
+ const propertyExisting = result.find(item => item.key === key);
70
+ if (propertyExisting) {
71
+ propertyExisting.schemaKeys = [...(propertyExisting.schemaKeys ?? []), schemaKey];
72
+ continue;
73
+ }
52
74
  result.push(property);
53
75
  }
54
76
  // sort
@@ -20,7 +20,7 @@ import {
20
20
  schemaToZodSchema,
21
21
  } from '../lib/schema.js';
22
22
  import { TypeOpenapiPermissions } from '../types/permissions.js';
23
- import { TypeSchemaScene } from '../types/rest.js';
23
+ import { ISchemaObjectExtensionField, TypeSchemaScene } from '../types/rest.js';
24
24
  import { IOpenapiSchemas, TypeOpenapiSchemasSdk } from '../types/schema.js';
25
25
  import { TypeRequestMethod } from '../types/sdk.js';
26
26
 
@@ -236,7 +236,7 @@ export class ModelSdk extends BeanModelBase {
236
236
  public loadSchemaProperties(
237
237
  schema: SchemaObject | undefined,
238
238
  schemaScene?: TypeSchemaScene,
239
- ): SchemaObject[] | undefined {
239
+ ): ISchemaObjectExtensionField[] | undefined {
240
240
  return loadSchemaProperties(schema, schemaName => this.getSchema(schemaName).data, schemaScene);
241
241
  }
242
242
 
@@ -47,6 +47,10 @@ export interface ISchemaObjectExtensionFieldFilter {
47
47
 
48
48
  export interface ISchemaObjectExtensionField extends SchemaObject {
49
49
  key?: string;
50
+ /** Original property name before fieldSource normalization. */
51
+ schemaKey?: string;
52
+ /** Additional schema aliases that resolve to this canonical key. */
53
+ schemaKeys?: string[];
50
54
  rest?: ISchemaObjectExtensionFieldRest;
51
55
  filter?: ISchemaObjectExtensionFieldFilter;
52
56
  }
@@ -1,6 +1,5 @@
1
- import type { DefaultError, UseQueryReturnType } from '@tanstack/vue-query';
2
1
  import type { OpenAPIObject, SchemaObject } from 'openapi3-ts/oas31';
3
- import type { UnwrapNestedRefs } from 'vue';
2
+ import type { ModelUseQueryReturnType } from 'zova-module-a-model';
4
3
 
5
4
  import type { IOpenapiSdkItem } from './sdk.js';
6
5
 
@@ -11,9 +10,7 @@ export interface IOpenapiSchema {
11
10
 
12
11
  export interface IOpenapiSchemaMeta {}
13
12
 
14
- export type TypeOpenapiSchemasSdk = UnwrapNestedRefs<
15
- UseQueryReturnType<IOpenapiSdkItem, DefaultError>
16
- >;
13
+ export type TypeOpenapiSchemasSdk = ModelUseQueryReturnType<IOpenapiSdkItem, Error>;
17
14
 
18
15
  export interface IOpenapiSchemas {
19
16
  sdk: TypeOpenapiSchemasSdk;
@@ -0,0 +1,179 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import type { ISchemaObjectExtensionField } from '../../src/types/rest.js';
5
+
6
+ import { loadSchemaProperties } from '../../src/lib/schema.js';
7
+
8
+ const onGetSchema = () => undefined;
9
+
10
+ function field(field: ISchemaObjectExtensionField) {
11
+ return field;
12
+ }
13
+
14
+ test('schema properties preserve the canonical fieldSource key and original schema key', () => {
15
+ const properties = loadSchemaProperties(
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ content: field({
20
+ type: 'object',
21
+ rest: { fieldSource: 'content.descriptionMarkdown' },
22
+ properties: {
23
+ descriptionMarkdown: field({
24
+ type: 'string',
25
+ rest: { render: 'start-markdown:formFieldMarkdown' as any },
26
+ }),
27
+ },
28
+ }),
29
+ },
30
+ },
31
+ onGetSchema,
32
+ );
33
+
34
+ assert.deepEqual(properties, [
35
+ {
36
+ key: 'content.descriptionMarkdown',
37
+ schemaKey: 'content',
38
+ type: 'string',
39
+ rest: { render: 'start-markdown:formFieldMarkdown' },
40
+ },
41
+ ]);
42
+ });
43
+
44
+ test('schema properties preserve virtual aliases without changing the form key', () => {
45
+ const properties = loadSchemaProperties(
46
+ {
47
+ type: 'object',
48
+ properties: {
49
+ content: field({
50
+ type: 'object',
51
+ rest: { visible: false },
52
+ properties: {
53
+ descriptionMarkdown: field({ type: 'string' }),
54
+ },
55
+ }),
56
+ _descriptionMarkdown: field({
57
+ type: 'object',
58
+ rest: { fieldSource: 'content.descriptionMarkdown' },
59
+ }),
60
+ },
61
+ },
62
+ onGetSchema,
63
+ );
64
+
65
+ assert.deepEqual(properties?.[1], {
66
+ key: 'content.descriptionMarkdown',
67
+ schemaKey: '_descriptionMarkdown',
68
+ type: 'string',
69
+ });
70
+ });
71
+
72
+ test('schema properties normalize a scene-specific fieldSource', () => {
73
+ const properties = loadSchemaProperties(
74
+ {
75
+ type: 'object',
76
+ properties: {
77
+ content: field({
78
+ type: 'object',
79
+ rest: { visible: false },
80
+ properties: {
81
+ descriptionMarkdown: field({ type: 'string' }),
82
+ },
83
+ }),
84
+ _descriptionMarkdown: field({
85
+ type: 'object',
86
+ rest: { form: { fieldSource: 'content.descriptionMarkdown' } },
87
+ }),
88
+ },
89
+ },
90
+ onGetSchema,
91
+ 'form',
92
+ );
93
+
94
+ assert.deepEqual(properties?.[1], {
95
+ key: 'content.descriptionMarkdown',
96
+ schemaKey: '_descriptionMarkdown',
97
+ type: 'string',
98
+ });
99
+ });
100
+
101
+ test('schema properties coalesce aliases when the source property has fieldSource', () => {
102
+ const properties = loadSchemaProperties(
103
+ {
104
+ type: 'object',
105
+ properties: {
106
+ content: field({
107
+ type: 'object',
108
+ rest: { fieldSource: 'content.descriptionMarkdown' },
109
+ properties: {
110
+ descriptionMarkdown: field({ type: 'string' }),
111
+ },
112
+ }),
113
+ _descriptionMarkdown: field({
114
+ type: 'object',
115
+ rest: { fieldSource: 'content.descriptionMarkdown' },
116
+ }),
117
+ studentContentForm: field({
118
+ type: 'object',
119
+ rest: { fieldSource: 'content.descriptionMarkdown' },
120
+ }),
121
+ },
122
+ },
123
+ onGetSchema,
124
+ );
125
+
126
+ assert.deepEqual(properties, [
127
+ {
128
+ key: 'content.descriptionMarkdown',
129
+ schemaKey: 'content',
130
+ schemaKeys: ['_descriptionMarkdown', 'studentContentForm'],
131
+ type: 'string',
132
+ },
133
+ ]);
134
+ });
135
+
136
+ test('schema properties coalesce aliases declared before their fieldSource parent', () => {
137
+ const properties = loadSchemaProperties(
138
+ {
139
+ type: 'object',
140
+ properties: {
141
+ _descriptionMarkdown: field({
142
+ type: 'object',
143
+ rest: { fieldSource: 'content.descriptionMarkdown' },
144
+ }),
145
+ content: field({
146
+ type: 'object',
147
+ rest: { fieldSource: 'content.descriptionMarkdown' },
148
+ properties: {
149
+ descriptionMarkdown: field({ type: 'string' }),
150
+ },
151
+ }),
152
+ },
153
+ },
154
+ onGetSchema,
155
+ );
156
+
157
+ assert.deepEqual(properties, [
158
+ {
159
+ key: 'content.descriptionMarkdown',
160
+ schemaKey: '_descriptionMarkdown',
161
+ schemaKeys: ['content'],
162
+ type: 'string',
163
+ },
164
+ ]);
165
+ });
166
+
167
+ test('ordinary schema properties do not gain a schema alias', () => {
168
+ const properties = loadSchemaProperties(
169
+ {
170
+ type: 'object',
171
+ properties: {
172
+ name: field({ type: 'string' }),
173
+ },
174
+ },
175
+ onGetSchema,
176
+ );
177
+
178
+ assert.deepEqual(properties, [{ key: 'name', type: 'string' }]);
179
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-zova",
3
- "version": "5.1.152",
3
+ "version": "5.1.154",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "zova",
6
6
  "license": "MIT",
@@ -16,13 +16,13 @@
16
16
  "zova-module-a-boundary": "^5.1.22",
17
17
  "zova-module-a-command": "^5.1.34",
18
18
  "zova-module-a-fetch": "^5.1.25",
19
- "zova-module-a-form": "^5.1.48",
19
+ "zova-module-a-form": "^5.1.50",
20
20
  "zova-module-a-icon": "^5.1.27",
21
21
  "zova-module-a-interceptor": "^5.1.31",
22
22
  "zova-module-a-logger": "^5.1.28",
23
23
  "zova-module-a-meta": "^5.1.22",
24
- "zova-module-a-model": "^5.1.35",
25
- "zova-module-a-openapi": "^5.1.50",
24
+ "zova-module-a-model": "^5.1.36",
25
+ "zova-module-a-openapi": "^5.1.51",
26
26
  "zova-module-a-router": "^5.1.34",
27
27
  "zova-module-a-routerstack": "^5.1.27",
28
28
  "zova-module-a-routertabs": "^5.1.34",