@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
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,19 +625,19 @@ 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>) =>
|
|
637
|
-
useOffsetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseOffsetInfiniteQueryOptions<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
|
}
|
|
639
|
-
|
|
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>;
|
|
640
636
|
//#endregion
|
|
641
637
|
//#region src/factory/createApiMutationUtils.d.ts
|
|
642
|
-
|
|
638
|
+
interface CreateApiMutationUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
|
|
639
|
+
useMutation: <TReqData = void, TResData = void, TParams = void>(options: ApiUseMutationOptions<TQueryKeys, TReqData, TResData, TParams, TErrorCode>) => UseMutationReturnType<TReqData, TResData, TParams, TErrorCode>;
|
|
640
|
+
}
|
|
643
641
|
//#endregion
|
|
644
642
|
//#region src/factory/createApiPrefetchInfiniteQueryUtils.d.ts
|
|
645
643
|
interface CreateApiPrefetchInfiniteQueryUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
|
|
@@ -650,7 +648,6 @@ interface CreateApiPrefetchInfiniteQueryUtilsReturnType<TQueryKeys extends objec
|
|
|
650
648
|
execute: () => Promise<void>;
|
|
651
649
|
};
|
|
652
650
|
}
|
|
653
|
-
declare function createApiPrefetchInfiniteQueryUtils<TQueryKeys extends object, TErrorCode extends string = string>(): CreateApiPrefetchInfiniteQueryUtilsReturnType<TQueryKeys, TErrorCode>;
|
|
654
651
|
//#endregion
|
|
655
652
|
//#region src/factory/createApiPrefetchQueryUtils.d.ts
|
|
656
653
|
interface CreateApiPrefetchQueryUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
|
|
@@ -658,7 +655,6 @@ interface CreateApiPrefetchQueryUtilsReturnType<TQueryKeys extends object, TErro
|
|
|
658
655
|
execute: () => Promise<void>;
|
|
659
656
|
};
|
|
660
657
|
}
|
|
661
|
-
declare function createApiPrefetchQueryUtils<TQueryKeys extends object, TErrorCode extends string = string>(): CreateApiPrefetchQueryUtilsReturnType<TQueryKeys, TErrorCode>;
|
|
662
658
|
//#endregion
|
|
663
659
|
//#region src/utils/query-client/queryClient.d.ts
|
|
664
660
|
/**
|
|
@@ -678,6 +674,16 @@ interface QueryClientUpdateOptions<TEntity> {
|
|
|
678
674
|
*/
|
|
679
675
|
value: (item: EntityItem<TEntity>) => EntityItem<TEntity>;
|
|
680
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Result of an update operation, providing a rollback function
|
|
679
|
+
*/
|
|
680
|
+
interface QueryClientUpdateResult {
|
|
681
|
+
/**
|
|
682
|
+
* Reverts the cache entries affected by this update to their state before the update was applied.
|
|
683
|
+
* Safe to call multiple times (subsequent calls are no-ops).
|
|
684
|
+
*/
|
|
685
|
+
rollback: () => void;
|
|
686
|
+
}
|
|
681
687
|
/**
|
|
682
688
|
* QueryClient utility class for type-safe query operations
|
|
683
689
|
*/
|
|
@@ -775,11 +781,14 @@ declare class QueryClient<TQueryKeys extends object> {
|
|
|
775
781
|
* @example
|
|
776
782
|
* ```typescript
|
|
777
783
|
* // Update a specific user by id
|
|
778
|
-
* queryClient.update('userDetail', {
|
|
784
|
+
* const { rollback } = queryClient.update('userDetail', {
|
|
779
785
|
* by: (user) => user.id === '123',
|
|
780
786
|
* value: (user) => ({ ...user, name: 'John Doe' })
|
|
781
787
|
* })
|
|
782
788
|
*
|
|
789
|
+
* // Revert if the mutation fails
|
|
790
|
+
* rollback()
|
|
791
|
+
*
|
|
783
792
|
* // Update all electronics products to out of stock
|
|
784
793
|
* queryClient.update('productList', {
|
|
785
794
|
* by: (product) => product.category === 'electronics',
|
|
@@ -787,21 +796,19 @@ declare class QueryClient<TQueryKeys extends object> {
|
|
|
787
796
|
* })
|
|
788
797
|
* ```
|
|
789
798
|
*/
|
|
790
|
-
update<TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>, TEntity extends QueryKeyEntityFromConfig<TQueryKeys, TKey$1> = QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>(key: TKey$1, options: QueryClientUpdateOptions<TEntity>):
|
|
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>):
|
|
799
|
+
update<TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>, TEntity extends QueryKeyEntityFromConfig<TQueryKeys, TKey$1> = QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>(key: TKey$1, options: QueryClientUpdateOptions<TEntity>): QueryClientUpdateResult;
|
|
800
|
+
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
801
|
}
|
|
793
802
|
//#endregion
|
|
794
803
|
//#region src/factory/createApiQueryClientUtils.d.ts
|
|
795
804
|
interface CreateApiQueryClientUtilsReturnType<TQueryKeys extends object> {
|
|
796
805
|
useQueryClient: () => QueryClient<TQueryKeys>;
|
|
797
806
|
}
|
|
798
|
-
declare function createApiQueryClientUtils<TQueryKeys extends object>(): CreateApiQueryClientUtilsReturnType<TQueryKeys>;
|
|
799
807
|
//#endregion
|
|
800
808
|
//#region src/factory/createApiQueryUtils.d.ts
|
|
801
809
|
interface CreateApiQueryUtilsReturnType<TQueryKeys extends object, TErrorCode extends string = string> {
|
|
802
810
|
useQuery: <TKey$1 extends QueryKeysWithEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => UseQueryReturnType<QueryKeyEntityFromConfig<TQueryKeys, TKey$1>>;
|
|
803
811
|
}
|
|
804
|
-
declare function createApiQueryUtils<TQueryKeys extends object, TErrorCode extends string = string>(): CreateApiQueryUtilsReturnType<TQueryKeys, TErrorCode>;
|
|
805
812
|
//#endregion
|
|
806
813
|
//#region src/factory/createApiUtils.d.ts
|
|
807
814
|
/**
|
|
@@ -820,7 +827,7 @@ declare function createApiQueryUtils<TQueryKeys extends object, TErrorCode exten
|
|
|
820
827
|
*/
|
|
821
828
|
declare function createApiUtils<TQueryKeys extends object, TErrorCode extends string = string>(): {
|
|
822
829
|
useQueryClient: () => QueryClient<TQueryKeys>;
|
|
823
|
-
useMutation: <TReqData = void, TResData = void, TParams = void>(options: ApiUseMutationOptions<TQueryKeys, TReqData, TResData, TParams, TErrorCode>) => UseMutationReturnType<TReqData, TResData, TParams,
|
|
830
|
+
useMutation: <TReqData = void, TResData = void, TParams = void>(options: ApiUseMutationOptions<TQueryKeys, TReqData, TResData, TParams, TErrorCode>) => UseMutationReturnType<TReqData, TResData, TParams, TErrorCode>;
|
|
824
831
|
useKeysetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseKeysetInfiniteQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => UseKeysetInfiniteQueryReturnType<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>;
|
|
825
832
|
useOffsetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseOffsetInfiniteQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => UseOffsetInfiniteQueryReturnType<QueryKeyArrayItemFromConfig<TQueryKeys, TKey$1>, TErrorCode>;
|
|
826
833
|
usePrefetchKeysetInfiniteQuery: <TKey$1 extends QueryKeysWithArrayEntityFromConfig<TQueryKeys>>(key: TKey$1, queryOptions: ApiUseKeysetInfinitePrefetchQueryOptions<TQueryKeys, TKey$1, TErrorCode>) => {
|
|
@@ -867,4 +874,4 @@ declare class SortUtil {
|
|
|
867
874
|
}[];
|
|
868
875
|
}
|
|
869
876
|
//#endregion
|
|
870
|
-
export {
|
|
877
|
+
export { type ApiUnexpectedError, type ApiUnknownErrorObject, type ApiUseKeysetInfinitePrefetchQueryOptions, type ApiUseKeysetInfiniteQueryOptions, type ApiUseKeysetInfiniteQueryReturnType, type ApiUseMutationOptions, type ApiUseOffsetInfinitePrefetchQueryOptions, type ApiUseOffsetInfiniteQueryOptions, type ApiUseOffsetInfiniteQueryReturnType, type ApiUsePrefetchQueryOptions, type ApiUseQueryOptions, 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 OffsetInfiniteQueryOptions, type OffsetPagination, type OffsetPaginationParams, type OffsetPaginationResponse, type PaginatedDataDto, QueryClient, type QueryClientUpdateOptions, type QueryClientUpdateResult, type QueryConfig, type QueryKeyArrayItemFromConfig, type QueryKeysWithArrayEntityFromConfig, type QueryParams, type Sort, SortDirection, SortUtil, type TanstackQueryClient, type UseKeysetInfiniteQueryReturnType, type UseMutationReturnType, type UseOffsetInfiniteQueryReturnType, type UseQueryOptions, type UseQueryReturnType, type WithFilterQuery, type WithSearchQuery, type WithSortQuery, type WithStaticFilterQuery, apiUtilsPlugin, createApiUtils, getQueryClient as getTanstackQueryClient, initializeApiUtils, setQueryConfig, type ApiError as "~ApiError", type ApiErrorObject as "~ApiErrorObject", type ApiExpectedError as "~ApiExpectedError", type ApiKnownErrorObject as "~ApiKnownErrorObject", type ApiResult as "~ApiResult", type AsyncApiResult as "~AsyncApiResult", type KeysetPaginationResult as "~KeysetPaginationResult", type OffsetPaginationResult as "~OffsetPaginationResult" };
|
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 {
|
|
773
|
+
export { AsyncResult, AsyncResultErr, AsyncResultLoading, AsyncResultOk, QueryClient, SortDirection, SortUtil, apiUtilsPlugin, 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
|
|
8
|
-
"license": "
|
|
7
|
+
"version": "1.2.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
|
-
"
|
|
32
|
+
"dist",
|
|
33
|
+
"skills"
|
|
27
34
|
],
|
|
28
35
|
"peerDependencies": {
|
|
29
36
|
"@tanstack/vue-query": ">=5.90.5",
|
|
@@ -31,10 +38,12 @@
|
|
|
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
48
|
"@wisemen/eslint-config-vue": "2.1.2"
|
|
40
49
|
},
|
|
@@ -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
|