@wisemen/vue-core-api-utils 1.1.0 → 2.0.0
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/dist/index.d.mts +254 -287
- package/dist/index.mjs +13162 -353
- package/package.json +2 -2
- package/skills/asyncresult-handling/SKILL.md +6 -8
- package/skills/cache-management/SKILL.md +113 -76
- package/skills/foundations/SKILL.md +1 -2
- package/skills/getting-started/SKILL.md +55 -19
- package/skills/optimistic-uis/SKILL.md +107 -164
- package/skills/writing-infinitequeries/SKILL.md +108 -42
- package/skills/writing-mutations/SKILL.md +59 -34
- package/skills/writing-queries/SKILL.md +26 -14
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "
|
|
7
|
+
"version": "2.0.0",
|
|
8
8
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typescript": "5.9.3",
|
|
46
46
|
"vue": "3.5.27",
|
|
47
47
|
"vitest": "4.1.0",
|
|
48
|
-
"@wisemen/eslint-config-vue": "2.1.
|
|
48
|
+
"@wisemen/eslint-config-vue": "2.1.3"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsdown",
|
|
@@ -4,10 +4,8 @@ description: >
|
|
|
4
4
|
Three-state AsyncResult type (Loading, Ok, Err), isLoading/isOk/isErr type predicates, getValue/getError accessors, match() pattern matching, map/mapErr transformations, safe value extraction without undefined.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "
|
|
7
|
+
library_version: "1.2.0"
|
|
8
8
|
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/concepts/result-types.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/overview.md"
|
|
11
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
|
|
12
10
|
---
|
|
13
11
|
|
|
@@ -149,15 +147,15 @@ If you omit a handler, TypeScript errors and the UI renders nothing during the o
|
|
|
149
147
|
|
|
150
148
|
Source: `docs/packages/api-utils/pages/concepts/result-types.md` Pattern Matching Section
|
|
151
149
|
|
|
152
|
-
### HIGH: Use state flags (isLoading, isError, isSuccess) instead of AsyncResult state
|
|
150
|
+
### HIGH: Use deprecated state flags (isLoading, isError, isSuccess) instead of AsyncResult state
|
|
153
151
|
|
|
154
152
|
```typescript
|
|
155
|
-
// ❌ Wrong:
|
|
153
|
+
// ❌ Wrong: using deprecated flags
|
|
156
154
|
const { result, isLoading } = useQuery(...)
|
|
157
155
|
if (isLoading.value) {
|
|
158
156
|
// Show spinner
|
|
159
157
|
} else {
|
|
160
|
-
const data = result.value.getValue() // Could be null!
|
|
158
|
+
const data = result.value.getValue() // Could be null if isErr!
|
|
161
159
|
}
|
|
162
160
|
```
|
|
163
161
|
|
|
@@ -171,9 +169,9 @@ if (result.value.isLoading()) {
|
|
|
171
169
|
}
|
|
172
170
|
```
|
|
173
171
|
|
|
174
|
-
|
|
172
|
+
`isLoading`, `isError`, and `isSuccess` on `UseQueryReturnType` are deprecated — they exist for backward compatibility but are less type-safe than AsyncResult. Always prefer `result.value.isLoading()`, `result.value.isErr()`, and `result.value.isOk()`.
|
|
175
173
|
|
|
176
|
-
Source:
|
|
174
|
+
Source: `src/composables/query/query.composable.ts` — `UseQueryReturnType` deprecated annotations
|
|
177
175
|
|
|
178
176
|
## Next Steps
|
|
179
177
|
|
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cache-management
|
|
3
3
|
description: >
|
|
4
|
-
Type-safe QueryClient with get/set/update/invalidate methods, predicate-based updates, cascade invalidation strategy, shared cache across components
|
|
4
|
+
Type-safe QueryClient class with get/set/update/invalidate methods, rollback support from update(), predicate-based updates, cascade invalidation strategy, shared cache across components.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "
|
|
7
|
+
library_version: "1.2.0"
|
|
8
8
|
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/query-client.md"
|
|
10
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/utils/query-client/queryClient.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/
|
|
10
|
+
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
# @wisemen/vue-core-api-utils — Cache Management
|
|
15
14
|
|
|
16
|
-
Manually read, write, update, and invalidate the query cache using the type-safe `
|
|
15
|
+
Manually read, write, update, and invalidate the query cache using the type-safe `QueryClient` class. This is useful for optimistic updates and strategically invalidating affected queries.
|
|
17
16
|
|
|
18
17
|
## Setup
|
|
19
18
|
|
|
19
|
+
The library exports a `QueryClient` class that wraps the underlying TanStack QueryClient with type-safe methods. Create a helper function in your project to get a typed instance:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/api/queryClient.ts
|
|
23
|
+
|
|
24
|
+
import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
|
|
25
|
+
import type { ProjectQueryKeys } from '@/types/queryKey.type'
|
|
26
|
+
|
|
27
|
+
export function useQueryClient() {
|
|
28
|
+
return new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then use it anywhere in your composables or components:
|
|
33
|
+
|
|
20
34
|
```typescript
|
|
21
|
-
import { useQueryClient } from '@/api'
|
|
35
|
+
import { useQueryClient } from '@/api/queryClient'
|
|
22
36
|
|
|
23
37
|
const queryClient = useQueryClient()
|
|
24
38
|
|
|
25
39
|
// Get cached data
|
|
26
|
-
const
|
|
40
|
+
const contacts = queryClient.get('contactDetail')
|
|
27
41
|
|
|
28
42
|
// Set cached data
|
|
29
43
|
queryClient.set(
|
|
@@ -31,14 +45,14 @@ queryClient.set(
|
|
|
31
45
|
updatedContact
|
|
32
46
|
)
|
|
33
47
|
|
|
34
|
-
// Update cached data with a predicate
|
|
35
|
-
queryClient.update('contactList', {
|
|
48
|
+
// Update cached data with a predicate (returns rollback function)
|
|
49
|
+
const { rollback } = queryClient.update('contactList', {
|
|
36
50
|
by: (contact) => contact.id === '123',
|
|
37
51
|
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
38
52
|
})
|
|
39
53
|
|
|
40
54
|
// Invalidate queries (trigger refetch)
|
|
41
|
-
queryClient.invalidate('contactList')
|
|
55
|
+
await queryClient.invalidate('contactList')
|
|
42
56
|
```
|
|
43
57
|
|
|
44
58
|
## Core Patterns
|
|
@@ -48,46 +62,42 @@ queryClient.invalidate('contactList')
|
|
|
48
62
|
```typescript
|
|
49
63
|
const queryClient = useQueryClient()
|
|
50
64
|
|
|
51
|
-
// Get
|
|
52
|
-
const
|
|
53
|
-
['contactDetail', { contactUuid: '123' }]
|
|
54
|
-
)
|
|
65
|
+
// Get all queries with a key (returns array)
|
|
66
|
+
const allContacts = queryClient.get('contactDetail')
|
|
55
67
|
|
|
56
|
-
// Get
|
|
57
|
-
const
|
|
68
|
+
// Get exact query stored as ['contactDetail']
|
|
69
|
+
const exactContact = queryClient.get('contactDetail', { isExact: true })
|
|
58
70
|
|
|
59
|
-
// Get
|
|
60
|
-
const
|
|
71
|
+
// Get specific query with params (returns single item or null)
|
|
72
|
+
const contact = queryClient.get(['contactDetail', { contactUuid: '123' }] as const)
|
|
61
73
|
```
|
|
62
74
|
|
|
63
|
-
Returns the cached
|
|
75
|
+
Returns the cached entity or null if not cached. The QueryClient infers the entity type from your query key definition.
|
|
64
76
|
|
|
65
77
|
### Set cached data
|
|
66
78
|
|
|
67
79
|
```typescript
|
|
68
80
|
const queryClient = useQueryClient()
|
|
69
81
|
|
|
82
|
+
// Set query with key + params
|
|
70
83
|
queryClient.set(
|
|
71
84
|
['contactDetail', { contactUuid: '123' }],
|
|
72
|
-
{
|
|
85
|
+
{ uuid: '123', name: 'John', email: 'john@email.com' }
|
|
73
86
|
)
|
|
74
87
|
|
|
75
|
-
//
|
|
76
|
-
queryClient.set('
|
|
77
|
-
{ id: '123', name: 'John' },
|
|
78
|
-
{ id: '456', name: 'Jane' },
|
|
79
|
-
])
|
|
88
|
+
// Set query with just the key (stores as ['contactDetail'])
|
|
89
|
+
queryClient.set('contactDetail', { uuid: '123', name: 'John', email: 'john@email.com' })
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
`set()` replaces all cached data for that query key.
|
|
92
|
+
`set()` replaces all cached data for that specific query key.
|
|
83
93
|
|
|
84
94
|
### Update cached data with predicates
|
|
85
95
|
|
|
86
96
|
```typescript
|
|
87
97
|
const queryClient = useQueryClient()
|
|
88
98
|
|
|
89
|
-
// Update a single item in a list
|
|
90
|
-
queryClient.update('contactList', {
|
|
99
|
+
// Update a single item in a list — returns { rollback } for reverting
|
|
100
|
+
const { rollback } = queryClient.update('contactList', {
|
|
91
101
|
by: (contact) => contact.id === '123', // Predicate
|
|
92
102
|
value: (contact) => ({ // Transform
|
|
93
103
|
...contact,
|
|
@@ -95,14 +105,25 @@ queryClient.update('contactList', {
|
|
|
95
105
|
}),
|
|
96
106
|
})
|
|
97
107
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
// If the operation should be reverted (e.g. mutation failed):
|
|
109
|
+
rollback()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`update()` returns a `{ rollback }` function that reverts the cache to its state before the update. Safe to call multiple times (subsequent calls are no-ops).
|
|
113
|
+
|
|
114
|
+
### Update specific query with params tuple
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const queryClient = useQueryClient()
|
|
118
|
+
|
|
119
|
+
// Update only the specific cached query for this contact
|
|
120
|
+
queryClient.update(['contactDetail', { contactUuid: '123' }] as const, {
|
|
121
|
+
by: () => true, // Single entity — always matches
|
|
101
122
|
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
102
123
|
})
|
|
103
124
|
```
|
|
104
125
|
|
|
105
|
-
|
|
126
|
+
Using a key+params tuple updates only the specific query, not all queries with that key.
|
|
106
127
|
|
|
107
128
|
### Invalidate and refetch
|
|
108
129
|
|
|
@@ -110,10 +131,10 @@ QueryClient knows whether the entity is an array or single item, so predicates w
|
|
|
110
131
|
const queryClient = useQueryClient()
|
|
111
132
|
|
|
112
133
|
// Invalidate all queries with this key
|
|
113
|
-
queryClient.invalidate('contactList')
|
|
134
|
+
await queryClient.invalidate('contactList')
|
|
114
135
|
|
|
115
|
-
// Invalidate specific query
|
|
116
|
-
queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
|
|
136
|
+
// Invalidate specific query with params
|
|
137
|
+
await queryClient.invalidate(['contactDetail', { contactUuid: '123' }] as const)
|
|
117
138
|
|
|
118
139
|
// After invalidation, the next query interaction triggers a refetch
|
|
119
140
|
```
|
|
@@ -122,80 +143,95 @@ Invalidation marks cached data as stale. The next interaction (component mount,
|
|
|
122
143
|
|
|
123
144
|
## Common Mistakes
|
|
124
145
|
|
|
125
|
-
### HIGH:
|
|
146
|
+
### HIGH: Create QueryClient without typed query keys; lose type safety
|
|
126
147
|
|
|
127
148
|
```typescript
|
|
128
|
-
// ❌ Wrong:
|
|
129
|
-
|
|
149
|
+
// ❌ Wrong: untyped QueryClient
|
|
150
|
+
import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
|
|
151
|
+
|
|
152
|
+
const queryClient = new QueryClient(getTanstackQueryClient())
|
|
153
|
+
// All methods fall back to `object` for query keys — no autocomplete, no type checking
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ✅ Correct: typed QueryClient
|
|
158
|
+
import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
|
|
159
|
+
import type { ProjectQueryKeys } from '@/types/queryKey.type'
|
|
160
|
+
|
|
161
|
+
const queryClient = new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
|
|
162
|
+
// queryClient.get('nonExistentKey') → TypeScript error
|
|
163
|
+
// queryClient.update('contactList', { by: (c) => c.nonExistentField }) → TypeScript error
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Always provide your `ProjectQueryKeys` type generic when instantiating QueryClient.
|
|
167
|
+
|
|
168
|
+
Source: `src/utils/query-client/queryClient.ts` — `QueryClient<TQueryKeys>` class
|
|
169
|
+
|
|
170
|
+
### HIGH: Discard the rollback return from update(); can't revert optimistic changes
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// ❌ Wrong: ignoring rollback
|
|
130
174
|
queryClient.update('contactList', {
|
|
131
|
-
by: (
|
|
132
|
-
value: (
|
|
175
|
+
by: (c) => c.id === '123',
|
|
176
|
+
value: (c) => ({ ...c, name: 'Updated' }),
|
|
133
177
|
})
|
|
134
|
-
//
|
|
135
|
-
// If you expect single match, this breaks silently
|
|
178
|
+
// No way to undo if the mutation fails
|
|
136
179
|
```
|
|
137
180
|
|
|
138
181
|
```typescript
|
|
139
|
-
// ✅ Correct:
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
queryClient.update('contactList', {
|
|
144
|
-
by: (contact) => contact.id === '123',
|
|
145
|
-
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
182
|
+
// ✅ Correct: capture rollback for error recovery
|
|
183
|
+
const { rollback } = queryClient.update('contactList', {
|
|
184
|
+
by: (c) => c.id === '123',
|
|
185
|
+
value: (c) => ({ ...c, name: 'Updated' }),
|
|
146
186
|
})
|
|
147
|
-
|
|
148
|
-
|
|
187
|
+
|
|
188
|
+
const result = await execute(formData)
|
|
189
|
+
if (result.isErr()) {
|
|
190
|
+
rollback() // Reverts the cache to its pre-update state
|
|
191
|
+
}
|
|
149
192
|
```
|
|
150
193
|
|
|
151
|
-
|
|
194
|
+
`update()` returns `{ rollback }` — always capture it when doing optimistic updates so you can revert on error.
|
|
152
195
|
|
|
153
|
-
Source: `
|
|
196
|
+
Source: `src/utils/query-client/queryClient.ts` — `QueryClientUpdateResult`
|
|
154
197
|
|
|
155
|
-
### MEDIUM: Call set() without
|
|
198
|
+
### MEDIUM: Call set() without rollback plan; UI flashes stale data on error
|
|
156
199
|
|
|
157
200
|
```typescript
|
|
158
|
-
// ❌ Wrong: immediate set without
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Cache updated but no indicator that request is pending
|
|
162
|
-
// UI looks responsive but actually has unconfirmed data
|
|
201
|
+
// ❌ Wrong: immediate set without error recovery
|
|
202
|
+
queryClient.set(['contactDetail', { contactUuid }], newData)
|
|
203
|
+
// Cache updated but if the mutation fails, data is wrong with no way to revert
|
|
163
204
|
```
|
|
164
205
|
|
|
165
206
|
```typescript
|
|
166
|
-
// ✅ Correct:
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
queryClient.set(['contactDetail', { contactUuid }], newData)
|
|
207
|
+
// ✅ Correct: prefer update() which has built-in rollback
|
|
208
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
209
|
+
by: () => true,
|
|
210
|
+
value: () => newData,
|
|
211
|
+
})
|
|
172
212
|
|
|
173
|
-
// Execute mutation
|
|
174
213
|
const result = await execute(formData)
|
|
175
214
|
if (result.isErr()) {
|
|
176
|
-
|
|
177
|
-
queryClient.set(['contactDetail', { contactUuid }], original)
|
|
215
|
+
rollback()
|
|
178
216
|
}
|
|
179
217
|
```
|
|
180
218
|
|
|
181
|
-
|
|
219
|
+
Prefer `update()` over `set()` for optimistic changes because `update()` automatically captures the previous state for rollback.
|
|
182
220
|
|
|
183
|
-
Source: `
|
|
221
|
+
Source: `src/utils/query-client/queryClient.ts`
|
|
184
222
|
|
|
185
223
|
## Cache Strategy
|
|
186
224
|
|
|
187
225
|
> Explicitly invalidate only the queries affected by the mutation. Let lazy refetch handle the rest when users navigate to pages needing other data.
|
|
188
|
-
>
|
|
189
|
-
> — Maintainer guidance
|
|
190
226
|
|
|
191
227
|
When a mutation succeeds, look at what changed:
|
|
192
228
|
- If you updated a contact, invalidate `contactDetail` and `contactList` (they both show that contact)
|
|
193
|
-
- If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless
|
|
229
|
+
- If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless it's the one you archived)
|
|
194
230
|
- Don't invalidate unrelated queries — let them refetch lazily when needed
|
|
195
231
|
|
|
196
232
|
## Shared Cache Across Components
|
|
197
233
|
|
|
198
|
-
|
|
234
|
+
Multiple components using the same query key share the same cached data. This is a feature, not a bug.
|
|
199
235
|
|
|
200
236
|
```typescript
|
|
201
237
|
// ComponentA
|
|
@@ -210,8 +246,8 @@ const { result: resultB } = useQuery('userDetail', {
|
|
|
210
246
|
queryFn: () => UserService.getById('same-id'),
|
|
211
247
|
})
|
|
212
248
|
|
|
213
|
-
// resultA and resultB
|
|
214
|
-
// Mutation in B invalidates A
|
|
249
|
+
// resultA and resultB share the SAME cached value
|
|
250
|
+
// Mutation in B that invalidates userDetail also triggers a refetch in A
|
|
215
251
|
```
|
|
216
252
|
|
|
217
253
|
Use this to your advantage: invalidate a query and all components using it refetch automatically.
|
|
@@ -220,3 +256,4 @@ Use this to your advantage: invalidate a query and all components using it refet
|
|
|
220
256
|
|
|
221
257
|
- [Writing Mutations](../writing-mutations/SKILL.md) — Every mutation needs to know which queries to invalidate
|
|
222
258
|
- [Writing Queries](../writing-queries/SKILL.md) — Understanding caching strategy informs cache management choices
|
|
259
|
+
- [Optimistic UIs](../optimistic-uis/SKILL.md) — Full optimistic update pattern using update() and rollback
|
|
@@ -4,9 +4,8 @@ description: >
|
|
|
4
4
|
neverthrow Result architectural basis; three-state AsyncResult relationship to Result; @tanstack/vue-query lifecycle (staleTime, gcTime, refetch); composition of TanStack Query + neverthrow + Vue 3 reactivity.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "
|
|
7
|
+
library_version: "1.2.0"
|
|
8
8
|
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/concepts/result-types.md"
|
|
10
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
|
|
11
10
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/apiError.type.ts"
|
|
12
11
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: getting-started
|
|
3
3
|
description: >
|
|
4
|
-
Install @wisemen/vue-core-api-utils, initialize apiUtilsPlugin with QueryClient config,
|
|
4
|
+
Install @wisemen/vue-core-api-utils, initialize apiUtilsPlugin with QueryClient config, register typed query keys via module augmentation, re-export composables.
|
|
5
5
|
type: lifecycle
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "1.0
|
|
7
|
+
library_version: "1.2.0"
|
|
8
8
|
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/getting-started/installation.md"
|
|
10
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/plugin/apiUtilsPlugin.ts"
|
|
10
|
+
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/register.ts"
|
|
11
11
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# @wisemen/vue-core-api-utils — Getting Started
|
|
15
15
|
|
|
16
|
-
Get `@wisemen/vue-core-api-utils` installed, your Vue Query plugin initialized, query keys defined, and typed composables
|
|
16
|
+
Get `@wisemen/vue-core-api-utils` installed, your Vue Query plugin initialized, query keys defined, and typed composables available.
|
|
17
17
|
|
|
18
18
|
## Setup
|
|
19
19
|
|
|
@@ -40,13 +40,13 @@ export interface ProjectQueryKeys {
|
|
|
40
40
|
// List query with offset pagination
|
|
41
41
|
contactList: {
|
|
42
42
|
entity: Contact[]
|
|
43
|
-
params: {
|
|
43
|
+
params: { search?: string }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// List query with keyset pagination
|
|
47
47
|
contactListKeyset: {
|
|
48
48
|
entity: Contact[]
|
|
49
|
-
params: {
|
|
49
|
+
params: { search?: string }
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
```
|
|
@@ -78,7 +78,9 @@ app.mount('#app')
|
|
|
78
78
|
|
|
79
79
|
The `apiUtilsPlugin` function creates a QueryClient with your config and handles @tanstack/vue-query setup internally.
|
|
80
80
|
|
|
81
|
-
### 4.
|
|
81
|
+
### 4. Register your types and re-export composables
|
|
82
|
+
|
|
83
|
+
Use module augmentation to register your query keys and error codes for library-wide type safety, then re-export composables for convenient importing across your project:
|
|
82
84
|
|
|
83
85
|
```typescript
|
|
84
86
|
// src/api/index.ts
|
|
@@ -88,15 +90,23 @@ import type {
|
|
|
88
90
|
KeysetPaginationResult as ApiUtilsKeysetPaginationResult,
|
|
89
91
|
OffsetPaginationResult as ApiUtilsOffsetPaginationResult,
|
|
90
92
|
} from '@wisemen/vue-core-api-utils'
|
|
91
|
-
import { createApiUtils } from '@wisemen/vue-core-api-utils'
|
|
92
93
|
|
|
93
94
|
import type { ProjectQueryKeys } from '@/types/queryKey.type'
|
|
94
95
|
|
|
95
96
|
// Define your error codes
|
|
96
97
|
export type ERROR_KEYS = 'NOT_FOUND' | 'UNAUTHORIZED' | 'NETWORK_ERROR' | 'VALIDATION_ERROR'
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
-
|
|
99
|
+
// Register query keys and error codes via module augmentation
|
|
100
|
+
// This makes all composables fully typed without a factory function
|
|
101
|
+
declare module '@wisemen/vue-core-api-utils' {
|
|
102
|
+
interface Register {
|
|
103
|
+
queryKeys: ProjectQueryKeys
|
|
104
|
+
errorCodes: ERROR_KEYS
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Re-export composables for convenient importing
|
|
109
|
+
export {
|
|
100
110
|
useKeysetInfiniteQuery,
|
|
101
111
|
useMutation,
|
|
102
112
|
useOffsetInfiniteQuery,
|
|
@@ -104,8 +114,7 @@ export const {
|
|
|
104
114
|
usePrefetchKeysetInfiniteQuery,
|
|
105
115
|
usePrefetchOffsetInfiniteQuery,
|
|
106
116
|
usePrefetchQuery,
|
|
107
|
-
|
|
108
|
-
} = createApiUtils<ProjectQueryKeys, ERROR_KEYS>()
|
|
117
|
+
} from '@wisemen/vue-core-api-utils'
|
|
109
118
|
|
|
110
119
|
// Export typed result types
|
|
111
120
|
export type ApiResult<T> = ApiUtilsApiResult<T, ERROR_KEYS>
|
|
@@ -113,6 +122,8 @@ export type OffsetPaginationResult<T> = ApiUtilsOffsetPaginationResult<T, ERROR_
|
|
|
113
122
|
export type KeysetPaginationResult<T> = ApiUtilsKeysetPaginationResult<T, ERROR_KEYS>
|
|
114
123
|
```
|
|
115
124
|
|
|
125
|
+
The `declare module` block tells the library about your project's types. After this, every composable automatically knows your query keys and error codes.
|
|
126
|
+
|
|
116
127
|
## Core Patterns
|
|
117
128
|
|
|
118
129
|
### Create a detail query composable
|
|
@@ -185,7 +196,7 @@ All queries and mutations return `AsyncResult` with three states: loading, ok, a
|
|
|
185
196
|
|
|
186
197
|
## Common Mistakes
|
|
187
198
|
|
|
188
|
-
### CRITICAL: Forget to initialize apiUtilsPlugin
|
|
199
|
+
### CRITICAL: Forget to initialize apiUtilsPlugin
|
|
189
200
|
|
|
190
201
|
```typescript
|
|
191
202
|
// ❌ Wrong: plugin not initialized
|
|
@@ -205,7 +216,7 @@ app.use(apiUtilsPlugin({
|
|
|
205
216
|
app.mount('#app')
|
|
206
217
|
```
|
|
207
218
|
|
|
208
|
-
|
|
219
|
+
Without the plugin, composables have no QueryClient and throw immediately.
|
|
209
220
|
|
|
210
221
|
Source: `src/config/config.ts` — `getQueryClient()` assertion
|
|
211
222
|
|
|
@@ -228,21 +239,46 @@ export interface ProjectQueryKeys {
|
|
|
228
239
|
}
|
|
229
240
|
contactList: {
|
|
230
241
|
entity: Contact[]
|
|
231
|
-
params: {
|
|
242
|
+
params: { search?: string }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Query keys without proper structure cause TypeScript errors and prevent correct query key resolution.
|
|
248
|
+
|
|
249
|
+
Source: `src/register.ts` — `RegisteredQueryKeyEntity` and `RegisteredQueryKeyParams` type derivation
|
|
250
|
+
|
|
251
|
+
### HIGH: Skip the module augmentation; composables lose type safety
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// ❌ Wrong: no declare module block in @/api/index.ts
|
|
255
|
+
// Composables fall back to `object` for query keys and `string` for error codes
|
|
256
|
+
// TypeScript won't catch wrong query key names or invalid error codes
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// ✅ Correct: declare module augments the Register interface
|
|
261
|
+
declare module '@wisemen/vue-core-api-utils' {
|
|
262
|
+
interface Register {
|
|
263
|
+
queryKeys: ProjectQueryKeys
|
|
264
|
+
errorCodes: ERROR_KEYS
|
|
232
265
|
}
|
|
233
266
|
}
|
|
267
|
+
// Now useQuery('nonExistentKey', ...) is a compile error
|
|
268
|
+
// And error.code is typed as ERROR_KEYS, not just string
|
|
234
269
|
```
|
|
235
270
|
|
|
236
|
-
|
|
271
|
+
Without the `declare module` block the library types fall back to generic `object` and `string`. All query key checks and error code checks become useless at compile time.
|
|
237
272
|
|
|
238
|
-
Source: `
|
|
273
|
+
Source: `src/register.ts`
|
|
239
274
|
|
|
240
275
|
## You're all set!
|
|
241
276
|
|
|
242
277
|
You now have:
|
|
243
278
|
- ✅ Plugin initialized with Vue Query
|
|
244
279
|
- ✅ Query keys defined with types
|
|
245
|
-
- ✅
|
|
280
|
+
- ✅ Types registered via module augmentation
|
|
281
|
+
- ✅ Composables re-exported from `@/api`
|
|
246
282
|
- ✅ Error codes enumerated
|
|
247
283
|
|
|
248
|
-
Head to [writing-queries](../writing-queries/SKILL.md) to fetch your first resource, or [
|
|
284
|
+
Head to [writing-queries](../writing-queries/SKILL.md) to fetch your first resource, or [asyncresult-handling](../asyncresult-handling/SKILL.md) to understand the three-state AsyncResult type.
|