@wisemen/vue-core-api-utils 1.0.1 → 1.1.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.
@@ -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