@wisemen/vue-core-api-utils 1.2.0 → 2.0.1

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.
@@ -4,19 +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: "0.0.3"
8
- sources:
9
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/paginated-query.md"
10
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/offsetInfiniteQuery.composable.ts"
11
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/keysetInfiniteQuery.composable.ts"
12
- subsystems:
13
- - "Offset Pagination"
14
- - "Keyset Pagination"
15
7
  ---
16
8
 
17
9
  # @wisemen/vue-core-api-utils — Writing Infinite Queries
18
10
 
19
- Paginate through large datasets with two strategies: offset-based (page/limit) for traditional pagination, or keyset-based (cursor) for real-time data and large datasets.
11
+ 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
12
 
21
13
  **Choose your strategy based on what your backend API provides — not preference.**
22
14
 
@@ -36,16 +28,16 @@ export function useContactList() {
36
28
  params: {
37
29
  search: computed(() => search.value),
38
30
  },
39
- queryFn: (pagination) => ContactService.getAll({
40
- page: pagination.pageParam,
41
- limit: pagination.limit,
31
+ queryFn: ({ offset, limit }) => ContactService.getAll({
32
+ offset,
33
+ limit,
42
34
  search: search.value,
43
35
  }),
44
36
  })
45
37
  }
46
38
  ```
47
39
 
48
- Pagination parameter `pageParam` starts at 0 and increments. Return results with `{ data: Contact[], meta: { page, limit, total } }`.
40
+ The `queryFn` receives `{ offset, limit }` offset is the starting index (0 on the first page) and limit is the page size.
49
41
 
50
42
  ### Keyset Pagination (cursor-based)
51
43
 
@@ -61,16 +53,16 @@ export function useContactListKeyset() {
61
53
  params: {
62
54
  search: computed(() => search.value),
63
55
  },
64
- queryFn: (pagination) => ContactService.getAllKeyset({
65
- limit: pagination.limit,
66
- cursor: pagination.pageParam,
56
+ queryFn: ({ key, limit }) => ContactService.getAllKeyset({
57
+ limit,
58
+ after: key,
67
59
  search: search.value,
68
60
  }),
69
61
  })
70
62
  }
71
63
  ```
72
64
 
73
- Pagination parameter `pageParam` is a cursor (string). Return results with `{ data: Contact[], meta: { next?: string } }` the next cursor or undefined if no more pages.
65
+ The `queryFn` receives `{ key, limit }` `key` is the cursor (`undefined` on the first page) and `limit` is the page size.
74
66
 
75
67
  ## Core Patterns
76
68
 
@@ -105,138 +97,14 @@ All pages are automatically concatenated into `data`. Access with `result.getVal
105
97
 
106
98
  Use `isFetchingNextPage` (not `isFetching`) to disable the load-more button only during pagination, not during initial load.
107
99
 
108
- ## Common Mistakes
109
-
110
- ### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of factory
111
-
112
- ```typescript
113
- // ❌ Wrong: using TanStack directly
114
- import { useInfiniteQuery } from '@tanstack/vue-query'
115
-
116
- const { data, error } = useInfiniteQuery({
117
- queryKey: ['contactList'],
118
- queryFn: ({ pageParam = 0 }) => ContactService.getAll({ page: pageParam }),
119
- getNextPageParam: (lastPage) => lastPage.nextPage,
120
- })
121
- // Loses AsyncResult, type safety, error codes
122
- ```
123
-
124
- ```typescript
125
- // ✅ Correct: use factory composable
126
- import { useOffsetInfiniteQuery } from '@/api'
127
-
128
- const { result, fetchNextPage, hasNextPage } = useOffsetInfiniteQuery('contactList', {
129
- params: { search: computed(() => '...') },
130
- queryFn: (pagination) => ContactService.getAll({
131
- page: pagination.pageParam,
132
- limit: pagination.limit,
133
- }),
134
- })
135
- // Full AsyncResult wrapping, type safety, automatic error codes
136
- ```
137
-
138
- Direct TanStack import loses the factory's type safety, AsyncResult wrapping, and error code typing.
139
-
140
- Source: Library architecture — always use composables from `createApiUtils()` factory
141
-
142
- ### CRITICAL: Return paginated data without wrapping in data/meta structure
143
-
144
- ```typescript
145
- // ❌ Wrong: returning array directly
146
- queryFn: (pagination) => ContactService.getAll({
147
- page: pagination.pageParam,
148
- limit: pagination.limit,
149
- })
150
- // Returns Contact[] directly instead of { data: Contact[], meta: {...} }
151
- // QueryClient doesn't know how to append pages; pages overwrite instead of concat
152
- ```
153
-
154
- ```typescript
155
- // ✅ Correct: return { data, meta } structure
156
- queryFn: (pagination) => ContactService.getAll({
157
- page: pagination.pageParam,
158
- limit: pagination.limit,
159
- }).then(data => ({
160
- data,
161
- meta: {
162
- page: pagination.pageParam,
163
- limit: pagination.limit,
164
- total: 100, // Total count if available
165
- }
166
- }))
167
- // QueryClient knows how to append pages
168
- ```
169
-
170
- Pagination requires the library to know which part of the response is the data array and which part is pagination metadata. Return an object with `data` (array) and `meta` (metadata).
171
-
172
- Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Handling Pagination Results
173
-
174
- ### HIGH: Mix offset and keyset pagination patterns in same query
175
-
176
- ```typescript
177
- // ❌ Wrong: mixing pagination patterns
178
- const { result } = useOffsetInfiniteQuery('contactList', {
179
- queryFn: (pagination) => ContactService.getAllKeyset({
180
- cursor: pagination.pageParam, // offset expects page number!
181
- limit: pagination.limit,
182
- }),
183
- })
184
- ```
185
-
186
- ```typescript
187
- // ✅ Correct: match composable to backend API
188
- // Use useOffsetInfiniteQuery for page/limit APIs:
189
- const { result } = useOffsetInfiniteQuery('contactList', {
190
- queryFn: (pagination) => ContactService.getAll({
191
- page: pagination.pageParam,
192
- limit: pagination.limit,
193
- }),
194
- })
195
-
196
- // Use useKeysetInfiniteQuery for cursor-based APIs:
197
- const { result } = useKeysetInfiniteQuery('contactList', {
198
- queryFn: (pagination) => ContactService.getAllKeyset({
199
- cursor: pagination.pageParam,
200
- limit: pagination.limit,
201
- }),
202
- })
203
- ```
204
-
205
- Each composable expects a specific pagination parameter type. Offset expects a number; keyset expects a cursor string. Choose the right composable for your backend API.
206
-
207
- Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Offset vs Keyset comparison
208
-
209
- ### MEDIUM: Forget isFetchingNextPage flag; show loading on first page load
210
-
211
- ```typescript
212
- // ❌ Wrong: using isFetching on load-more button
213
- const { result, isFetching, fetchNextPage } = useOffsetInfiniteQuery(...)
214
- <button @click="fetchNextPage" :disabled="isFetching">
215
- {{ isFetching ? 'Loading...' : 'Load More' }}
216
- </button>
217
- // Button disabled on initial load too!
218
- ```
219
-
220
- ```typescript
221
- // ✅ Correct: use isFetchingNextPage for pagination button
222
- const { result, isFetchingNextPage, fetchNextPage } = useOffsetInfiniteQuery(...)
223
- <button @click="fetchNextPage" :disabled="isFetchingNextPage">
224
- {{ isFetchingNextPage ? 'Loading...' : 'Load More' }}
225
- </button>
226
- ```
227
-
228
- `isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages. Use `isFetchingNextPage` for the load-more button.
229
-
230
- Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Return Values
231
-
232
100
  ## Backend API Strategy
233
101
 
234
102
  > Offset vs keyset pagination depends entirely on your backend endpoint. Use the strategy your API provides.
235
103
  >
236
104
  > — Maintainer guidance
237
105
 
238
- If your API provides `page` and `limit` parameters, use `useOffsetInfiniteQuery`.
239
- If your API provides a `cursor` parameter, use `useKeysetInfiniteQuery`.
106
+ If your API provides `offset` and `limit` parameters, use `useOffsetInfiniteQuery`.
107
+ If your API provides a cursor/key parameter, use `useKeysetInfiniteQuery`.
240
108
 
241
109
  ## See Also
242
110
 
@@ -1,14 +1,9 @@
1
1
  ---
2
2
  name: writing-mutations
3
3
  description: >
4
- Create, update, delete resources using factory-provided useMutation, typed queryKeysToInvalidate, AsyncResult error handling, execute function, request shape with body/params separation.
4
+ Create, update, delete resources using useMutation, typed queryKeysToInvalidate, 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: "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
7
  ---
13
8
 
14
9
  # @wisemen/vue-core-api-utils — Writing Mutations
@@ -23,8 +18,8 @@ import { ContactService } from '@/services'
23
18
 
24
19
  export function useCreateContact() {
25
20
  return useMutation({
26
- queryFn: async (options: { body: ContactCreateForm }) => {
27
- return await ContactService.create(options.body)
21
+ queryFn: async ({ body }: { body: ContactCreateForm }) => {
22
+ return await ContactService.create(body)
28
23
  },
29
24
  queryKeysToInvalidate: {
30
25
  contactList: {}, // Invalidate all contactList queries
@@ -52,8 +47,7 @@ async function handleSubmit(formData: ContactCreateForm) {
52
47
  // Invalidated queries will refetch automatically
53
48
  } else if (response.isErr()) {
54
49
  const error = response.getError()
55
- // Handle error based on code
56
- if (error.errors[0].code === 'EMAIL_EXISTS') {
50
+ if ('errors' in error && error.errors[0].code === 'EMAIL_EXISTS') {
57
51
  toast.error('That email is already registered')
58
52
  } else {
59
53
  toast.error('Creation failed')
@@ -69,12 +63,14 @@ Always `await execute()` and check the result state before continuing.
69
63
  ```typescript
70
64
  export function useUpdateContact(contactUuid: string) {
71
65
  return useMutation({
72
- queryFn: async (options: { body: ContactUpdateForm }) => {
73
- return await ContactService.update(contactUuid, options.body)
66
+ queryFn: async ({ body }: { body: ContactUpdateForm }) => {
67
+ return await ContactService.update(contactUuid, body)
74
68
  },
75
69
  queryKeysToInvalidate: {
76
- contactDetail: {}, // Invalidate the specific contact
77
- contactList: {}, // And the list
70
+ contactDetail: {
71
+ contactUuid: (_params, _result) => contactUuid, // Invalidate only this contact's detail query
72
+ },
73
+ contactList: {}, // Invalidate all contactList queries
78
74
  },
79
75
  })
80
76
  }
@@ -109,7 +105,7 @@ async function handleSubmit() {
109
105
  <button :disabled="result.isLoading()">
110
106
  {{ result.isLoading() ? 'Creating...' : 'Create' }}
111
107
  </button>
112
- <div v-if="result.isErr()">
108
+ <div v-if="result.isErr() && 'errors' in result.getError()">
113
109
  Error: {{ result.getError().errors[0].detail }}
114
110
  </div>
115
111
  </form>
@@ -118,122 +114,6 @@ async function handleSubmit() {
118
114
 
119
115
  Use `result.isLoading()` to disable the button during mutation.
120
116
 
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
117
  ## See Also
238
118
 
239
119
  - [Cache Management](../cache-management/SKILL.md) — Understanding which queries to invalidate
@@ -1,14 +1,9 @@
1
1
  ---
2
2
  name: writing-queries
3
3
  description: >
4
- Single resource queries using factory-provided useQuery, computed ref params, staleTime configuration, queryFn, refetch, isFetching vs isLoading distinctions, automatic cache management.
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: "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
7
  ---
13
8
 
14
9
  # @wisemen/vue-core-api-utils — Writing Queries
@@ -90,115 +85,6 @@ if (result.value.isOk()) {
90
85
  }
91
86
  ```
92
87
 
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
88
  ## See Also
203
89
 
204
90
  - [Cache Management](../cache-management/SKILL.md) — Understanding caching strategy informs staleTime choices