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.
Files changed (122) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/commands/release.md +12 -1
  3. package/.claude/hooks/contract-loop-gate.ts +35 -17
  4. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +4 -3
  5. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +3 -2
  6. package/.claude/skills/cabloy-contract-loop/SKILL.md +4 -3
  7. package/.claude/skills/cabloy-domain-planning/SKILL.md +4 -3
  8. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +5 -3
  9. package/.claude/skills/cabloy-master-detail/SKILL.md +4 -3
  10. package/.claude/skills/cabloy-module-removal/SKILL.md +5 -4
  11. package/.claude/skills/cabloy-resource-field-update/SKILL.md +8 -5
  12. package/.claude/skills/cabloy-spec-execution/SKILL.md +169 -0
  13. package/.claude/skills/cabloy-spec-execution/evals/evals.json +71 -0
  14. package/.claude/skills/cabloy-spec-execution/references/execution-protocol.md +137 -0
  15. package/.claude/skills/cabloy-spec-execution/references/status-and-evidence.md +117 -0
  16. package/.claude/skills/cabloy-spec-generation/SKILL.md +227 -0
  17. package/.claude/skills/cabloy-spec-generation/evals/evals.json +83 -0
  18. package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +101 -0
  19. package/.claude/skills/cabloy-spec-generation/references/repo-specs-document-set.md +267 -0
  20. package/.claude/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +135 -0
  21. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +350 -0
  22. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +202 -0
  23. package/.claude/skills/cabloy-workflow/SKILL.md +21 -10
  24. package/.claude/skills/cabloy-workflow/evals/evals.json +8 -2
  25. package/.claude/skills/cabloy-workflow/references/edition-detection.md +5 -3
  26. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +4 -3
  27. package/CHANGELOG.md +41 -0
  28. package/CLAUDE.md +12 -11
  29. package/package.json +4 -2
  30. package/repo-docs/.vitepress/config.mjs +4 -0
  31. package/repo-docs/ai/docs-skills-rules-mapping.md +6 -6
  32. package/repo-docs/ai/future-skill-roadmap.md +2 -2
  33. package/repo-docs/ai/introduction.md +5 -5
  34. package/repo-docs/ai/playbook-contract-regeneration.md +3 -9
  35. package/repo-docs/ai/repo-guidance.md +5 -4
  36. package/repo-docs/ai/rules-and-config.md +2 -2
  37. package/repo-docs/ai/virtual-decorator-guidance.md +1 -3
  38. package/repo-docs/backend/cache-guide.md +36 -0
  39. package/repo-docs/backend/dto-guide.md +7 -6
  40. package/repo-docs/backend/dto-infer-generation.md +25 -5
  41. package/repo-docs/backend/introduction.md +1 -1
  42. package/repo-docs/backend/migration-and-changes.md +27 -2
  43. package/repo-docs/backend/model-guide.md +1 -1
  44. package/repo-docs/backend/resource-field-update.md +156 -0
  45. package/repo-docs/frontend/a-model-under-the-hood.md +38 -0
  46. package/repo-docs/frontend/a-openapi-under-the-hood.md +13 -2
  47. package/repo-docs/frontend/form-layout-guide.md +26 -9
  48. package/repo-docs/frontend/model-resource-best-practices.md +2 -0
  49. package/repo-docs/frontend/model-resource-cookbook.md +7 -1
  50. package/repo-docs/frontend/router-tabs-introduction.md +2 -4
  51. package/repo-docs/frontend/use-state-data-best-practices.md +44 -0
  52. package/repo-docs/frontend/zova-form-source-reading-map.md +26 -9
  53. package/repo-docs/frontend/zova-form-under-the-hood.md +11 -0
  54. package/repo-docs/fullstack/a-pay-payment-suite.md +1 -1
  55. package/repo-docs/fullstack/contract-loop-playbook.md +4 -0
  56. package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +2 -2
  57. package/repo-docs/reference/package-map.md +1 -1
  58. package/repo-e2e/specs/a-commerce.spec.ts +40 -0
  59. package/scripts/release.ts +48 -1
  60. package/scripts/upgrade.ts +18 -7
  61. package/vona/packages-cli/cli/package.json +1 -1
  62. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +2 -2
  63. package/vona/packages-cli/cli-set-api/package.json +1 -1
  64. package/vona/pnpm-lock.yaml +83 -75
  65. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +108 -108
  66. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +12 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +19 -1
  68. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +53 -4
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +148 -148
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +37 -3
  71. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +2 -2
  72. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +7 -6
  73. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +1 -1
  74. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +7 -6
  75. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +10 -7
  76. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +4 -4
  77. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +39 -67
  78. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +56 -37
  79. package/zova/packages-zova/zova/package.json +2 -2
  80. package/zova/pnpm-lock.yaml +5 -5
  81. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  82. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +6 -1
  83. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionAdjustStock.ts +34 -0
  84. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +81 -6
  85. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeStockBalance.ts +32 -0
  86. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +44 -44
  87. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +356 -338
  88. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeStockBalance.ts +17 -0
  89. package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionAdjustStock.tsx +35 -0
  90. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +138 -0
  91. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +3 -0
  92. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +3 -0
  93. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +36 -0
  94. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +4 -4
  95. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +8 -8
  96. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +11 -11
  97. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +2 -2
  98. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +2 -0
  99. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  100. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +2 -1
  101. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/model/resource.types.ts +11 -0
  102. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  103. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  104. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +58 -9
  105. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +157 -0
  106. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  107. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +3 -1
  108. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +11 -13
  109. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +9 -7
  110. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -12
  111. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryPersister.ts +17 -0
  112. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryRefetch.ts +34 -0
  113. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +27 -3
  114. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryPersister.test.ts +115 -0
  115. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryRefetch.test.ts +84 -0
  116. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  117. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts +27 -5
  118. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +2 -2
  119. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +4 -0
  120. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +2 -5
  121. package/zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts +179 -0
  122. 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
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-model",
3
- "version": "5.1.35",
3
+ "version": "5.1.36",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "core",
6
6
  "keywords": [
@@ -7,6 +7,7 @@ import { SymbolBeanFullName } from 'zova';
7
7
  import type { QueryMetaPersister } from '../../types/index.js';
8
8
 
9
9
  import { CookieWrapper } from '../../common/cookieWrapper.js';
10
+ import { createQueryPersister } from '../../lib/queryPersister.js';
10
11
  import { resolveMaxAgeTime } from '../../types/index.js';
11
12
  import { BeanModelLast } from './bean.model.last.js';
12
13
 
@@ -145,13 +146,14 @@ export class BeanModelPersister extends BeanModelLast {
145
146
  protected _createPersister(options?: QueryMetaPersister | boolean): any {
146
147
  options = this._adjustPersisterOptions(options);
147
148
  if (!options) return undefined;
148
- return experimental_createQueryPersister({
149
+ const persisterFn = experimental_createQueryPersister({
149
150
  storage: this._getPersisterStorage(options) as any,
150
151
  maxAge: options.maxAge as number,
151
152
  refetchOnRestore: options.refetchOnRestore,
152
153
  prefix: options.prefix,
153
154
  buster: options.buster,
154
155
  }).persisterFn;
156
+ return createQueryPersister(persisterFn);
155
157
  }
156
158
 
157
159
  protected _adjustPersisterOptions(options?: QueryMetaPersister | boolean) {
@@ -1,12 +1,4 @@
1
- import type {
2
- DefaultError,
3
- QueryClient,
4
- QueryKey,
5
- UseQueryDefinedReturnType,
6
- UseQueryOptions,
7
- UseQueryReturnType,
8
- } from '@tanstack/vue-query';
9
- import type { UnwrapNestedRefs } from 'vue';
1
+ import type { DefaultError, QueryClient, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
10
2
 
11
3
  import { checkErrorJwtExpired } from '@cabloy/utils';
12
4
  import { useQuery } from '@tanstack/vue-query';
@@ -16,7 +8,9 @@ import type {
16
8
  DefinedInitialQueryOptions,
17
9
  UndefinedInitialQueryOptions,
18
10
  } from '../../common/types.js';
11
+ import type { ModelUseQueryDefinedReturnType, ModelUseQueryReturnType } from '../../types/query.js';
19
12
 
13
+ import { createQueryRefetch } from '../../lib/queryRefetch.js';
20
14
  import { resolveStaleTime } from '../../types/index.js';
21
15
  import { BeanModelQuery } from './bean.model.query.js';
22
16
 
@@ -29,7 +23,7 @@ export class BeanModelUseQuery extends BeanModelQuery {
29
23
  >(
30
24
  options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
31
25
  queryClient?: QueryClient,
32
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
26
+ ): ModelUseQueryReturnType<TData, TError>;
33
27
  $useQuery<
34
28
  TQueryFnData = unknown,
35
29
  TError = DefaultError,
@@ -38,7 +32,7 @@ export class BeanModelUseQuery extends BeanModelQuery {
38
32
  >(
39
33
  options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
40
34
  queryClient?: QueryClient,
41
- ): UnwrapNestedRefs<UseQueryDefinedReturnType<TData, TError>>;
35
+ ): ModelUseQueryDefinedReturnType<TData, TError>;
42
36
  $useQuery<
43
37
  TQueryFnData = unknown,
44
38
  TError = DefaultError,
@@ -47,7 +41,7 @@ export class BeanModelUseQuery extends BeanModelQuery {
47
41
  >(
48
42
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
49
43
  queryClient?: QueryClient,
50
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
44
+ ): ModelUseQueryReturnType<TData, TError>;
51
45
  $useQuery(options, queryClient) {
52
46
  const queryKey = this.self._forceQueryKeyPrefix(options.queryKey);
53
47
  const persister = this._createPersister(options.meta?.persister);
@@ -81,7 +75,11 @@ export class BeanModelUseQuery extends BeanModelQuery {
81
75
  };
82
76
  }
83
77
  return this.ctx.util.instanceScope(() => {
84
- return useQuery(options, queryClient);
78
+ const query = useQuery(options, queryClient) as unknown as ModelUseQueryReturnType<any, any>;
79
+ query.refetch = createQueryRefetch(query.refetch, () =>
80
+ this.$queryFind({ queryKey }),
81
+ ) as typeof query.refetch;
82
+ return query;
85
83
  });
86
84
  }
87
85
  }
@@ -4,11 +4,8 @@ import type {
4
4
  QueryClient,
5
5
  QueryKey,
6
6
  QueryMeta,
7
- UseQueryDefinedReturnType,
8
7
  UseQueryOptions,
9
- UseQueryReturnType,
10
8
  } from '@tanstack/vue-query';
11
- import type { UnwrapNestedRefs } from 'vue';
12
9
 
13
10
  import { hashKey } from '@tanstack/vue-query';
14
11
  import { deepExtend } from 'zova';
@@ -17,7 +14,12 @@ import type {
17
14
  DefinedInitialQueryOptions,
18
15
  UndefinedInitialQueryOptions,
19
16
  } from '../../common/types.js';
20
- import type { QueryMetaPersister, UseQueryComputedOptions } from '../../types/query.js';
17
+ import type {
18
+ ModelUseQueryDefinedReturnType,
19
+ ModelUseQueryReturnType,
20
+ QueryMetaPersister,
21
+ UseQueryComputedOptions,
22
+ } from '../../types/query.js';
21
23
 
22
24
  import { BeanModelUseQuery } from './bean.model.useQuery.js';
23
25
 
@@ -311,7 +313,7 @@ export class BeanModelUseState extends BeanModelUseQuery {
311
313
  >(
312
314
  options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
313
315
  queryClient?: QueryClient,
314
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
316
+ ): ModelUseQueryReturnType<TData, TError>;
315
317
  $useStateData<
316
318
  TQueryFnData = unknown,
317
319
  TError = DefaultError,
@@ -320,7 +322,7 @@ export class BeanModelUseState extends BeanModelUseQuery {
320
322
  >(
321
323
  options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
322
324
  queryClient?: QueryClient,
323
- ): UnwrapNestedRefs<UseQueryDefinedReturnType<TData, TError>>;
325
+ ): ModelUseQueryDefinedReturnType<TData, TError>;
324
326
  $useStateData<
325
327
  TQueryFnData = unknown,
326
328
  TError = DefaultError,
@@ -329,7 +331,7 @@ export class BeanModelUseState extends BeanModelUseQuery {
329
331
  >(
330
332
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
331
333
  queryClient?: QueryClient,
332
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
334
+ ): ModelUseQueryReturnType<TData, TError>;
333
335
  $useStateData(options, queryClient) {
334
336
  const queryKey = this.self._forceQueryKeyPrefix(options.queryKey);
335
337
  const queryHash = hashKey(queryKey);
@@ -1,17 +1,10 @@
1
- import type {
2
- DefaultError,
3
- QueryClient,
4
- QueryKey,
5
- UseQueryDefinedReturnType,
6
- UseQueryOptions,
7
- UseQueryReturnType,
8
- } from '@tanstack/vue-query';
9
- import type { UnwrapNestedRefs } from 'vue';
1
+ import type { DefaultError, QueryClient, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
10
2
 
11
3
  import type {
12
4
  DefinedInitialQueryOptions,
13
5
  UndefinedInitialQueryOptions,
14
6
  } from '../../common/types.js';
7
+ import type { ModelUseQueryDefinedReturnType, ModelUseQueryReturnType } from '../../types/query.js';
15
8
  import type { StateType } from '../../types/query.js';
16
9
 
17
10
  import { BeanModelUseState } from './bean.model.useState.js';
@@ -56,7 +49,7 @@ export class BeanModelUseStateGeneral extends BeanModelUseState {
56
49
  stateType: 'data',
57
50
  options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
58
51
  queryClient?: QueryClient,
59
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
52
+ ): ModelUseQueryReturnType<TData, TError>;
60
53
  $useState<
61
54
  TQueryFnData = unknown,
62
55
  TError = DefaultError,
@@ -66,7 +59,7 @@ export class BeanModelUseStateGeneral extends BeanModelUseState {
66
59
  stateType: 'data',
67
60
  options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
68
61
  queryClient?: QueryClient,
69
- ): UnwrapNestedRefs<UseQueryDefinedReturnType<TData, TError>>;
62
+ ): ModelUseQueryDefinedReturnType<TData, TError>;
70
63
  $useState<
71
64
  TQueryFnData = unknown,
72
65
  TError = DefaultError,
@@ -76,7 +69,7 @@ export class BeanModelUseStateGeneral extends BeanModelUseState {
76
69
  stateType: 'data',
77
70
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
78
71
  queryClient?: QueryClient,
79
- ): UnwrapNestedRefs<UseQueryReturnType<TData, TError>>;
72
+ ): ModelUseQueryReturnType<TData, TError>;
80
73
  $useState(stateType: StateType, options, queryClient) {
81
74
  switch (stateType) {
82
75
  case 'localAsync':
@@ -0,0 +1,17 @@
1
+ import type { QueryPersister } from '@tanstack/query-core';
2
+
3
+ // TanStack's FetchMeta is internal to the public QueryPersister call path.
4
+ type QueryFetchMeta = {
5
+ bypassPersister?: boolean;
6
+ };
7
+
8
+ export function createQueryPersister<T, TQueryKey extends readonly unknown[]>(
9
+ persisterFn: QueryPersister<T, TQueryKey>,
10
+ ): QueryPersister<T, TQueryKey> {
11
+ return (queryFn, context, query) => {
12
+ if ((query.state.fetchMeta as QueryFetchMeta | null)?.bypassPersister === true) {
13
+ return queryFn(context);
14
+ }
15
+ return persisterFn(queryFn, context, query);
16
+ };
17
+ }
@@ -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": [