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.
- 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 +33 -0
- package/CLAUDE.md +1 -0
- package/package.json +1 -1
- package/repo-docs/.vitepress/config.mjs +4 -0
- package/repo-docs/backend/dto-guide.md +7 -6
- package/repo-docs/backend/dto-infer-generation.md +25 -5
- package/repo-docs/backend/markdown-guide.md +1 -0
- package/repo-docs/backend/relations-guide.md +1 -0
- 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 +39 -8
- package/repo-docs/frontend/markdown-guide.md +1 -0
- 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/edition-collaboration-differences.md +1 -0
- package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +447 -0
- package/repo-e2e/specs/a-commerce.spec.ts +40 -0
- package/repo-e2e/specs/cabloy-basic.spec.ts +176 -1
- 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/package.json +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +177 -103
- package/vona/src/suite/a-training/modules/training-student/src/bean/meta.index.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/bean/meta.version.ts +19 -1
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +22 -2
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +7 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +22 -2
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +25 -3
- package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -3
- package/vona/src/suite/a-training/modules/training-student/src/entity/studentContent.tsx +36 -0
- package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +6 -0
- package/vona/src/suite/a-training/modules/training-student/src/model/studentContent.ts +22 -0
- package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +43 -14
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +137 -8
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +6 -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/package.json +3 -1
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +44 -44
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +360 -339
- package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +19 -8
- 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 +69 -12
- 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
|
@@ -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> =
|
|
112
|
-
|
|
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
|
+
});
|
|
@@ -21,7 +21,7 @@ export function loadSchemaProperties(
|
|
|
21
21
|
schema: SchemaObject | undefined,
|
|
22
22
|
onGetSchema: (schemaName: string) => SchemaObject | undefined,
|
|
23
23
|
schemaScene?: TypeSchemaScene,
|
|
24
|
-
):
|
|
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
|
|
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
|
-
|
|
39
|
-
parts[0] ===
|
|
40
|
-
|
|
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
|
-
):
|
|
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 {
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
-
"zova-module-a-openapi": "^5.1.
|
|
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",
|