cabloy 5.1.149 → 5.1.151
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/commands/release.md +12 -1
- package/.claude/hooks/contract-loop-gate.ts +35 -17
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +4 -3
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +3 -2
- package/.claude/skills/cabloy-contract-loop/SKILL.md +4 -3
- package/.claude/skills/cabloy-domain-planning/SKILL.md +4 -3
- package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +5 -3
- package/.claude/skills/cabloy-master-detail/SKILL.md +4 -3
- package/.claude/skills/cabloy-module-removal/SKILL.md +5 -4
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +8 -5
- package/.claude/skills/cabloy-spec-execution/SKILL.md +169 -0
- package/.claude/skills/cabloy-spec-execution/evals/evals.json +71 -0
- package/.claude/skills/cabloy-spec-execution/references/execution-protocol.md +137 -0
- package/.claude/skills/cabloy-spec-execution/references/status-and-evidence.md +117 -0
- package/.claude/skills/cabloy-spec-generation/SKILL.md +227 -0
- package/.claude/skills/cabloy-spec-generation/evals/evals.json +83 -0
- package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +101 -0
- package/.claude/skills/cabloy-spec-generation/references/repo-specs-document-set.md +267 -0
- package/.claude/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +135 -0
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +350 -0
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +202 -0
- package/.claude/skills/cabloy-workflow/SKILL.md +21 -10
- package/.claude/skills/cabloy-workflow/evals/evals.json +8 -2
- package/.claude/skills/cabloy-workflow/references/edition-detection.md +5 -3
- package/.claude/skills/cabloy-zova-source-reading/SKILL.md +4 -3
- package/CHANGELOG.md +41 -0
- package/CLAUDE.md +12 -11
- package/package.json +4 -2
- package/repo-docs/.vitepress/config.mjs +4 -0
- package/repo-docs/ai/docs-skills-rules-mapping.md +6 -6
- package/repo-docs/ai/future-skill-roadmap.md +2 -2
- package/repo-docs/ai/introduction.md +5 -5
- package/repo-docs/ai/playbook-contract-regeneration.md +3 -9
- package/repo-docs/ai/repo-guidance.md +5 -4
- package/repo-docs/ai/rules-and-config.md +2 -2
- package/repo-docs/ai/virtual-decorator-guidance.md +1 -3
- package/repo-docs/backend/cache-guide.md +36 -0
- package/repo-docs/backend/dto-guide.md +7 -6
- package/repo-docs/backend/dto-infer-generation.md +25 -5
- package/repo-docs/backend/introduction.md +1 -1
- package/repo-docs/backend/migration-and-changes.md +27 -2
- package/repo-docs/backend/model-guide.md +1 -1
- package/repo-docs/backend/resource-field-update.md +156 -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 +26 -9
- package/repo-docs/frontend/model-resource-best-practices.md +2 -0
- package/repo-docs/frontend/model-resource-cookbook.md +7 -1
- package/repo-docs/frontend/router-tabs-introduction.md +2 -4
- 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/a-pay-payment-suite.md +1 -1
- package/repo-docs/fullstack/contract-loop-playbook.md +4 -0
- package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +2 -2
- package/repo-docs/reference/package-map.md +1 -1
- package/repo-e2e/specs/a-commerce.spec.ts +40 -0
- package/scripts/release.ts +48 -1
- package/scripts/upgrade.ts +18 -7
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +2 -2
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/pnpm-lock.yaml +83 -75
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +108 -108
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +12 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +19 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +53 -4
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +148 -148
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +37 -3
- package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +2 -2
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +7 -6
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +1 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +7 -6
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +10 -7
- package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +4 -4
- package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +39 -67
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +56 -37
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +5 -5
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +6 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionAdjustStock.ts +34 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +81 -6
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeStockBalance.ts +32 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +44 -44
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +356 -338
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeStockBalance.ts +17 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionAdjustStock.tsx +35 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +138 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +36 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +4 -4
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +8 -8
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +11 -11
- package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +2 -2
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +2 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +2 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/model/resource.types.ts +11 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +58 -9
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +157 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +3 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +11 -13
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +9 -7
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -12
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryPersister.ts +17 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryRefetch.ts +34 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +27 -3
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryPersister.test.ts +115 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryRefetch.test.ts +84 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts +27 -5
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +4 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +2 -5
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts +179 -0
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
|
@@ -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
|
+
});
|
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts
CHANGED
|
@@ -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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
}
|
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
+
}
|
|
@@ -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
|
+
});
|