@wisemen/vue-core-api-utils 1.2.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 -281
- 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
|
@@ -4,11 +4,11 @@ description: >
|
|
|
4
4
|
Infinite pagination with useOffsetInfiniteQuery and useKeysetInfiniteQuery, offset vs keyset strategies determined by backend API, fetchNextPage, hasNextPage, isFetchingNextPage, data/meta result structure, proper page assembly.
|
|
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/paginated-query.md"
|
|
10
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/offsetInfiniteQuery.composable.ts"
|
|
11
10
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/keysetInfiniteQuery.composable.ts"
|
|
11
|
+
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/pagination.type.ts"
|
|
12
12
|
subsystems:
|
|
13
13
|
- "Offset Pagination"
|
|
14
14
|
- "Keyset Pagination"
|
|
@@ -16,13 +16,13 @@ subsystems:
|
|
|
16
16
|
|
|
17
17
|
# @wisemen/vue-core-api-utils — Writing Infinite Queries
|
|
18
18
|
|
|
19
|
-
Paginate through large datasets with two strategies: offset-based (
|
|
19
|
+
Paginate through large datasets with two strategies: offset-based (offset/limit) for traditional pagination, or keyset-based (cursor key) for real-time data and large datasets.
|
|
20
20
|
|
|
21
21
|
**Choose your strategy based on what your backend API provides — not preference.**
|
|
22
22
|
|
|
23
23
|
## Setup
|
|
24
24
|
|
|
25
|
-
### Offset Pagination (
|
|
25
|
+
### Offset Pagination (offset/limit-based)
|
|
26
26
|
|
|
27
27
|
```typescript
|
|
28
28
|
import { ref, computed } from 'vue'
|
|
@@ -37,7 +37,7 @@ export function useContactList() {
|
|
|
37
37
|
search: computed(() => search.value),
|
|
38
38
|
},
|
|
39
39
|
queryFn: (pagination) => ContactService.getAll({
|
|
40
|
-
|
|
40
|
+
offset: pagination.offset,
|
|
41
41
|
limit: pagination.limit,
|
|
42
42
|
search: search.value,
|
|
43
43
|
}),
|
|
@@ -45,7 +45,7 @@ export function useContactList() {
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
The `queryFn` receives `{ offset: number, limit: number }`. Offset starts at 0 and advances by `limit` for each next page. Return results with `{ data: Contact[], meta: { offset, limit, total } }`.
|
|
49
49
|
|
|
50
50
|
### Keyset Pagination (cursor-based)
|
|
51
51
|
|
|
@@ -63,14 +63,14 @@ export function useContactListKeyset() {
|
|
|
63
63
|
},
|
|
64
64
|
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
65
65
|
limit: pagination.limit,
|
|
66
|
-
|
|
66
|
+
key: pagination.key,
|
|
67
67
|
search: search.value,
|
|
68
68
|
}),
|
|
69
69
|
})
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
The `queryFn` receives `{ key?: any, limit: number }`. The `key` is the cursor value from the previous page's `meta.next`, or `undefined` for the first page. Return results with `{ data: Contact[], meta: { next: unknown } }` — set `meta.next` to `null`/`undefined` when there are no more pages.
|
|
74
74
|
|
|
75
75
|
## Core Patterns
|
|
76
76
|
|
|
@@ -105,9 +105,56 @@ All pages are automatically concatenated into `data`. Access with `result.getVal
|
|
|
105
105
|
|
|
106
106
|
Use `isFetchingNextPage` (not `isFetching`) to disable the load-more button only during pagination, not during initial load.
|
|
107
107
|
|
|
108
|
+
### Custom page limit
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
useOffsetInfiniteQuery('contactList', {
|
|
112
|
+
params: { search: computed(() => search.value) },
|
|
113
|
+
limit: 50, // Default is 20
|
|
114
|
+
queryFn: (pagination) => ContactService.getAll({
|
|
115
|
+
offset: pagination.offset,
|
|
116
|
+
limit: pagination.limit,
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Pass `limit` as a top-level option to override the default page size (20).
|
|
122
|
+
|
|
123
|
+
## Response Structures
|
|
124
|
+
|
|
125
|
+
### Offset pagination response
|
|
126
|
+
|
|
127
|
+
Your `queryFn` must return:
|
|
128
|
+
```typescript
|
|
129
|
+
{
|
|
130
|
+
data: Contact[],
|
|
131
|
+
meta: {
|
|
132
|
+
offset: number, // Current offset
|
|
133
|
+
limit: number, // Items per page
|
|
134
|
+
total: number, // Total items across all pages
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The library uses `meta.offset + meta.limit >= meta.total` to determine if there are more pages.
|
|
140
|
+
|
|
141
|
+
### Keyset pagination response
|
|
142
|
+
|
|
143
|
+
Your `queryFn` must return:
|
|
144
|
+
```typescript
|
|
145
|
+
{
|
|
146
|
+
data: Contact[],
|
|
147
|
+
meta: {
|
|
148
|
+
next: unknown, // Cursor for the next page; null/undefined if no more pages
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The library uses `meta.next` as the `key` parameter for the subsequent page fetch.
|
|
154
|
+
|
|
108
155
|
## Common Mistakes
|
|
109
156
|
|
|
110
|
-
### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of
|
|
157
|
+
### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of your api module
|
|
111
158
|
|
|
112
159
|
```typescript
|
|
113
160
|
// ❌ Wrong: using TanStack directly
|
|
@@ -122,54 +169,75 @@ const { data, error } = useInfiniteQuery({
|
|
|
122
169
|
```
|
|
123
170
|
|
|
124
171
|
```typescript
|
|
125
|
-
// ✅ Correct: use
|
|
172
|
+
// ✅ Correct: use the composable from your api module
|
|
126
173
|
import { useOffsetInfiniteQuery } from '@/api'
|
|
127
174
|
|
|
128
175
|
const { result, fetchNextPage, hasNextPage } = useOffsetInfiniteQuery('contactList', {
|
|
129
176
|
params: { search: computed(() => '...') },
|
|
130
177
|
queryFn: (pagination) => ContactService.getAll({
|
|
131
|
-
|
|
178
|
+
offset: pagination.offset,
|
|
132
179
|
limit: pagination.limit,
|
|
133
180
|
}),
|
|
134
181
|
})
|
|
135
182
|
// Full AsyncResult wrapping, type safety, automatic error codes
|
|
136
183
|
```
|
|
137
184
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Source: Library architecture — always use composables from `createApiUtils()` factory
|
|
185
|
+
Source: `src/composables/query/offsetInfiniteQuery.composable.ts`
|
|
141
186
|
|
|
142
187
|
### CRITICAL: Return paginated data without wrapping in data/meta structure
|
|
143
188
|
|
|
144
189
|
```typescript
|
|
145
190
|
// ❌ Wrong: returning array directly
|
|
146
191
|
queryFn: (pagination) => ContactService.getAll({
|
|
147
|
-
|
|
192
|
+
offset: pagination.offset,
|
|
148
193
|
limit: pagination.limit,
|
|
149
194
|
})
|
|
150
|
-
// Returns Contact[] directly instead of { data: Contact[], meta: {
|
|
151
|
-
//
|
|
195
|
+
// Returns Contact[] directly instead of { data: Contact[], meta: { offset, limit, total } }
|
|
196
|
+
// Library can't determine if there are more pages — infinite loop or stops too early
|
|
152
197
|
```
|
|
153
198
|
|
|
154
199
|
```typescript
|
|
155
200
|
// ✅ Correct: return { data, meta } structure
|
|
156
201
|
queryFn: (pagination) => ContactService.getAll({
|
|
157
|
-
|
|
202
|
+
offset: pagination.offset,
|
|
158
203
|
limit: pagination.limit,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
})
|
|
205
|
+
// Where ContactService.getAll already returns { data: Contact[], meta: { offset, limit, total } }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The library requires the `{ data, meta }` shape to know how to concatenate pages and when to stop.
|
|
209
|
+
|
|
210
|
+
Source: `src/types/pagination.type.ts` — `OffsetPaginationResponse`
|
|
211
|
+
|
|
212
|
+
### HIGH: Use pageParam or cursor instead of offset/key
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// ❌ Wrong: using old pageParam naming
|
|
216
|
+
queryFn: (pagination) => ContactService.getAll({
|
|
217
|
+
page: pagination.pageParam, // pageParam doesn't exist!
|
|
218
|
+
cursor: pagination.cursor, // cursor doesn't exist!
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// ✅ Correct: use offset for offset pagination, key for keyset
|
|
224
|
+
// Offset:
|
|
225
|
+
queryFn: (pagination) => ContactService.getAll({
|
|
226
|
+
offset: pagination.offset, // OffsetPaginationParams.offset
|
|
227
|
+
limit: pagination.limit,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// Keyset:
|
|
231
|
+
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
232
|
+
key: pagination.key, // KeysetPaginationParams.key
|
|
233
|
+
limit: pagination.limit,
|
|
234
|
+
})
|
|
168
235
|
```
|
|
169
236
|
|
|
170
|
-
|
|
237
|
+
`OffsetPaginationParams` has `{ offset: number, limit: number }`.
|
|
238
|
+
`KeysetPaginationParams` has `{ key?: any, limit: number }`.
|
|
171
239
|
|
|
172
|
-
Source: `
|
|
240
|
+
Source: `src/types/pagination.type.ts`
|
|
173
241
|
|
|
174
242
|
### HIGH: Mix offset and keyset pagination patterns in same query
|
|
175
243
|
|
|
@@ -177,7 +245,7 @@ Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Handling Pagina
|
|
|
177
245
|
// ❌ Wrong: mixing pagination patterns
|
|
178
246
|
const { result } = useOffsetInfiniteQuery('contactList', {
|
|
179
247
|
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
180
|
-
|
|
248
|
+
key: pagination.key, // offset composable doesn't have key!
|
|
181
249
|
limit: pagination.limit,
|
|
182
250
|
}),
|
|
183
251
|
})
|
|
@@ -185,26 +253,26 @@ const { result } = useOffsetInfiniteQuery('contactList', {
|
|
|
185
253
|
|
|
186
254
|
```typescript
|
|
187
255
|
// ✅ Correct: match composable to backend API
|
|
188
|
-
// Use useOffsetInfiniteQuery for
|
|
256
|
+
// Use useOffsetInfiniteQuery for offset/limit APIs:
|
|
189
257
|
const { result } = useOffsetInfiniteQuery('contactList', {
|
|
190
258
|
queryFn: (pagination) => ContactService.getAll({
|
|
191
|
-
|
|
259
|
+
offset: pagination.offset,
|
|
192
260
|
limit: pagination.limit,
|
|
193
261
|
}),
|
|
194
262
|
})
|
|
195
263
|
|
|
196
264
|
// Use useKeysetInfiniteQuery for cursor-based APIs:
|
|
197
|
-
const { result } = useKeysetInfiniteQuery('
|
|
265
|
+
const { result } = useKeysetInfiniteQuery('contactListKeyset', {
|
|
198
266
|
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
199
|
-
|
|
267
|
+
key: pagination.key,
|
|
200
268
|
limit: pagination.limit,
|
|
201
269
|
}),
|
|
202
270
|
})
|
|
203
271
|
```
|
|
204
272
|
|
|
205
|
-
Each composable expects a specific pagination parameter type.
|
|
273
|
+
Each composable expects a specific pagination parameter type. Choose the right composable for your backend API.
|
|
206
274
|
|
|
207
|
-
Source: `
|
|
275
|
+
Source: `src/composables/query/offsetInfiniteQuery.composable.ts` and `keysetInfiniteQuery.composable.ts`
|
|
208
276
|
|
|
209
277
|
### MEDIUM: Forget isFetchingNextPage flag; show loading on first page load
|
|
210
278
|
|
|
@@ -225,18 +293,16 @@ const { result, isFetchingNextPage, fetchNextPage } = useOffsetInfiniteQuery(...
|
|
|
225
293
|
</button>
|
|
226
294
|
```
|
|
227
295
|
|
|
228
|
-
`isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages.
|
|
296
|
+
`isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages.
|
|
229
297
|
|
|
230
|
-
Source: `
|
|
298
|
+
Source: `src/composables/query/offsetInfiniteQuery.composable.ts` — `UseOffsetInfiniteQueryReturnType`
|
|
231
299
|
|
|
232
300
|
## Backend API Strategy
|
|
233
301
|
|
|
234
302
|
> Offset vs keyset pagination depends entirely on your backend endpoint. Use the strategy your API provides.
|
|
235
|
-
>
|
|
236
|
-
> — Maintainer guidance
|
|
237
303
|
|
|
238
|
-
If your API
|
|
239
|
-
If your API
|
|
304
|
+
If your API accepts `offset` and `limit` parameters, use `useOffsetInfiniteQuery`.
|
|
305
|
+
If your API accepts a cursor `key` parameter, use `useKeysetInfiniteQuery`.
|
|
240
306
|
|
|
241
307
|
## See Also
|
|
242
308
|
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: writing-mutations
|
|
3
3
|
description: >
|
|
4
|
-
Create, update, delete resources using
|
|
4
|
+
Create, update, delete resources using useMutation, typed queryKeysToInvalidate with optional param extractors, AsyncResult error handling, execute function, request shape with body/params separation.
|
|
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/mutation.md"
|
|
10
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/mutation/mutation.composable.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/factory/createApiMutationUtils.ts"
|
|
12
10
|
---
|
|
13
11
|
|
|
14
12
|
# @wisemen/vue-core-api-utils — Writing Mutations
|
|
@@ -33,7 +31,7 @@ export function useCreateContact() {
|
|
|
33
31
|
}
|
|
34
32
|
```
|
|
35
33
|
|
|
36
|
-
Every mutation
|
|
34
|
+
Every mutation should list which queries to invalidate via `queryKeysToInvalidate`.
|
|
37
35
|
|
|
38
36
|
## Core Patterns
|
|
39
37
|
|
|
@@ -52,8 +50,7 @@ async function handleSubmit(formData: ContactCreateForm) {
|
|
|
52
50
|
// Invalidated queries will refetch automatically
|
|
53
51
|
} else if (response.isErr()) {
|
|
54
52
|
const error = response.getError()
|
|
55
|
-
|
|
56
|
-
if (error.errors[0].code === 'EMAIL_EXISTS') {
|
|
53
|
+
if ('errors' in error && error.errors[0].code === 'EMAIL_EXISTS') {
|
|
57
54
|
toast.error('That email is already registered')
|
|
58
55
|
} else {
|
|
59
56
|
toast.error('Creation failed')
|
|
@@ -64,29 +61,54 @@ async function handleSubmit(formData: ContactCreateForm) {
|
|
|
64
61
|
|
|
65
62
|
Always `await execute()` and check the result state before continuing.
|
|
66
63
|
|
|
67
|
-
### Update mutation with specific query invalidation
|
|
64
|
+
### Update mutation with specific query invalidation using param extractors
|
|
65
|
+
|
|
66
|
+
When you need to invalidate a specific query (rather than all queries with a key), pass param extractor functions:
|
|
68
67
|
|
|
69
68
|
```typescript
|
|
70
|
-
export function useUpdateContact(
|
|
71
|
-
return useMutation({
|
|
72
|
-
queryFn: async (options
|
|
73
|
-
return await ContactService.update(contactUuid, options.body)
|
|
69
|
+
export function useUpdateContact() {
|
|
70
|
+
return useMutation<ContactUpdateForm, Contact, { contactUuid: string }>({
|
|
71
|
+
queryFn: async (options) => {
|
|
72
|
+
return await ContactService.update(options.params.contactUuid, options.body)
|
|
73
|
+
},
|
|
74
|
+
queryKeysToInvalidate: {
|
|
75
|
+
// Invalidate only the specific contact that was updated
|
|
76
|
+
contactDetail: {
|
|
77
|
+
contactUuid: (params) => params.contactUuid,
|
|
78
|
+
},
|
|
79
|
+
// Invalidate all contact lists
|
|
80
|
+
contactList: {},
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Param extractors receive `(mutationParams, responseData)` and return the value for that query param. Empty object `{}` invalidates all queries with that key.
|
|
87
|
+
|
|
88
|
+
### Mutation with URL params only (no body)
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
export function useDeleteContact() {
|
|
92
|
+
return useMutation<void, void, { contactUuid: string }>({
|
|
93
|
+
queryFn: async (options) => {
|
|
94
|
+
return await ContactService.delete(options.params.contactUuid)
|
|
74
95
|
},
|
|
75
96
|
queryKeysToInvalidate: {
|
|
76
|
-
|
|
77
|
-
contactList: {}, // And the list
|
|
97
|
+
contactList: {},
|
|
78
98
|
},
|
|
79
99
|
})
|
|
80
100
|
}
|
|
101
|
+
|
|
102
|
+
// execute({ params: { contactUuid: '123' } })
|
|
81
103
|
```
|
|
82
104
|
|
|
83
|
-
|
|
105
|
+
When `TReqData` is `void`, the `execute` call takes `{ params: TParams }` instead of `{ body, params }`.
|
|
84
106
|
|
|
85
107
|
### Form integration
|
|
86
108
|
|
|
87
109
|
```vue
|
|
88
110
|
<script setup lang="ts">
|
|
89
|
-
import {
|
|
111
|
+
import { reactive } from 'vue'
|
|
90
112
|
import { useCreateContact } from '@/composables'
|
|
91
113
|
|
|
92
114
|
const form = reactive({ name: '', email: '' })
|
|
@@ -110,7 +132,7 @@ async function handleSubmit() {
|
|
|
110
132
|
{{ result.isLoading() ? 'Creating...' : 'Create' }}
|
|
111
133
|
</button>
|
|
112
134
|
<div v-if="result.isErr()">
|
|
113
|
-
Error: {{ result.getError().errors[0]
|
|
135
|
+
Error: {{ result.getError().errors?.[0]?.detail }}
|
|
114
136
|
</div>
|
|
115
137
|
</form>
|
|
116
138
|
</template>
|
|
@@ -120,7 +142,7 @@ Use `result.isLoading()` to disable the button during mutation.
|
|
|
120
142
|
|
|
121
143
|
## Common Mistakes
|
|
122
144
|
|
|
123
|
-
### CRITICAL: Import useMutation from @tanstack/vue-query instead of
|
|
145
|
+
### CRITICAL: Import useMutation from @tanstack/vue-query instead of your api module
|
|
124
146
|
|
|
125
147
|
```typescript
|
|
126
148
|
// ❌ Wrong: using TanStack directly
|
|
@@ -136,7 +158,7 @@ const mutation = useMutation({
|
|
|
136
158
|
```
|
|
137
159
|
|
|
138
160
|
```typescript
|
|
139
|
-
// ✅ Correct: use
|
|
161
|
+
// ✅ Correct: use the composable from your api module
|
|
140
162
|
import { useMutation } from '@/api'
|
|
141
163
|
|
|
142
164
|
const { execute, result } = useMutation({
|
|
@@ -150,9 +172,9 @@ const { execute, result } = useMutation({
|
|
|
150
172
|
// Full AsyncResult, type-safe queryKeysToInvalidate, error codes
|
|
151
173
|
```
|
|
152
174
|
|
|
153
|
-
Direct TanStack import loses
|
|
175
|
+
Direct TanStack import loses type safety and AsyncResult wrapping.
|
|
154
176
|
|
|
155
|
-
Source:
|
|
177
|
+
Source: `src/composables/mutation/mutation.composable.ts`
|
|
156
178
|
|
|
157
179
|
### CRITICAL: Forget to list queryKeysToInvalidate; cache becomes stale
|
|
158
180
|
|
|
@@ -180,9 +202,9 @@ const { execute } = useMutation({
|
|
|
180
202
|
// After success, contactList queries refetch
|
|
181
203
|
```
|
|
182
204
|
|
|
183
|
-
If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data.
|
|
205
|
+
If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data.
|
|
184
206
|
|
|
185
|
-
Source: `
|
|
207
|
+
Source: `src/composables/mutation/mutation.composable.ts` — `onSuccess` invalidation logic
|
|
186
208
|
|
|
187
209
|
### HIGH: Not await execute(); code runs before mutation completes
|
|
188
210
|
|
|
@@ -205,34 +227,37 @@ async function handleSubmit() {
|
|
|
205
227
|
}
|
|
206
228
|
```
|
|
207
229
|
|
|
208
|
-
Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result.
|
|
230
|
+
Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result.
|
|
209
231
|
|
|
210
|
-
Source: `
|
|
232
|
+
Source: `src/composables/mutation/mutation.composable.ts` — `execute` returns `Promise<ApiResult>`
|
|
211
233
|
|
|
212
|
-
### HIGH: Use body instead of params for
|
|
234
|
+
### HIGH: Use body instead of params for URL parameters
|
|
213
235
|
|
|
214
236
|
```typescript
|
|
215
|
-
// ❌ Wrong:
|
|
216
|
-
const { execute } = useMutation({
|
|
237
|
+
// ❌ Wrong: URL params passed as body
|
|
238
|
+
const { execute } = useMutation<SearchForm, Results, void>({
|
|
217
239
|
queryFn: async (options) => {
|
|
218
|
-
return await SearchService.search(options.body) // params
|
|
240
|
+
return await SearchService.search(options.body) // URL params shouldn't be in body
|
|
219
241
|
},
|
|
220
242
|
})
|
|
221
243
|
```
|
|
222
244
|
|
|
223
245
|
```typescript
|
|
224
|
-
// ✅ Correct: separate body
|
|
225
|
-
const { execute } = useMutation({
|
|
246
|
+
// ✅ Correct: separate body (payload) from params (URL query string)
|
|
247
|
+
const { execute } = useMutation<SearchForm, Results, { category: string }>({
|
|
226
248
|
queryFn: async (options) => {
|
|
227
249
|
const { body, params } = options
|
|
228
|
-
return await SearchService.search(params)
|
|
250
|
+
return await SearchService.search(body, params.category)
|
|
229
251
|
},
|
|
230
252
|
})
|
|
253
|
+
|
|
254
|
+
// Call with both:
|
|
255
|
+
execute({ body: searchForm, params: { category: 'contacts' } })
|
|
231
256
|
```
|
|
232
257
|
|
|
233
|
-
|
|
258
|
+
`body` is for the request payload (POST/PUT body); `params` is for URL query string parameters. The `RequestParams` type enforces this shape automatically based on your generics.
|
|
234
259
|
|
|
235
|
-
Source:
|
|
260
|
+
Source: `src/composables/mutation/mutation.composable.ts` — `RequestParams` type
|
|
236
261
|
|
|
237
262
|
## See Also
|
|
238
263
|
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: writing-queries
|
|
3
3
|
description: >
|
|
4
|
-
Single resource queries using
|
|
4
|
+
Single resource queries using useQuery, computed ref params, staleTime configuration, queryFn, refetch, isFetching vs isLoading distinctions, automatic cache management.
|
|
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.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/composables/query/query.composable.ts"
|
|
12
10
|
---
|
|
13
11
|
|
|
@@ -73,6 +71,20 @@ const { result } = useQuery('contactDetail', {
|
|
|
73
71
|
|
|
74
72
|
`staleTime` determines how long cached data is considered fresh. After this time, the next query interaction triggers a background refetch.
|
|
75
73
|
|
|
74
|
+
### Conditionally enable a query
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const isEnabled = computed(() => contactUuid.value !== null)
|
|
78
|
+
|
|
79
|
+
const { result } = useQuery('contactDetail', {
|
|
80
|
+
params: { contactUuid: computed(() => contactUuid.value!) },
|
|
81
|
+
queryFn: () => ContactService.getByUuid(contactUuid.value!),
|
|
82
|
+
isEnabled,
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `isEnabled` to prevent the query from running until required data is available.
|
|
87
|
+
|
|
76
88
|
### Manually refetch on demand
|
|
77
89
|
|
|
78
90
|
```typescript
|
|
@@ -92,7 +104,7 @@ if (result.value.isOk()) {
|
|
|
92
104
|
|
|
93
105
|
## Common Mistakes
|
|
94
106
|
|
|
95
|
-
### CRITICAL: Import useQuery from @tanstack/vue-query instead of
|
|
107
|
+
### CRITICAL: Import useQuery from @tanstack/vue-query instead of your api module
|
|
96
108
|
|
|
97
109
|
```typescript
|
|
98
110
|
// ❌ Wrong: using TanStack directly
|
|
@@ -106,7 +118,7 @@ const { data, error, isLoading } = useQuery({
|
|
|
106
118
|
```
|
|
107
119
|
|
|
108
120
|
```typescript
|
|
109
|
-
// ✅ Correct: use
|
|
121
|
+
// ✅ Correct: use the composable from your api module (or directly from the library)
|
|
110
122
|
import { useQuery } from '@/api'
|
|
111
123
|
import { computed } from 'vue'
|
|
112
124
|
|
|
@@ -118,9 +130,9 @@ const { result, isLoading } = useQuery('contactDetail', {
|
|
|
118
130
|
// Full type safety, AsyncResult wrapping, automatic error codes
|
|
119
131
|
```
|
|
120
132
|
|
|
121
|
-
Importing directly from @tanstack/vue-query bypasses the typed
|
|
133
|
+
Importing directly from @tanstack/vue-query bypasses the typed composable, losing AsyncResult wrapping, type-safe query keys, and error code typing.
|
|
122
134
|
|
|
123
|
-
Source:
|
|
135
|
+
Source: `src/composables/query/query.composable.ts`
|
|
124
136
|
|
|
125
137
|
### HIGH: Use plain ref for params instead of computed
|
|
126
138
|
|
|
@@ -147,9 +159,9 @@ const { result } = useQuery('userDetail', {
|
|
|
147
159
|
|
|
148
160
|
When params are plain refs, the query doesn't watch them and the cache isn't invalidated when the param changes.
|
|
149
161
|
|
|
150
|
-
Source: `
|
|
162
|
+
Source: `src/composables/query/query.composable.ts` — `NestedMaybeRefOrGetter` type
|
|
151
163
|
|
|
152
|
-
### HIGH: Not set staleTime;
|
|
164
|
+
### HIGH: Not set staleTime; background refetch on every interaction
|
|
153
165
|
|
|
154
166
|
```typescript
|
|
155
167
|
// ❌ Wrong: no staleTime; background refetch constantly
|
|
@@ -173,8 +185,6 @@ const { result } = useQuery('userDetail', {
|
|
|
173
185
|
|
|
174
186
|
Default `staleTime` is 0, meaning the cache is immediately considered stale. Every interaction triggers a background refetch. Set `staleTime` based on how frequently the data changes.
|
|
175
187
|
|
|
176
|
-
Source: `docs/packages/api-utils/pages/getting-started/installation.md` Setup section
|
|
177
|
-
|
|
178
188
|
### MEDIUM: Confuse isFetching with isLoading
|
|
179
189
|
|
|
180
190
|
```typescript
|
|
@@ -195,9 +205,11 @@ if (result.value.isLoading()) {
|
|
|
195
205
|
// Use isFetching separately for background fetch indicator
|
|
196
206
|
```
|
|
197
207
|
|
|
198
|
-
`isLoading` is true only during the initial fetch. `isFetching` is true whenever any fetch is in progress (including background refetches). Use `result.isLoading()` for conditional rendering; use `isFetching` for loading indicators
|
|
208
|
+
`isLoading` is true only during the initial fetch. `isFetching` is true whenever any fetch is in progress (including background refetches). Use `result.isLoading()` for conditional rendering; use `isFetching` for loading indicators.
|
|
209
|
+
|
|
210
|
+
Note: `isLoading`, `isError`, and `isSuccess` on the return type are deprecated — prefer `result.value.isLoading()`, `result.value.isErr()`, and `result.value.isOk()`.
|
|
199
211
|
|
|
200
|
-
Source: `
|
|
212
|
+
Source: `src/composables/query/query.composable.ts` — `UseQueryReturnType`
|
|
201
213
|
|
|
202
214
|
## See Also
|
|
203
215
|
|