@wisemen/vue-core-api-utils 1.0.1 → 1.2.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 +25 -18
- package/dist/index.mjs +10 -1
- package/package.json +12 -3
- package/skills/asyncresult-handling/SKILL.md +181 -0
- package/skills/cache-management/SKILL.md +222 -0
- package/skills/foundations/SKILL.md +461 -0
- package/skills/getting-started/SKILL.md +248 -0
- package/skills/optimistic-uis/SKILL.md +402 -0
- package/skills/writing-infinitequeries/SKILL.md +243 -0
- package/skills/writing-mutations/SKILL.md +240 -0
- package/skills/writing-queries/SKILL.md +205 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-mutations
|
|
3
|
+
description: >
|
|
4
|
+
Create, update, delete resources using factory-provided useMutation, typed queryKeysToInvalidate, AsyncResult error handling, execute function, request shape with body/params separation.
|
|
5
|
+
type: core
|
|
6
|
+
library: vue-core-api-utils
|
|
7
|
+
library_version: "0.0.3"
|
|
8
|
+
sources:
|
|
9
|
+
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/mutation.md"
|
|
10
|
+
- "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
|
+
---
|
|
13
|
+
|
|
14
|
+
# @wisemen/vue-core-api-utils — Writing Mutations
|
|
15
|
+
|
|
16
|
+
Create, update, and delete resources. Mutations automatically invalidate affected queries and return AsyncResult for explicit error handling.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { useMutation } from '@/api'
|
|
22
|
+
import { ContactService } from '@/services'
|
|
23
|
+
|
|
24
|
+
export function useCreateContact() {
|
|
25
|
+
return useMutation({
|
|
26
|
+
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
27
|
+
return await ContactService.create(options.body)
|
|
28
|
+
},
|
|
29
|
+
queryKeysToInvalidate: {
|
|
30
|
+
contactList: {}, // Invalidate all contactList queries
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every mutation must list which queries to invalidate via `queryKeysToInvalidate`.
|
|
37
|
+
|
|
38
|
+
## Core Patterns
|
|
39
|
+
|
|
40
|
+
### Execute a mutation and handle the result
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { useCreateContact } from '@/composables'
|
|
44
|
+
|
|
45
|
+
const { execute, result } = useCreateContact()
|
|
46
|
+
|
|
47
|
+
async function handleSubmit(formData: ContactCreateForm) {
|
|
48
|
+
const response = await execute({ body: formData })
|
|
49
|
+
|
|
50
|
+
if (response.isOk()) {
|
|
51
|
+
console.log('Created contact:', response.getValue())
|
|
52
|
+
// Invalidated queries will refetch automatically
|
|
53
|
+
} else if (response.isErr()) {
|
|
54
|
+
const error = response.getError()
|
|
55
|
+
// Handle error based on code
|
|
56
|
+
if (error.errors[0].code === 'EMAIL_EXISTS') {
|
|
57
|
+
toast.error('That email is already registered')
|
|
58
|
+
} else {
|
|
59
|
+
toast.error('Creation failed')
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Always `await execute()` and check the result state before continuing.
|
|
66
|
+
|
|
67
|
+
### Update mutation with specific query invalidation
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
export function useUpdateContact(contactUuid: string) {
|
|
71
|
+
return useMutation({
|
|
72
|
+
queryFn: async (options: { body: ContactUpdateForm }) => {
|
|
73
|
+
return await ContactService.update(contactUuid, options.body)
|
|
74
|
+
},
|
|
75
|
+
queryKeysToInvalidate: {
|
|
76
|
+
contactDetail: {}, // Invalidate the specific contact
|
|
77
|
+
contactList: {}, // And the list
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You can invalidate multiple queries. Include queries that depend on the data you're changing.
|
|
84
|
+
|
|
85
|
+
### Form integration
|
|
86
|
+
|
|
87
|
+
```vue
|
|
88
|
+
<script setup lang="ts">
|
|
89
|
+
import { ref } from 'vue'
|
|
90
|
+
import { useCreateContact } from '@/composables'
|
|
91
|
+
|
|
92
|
+
const form = reactive({ name: '', email: '' })
|
|
93
|
+
const { execute, result } = useCreateContact()
|
|
94
|
+
|
|
95
|
+
async function handleSubmit() {
|
|
96
|
+
const response = await execute({ body: form })
|
|
97
|
+
|
|
98
|
+
if (response.isOk()) {
|
|
99
|
+
router.push('/contacts')
|
|
100
|
+
}
|
|
101
|
+
// If isErr, form stays visible for user to retry
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<form @submit.prevent="handleSubmit">
|
|
107
|
+
<input v-model="form.name" />
|
|
108
|
+
<input v-model="form.email" />
|
|
109
|
+
<button :disabled="result.isLoading()">
|
|
110
|
+
{{ result.isLoading() ? 'Creating...' : 'Create' }}
|
|
111
|
+
</button>
|
|
112
|
+
<div v-if="result.isErr()">
|
|
113
|
+
Error: {{ result.getError().errors[0].detail }}
|
|
114
|
+
</div>
|
|
115
|
+
</form>
|
|
116
|
+
</template>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use `result.isLoading()` to disable the button during mutation.
|
|
120
|
+
|
|
121
|
+
## Common Mistakes
|
|
122
|
+
|
|
123
|
+
### CRITICAL: Import useMutation from @tanstack/vue-query instead of factory
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// ❌ Wrong: using TanStack directly
|
|
127
|
+
import { useMutation } from '@tanstack/vue-query'
|
|
128
|
+
|
|
129
|
+
const mutation = useMutation({
|
|
130
|
+
mutationFn: async (data) => ContactService.create(data),
|
|
131
|
+
onSuccess: () => {
|
|
132
|
+
queryClient.invalidateQueries({ queryKey: ['contactList'] })
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
// Loses AsyncResult, type safety, error codes
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// ✅ Correct: use factory composable
|
|
140
|
+
import { useMutation } from '@/api'
|
|
141
|
+
|
|
142
|
+
const { execute, result } = useMutation({
|
|
143
|
+
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
144
|
+
return await ContactService.create(options.body)
|
|
145
|
+
},
|
|
146
|
+
queryKeysToInvalidate: {
|
|
147
|
+
contactList: {}, // Typed, type-safe
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
// Full AsyncResult, type-safe queryKeysToInvalidate, error codes
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Direct TanStack import loses the factory's type safety and AsyncResult wrapping.
|
|
154
|
+
|
|
155
|
+
Source: Library architecture — always use composables from `createApiUtils()` factory
|
|
156
|
+
|
|
157
|
+
### CRITICAL: Forget to list queryKeysToInvalidate; cache becomes stale
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// ❌ Wrong: no queryKeysToInvalidate
|
|
161
|
+
const { execute } = useMutation({
|
|
162
|
+
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
163
|
+
return await ContactService.create(options.body)
|
|
164
|
+
},
|
|
165
|
+
// Forgot queryKeysToInvalidate!
|
|
166
|
+
})
|
|
167
|
+
// Mutation succeeds but list query still shows old data
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// ✅ Correct: invalidate affected queries
|
|
172
|
+
const { execute } = useMutation({
|
|
173
|
+
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
174
|
+
return await ContactService.create(options.body)
|
|
175
|
+
},
|
|
176
|
+
queryKeysToInvalidate: {
|
|
177
|
+
contactList: {}, // Invalidate all contactList queries
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
// After success, contactList queries refetch
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data. Update returns success but list still shows old items.
|
|
184
|
+
|
|
185
|
+
Source: `docs/packages/api-utils/pages/usage/mutation.md` Create Mutation Example
|
|
186
|
+
|
|
187
|
+
### HIGH: Not await execute(); code runs before mutation completes
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// ❌ Wrong: fire and forget
|
|
191
|
+
async function handleSubmit() {
|
|
192
|
+
execute({ body: formData })
|
|
193
|
+
router.push('/contacts') // Redirects before mutation finishes!
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// ✅ Correct: await the result
|
|
199
|
+
async function handleSubmit() {
|
|
200
|
+
const result = await execute({ body: formData })
|
|
201
|
+
if (result.isOk()) {
|
|
202
|
+
router.push('/contacts')
|
|
203
|
+
}
|
|
204
|
+
// If isErr, form stays visible for retry
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result. Always await and check the result before continuing.
|
|
209
|
+
|
|
210
|
+
Source: `docs/packages/api-utils/pages/usage/mutation.md` Usage in Component
|
|
211
|
+
|
|
212
|
+
### HIGH: Use body instead of params for query parameters
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// ❌ Wrong: filter/search as body
|
|
216
|
+
const { execute } = useMutation({
|
|
217
|
+
queryFn: async (options) => {
|
|
218
|
+
return await SearchService.search(options.body) // params should go in URL!
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// ✅ Correct: separate body and params
|
|
225
|
+
const { execute } = useMutation({
|
|
226
|
+
queryFn: async (options) => {
|
|
227
|
+
const { body, params } = options
|
|
228
|
+
return await SearchService.search(params) // URL params
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
When mutations accept query parameters (filters, search), pass them in the `params` field of the options, not `body`. `body` is for request payload; `params` is for URL query string.
|
|
234
|
+
|
|
235
|
+
Source: Source code `mutation.composable.ts` — request shape documentation
|
|
236
|
+
|
|
237
|
+
## See Also
|
|
238
|
+
|
|
239
|
+
- [Cache Management](../cache-management/SKILL.md) — Understanding which queries to invalidate
|
|
240
|
+
- [Writing Queries](../writing-queries/SKILL.md) — Mutations invalidate queries; understand queries first
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-queries
|
|
3
|
+
description: >
|
|
4
|
+
Single resource queries using factory-provided useQuery, computed ref params, staleTime configuration, queryFn, refetch, isFetching vs isLoading distinctions, automatic cache management.
|
|
5
|
+
type: core
|
|
6
|
+
library: vue-core-api-utils
|
|
7
|
+
library_version: "0.0.3"
|
|
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
|
+
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/query.composable.ts"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# @wisemen/vue-core-api-utils — Writing Queries
|
|
15
|
+
|
|
16
|
+
Fetch single resources with automatic caching, parameter reactivity, and configurable staleness.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { computed } from 'vue'
|
|
22
|
+
import { useQuery } from '@/api'
|
|
23
|
+
import { ContactService } from '@/services'
|
|
24
|
+
|
|
25
|
+
export function useContactDetail(contactUuid: string) {
|
|
26
|
+
return useQuery('contactDetail', {
|
|
27
|
+
params: { contactUuid: computed(() => contactUuid) },
|
|
28
|
+
queryFn: () => ContactService.getByUuid(contactUuid),
|
|
29
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The `params` object must contain computed refs so the query automatically refetches when params change.
|
|
35
|
+
|
|
36
|
+
## Core Patterns
|
|
37
|
+
|
|
38
|
+
### Query with reactive parameters
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { computed, ref } from 'vue'
|
|
42
|
+
import { useQuery } from '@/api'
|
|
43
|
+
|
|
44
|
+
const contactUuid = ref('123')
|
|
45
|
+
|
|
46
|
+
const { result, refetch } = useQuery('contactDetail', {
|
|
47
|
+
params: {
|
|
48
|
+
contactUuid: computed(() => contactUuid.value), // Computed so query watches it
|
|
49
|
+
},
|
|
50
|
+
queryFn: () => ContactService.getByUuid(contactUuid.value),
|
|
51
|
+
staleTime: 1000 * 60 * 5,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// When contactUuid.value changes, the query automatically refetches
|
|
55
|
+
contactUuid.value = '456'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Parameters must be computed refs. If you pass a plain ref, the query doesn't watch changes.
|
|
59
|
+
|
|
60
|
+
### Set cache expiry with staleTime
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Cache is fresh for 5 minutes — no background refetch
|
|
64
|
+
const { result } = useQuery('contactDetail', {
|
|
65
|
+
params: { contactUuid: computed(() => '123') },
|
|
66
|
+
queryFn: () => ContactService.getByUuid('123'),
|
|
67
|
+
staleTime: 1000 * 60 * 5, // 5 minutes = 300 seconds
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Default staleTime is 0 — cache immediately becomes stale
|
|
71
|
+
// Combine with global defaults in apiUtilsPlugin config
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`staleTime` determines how long cached data is considered fresh. After this time, the next query interaction triggers a background refetch.
|
|
75
|
+
|
|
76
|
+
### Manually refetch on demand
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const { result, refetch } = useQuery('contactDetail', {
|
|
80
|
+
params: { contactUuid: computed(() => '123') },
|
|
81
|
+
queryFn: () => ContactService.getByUuid('123'),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Manually trigger a new fetch
|
|
85
|
+
await refetch()
|
|
86
|
+
|
|
87
|
+
// After refetch completes, result contains new data
|
|
88
|
+
if (result.value.isOk()) {
|
|
89
|
+
console.log(result.value.getValue())
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Common Mistakes
|
|
94
|
+
|
|
95
|
+
### CRITICAL: Import useQuery from @tanstack/vue-query instead of factory
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// ❌ Wrong: using TanStack directly
|
|
99
|
+
import { useQuery } from '@tanstack/vue-query'
|
|
100
|
+
|
|
101
|
+
const { data, error, isLoading } = useQuery({
|
|
102
|
+
queryKey: ['contactDetail', '123'],
|
|
103
|
+
queryFn: () => ContactService.getByUuid('123'),
|
|
104
|
+
})
|
|
105
|
+
// Loses AsyncResult wrapping, type safety, error code typing
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// ✅ Correct: use factory-provided composable
|
|
110
|
+
import { useQuery } from '@/api'
|
|
111
|
+
import { computed } from 'vue'
|
|
112
|
+
|
|
113
|
+
const { result, isLoading } = useQuery('contactDetail', {
|
|
114
|
+
params: { contactUuid: computed(() => '123') },
|
|
115
|
+
queryFn: () => ContactService.getByUuid('123'),
|
|
116
|
+
staleTime: 1000 * 60 * 5,
|
|
117
|
+
})
|
|
118
|
+
// Full type safety, AsyncResult wrapping, automatic error codes
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Importing directly from @tanstack/vue-query bypasses the typed factory, losing AsyncResult wrapping, type-safe query keys, and error code typing.
|
|
122
|
+
|
|
123
|
+
Source: Library architecture — always use composables from `createApiUtils()` factory
|
|
124
|
+
|
|
125
|
+
### HIGH: Use plain ref for params instead of computed
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ❌ Wrong: plain ref doesn't trigger refetch
|
|
129
|
+
const userId = ref('123')
|
|
130
|
+
const { result } = useQuery('userDetail', {
|
|
131
|
+
params: { userId }, // plain ref, not computed
|
|
132
|
+
queryFn: () => UserService.getById(userId.value),
|
|
133
|
+
})
|
|
134
|
+
// Later: userId.value = '456'
|
|
135
|
+
// Query does NOT refetch — cache stays stale!
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// ✅ Correct: use computed so query watches changes
|
|
140
|
+
const userId = computed(() => props.userId)
|
|
141
|
+
const { result } = useQuery('userDetail', {
|
|
142
|
+
params: { userId },
|
|
143
|
+
queryFn: () => UserService.getById(userId.value),
|
|
144
|
+
})
|
|
145
|
+
// userId changes → computed updates → query watches → refetch happens
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
When params are plain refs, the query doesn't watch them and the cache isn't invalidated when the param changes.
|
|
149
|
+
|
|
150
|
+
Source: `docs/packages/api-utils/pages/usage/query.md` Usage in Vue Component section
|
|
151
|
+
|
|
152
|
+
### HIGH: Not set staleTime; serve stale cache indefinitely
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// ❌ Wrong: no staleTime; background refetch constantly
|
|
156
|
+
const { result } = useQuery('userDetail', {
|
|
157
|
+
params: { userId: computed(() => '123') },
|
|
158
|
+
queryFn: () => UserService.getById('123'),
|
|
159
|
+
// staleTime defaults to 0 — cache is immediately stale!
|
|
160
|
+
})
|
|
161
|
+
// Every component interaction triggers a refetch
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// ✅ Correct: set staleTime to a reasonable value
|
|
166
|
+
const { result } = useQuery('userDetail', {
|
|
167
|
+
params: { userId: computed(() => '123') },
|
|
168
|
+
queryFn: () => UserService.getById('123'),
|
|
169
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
170
|
+
})
|
|
171
|
+
// Cache remains fresh for 5 minutes — background refetch only after expiry
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
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
|
+
|
|
176
|
+
Source: `docs/packages/api-utils/pages/getting-started/installation.md` Setup section
|
|
177
|
+
|
|
178
|
+
### MEDIUM: Confuse isFetching with isLoading
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// ❌ Wrong: checking isLoading for conditional render
|
|
182
|
+
const { result, isLoading } = useQuery(...)
|
|
183
|
+
if (isLoading.value) {
|
|
184
|
+
return // Exits only on initial load!
|
|
185
|
+
}
|
|
186
|
+
// Code here runs while background refetch happens
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// ✅ Correct: use result.isLoading() for state checks
|
|
191
|
+
const { result } = useQuery(...)
|
|
192
|
+
if (result.value.isLoading()) {
|
|
193
|
+
return // True only on initial load
|
|
194
|
+
}
|
|
195
|
+
// Use isFetching separately for background fetch indicator
|
|
196
|
+
```
|
|
197
|
+
|
|
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 on load-more buttons.
|
|
199
|
+
|
|
200
|
+
Source: `docs/packages/api-utils/pages/usage/query.md` Return Values section
|
|
201
|
+
|
|
202
|
+
## See Also
|
|
203
|
+
|
|
204
|
+
- [Cache Management](../cache-management/SKILL.md) — Understanding caching strategy informs staleTime choices
|
|
205
|
+
- [Writing Infinite Queries](../writing-infinitequeries/SKILL.md) — Pagination uses the same patterns
|