@wisemen/vue-core-api-utils 1.0.0 → 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.
package/LICENSE.md ADDED
@@ -0,0 +1,59 @@
1
+ # PolyForm Strict License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/strict/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose, other than distributing the software or making changes or new works based on the software.
12
+
13
+ ## Patent License
14
+
15
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
16
+
17
+ ## Noncommercial Purposes
18
+
19
+ Any noncommercial purpose is a permitted purpose.
20
+
21
+ ## Personal Uses
22
+
23
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
24
+
25
+ ## Noncommercial Organizations
26
+
27
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
28
+
29
+ ## Fair Use
30
+
31
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
32
+
33
+ ## No Other Rights
34
+
35
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
36
+
37
+ ## Patent Defense
38
+
39
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
40
+
41
+ ## Violations
42
+
43
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
44
+
45
+ ## No Liability
46
+
47
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
48
+
49
+ ## Definitions
50
+
51
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
52
+
53
+ **You** refers to the individual or entity agreeing to these terms.
54
+
55
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
56
+
57
+ **Your licenses** are all the licenses granted to you for the software under these terms.
58
+
59
+ **Use** means anything you do with the software requiring one of your licenses.
package/dist/index.d.mts CHANGED
@@ -320,7 +320,6 @@ interface UseKeysetInfiniteQueryReturnType<TData, TErrorCode extends string = st
320
320
  */
321
321
  result: ComputedRef<AsyncResult<KeysetPaginationResponse<TData>, ApiError<TErrorCode>>>;
322
322
  }
323
- declare function useKeysetInfiniteQuery<TData, TErrorCode extends string = string>(options: KeysetInfiniteQueryOptions<TData, TErrorCode>): UseKeysetInfiniteQueryReturnType<TData, TErrorCode>;
324
323
  //#endregion
325
324
  //#region src/composables/query/offsetInfiniteQuery.composable.d.ts
326
325
  interface OffsetInfiniteQueryOptions<TData, TErrorCode extends string = string> {
@@ -398,7 +397,6 @@ interface UseOffsetInfiniteQueryReturnType<TData, TErrorCode extends string = st
398
397
  */
399
398
  result: ComputedRef<AsyncResult<OffsetPaginationResponse<TData>, ApiError<TErrorCode>>>;
400
399
  }
401
- declare function useOffsetInfiniteQuery<TData, TErrorCode extends string = string>(options: OffsetInfiniteQueryOptions<TData, TErrorCode>): UseOffsetInfiniteQueryReturnType<TData, TErrorCode>;
402
400
  //#endregion
403
401
  //#region src/composables/query/query.composable.d.ts
404
402
  interface UseQueryOptions<TResData, TErrorCode extends string = string> {
@@ -627,18 +625,20 @@ interface ApiUseMutationOptions<TQueryKeys extends object, TReqData, TResData, T
627
625
  */
628
626
  queryKeysToInvalidate?: { [TKey in keyof TQueryKeys]?: QueryKeyInvalidationConfig<TQueryKeys, TKey, TParams, TResData> };
629
627
  }
630
- interface CreateApiMutationUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
631
- useMutation: <TReqData = void, TResData = void, TParams = void>(options: ApiUseMutationOptions<TQueryKeys, TReqData, TResData, TParams, TErrorCode>) => UseMutationReturnType<TReqData, TResData, TParams>;
632
- }
633
628
  //#endregion
634
629
  //#region src/factory/createApiInfiniteQueryUtils.d.ts
635
630
  interface CreateApiInfiniteQueryUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
636
- useKeysetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseKeysetInfiniteQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => ReturnType<typeof useKeysetInfiniteQuery<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>>;
637
- useOffsetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseOffsetInfiniteQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => ReturnType<typeof useOffsetInfiniteQuery<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>>;
631
+ useKeysetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseKeysetInfiniteQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => UseKeysetInfiniteQueryReturnType<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>;
632
+ useOffsetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseOffsetInfiniteQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => UseOffsetInfiniteQueryReturnType<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>;
638
633
  }
634
+ type ApiUseKeysetInfiniteQueryReturnType<TQueryKeys extends object, TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>, TErrorCode extends string = string> = UseKeysetInfiniteQueryReturnType<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>;
635
+ type ApiUseOffsetInfiniteQueryReturnType<TQueryKeys extends object, TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>, TErrorCode extends string = string> = UseOffsetInfiniteQueryReturnType<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>;
639
636
  declare function createApiInfiniteQueryUtils<TQueryKeys extends object, TErrorCode extends string = string>(): CreateApiInfiniteQueryUtilsReturnType<TQueryKeys, TErrorCode>;
640
637
  //#endregion
641
638
  //#region src/factory/createApiMutationUtils.d.ts
639
+ interface CreateApiMutationUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
640
+ useMutation: <TReqData = void, TResData = void, TParams = void>(options: ApiUseMutationOptions<TQueryKeys, TReqData, TResData, TParams, TErrorCode>) => UseMutationReturnType<TReqData, TResData, TParams>;
641
+ }
642
642
  declare function createApiMutationUtils<TQueryKeys extends object, TErrorCode extends string = string>(): CreateApiMutationUtilsReturnType<TQueryKeys, TErrorCode>;
643
643
  //#endregion
644
644
  //#region src/factory/createApiPrefetchInfiniteQueryUtils.d.ts
@@ -678,6 +678,16 @@ interface QueryClientUpdateOptions<TEntity> {
678
678
  */
679
679
  value: (item: EntityItem<TEntity>) => EntityItem<TEntity>;
680
680
  }
681
+ /**
682
+ * Result of an update operation, providing a rollback function
683
+ */
684
+ interface QueryClientUpdateResult {
685
+ /**
686
+ * Reverts the cache entries affected by this update to their state before the update was applied.
687
+ * Safe to call multiple times (subsequent calls are no-ops).
688
+ */
689
+ rollback: () => void;
690
+ }
681
691
  /**
682
692
  * QueryClient utility class for type-safe query operations
683
693
  */
@@ -775,11 +785,14 @@ declare class QueryClient<TQueryKeys extends object> {
775
785
  * @example
776
786
  * ```typescript
777
787
  * // Update a specific user by id
778
- * queryClient.update('userDetail', {
788
+ * const { rollback } = queryClient.update('userDetail', {
779
789
  * by: (user) => user.id === '123',
780
790
  * value: (user) => ({ ...user, name: 'John Doe' })
781
791
  * })
782
792
  *
793
+ * // Revert if the mutation fails
794
+ * rollback()
795
+ *
783
796
  * // Update all electronics products to out of stock
784
797
  * queryClient.update('productList', {
785
798
  * by: (product) => product.category === 'electronics',
@@ -787,8 +800,8 @@ declare class QueryClient<TQueryKeys extends object> {
787
800
  * })
788
801
  * ```
789
802
  */
790
- update<TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>, TEntity extends QueryKeyEntityFromConfig<TQueryKeys, TKey$1> = QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>(key: TKey$1, options: QueryClientUpdateOptions<TEntity>): void;
791
- update<TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>, TEntity extends QueryKeyEntityFromConfig<TQueryKeys, TKey$1> = QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>(keyTuple: readonly [TKey$1, Partial<QueryKeyRawParamsFromConfig<TQueryKeys, TKey$1>>], options: QueryClientUpdateOptions<TEntity>): void;
803
+ update<TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>, TEntity extends QueryKeyEntityFromConfig<TQueryKeys, TKey$1> = QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>(key: TKey$1, options: QueryClientUpdateOptions<TEntity>): QueryClientUpdateResult;
804
+ update<TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>, TEntity extends QueryKeyEntityFromConfig<TQueryKeys, TKey$1> = QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>(keyTuple: readonly [TKey$1, Partial<QueryKeyRawParamsFromConfig<TQueryKeys, TKey$1>>], options: QueryClientUpdateOptions<TEntity>): QueryClientUpdateResult;
792
805
  }
793
806
  //#endregion
794
807
  //#region src/factory/createApiQueryClientUtils.d.ts
@@ -867,4 +880,4 @@ declare class SortUtil {
867
880
  }[];
868
881
  }
869
882
  //#endregion
870
- export { ApiError, ApiErrorObject, ApiExpectedError, ApiKnownErrorObject, ApiResult, ApiUnexpectedError, ApiUnknownErrorObject, ApiUseKeysetInfinitePrefetchQueryOptions, ApiUseKeysetInfiniteQueryOptions, ApiUseMutationOptions, ApiUseOffsetInfinitePrefetchQueryOptions, ApiUseOffsetInfiniteQueryOptions, ApiUsePrefetchQueryOptions, ApiUseQueryOptions, AsyncApiResult, AsyncResult, AsyncResultErr, AsyncResultLoading, AsyncResultOk, type CreateApiInfiniteQueryUtilsReturnType, type CreateApiMutationUtilsReturnType, type CreateApiPrefetchInfiniteQueryUtilsReturnType, type CreateApiPrefetchQueryUtilsReturnType, type CreateApiQueryClientUtilsReturnType, type CreateApiQueryUtilsReturnType, type InfiniteQueryOptions, type KeysetInfiniteQueryOptions, KeysetPagination, KeysetPaginationParams, KeysetPaginationResponse, KeysetPaginationResult, type OffsetInfiniteQueryOptions, OffsetPagination, OffsetPaginationParams, OffsetPaginationResponse, OffsetPaginationResult, PaginatedDataDto, QueryClient, QueryClientUpdateOptions, type QueryConfig, QueryKeyArrayItemFromConfig, QueryKeysWithArrayEntityFromConfig, type QueryParams, Sort, SortDirection, SortUtil, type TanstackQueryClient, type UseMutationReturnType, type UseQueryOptions, type UseQueryReturnType, type WithFilterQuery, type WithSearchQuery, type WithSortQuery, type WithStaticFilterQuery, apiUtilsPlugin, createApiInfiniteQueryUtils, createApiMutationUtils, createApiPrefetchInfiniteQueryUtils, createApiPrefetchQueryUtils, createApiQueryClientUtils, createApiQueryUtils, createApiUtils, getQueryClient as getTanstackQueryClient, initializeApiUtils, setQueryConfig };
883
+ export { type ApiError, type ApiErrorObject, type ApiExpectedError, type ApiKnownErrorObject, type ApiResult, type ApiUnexpectedError, type ApiUnknownErrorObject, type ApiUseKeysetInfinitePrefetchQueryOptions, type ApiUseKeysetInfiniteQueryOptions, type ApiUseKeysetInfiniteQueryReturnType, type ApiUseMutationOptions, type ApiUseOffsetInfinitePrefetchQueryOptions, type ApiUseOffsetInfiniteQueryOptions, type ApiUseOffsetInfiniteQueryReturnType, type ApiUsePrefetchQueryOptions, type ApiUseQueryOptions, type AsyncApiResult, AsyncResult, AsyncResultErr, AsyncResultLoading, AsyncResultOk, type CreateApiInfiniteQueryUtilsReturnType, type CreateApiMutationUtilsReturnType, type CreateApiPrefetchInfiniteQueryUtilsReturnType, type CreateApiPrefetchQueryUtilsReturnType, type CreateApiQueryClientUtilsReturnType, type CreateApiQueryUtilsReturnType, type InfiniteQueryOptions, type KeysetInfiniteQueryOptions, type KeysetPagination, type KeysetPaginationParams, type KeysetPaginationResponse, type KeysetPaginationResult, type OffsetInfiniteQueryOptions, type OffsetPagination, type OffsetPaginationParams, type OffsetPaginationResponse, type OffsetPaginationResult, type PaginatedDataDto, QueryClient, type QueryClientUpdateOptions, type QueryClientUpdateResult, type QueryConfig, type QueryKeyArrayItemFromConfig, type QueryKeysWithArrayEntityFromConfig, type QueryParams, type Sort, type SortDirection, SortUtil, type TanstackQueryClient, type UseKeysetInfiniteQueryReturnType, type UseMutationReturnType, type UseOffsetInfiniteQueryReturnType, type UseQueryOptions, type UseQueryReturnType, type WithFilterQuery, type WithSearchQuery, type WithSortQuery, type WithStaticFilterQuery, apiUtilsPlugin, createApiInfiniteQueryUtils, createApiMutationUtils, createApiPrefetchInfiniteQueryUtils, createApiPrefetchQueryUtils, createApiQueryClientUtils, createApiQueryUtils, createApiUtils, getQueryClient as getTanstackQueryClient, initializeApiUtils, setQueryConfig };
package/dist/index.mjs CHANGED
@@ -600,9 +600,11 @@ var QueryClient = class {
600
600
  });
601
601
  return !isSpecific;
602
602
  } });
603
+ const snapshots = /* @__PURE__ */ new Map();
603
604
  for (const query of queries) {
604
605
  const currentData = query.state.data;
605
606
  if (this.isInfiniteDataLike(currentData)) {
607
+ snapshots.set(query.queryKey, currentData);
606
608
  const updatedInfiniteData = {
607
609
  ...currentData,
608
610
  pages: currentData.pages.map((page) => {
@@ -616,10 +618,17 @@ var QueryClient = class {
616
618
  if (!isAsyncResult(currentData)) continue;
617
619
  const rawEntity = this.extractEntityFromAsyncResult(currentData);
618
620
  if (rawEntity === null) continue;
621
+ snapshots.set(query.queryKey, currentData);
619
622
  const updatedEntity = this.updateEntity(by, rawEntity, value);
620
623
  const wrappedData = this.wrapEntityInAsyncResult(updatedEntity);
621
624
  this.queryClient.setQueryData(query.queryKey, wrappedData);
622
625
  }
626
+ let rolledBack = false;
627
+ return { rollback: () => {
628
+ if (rolledBack) return;
629
+ rolledBack = true;
630
+ for (const [queryKey, data] of snapshots) this.queryClient.setQueryData(queryKey, data);
631
+ } };
623
632
  }
624
633
  };
625
634
 
@@ -761,4 +770,4 @@ var SortUtil = class {
761
770
  };
762
771
 
763
772
  //#endregion
764
- export { ApiError, ApiErrorObject, ApiExpectedError, ApiKnownErrorObject, ApiResult, ApiUnexpectedError, ApiUnknownErrorObject, AsyncApiResult, AsyncResult, AsyncResultErr, AsyncResultLoading, AsyncResultOk, KeysetPagination, KeysetPaginationParams, KeysetPaginationResponse, KeysetPaginationResult, OffsetPagination, OffsetPaginationParams, OffsetPaginationResponse, OffsetPaginationResult, PaginatedDataDto, QueryClient, QueryClientUpdateOptions, Sort, SortDirection, SortUtil, apiUtilsPlugin, createApiInfiniteQueryUtils, createApiMutationUtils, createApiPrefetchInfiniteQueryUtils, createApiPrefetchQueryUtils, createApiQueryClientUtils, createApiQueryUtils, createApiUtils, getQueryClient as getTanstackQueryClient, initializeApiUtils, setQueryConfig };
773
+ export { AsyncResult, AsyncResultErr, AsyncResultLoading, AsyncResultOk, QueryClient, SortUtil, apiUtilsPlugin, createApiInfiniteQueryUtils, createApiMutationUtils, createApiPrefetchInfiniteQueryUtils, createApiPrefetchQueryUtils, createApiQueryClientUtils, createApiQueryUtils, createApiUtils, getQueryClient as getTanstackQueryClient, initializeApiUtils, setQueryConfig };
package/package.json CHANGED
@@ -4,8 +4,8 @@
4
4
  "access": "public"
5
5
  },
6
6
  "type": "module",
7
- "version": "1.0.0",
8
- "license": "MIT",
7
+ "version": "1.1.0",
8
+ "license": "SEE LICENSE IN LICENSE.md",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "https://github.com/wisemen-digital/wisemen-core",
@@ -22,8 +22,15 @@
22
22
  "module": "./dist/index.mjs",
23
23
  "types": "./dist/index.d.mts",
24
24
  "typings": "./dist/index.d.mts",
25
+ "keywords": [
26
+ "tanstack-intent",
27
+ "vue-query",
28
+ "result",
29
+ "error-handling"
30
+ ],
25
31
  "files": [
26
- "./dist"
32
+ "dist",
33
+ "skills"
27
34
  ],
28
35
  "peerDependencies": {
29
36
  "@tanstack/vue-query": ">=5.90.5",
@@ -31,12 +38,14 @@
31
38
  "vue": ">=3.5.22"
32
39
  },
33
40
  "devDependencies": {
41
+ "@tanstack/intent": "^0.0.29",
34
42
  "@types/node": "24.8.1",
35
43
  "eslint": "9.39.4",
36
44
  "tsdown": "0.18.4",
37
45
  "typescript": "5.9.3",
46
+ "vue": "3.5.27",
38
47
  "vitest": "4.1.0",
39
- "@wisemen/eslint-config-vue": "2.1.1"
48
+ "@wisemen/eslint-config-vue": "2.1.2"
40
49
  },
41
50
  "scripts": {
42
51
  "build": "tsdown",
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: asyncresult-handling
3
+ description: >
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
+ 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/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
+ ---
13
+
14
+ # @wisemen/vue-core-api-utils — Handling AsyncResult Types
15
+
16
+ All queries and mutations return `AsyncResult<T, E>` — a type-safe alternative to separate `data`, `error`, and `isLoading` states. AsyncResult is always in one of three states: Loading, Ok, or Err.
17
+
18
+ ## Setup
19
+
20
+ ```typescript
21
+ import { useQuery } from '@/api'
22
+
23
+ const { result } = useQuery('contactDetail', {
24
+ params: { contactUuid: computed(() => '123') },
25
+ queryFn: () => ContactService.getByUuid('123'),
26
+ })
27
+
28
+ // result is a ComputedRef<AsyncResult<Contact, ApiError>>
29
+ // It's always in one of three states:
30
+ // - AsyncResult.Loading()
31
+ // - AsyncResult.Ok(contact: Contact)
32
+ // - AsyncResult.Err(error: ApiError)
33
+ ```
34
+
35
+ ## Core Patterns
36
+
37
+ ### Check state and extract values safely
38
+
39
+ ```typescript
40
+ const { result } = useQuery('contactDetail', { /* ... */ })
41
+
42
+ if (result.value.isLoading()) {
43
+ console.log('Request in flight...')
44
+ } else if (result.value.isOk()) {
45
+ const contact = result.value.getValue()
46
+ console.log('Name:', contact.name) // TypeScript knows contact is Contact
47
+ } else if (result.value.isErr()) {
48
+ const error = result.value.getError()
49
+ console.log('Error:', error.detail)
50
+ }
51
+ ```
52
+
53
+ The type predicates `isLoading()`, `isOk()`, and `isErr()` narrow the type so `getValue()` and `getError()` are safe.
54
+
55
+ ### Pattern match all three states
56
+
57
+ ```typescript
58
+ const { result } = useQuery('contactDetail', { /* ... */ })
59
+
60
+ result.value.match({
61
+ loading: () => <div>Loading...</div>,
62
+ ok: (contact) => <div>Name: {contact.name}</div>,
63
+ err: (error) => <div>Error: {error.detail}</div>,
64
+ })
65
+ ```
66
+
67
+ `match()` is exhaustive — you must handle all three cases or TypeScript errors.
68
+
69
+ ### Transform results with map and mapErr
70
+
71
+ ```typescript
72
+ const { result } = useQuery('contactDetail', { /* ... */ })
73
+
74
+ // Transform the success value
75
+ const contactName = result.value.map(contact => contact.name)
76
+
77
+ // Transform the error
78
+ const errorMessage = result.value.mapErr(error => error.detail)
79
+
80
+ // Chain transformations
81
+ const displayText = result.value
82
+ .map(contact => `Hello, ${contact.name}`)
83
+ .mapErr(error => `Failed: ${error.detail}`)
84
+ .unwrapOr('No data')
85
+ ```
86
+
87
+ `map()` and `mapErr()` return new AsyncResult values, letting you transform without unwrapping.
88
+
89
+ ### Use unwrapOr for fallback values
90
+
91
+ ```typescript
92
+ const { result } = useQuery('contactDetail', { /* ... */ })
93
+
94
+ // Get the value if Ok, otherwise use fallback
95
+ const contact = result.value.unwrapOr(null)
96
+ // Type: Contact | null
97
+
98
+ const name = result.value
99
+ .map(c => c.name)
100
+ .unwrapOr('Unknown')
101
+ // Type: string
102
+ ```
103
+
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
+ ## Next Steps
179
+
180
+ - [Writing Queries](../writing-queries/SKILL.md) — Fetch single resources with caching
181
+ - [Handling Mutations](../writing-mutations/SKILL.md) — Create/update/delete with result handling
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: cache-management
3
+ description: >
4
+ Type-safe QueryClient with get/set/update/invalidate methods, predicate-based updates, cascade invalidation strategy, shared cache across components, lazy refetch patterns.
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-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
+ ---
13
+
14
+ # @wisemen/vue-core-api-utils — Cache Management
15
+
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.
17
+
18
+ ## Setup
19
+
20
+ ```typescript
21
+ import { useQueryClient } from '@/api'
22
+
23
+ const queryClient = useQueryClient()
24
+
25
+ // Get cached data
26
+ const contact = queryClient.get(['contactDetail', { contactUuid: '123' }])
27
+
28
+ // Set cached data
29
+ queryClient.set(
30
+ ['contactDetail', { contactUuid: '123' }],
31
+ updatedContact
32
+ )
33
+
34
+ // Update cached data with a predicate
35
+ queryClient.update('contactList', {
36
+ by: (contact) => contact.id === '123',
37
+ value: (contact) => ({ ...contact, name: 'Updated' }),
38
+ })
39
+
40
+ // Invalidate queries (trigger refetch)
41
+ queryClient.invalidate('contactList')
42
+ ```
43
+
44
+ ## Core Patterns
45
+
46
+ ### Get cached data
47
+
48
+ ```typescript
49
+ const queryClient = useQueryClient()
50
+
51
+ // Get specific query
52
+ const contact = queryClient.get(
53
+ ['contactDetail', { contactUuid: '123' }]
54
+ )
55
+
56
+ // Get all queries with a key
57
+ const allContacts = queryClient.get('contactList')
58
+
59
+ // Get exact query only
60
+ const specificQuery = queryClient.get('contactList', { isExact: true })
61
+ ```
62
+
63
+ Returns the cached data or null if not cached. The QueryClient infers entity type from your query key definition.
64
+
65
+ ### Set cached data
66
+
67
+ ```typescript
68
+ const queryClient = useQueryClient()
69
+
70
+ queryClient.set(
71
+ ['contactDetail', { contactUuid: '123' }],
72
+ { id: '123', name: 'John', email: 'john@email.com' }
73
+ )
74
+
75
+ // For lists, set works with arrays too
76
+ queryClient.set('contactList', [
77
+ { id: '123', name: 'John' },
78
+ { id: '456', name: 'Jane' },
79
+ ])
80
+ ```
81
+
82
+ `set()` replaces all cached data for that query key.
83
+
84
+ ### Update cached data with predicates
85
+
86
+ ```typescript
87
+ const queryClient = useQueryClient()
88
+
89
+ // Update a single item in a list
90
+ queryClient.update('contactList', {
91
+ by: (contact) => contact.id === '123', // Predicate
92
+ value: (contact) => ({ // Transform
93
+ ...contact,
94
+ name: 'Updated John'
95
+ }),
96
+ })
97
+
98
+ // For single entities, the predicate always matches
99
+ queryClient.update('contactDetail', {
100
+ by: (contact) => true, // Always update
101
+ value: (contact) => ({ ...contact, name: 'Updated' }),
102
+ })
103
+ ```
104
+
105
+ QueryClient knows whether the entity is an array or single item, so predicates work transparently on lists.
106
+
107
+ ### Invalidate and refetch
108
+
109
+ ```typescript
110
+ const queryClient = useQueryClient()
111
+
112
+ // Invalidate all queries with this key
113
+ queryClient.invalidate('contactList')
114
+
115
+ // Invalidate specific query
116
+ queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
117
+
118
+ // After invalidation, the next query interaction triggers a refetch
119
+ ```
120
+
121
+ Invalidation marks cached data as stale. The next interaction (component mount, user action) triggers a refetch.
122
+
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
+ ## Cache Strategy
186
+
187
+ > Explicitly invalidate only the queries affected by the mutation. Let lazy refetch handle the rest when users navigate to pages needing other data.
188
+ >
189
+ > — Maintainer guidance
190
+
191
+ When a mutation succeeds, look at what changed:
192
+ - If you updated a contact, invalidate `contactDetail` and `contactList` (they both show that contact)
193
+ - If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless showing the one you archived)
194
+ - Don't invalidate unrelated queries — let them refetch lazily when needed
195
+
196
+ ## Shared Cache Across Components
197
+
198
+ Important: Multiple components using the same query key share the same cached data. This is a feature, not a bug.
199
+
200
+ ```typescript
201
+ // ComponentA
202
+ const { result: resultA } = useQuery('userDetail', {
203
+ params: { id: computed(() => 'same-id') },
204
+ queryFn: () => UserService.getById('same-id'),
205
+ })
206
+
207
+ // ComponentB
208
+ const { result: resultB } = useQuery('userDetail', {
209
+ params: { id: computed(() => 'same-id') },
210
+ queryFn: () => UserService.getById('same-id'),
211
+ })
212
+
213
+ // resultA and resultB are the SAME cached value
214
+ // Mutation in B invalidates A's cache
215
+ ```
216
+
217
+ Use this to your advantage: invalidate a query and all components using it refetch automatically.
218
+
219
+ ## See Also
220
+
221
+ - [Writing Mutations](../writing-mutations/SKILL.md) — Every mutation needs to know which queries to invalidate
222
+ - [Writing Queries](../writing-queries/SKILL.md) — Understanding caching strategy informs cache management choices