@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,11 +4,6 @@ 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: "0.0.3"
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
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
12
7
  ---
13
8
 
14
9
  # @wisemen/vue-core-api-utils — Handling AsyncResult Types
@@ -101,80 +96,6 @@ const name = result.value
101
96
  // Type: string
102
97
  ```
103
98
 
104
- ## Common Mistakes
105
-
106
- ### CRITICAL: Forget to check state before calling getValue/getError
107
-
108
- ```typescript
109
- // ❌ Wrong: getValue without isOk check
110
- const { result } = useQuery('contactDetail', { /* ... */ })
111
- const contact = result.value.getValue()
112
- console.log(contact.name) // contact could be null!
113
- ```
114
-
115
- ```typescript
116
- // ✅ Correct: check isOk first
117
- const { result } = useQuery('contactDetail', { /* ... */ })
118
- if (result.value.isOk()) {
119
- const contact = result.value.getValue()
120
- console.log(contact.name) // Safe!
121
- }
122
- ```
123
-
124
- Calling `getValue()` without `isOk()` returns null if the result is loading or an error. You get no compile error, and the UI renders nothing or crashes at runtime.
125
-
126
- Source: `docs/packages/api-utils/pages/concepts/result-types.md`
127
-
128
- ### HIGH: Not handle all three states in match()
129
-
130
- ```typescript
131
- // ❌ Wrong: missing loading handler
132
- result.value.match({
133
- ok: (data) => <div>{data.name}</div>,
134
- err: (error) => <div>Error: {error.detail}</div>,
135
- // Forgot loading!
136
- })
137
- ```
138
-
139
- ```typescript
140
- // ✅ Correct: handle all three states
141
- result.value.match({
142
- loading: () => <div>Loading...</div>,
143
- ok: (data) => <div>{data.name}</div>,
144
- err: (error) => <div>Error: {error.detail}</div>,
145
- })
146
- ```
147
-
148
- If you omit a handler, TypeScript errors and the UI renders nothing during the omitted state. The match is exhaustive by design.
149
-
150
- Source: `docs/packages/api-utils/pages/concepts/result-types.md` Pattern Matching Section
151
-
152
- ### HIGH: Use state flags (isLoading, isError, isSuccess) instead of AsyncResult state
153
-
154
- ```typescript
155
- // ❌ Wrong: mixing old flags with AsyncResult
156
- const { result, isLoading } = useQuery(...)
157
- if (isLoading.value) {
158
- // Show spinner
159
- } else {
160
- const data = result.value.getValue() // Could be null!
161
- }
162
- ```
163
-
164
- ```typescript
165
- // ✅ Correct: use AsyncResult state exclusively
166
- const { result } = useQuery(...)
167
- if (result.value.isLoading()) {
168
- // Show spinner
169
- } else if (result.value.isOk()) {
170
- const data = result.value.getValue() // Safe!
171
- }
172
- ```
173
-
174
- Composables export both AsyncResult (exhaustive) and backward-compatible flags (`isLoading`, `isError`, `isSuccess`). Mixing them causes logic bugs where flags say the query is done but the result is still loading.
175
-
176
- Source: Maintainer interview — library provides both patterns for backward compatibility, but agents should prefer AsyncResult
177
-
178
99
  ## Next Steps
179
100
 
180
101
  - [Writing Queries](../writing-queries/SKILL.md) — Fetch single resources with caching
@@ -4,16 +4,11 @@ description: >
4
4
  Type-safe QueryClient with get/set/update/invalidate methods, predicate-based updates, cascade invalidation strategy, shared cache across components, lazy refetch patterns.
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-client.md"
10
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/utils/query-client/queryClient.ts"
11
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/factory/createApiQueryClientUtils.ts"
12
7
  ---
13
8
 
14
9
  # @wisemen/vue-core-api-utils — Cache Management
15
10
 
16
- Manually read, write, update, and invalidate the query cache using the type-safe `useQueryClient()` composable. This is useful for optimistic updates and strategically invalidating affected queries.
11
+ Manually read, write, update, and invalidate the query cache using the type-safe `QueryClient` wrapper. This is useful for optimistic updates and strategically invalidating affected queries.
17
12
 
18
13
  ## Setup
19
14
 
@@ -31,16 +26,18 @@ queryClient.set(
31
26
  updatedContact
32
27
  )
33
28
 
34
- // Update cached data with a predicate
35
- queryClient.update('contactList', {
29
+ // Update cached data with a predicate (returns { rollback } for reverting)
30
+ const { rollback } = queryClient.update('contactList', {
36
31
  by: (contact) => contact.id === '123',
37
32
  value: (contact) => ({ ...contact, name: 'Updated' }),
38
33
  })
39
34
 
40
- // Invalidate queries (trigger refetch)
41
- queryClient.invalidate('contactList')
35
+ // Invalidate queries (async — triggers refetch)
36
+ await queryClient.invalidate('contactList')
42
37
  ```
43
38
 
39
+ `useQueryClient()` is a helper you create in your `@/api` module (see [getting-started](../getting-started/SKILL.md)) that wraps `new QueryClient(getTanstackQueryClient())`.
40
+
44
41
  ## Core Patterns
45
42
 
46
43
  ### Get cached data
@@ -86,8 +83,8 @@ queryClient.set('contactList', [
86
83
  ```typescript
87
84
  const queryClient = useQueryClient()
88
85
 
89
- // Update a single item in a list
90
- queryClient.update('contactList', {
86
+ // Update a single item in a list — returns { rollback } for reverting
87
+ const { rollback } = queryClient.update('contactList', {
91
88
  by: (contact) => contact.id === '123', // Predicate
92
89
  value: (contact) => ({ // Transform
93
90
  ...contact,
@@ -96,13 +93,13 @@ queryClient.update('contactList', {
96
93
  })
97
94
 
98
95
  // For single entities, the predicate always matches
99
- queryClient.update('contactDetail', {
100
- by: (contact) => true, // Always update
96
+ const { rollback: rollbackDetail } = queryClient.update('contactDetail', {
97
+ by: (contact) => true,
101
98
  value: (contact) => ({ ...contact, name: 'Updated' }),
102
99
  })
103
100
  ```
104
101
 
105
- QueryClient knows whether the entity is an array or single item, so predicates work transparently on lists.
102
+ `update()` returns `{ rollback }` — a function that reverts the cache to its previous state. Use this for optimistic updates. QueryClient knows whether the entity is an array or single item, so predicates work transparently on lists.
106
103
 
107
104
  ### Invalidate and refetch
108
105
 
@@ -110,78 +107,16 @@ QueryClient knows whether the entity is an array or single item, so predicates w
110
107
  const queryClient = useQueryClient()
111
108
 
112
109
  // Invalidate all queries with this key
113
- queryClient.invalidate('contactList')
110
+ await queryClient.invalidate('contactList')
114
111
 
115
112
  // Invalidate specific query
116
- queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
113
+ await queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
117
114
 
118
115
  // After invalidation, the next query interaction triggers a refetch
119
116
  ```
120
117
 
121
118
  Invalidation marks cached data as stale. The next interaction (component mount, user action) triggers a refetch.
122
119
 
123
- ## Common Mistakes
124
-
125
- ### HIGH: Call update/set without checking data structure (array vs entity)
126
-
127
- ```typescript
128
- // ❌ Wrong: treating array like entity
129
- const queryClient = useQueryClient()
130
- queryClient.update('contactList', {
131
- by: (contact) => contact.id === '123',
132
- value: (contact) => ({ ...contact, name: 'Updated' }),
133
- })
134
- // If contactList is Contact[], predicate matches each item individually
135
- // If you expect single match, this breaks silently
136
- ```
137
-
138
- ```typescript
139
- // ✅ Correct: QueryClient infers type from query key
140
- const queryClient = useQueryClient()
141
- // If contactList: { entity: Contact[], ... }
142
- // QueryClient knows to iterate the array and apply predicate to each item
143
- queryClient.update('contactList', {
144
- by: (contact) => contact.id === '123',
145
- value: (contact) => ({ ...contact, name: 'Updated' }),
146
- })
147
- // For single entities: { entity: Contact, ... }
148
- // QueryClient provides the entity directly to predicate
149
- ```
150
-
151
- QueryClient infers data structure from your query key definition, so the same `update()` call works correctly for arrays and single entities. The type system ensures you're using the right predicate signature.
152
-
153
- Source: `docs/packages/api-utils/pages/usage/query-client.md` Usage
154
-
155
- ### MEDIUM: Call set() without async loading state; UI flashes stale data
156
-
157
- ```typescript
158
- // ❌ Wrong: immediate set without loading indicator
159
- const queryClient = useQueryClient()
160
- queryClient.set(['contactDetail', { contactUuid }], updatedData)
161
- // Cache updated but no indicator that request is pending
162
- // UI looks responsive but actually has unconfirmed data
163
- ```
164
-
165
- ```typescript
166
- // ✅ Correct: pair optimistic update with mutation result handling
167
- const queryClient = useQueryClient()
168
- const original = queryClient.get(['contactDetail', { contactUuid }])
169
-
170
- // Update cache optimistically
171
- queryClient.set(['contactDetail', { contactUuid }], newData)
172
-
173
- // Execute mutation
174
- const result = await execute(formData)
175
- if (result.isErr()) {
176
- // Rollback on error
177
- queryClient.set(['contactDetail', { contactUuid }], original)
178
- }
179
- ```
180
-
181
- If you update the cache without a mutation in flight, the UI looks responsive but the data is unconfirmed. Always pair cache updates with mutation execution and rollback on error.
182
-
183
- Source: `docs/packages/api-utils/pages/usage/query-client.md` Real-World Example
184
-
185
120
  ## Cache Strategy
186
121
 
187
122
  > Explicitly invalidate only the queries affected by the mutation. Let lazy refetch handle the rest when users navigate to pages needing other data.
@@ -4,12 +4,6 @@ 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: "0.0.3"
8
- sources:
9
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/concepts/result-types.md"
10
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
11
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/apiError.type.ts"
12
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
13
7
  ---
14
8
 
15
9
  # @wisemen/vue-core-api-utils — Foundations
@@ -197,8 +191,8 @@ async function handleRefresh() {
197
191
 
198
192
  // Automatic refetch on mutation (via queryKeysToInvalidate)
199
193
  const { execute } = useMutation({
200
- queryFn: (data) => ContactService.update(data),
201
- queryKeysToInvalidate: { contactDetail: () => true },
194
+ queryFn: ({ body }: { body: ContactUpdateForm }) => ContactService.update(body),
195
+ queryKeysToInvalidate: { contactDetail: {} },
202
196
  })
203
197
  // After execute succeeds, contactDetail is invalidated
204
198
  // Next useQuery('contactDetail') refetches fresh data
@@ -246,8 +240,9 @@ Errors are typed and structured using `neverthrow`:
246
240
  interface ApiExpectedError {
247
241
  errors: Array<{
248
242
  code: string
249
- message: string
250
- details?: unknown
243
+ detail: string
244
+ status: string
245
+ source?: { pointer: string }
251
246
  }>
252
247
  }
253
248
 
@@ -259,10 +254,11 @@ const result = new AsyncResultErr(apiError)
259
254
  result.match({
260
255
  ok: (data) => {}, // not executed
261
256
  err: (error) => {
262
- // error is ApiError
263
- if (error instanceof ApiExpectedError) {
257
+ // error is ApiError — narrow with 'errors' in error
258
+ if ('errors' in error) {
264
259
  // Handle known API errors
265
260
  const codes = error.errors.map(e => e.code)
261
+ const detail = error.errors[0].detail
266
262
  } else {
267
263
  // Handle network/parsing errors
268
264
  console.error(error.message)
@@ -273,154 +269,6 @@ result.match({
273
269
 
274
270
  Error types are defined at library initialization via the generic `TErrorCode`. This ensures type-safe error handling across queries and mutations.
275
271
 
276
- ## Common Mistakes
277
-
278
- ### CRITICAL: Confuse Result (neverthrow) with AsyncResult; treat ok/err as boolean
279
-
280
- ```typescript
281
- // ❌ Wrong: neverthrow Result is not AsyncResult
282
- const result = new Result(contact, null) // This is not how neverthrow works
283
- if (result.ok) { // `.ok` doesn't exist
284
- console.log(result.value)
285
- }
286
-
287
- // Or even worse: treating AsyncResult like a boolean
288
- const { result } = useQuery(...)
289
- if (result.value) {
290
- // This is always true; result is always defined (Loading | Ok | Err)
291
- }
292
- ```
293
-
294
- ```typescript
295
- // ✅ Correct: AsyncResult requires exhaustive pattern matching
296
- const { result } = useQuery('contactDetail', {
297
- queryFn: () => ContactService.getDetail(uuid),
298
- })
299
-
300
- result.value.match({
301
- loading: () => showSpinner(),
302
- ok: (contact) => showContact(contact),
303
- err: (error) => showError(error),
304
- })
305
-
306
- // Or use type guards
307
- if (result.value.isOk()) {
308
- console.log(result.value.getValue())
309
- } else if (result.value.isErr()) {
310
- console.log(result.value.getError())
311
- } else {
312
- showSpinner()
313
- }
314
- ```
315
-
316
- AsyncResult requires explicit handling of all three states. The type system won't let you skip a state.
317
-
318
- Source: `docs/packages/api-utils/pages/concepts/result-types.md`
319
-
320
- ### MEDIUM: Misunderstand staleTime; think data refreshes automatically after staleTime
321
-
322
- ```typescript
323
- // ❌ Wrong: assuming staleTime auto-refetches
324
- const { result } = useQuery('contactDetail', {
325
- queryFn: () => ContactService.getDetail(uuid),
326
- staleTime: 5 * 60 * 1000, // Not an auto-refresh interval
327
- })
328
-
329
- // At T=5m, data doesn't automatically refetch.
330
- // It's just marked stale. Refetch happens on next interaction
331
- // (component mount, user action, mutation invalidation)
332
- ```
333
-
334
- ```typescript
335
- // ✅ Correct: staleTime is a grace period, not an interval
336
- const { result, refetch } = useQuery('contactDetail', {
337
- queryFn: () => ContactService.getDetail(uuid),
338
- staleTime: 5 * 60 * 1000,
339
- })
340
-
341
- // Data is fresh for 5 minutes (instant returns)
342
- // After 5 minutes, next access triggers refetch
343
- // For auto-refresh, use refetch() in a watchEffect or timer
344
-
345
- watchEffect(async () => {
346
- // Refetch every 10 seconds
347
- const interval = setInterval(() => refetch(), 10 * 1000)
348
- onCleanup(() => clearInterval(interval))
349
- })
350
- ```
351
-
352
- Stale time is not an auto-refresh interval. It's the duration the cache is considered fresh without refetching. Refetch happens on next access or when explicitly triggered.
353
-
354
- ### HIGH: Misunderstand gcTime and cache eviction; assume cache persists forever
355
-
356
- ```typescript
357
- // ❌ Wrong: assuming cache is permanent
358
- const { result } = useQuery('contactDetail', {
359
- queryFn: () => ContactService.getDetail(uuid),
360
- gcTime: 5 * 60 * 1000, // Default is 5 minutes
361
- })
362
-
363
- // If component unmounts and user is gone for > 5 minutes,
364
- // Next access refetches fresh (cache is evicted)
365
- // This is correct behavior, but if you expected cached data...
366
- ```
367
-
368
- ```typescript
369
- // ✅ Correct: increase gcTime if you want longer-lived cache
370
- const { result } = useQuery('contactDetail', {
371
- queryFn: () => ContactService.getDetail(uuid),
372
- staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
373
- gcTime: 60 * 60 * 1000, // Keep in memory for 1 hour
374
- })
375
-
376
- // After 5 minutes (stale) but before 1 hour (gc),
377
- // Returning cached data (but trigger refetch automatically)
378
- // After 1 hour, cache is deleted; next access refetches fresh
379
- ```
380
-
381
- gcTime controls cache eviction. Longer gcTime = cache lives longer. Longer staleTime = more queries use cache without refetching. Both are configurable defaults in the plugin config.
382
-
383
- Source: `packages/web/api-utils/src/config/config.ts`
384
-
385
- ### MEDIUM: Forget that QueryClient is shared; one query invalidation affects all components
386
-
387
- ```typescript
388
- // ❌ Wrong: not realizing cache is global
389
- const { execute } = useMutation({
390
- queryFn: () => ContactService.update(data),
391
- queryKeysToInvalidate: { contactDetail: () => true },
392
- })
393
-
394
- // Component A: detail view
395
- // Component B: list view (also uses contactDetail)
396
- // Even though A only updated one contact,
397
- // B's cache is invalidated too (because same query key)
398
- ```
399
-
400
- ```typescript
401
- // ✅ Correct: QueryClient is intentionally shared
402
- export function useContactMutation() {
403
- const { execute } = useMutation({
404
- queryFn: () => ContactService.update(data),
405
- queryKeysToInvalidate: {
406
- // Invalidates all components using this key
407
- contactDetail: () => true,
408
- // Also invalidate lists that show this contact
409
- contactList: () => true,
410
- },
411
- })
412
-
413
- return { execute }
414
- }
415
-
416
- // When A updates a contact, both A and B refetch.
417
- // This is the intended design: shared cache across app.
418
- ```
419
-
420
- QueryClient is application-wide (singleton). Invalidating a query invalidates for all components using that key. This is a feature: synchronized cache across the app.
421
-
422
- Source: `packages/web/api-utils/src/utils/query-client/queryClient.ts`
423
-
424
272
  ## Integration pattern
425
273
 
426
274
  The full integration: