@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 +59 -0
- package/dist/index.d.mts +24 -11
- package/dist/index.mjs +10 -1
- package/package.json +13 -4
- 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/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>) =>
|
|
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
|
}
|
|
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>):
|
|
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>):
|
|
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 {
|
|
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.
|
|
8
|
-
"license": "
|
|
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
|
-
"
|
|
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.
|
|
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
|