@codeleap/query 6.2.3 → 6.8.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/factors/createQueryManager.d.ts +36 -0
- package/dist/factors/createQueryManager.d.ts.map +1 -0
- package/dist/factors/createQueryOperations.d.ts +35 -0
- package/dist/factors/createQueryOperations.d.ts.map +1 -0
- package/dist/factors/index.d.ts +3 -0
- package/dist/factors/index.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/Mutations.d.ts +103 -0
- package/dist/lib/Mutations.d.ts.map +1 -0
- package/dist/lib/QueryClientEnhanced/index.d.ts +41 -0
- package/dist/lib/QueryClientEnhanced/index.d.ts.map +1 -0
- package/dist/lib/QueryClientEnhanced/types.d.ts +51 -0
- package/dist/lib/QueryClientEnhanced/types.d.ts.map +1 -0
- package/dist/lib/QueryKeys.d.ts +173 -0
- package/dist/lib/QueryKeys.d.ts.map +1 -0
- package/dist/lib/QueryManager.d.ts +202 -0
- package/dist/lib/QueryManager.d.ts.map +1 -0
- package/dist/lib/QueryOperations/index.d.ts +228 -0
- package/dist/lib/QueryOperations/index.d.ts.map +1 -0
- package/dist/lib/QueryOperations/types.d.ts +42 -0
- package/dist/lib/QueryOperations/types.d.ts.map +1 -0
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/types/core.d.ts +29 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/create.d.ts +21 -0
- package/dist/types/create.d.ts.map +1 -0
- package/dist/types/delete.d.ts +18 -0
- package/dist/types/delete.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/list.d.ts +19 -0
- package/dist/types/list.d.ts.map +1 -0
- package/dist/types/retrieve.d.ts +17 -0
- package/dist/types/retrieve.d.ts.map +1 -0
- package/dist/types/update.d.ts +13 -0
- package/dist/types/update.d.ts.map +1 -0
- package/dist/types/utility.d.ts +23 -0
- package/dist/types/utility.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/misc.d.ts +5 -0
- package/dist/utils/misc.d.ts.map +1 -0
- package/package.json +23 -8
- package/src/lib/Mutations.ts +30 -44
- package/src/lib/QueryClientEnhanced/index.ts +32 -4
- package/src/lib/QueryClientEnhanced/types.ts +25 -0
- package/src/lib/QueryKeys.ts +4 -2
- package/src/lib/QueryManager.ts +37 -33
- package/src/lib/QueryOperations/index.ts +11 -3
- package/src/lib/QueryOperations/types.ts +4 -3
- package/src/tests/setup.ts +14 -0
- package/src/types/core.ts +8 -1
- package/src/types/create.ts +10 -1
- package/src/types/delete.ts +8 -1
- package/src/types/list.ts +5 -0
- package/src/types/retrieve.ts +9 -0
- package/src/types/update.ts +3 -0
- package/src/types/utility.ts +5 -0
- package/src/utils/misc.ts +2 -0
- package/package.json.bak +0 -27
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { UseMutationOptions } from '@tanstack/react-query';
|
|
2
|
+
import { QueryItem } from './core';
|
|
3
|
+
/** Rollback context captured before an optimistic update is applied. Both fields are `undefined` when the item was not in cache at mutation time. */
|
|
4
|
+
export type UpdateMutationCtx<T> = {
|
|
5
|
+
previousItem: T | undefined;
|
|
6
|
+
optimisticItem: T | undefined;
|
|
7
|
+
};
|
|
8
|
+
/** Options forwarded to React Query's `useMutation` for an update operation. `mutationKey` and `mutationFn` are managed internally. */
|
|
9
|
+
export type UpdateMutationOptions<T extends QueryItem, F> = Omit<UseMutationOptions<T, Error, Partial<T>, UpdateMutationCtx<T>>, 'mutationKey' | 'mutationFn'> & {
|
|
10
|
+
/** Apply the updated fields to the cache immediately and revert to `previousItem` on error. */
|
|
11
|
+
optimistic?: boolean;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/types/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAElC,qJAAqJ;AACrJ,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI;IACjC,YAAY,EAAE,CAAC,GAAG,SAAS,CAAA;IAC3B,cAAc,EAAE,CAAC,GAAG,SAAS,CAAA;CAC9B,CAAA;AAED,uIAAuI;AACvI,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,IAAI,IAAI,CAC9D,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAC9D,aAAa,GAAG,YAAY,CAC7B,GAAG;IACF,+FAA+F;IAC/F,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { QueryItem } from './core';
|
|
2
|
+
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
/** Alias for the React Query client instance type — avoids importing `useQueryClient` at every call site. */
|
|
4
|
+
export type QueryClient = ReturnType<typeof useQueryClient>;
|
|
5
|
+
/** An entity that may carry a client-generated `tempId` while an optimistic create is in-flight. */
|
|
6
|
+
export type WithTempId<T extends QueryItem> = T & {
|
|
7
|
+
tempId?: QueryItem['id'];
|
|
8
|
+
};
|
|
9
|
+
/** Exact location of an item inside the paginated cache: which page it lives on and its index within that page. */
|
|
10
|
+
export type ItemPosition = {
|
|
11
|
+
pageIndex: number;
|
|
12
|
+
itemIndex: number;
|
|
13
|
+
};
|
|
14
|
+
/** Sparse map of every cached list that contained a deleted item, together with the position it occupied. Used to re-insert the item on rollback. */
|
|
15
|
+
export type RemovedItemMap = [QueryKey, ItemPosition][];
|
|
16
|
+
/** Standard DRF-style paginated envelope. Wrap your API response in this type when the backend uses `count / next / previous / results`. */
|
|
17
|
+
export type PaginationResponse<T extends QueryItem> = {
|
|
18
|
+
count: number;
|
|
19
|
+
next: string;
|
|
20
|
+
previous: string;
|
|
21
|
+
results: T[];
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=utility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utility.d.ts","sourceRoot":"","sources":["../../src/types/utility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEhE,6GAA6G;AAC7G,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA;AAE3D,oGAAoG;AACpG,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,SAAS,IAAI,CAAC,GAAG;IAChD,MAAM,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;CACzB,CAAA;AAED,mHAAmH;AACnH,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,qJAAqJ;AACrJ,MAAM,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAA;AAEvD,4IAA4I;AAC5I,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,SAAS,IAAI;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,CAAC,EAAE,CAAA;CACb,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Prefix used on every optimistically-created item id. Check `id.startsWith(tempIdPrefix)` to distinguish placeholder items from server-assigned ids. */
|
|
2
|
+
export declare const tempIdPrefix = "temp-id";
|
|
3
|
+
/** Returns a unique placeholder id for optimistic creates. The format is `temp-id-<uuidv4>` and is guaranteed never to collide with server-assigned ids. */
|
|
4
|
+
export declare const generateTempId: () => string;
|
|
5
|
+
//# sourceMappingURL=misc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"misc.d.ts","sourceRoot":"","sources":["../../src/utils/misc.ts"],"names":[],"mappings":"AAsCA,0JAA0J;AAC1J,eAAO,MAAM,YAAY,YAAY,CAAA;AAErC,4JAA4J;AAC5J,eAAO,MAAM,cAAc,cAE1B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeleap/query",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
5
18
|
"license": "UNLICENSED",
|
|
6
19
|
"repository": {
|
|
7
20
|
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
@@ -9,19 +22,21 @@
|
|
|
9
22
|
"directory": "packages/query"
|
|
10
23
|
},
|
|
11
24
|
"devDependencies": {
|
|
12
|
-
"@codeleap/config": "6.
|
|
13
|
-
"@codeleap/types": "6.
|
|
25
|
+
"@codeleap/config": "6.8.0",
|
|
26
|
+
"@codeleap/types": "6.8.0",
|
|
27
|
+
"@codeleap/utils": "6.8.0",
|
|
14
28
|
"ts-node-dev": "1.1.8"
|
|
15
29
|
},
|
|
16
30
|
"scripts": {
|
|
17
|
-
"build": "
|
|
31
|
+
"build": "tsc --build tsconfig.build.json",
|
|
32
|
+
"typecheck": "bun tsc --noEmit -p ./tsconfig.json"
|
|
18
33
|
},
|
|
19
34
|
"peerDependencies": {
|
|
20
|
-
"@codeleap/types": "6.
|
|
21
|
-
"typescript": "
|
|
22
|
-
"@tanstack/react-query": "5.
|
|
35
|
+
"@codeleap/types": "6.8.0",
|
|
36
|
+
"typescript": "6.0.3",
|
|
37
|
+
"@tanstack/react-query": "5.100.9"
|
|
23
38
|
},
|
|
24
39
|
"dependencies": {
|
|
25
40
|
"fast-deep-equal": "3.1.3"
|
|
26
41
|
}
|
|
27
|
-
}
|
|
42
|
+
}
|
package/src/lib/Mutations.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InfiniteData } from '@tanstack/query-core'
|
|
1
|
+
import { InfiniteData, Query, QueryKey } from '@tanstack/query-core'
|
|
2
2
|
import { QueryKeys } from './QueryKeys'
|
|
3
3
|
import { ItemPosition, ListPaginationResponse, PageParam, QueryClient, QueryItem, RemovedItemMap, WithTempId } from '../types'
|
|
4
4
|
import deepEqual from 'fast-deep-equal'
|
|
@@ -40,6 +40,30 @@ export class Mutations<T extends QueryItem, F> {
|
|
|
40
40
|
* mutations.addItem(newUser, removedItemMap)
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
|
+
private _addItemAtKey(newItem: T, position: 'start' | 'end', queryKey: QueryKey): void {
|
|
44
|
+
const currentData = this.queryClient.getQueryData<InfiniteData<ListPaginationResponse<T>, PageParam>>(queryKey)
|
|
45
|
+
|
|
46
|
+
if (!currentData) {
|
|
47
|
+
this.queryClient.setQueryData(queryKey, { pageParams: [0], pages: [[newItem]] })
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const updatedPages = [...currentData.pages]
|
|
52
|
+
|
|
53
|
+
if (position === 'start') {
|
|
54
|
+
updatedPages.length > 0
|
|
55
|
+
? updatedPages[0] = [newItem, ...updatedPages[0]]
|
|
56
|
+
: updatedPages.push([newItem])
|
|
57
|
+
} else {
|
|
58
|
+
const last = updatedPages.length - 1
|
|
59
|
+
updatedPages.length > 0
|
|
60
|
+
? updatedPages[last] = [...updatedPages[last], newItem]
|
|
61
|
+
: updatedPages.push([newItem])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.queryClient.setQueryData(queryKey, { ...currentData, pages: updatedPages })
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
addItem(newItem: T, position: 'start' | 'end' | RemovedItemMap = 'start', listFilters?: F) {
|
|
44
68
|
const isMultiQueryKeys = Array.isArray(position) && position?.length >= 1
|
|
45
69
|
|
|
@@ -64,55 +88,17 @@ export class Mutations<T extends QueryItem, F> {
|
|
|
64
88
|
}
|
|
65
89
|
}
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
...currentData,
|
|
69
|
-
pages: updatedPages
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.queryClient.setQueryData(queryKey, newData)
|
|
91
|
+
this.queryClient.setQueryData(queryKey, { ...currentData, pages: updatedPages })
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
return
|
|
76
95
|
}
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const currentData = this.queryClient.getQueryData<InfiniteData<ListPaginationResponse<T>, PageParam>>(queryKey)
|
|
81
|
-
|
|
82
|
-
if (!currentData) {
|
|
83
|
-
const newData = {
|
|
84
|
-
pageParams: [0],
|
|
85
|
-
pages: [[newItem]]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
this.queryClient.setQueryData(queryKey, newData)
|
|
89
|
-
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const updatedPages = [...currentData.pages]
|
|
94
|
-
|
|
95
|
-
if (position === 'start') {
|
|
96
|
-
if (updatedPages.length > 0) {
|
|
97
|
-
updatedPages[0] = [newItem, ...updatedPages[0]]
|
|
98
|
-
} else {
|
|
99
|
-
updatedPages.push([newItem])
|
|
100
|
-
}
|
|
101
|
-
} else if (position === 'end') {
|
|
102
|
-
if (updatedPages.length > 0) {
|
|
103
|
-
const lastPageIndex = updatedPages.length - 1
|
|
104
|
-
updatedPages[lastPageIndex] = [...updatedPages[lastPageIndex], newItem]
|
|
105
|
-
} else {
|
|
106
|
-
updatedPages.push([newItem])
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const newData = {
|
|
111
|
-
...currentData,
|
|
112
|
-
pages: updatedPages
|
|
113
|
-
}
|
|
97
|
+
this._addItemAtKey(newItem, position as 'start' | 'end', this.queryKeys.listKeyWithFilters(listFilters))
|
|
98
|
+
}
|
|
114
99
|
|
|
115
|
-
|
|
100
|
+
addItemToQuery(newItem: T, position: 'start' | 'end' = 'start', query: Query): void {
|
|
101
|
+
this._addItemAtKey(newItem, position, query.queryKey)
|
|
116
102
|
}
|
|
117
103
|
|
|
118
104
|
/**
|
|
@@ -2,9 +2,17 @@ import { waitFor } from '@codeleap/utils'
|
|
|
2
2
|
import { QueryClient, QueryKey, Query, hashKey, QueryCacheNotifyEvent, matchQuery, QueryOptions } from '@tanstack/react-query'
|
|
3
3
|
import { DynamicEnhancedQuery, EnhancedQuery, PollQueryOptions, PollingResult, QueryKeyBuilder } from './types'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Thin wrapper around React Query's `QueryClient` that adds async primitives for polling, event listening, and proxy-based query handles.
|
|
7
|
+
* Pass an instance of this class wherever `QueryClient | QueryClientEnhanced` is accepted to unlock the enhanced API.
|
|
8
|
+
*/
|
|
5
9
|
export class QueryClientEnhanced {
|
|
6
10
|
constructor(public client: QueryClient) { }
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Subscribes to cache events for a single query key.
|
|
14
|
+
* Returns the unsubscribe function, or `undefined` when the query is not yet in the cache.
|
|
15
|
+
*/
|
|
8
16
|
listenToQuery(key: QueryKey, callback: (e: QueryCacheNotifyEvent) => void) {
|
|
9
17
|
const cache = this.client.getQueryCache()
|
|
10
18
|
|
|
@@ -26,6 +34,10 @@ export class QueryClientEnhanced {
|
|
|
26
34
|
return removeListener
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Repeatedly refetches a query on a fixed `interval` until the `callback` returns `{ stop: true }`.
|
|
39
|
+
* Rejects immediately if the query key is not present in the cache when called.
|
|
40
|
+
*/
|
|
29
41
|
async pollQuery<T, R>(
|
|
30
42
|
key: QueryKey,
|
|
31
43
|
options: PollQueryOptions<T, R>,
|
|
@@ -42,7 +54,7 @@ export class QueryClientEnhanced {
|
|
|
42
54
|
let count = 0
|
|
43
55
|
let result: PollingResult<R> = {
|
|
44
56
|
stop: false,
|
|
45
|
-
data: initialData,
|
|
57
|
+
data: initialData as R,
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
while (!result?.stop) {
|
|
@@ -68,6 +80,10 @@ export class QueryClientEnhanced {
|
|
|
68
80
|
return result?.data
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns an `EnhancedQuery<T>` Proxy for `key`.
|
|
85
|
+
* `getData` and `setData` work even when the query is not yet in the cache; all other members log a warning and return `undefined` when the query is absent.
|
|
86
|
+
*/
|
|
71
87
|
queryProxy<T>(key: QueryKey) {
|
|
72
88
|
const getClient = () => this
|
|
73
89
|
|
|
@@ -116,11 +132,11 @@ export class QueryClientEnhanced {
|
|
|
116
132
|
}
|
|
117
133
|
|
|
118
134
|
case 'ensureData':
|
|
119
|
-
return (options) => {
|
|
135
|
+
return (options: Partial<Parameters<typeof client.client.ensureQueryData<T>>[0]>) => {
|
|
120
136
|
return client.client.ensureQueryData<T>({
|
|
121
137
|
queryKey: key,
|
|
122
138
|
...options
|
|
123
|
-
})
|
|
139
|
+
} as Parameters<typeof client.client.ensureQueryData<T>>[0])
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
case 'refresh':
|
|
@@ -145,6 +161,10 @@ export class QueryClientEnhanced {
|
|
|
145
161
|
})
|
|
146
162
|
}
|
|
147
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Resolves with the refreshed `Query` object once it reaches a settled `idle` state with a `dataUpdatedAt` or `errorUpdatedAt` timestamp newer than the moment of the call.
|
|
166
|
+
* Rejects when the query is absent from the cache or when the fetch results in an error.
|
|
167
|
+
*/
|
|
148
168
|
waitForRefresh<T>(key: QueryKey) {
|
|
149
169
|
const initialQuery = this.client.getQueryCache().find({ exact: true, queryKey: key })
|
|
150
170
|
|
|
@@ -175,13 +195,17 @@ export class QueryClientEnhanced {
|
|
|
175
195
|
reject()
|
|
176
196
|
}
|
|
177
197
|
|
|
178
|
-
removeListener()
|
|
198
|
+
removeListener?.()
|
|
179
199
|
}
|
|
180
200
|
})
|
|
181
201
|
})
|
|
182
202
|
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Registers a static query key and returns an `EnhancedQuery<Data>` proxy for it.
|
|
207
|
+
* When `options` are provided they are applied as query defaults and the query is pre-seeded in the cache — useful for initialising queries outside of a React component.
|
|
208
|
+
*/
|
|
185
209
|
queryKey<Data>(k: QueryKey, options?: QueryOptions<Data>) {
|
|
186
210
|
if (options) {
|
|
187
211
|
this.client.setQueryDefaults(k, options)
|
|
@@ -201,6 +225,10 @@ export class QueryClientEnhanced {
|
|
|
201
225
|
return this.queryProxy<Data>(k)
|
|
202
226
|
}
|
|
203
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Returns a `DynamicEnhancedQuery` proxy whose every member is a function that accepts `BuilderArgs` to derive the final key before delegating.
|
|
230
|
+
* Use this when the query key depends on runtime parameters (e.g. `(id: string) => ['users', id]`).
|
|
231
|
+
*/
|
|
204
232
|
dynamicQueryKey<Data, BuilderArgs extends any[] = any[]>(k: QueryKeyBuilder<BuilderArgs>) {
|
|
205
233
|
const getClient = () => this
|
|
206
234
|
|
|
@@ -5,34 +5,59 @@ import {
|
|
|
5
5
|
EnsureQueryDataOptions,
|
|
6
6
|
} from '@tanstack/react-query'
|
|
7
7
|
|
|
8
|
+
/** A function that accepts typed arguments and returns the `QueryKey` array for a given query. Used with `dynamicQueryKey` to create parameterised query proxies. */
|
|
8
9
|
export type QueryKeyBuilder<Args extends any[] = any[]> = (...args: Args) => QueryKey
|
|
9
10
|
|
|
11
|
+
/** Value returned by a `PollingCallback` on each tick. Set `stop: true` to terminate the polling loop. */
|
|
10
12
|
export type PollingResult<T> = {
|
|
11
13
|
stop: boolean
|
|
12
14
|
data: T
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Called after each poll tick.
|
|
19
|
+
* @param query — the refreshed query snapshot
|
|
20
|
+
* @param count — zero-based tick counter
|
|
21
|
+
* @param prev — result data from the previous tick, `undefined` on the first call
|
|
22
|
+
* Return `{ stop: true, data }` to end polling; `{ stop: false, data }` to continue.
|
|
23
|
+
*/
|
|
15
24
|
export type PollingCallback<T, R> = (query: Query<T>, count: number, prev?: R) => Promise<PollingResult<R>>
|
|
16
25
|
|
|
17
26
|
export type PollQueryOptions<T, R> = {
|
|
27
|
+
/** Milliseconds to wait between each refetch. */
|
|
18
28
|
interval: number
|
|
19
29
|
callback: PollingCallback<T, R>
|
|
30
|
+
/** When `true`, fires the first refetch immediately instead of waiting one `interval`. Defaults to `false`. */
|
|
20
31
|
leading?: boolean
|
|
32
|
+
/** Seed value passed as `prev` to the callback on the very first tick. */
|
|
21
33
|
initialData?: R
|
|
22
34
|
}
|
|
23
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A Proxy-based handle to a specific query inside `QueryClientEnhanced`.
|
|
38
|
+
* Extends the raw React Query `Query` object with ergonomic async helpers.
|
|
39
|
+
* Obtain via `queryKey()` or `queryProxy()` — do not instantiate directly.
|
|
40
|
+
*/
|
|
24
41
|
export interface EnhancedQuery<T> extends Query<T> {
|
|
42
|
+
/** Resolves once the query transitions to a settled `idle` state newer than when the call was made. Rejects if the query errors. */
|
|
25
43
|
waitForRefresh(): Promise<Query<T>>
|
|
44
|
+
/** Subscribe to every cache notification for this query. Returns the unsubscribe function. */
|
|
26
45
|
listen(callback: (e: QueryCacheNotifyEvent) => void): () => void
|
|
46
|
+
/** Triggers a refetch and resolves with the fresh data once settled. */
|
|
27
47
|
refresh(): Promise<T>
|
|
28
48
|
poll<R>(
|
|
29
49
|
options: PollQueryOptions<T, R>
|
|
30
50
|
): Promise<R>
|
|
51
|
+
/** Synchronous cache read — returns `undefined` when the query has never been fetched. */
|
|
31
52
|
getData(): T
|
|
32
53
|
ensureData(options?: Partial<EnsureQueryDataOptions<T, Error, T, QueryKey, never>>): Promise<T>
|
|
33
54
|
key: QueryKey
|
|
34
55
|
}
|
|
35
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Parameterised variant of `EnhancedQuery` produced by `dynamicQueryKey`.
|
|
59
|
+
* Every method accepts `BuilderArgs` to resolve the key before delegating to the underlying `EnhancedQuery`.
|
|
60
|
+
*/
|
|
36
61
|
export type DynamicEnhancedQuery<T, BuilderArgs extends any[]> = {
|
|
37
62
|
[P in keyof EnhancedQuery<T>]: (...args: BuilderArgs) => EnhancedQuery<T>[P]
|
|
38
63
|
}
|
package/src/lib/QueryKeys.ts
CHANGED
|
@@ -43,14 +43,16 @@ export class QueryKeys<T extends QueryItem, F> {
|
|
|
43
43
|
* @returns Query key array with or without filters
|
|
44
44
|
*/
|
|
45
45
|
listKeyWithFilters(filters?: F): QueryKey {
|
|
46
|
-
const hasValidFilters = filters
|
|
46
|
+
const hasValidFilters = !TypeGuards.isNil(filters) && (
|
|
47
47
|
typeof filters !== 'object'
|
|
48
48
|
? Boolean(filters)
|
|
49
|
-
: Object.values(filters).some(value =>
|
|
49
|
+
: Object.values(filters ?? {}).some(value =>
|
|
50
50
|
value != null && (typeof value !== 'string' || value.trim() !== '')
|
|
51
51
|
)
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
+
|
|
55
|
+
|
|
54
56
|
return hasValidFilters ? [...this.keys.list, filters] : this.keys.list
|
|
55
57
|
}
|
|
56
58
|
|
package/src/lib/QueryManager.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { FetchInfiniteQueryOptions, FetchQueryOptions, InfiniteData, MutationFunctionContext, QueryKey, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
|
|
1
|
+
import { FetchInfiniteQueryOptions, FetchQueryOptions, InfiniteData, MutationFunctionContext, QueryClient, QueryKey, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
|
|
2
2
|
import { useCallback } from 'react'
|
|
3
3
|
import { createQueryKeys, QueryKeys } from './QueryKeys'
|
|
4
4
|
import { createMutations, Mutations } from './Mutations'
|
|
5
5
|
import { CreateMutationCtx, CreateMutationOptions, ListPaginationResponse, ListQueryOptions, PageParam, QueryItem, QueryManagerOptions, RetrieveQueryOptions, UpdateMutationCtx, UpdateMutationOptions, DeleteMutationCtx, DeleteMutationOptions } from '../types'
|
|
6
6
|
import { generateTempId } from '../utils'
|
|
7
7
|
import { TypeGuards } from '@codeleap/types'
|
|
8
|
+
import { QueryClientEnhanced } from './QueryClientEnhanced'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Comprehensive query manager class that provides hooks and utilities for managing CRUD operations with React Query
|
|
@@ -19,13 +20,15 @@ import { TypeGuards } from '@codeleap/types'
|
|
|
19
20
|
* - Built-in error handling and rollback mechanisms
|
|
20
21
|
*/
|
|
21
22
|
export class QueryManager<T extends QueryItem, F> {
|
|
23
|
+
queryClient: QueryClient
|
|
22
24
|
/**
|
|
23
25
|
* Creates a new QueryManager instance
|
|
24
26
|
* @param options - Configuration options for the query manager
|
|
25
27
|
*/
|
|
26
28
|
constructor(private options: QueryManagerOptions<T, F>) {
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
+
this.queryClient = options.queryClient instanceof QueryClientEnhanced ? options.queryClient.client : options.queryClient
|
|
30
|
+
this.queryKeys = createQueryKeys<T, F>(options.name, this.queryClient)
|
|
31
|
+
this.mutations = createMutations<T, F>(this.queryKeys, this.queryClient, options.name)
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -96,7 +99,7 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
96
99
|
queryFn: async (query) => {
|
|
97
100
|
const listOffset = query?.pageParam ?? 0
|
|
98
101
|
|
|
99
|
-
const data = await this.options?.listFn?.(listLimit, listOffset, filters)
|
|
102
|
+
const data = await this.options?.listFn?.(listLimit, listOffset, filters as F)
|
|
100
103
|
|
|
101
104
|
return TypeGuards.isArray(data) ? data : [] as ListPaginationResponse<T>
|
|
102
105
|
},
|
|
@@ -185,7 +188,7 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
185
188
|
queryKey,
|
|
186
189
|
|
|
187
190
|
queryFn: () => {
|
|
188
|
-
return this.options.retrieveFn(id)
|
|
191
|
+
return this.options.retrieveFn!(id)
|
|
189
192
|
},
|
|
190
193
|
|
|
191
194
|
select: (data) => {
|
|
@@ -234,7 +237,7 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
234
237
|
...mutationOptions
|
|
235
238
|
} = options
|
|
236
239
|
|
|
237
|
-
const onMutate = useCallback(async (data: Partial<T>, context: MutationFunctionContext) => {
|
|
240
|
+
const onMutate = useCallback(async (data: Partial<T>, context: MutationFunctionContext): Promise<CreateMutationCtx | undefined> => {
|
|
238
241
|
if (providedOnMutate) providedOnMutate?.(data, context)
|
|
239
242
|
|
|
240
243
|
if (optimistic) {
|
|
@@ -252,16 +255,16 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
252
255
|
}
|
|
253
256
|
}, [providedOnMutate, optimistic, listFilters])
|
|
254
257
|
|
|
255
|
-
const onError = useCallback((error: Error, variables: Partial<T>, onMutateResult: CreateMutationCtx, context: MutationFunctionContext) => {
|
|
256
|
-
if (providedOnError) providedOnError?.(error, variables, onMutateResult, context)
|
|
258
|
+
const onError = useCallback((error: Error, variables: Partial<T>, onMutateResult: CreateMutationCtx | undefined, context: MutationFunctionContext) => {
|
|
259
|
+
if (providedOnError && onMutateResult) providedOnError?.(error, variables, onMutateResult, context)
|
|
257
260
|
|
|
258
261
|
if (!TypeGuards.isNil(onMutateResult?.tempId)) {
|
|
259
262
|
this.mutations.removeItem(onMutateResult?.tempId)
|
|
260
263
|
}
|
|
261
264
|
}, [providedOnError])
|
|
262
265
|
|
|
263
|
-
const onSuccess = useCallback((data: T, variables: Partial<T>, onMutateResult: CreateMutationCtx, context: MutationFunctionContext) => {
|
|
264
|
-
if (providedOnSuccess) providedOnSuccess?.(data, variables, onMutateResult, context)
|
|
266
|
+
const onSuccess = useCallback((data: T, variables: Partial<T>, onMutateResult: CreateMutationCtx | undefined, context: MutationFunctionContext) => {
|
|
267
|
+
if (providedOnSuccess) providedOnSuccess?.(data, variables, onMutateResult , context)
|
|
265
268
|
|
|
266
269
|
if (TypeGuards.isNil(onMutateResult?.tempId)) {
|
|
267
270
|
this.mutations.addItem(data, appendTo, listFilters)
|
|
@@ -270,13 +273,13 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
270
273
|
}
|
|
271
274
|
}, [providedOnSuccess, listFilters])
|
|
272
275
|
|
|
273
|
-
const mutation = useMutation<T, Error, Partial<T>, CreateMutationCtx>({
|
|
276
|
+
const mutation = useMutation<T, Error, Partial<T>, CreateMutationCtx | undefined>({
|
|
274
277
|
...mutationOptions,
|
|
275
278
|
|
|
276
279
|
mutationKey: this.queryKeys.keys.create,
|
|
277
280
|
|
|
278
281
|
mutationFn: (data: Partial<T>) => {
|
|
279
|
-
return this.options.createFn(data)
|
|
282
|
+
return this.options.createFn!(data)
|
|
280
283
|
},
|
|
281
284
|
|
|
282
285
|
onMutate,
|
|
@@ -320,11 +323,12 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
320
323
|
...mutationOptions
|
|
321
324
|
} = options
|
|
322
325
|
|
|
323
|
-
const onMutate = useCallback(async (data: Partial<T>, context: MutationFunctionContext) => {
|
|
326
|
+
const onMutate = useCallback(async (data: Partial<T>, context: MutationFunctionContext): Promise<UpdateMutationCtx<T> | undefined> => {
|
|
324
327
|
if (providedOnMutate) providedOnMutate?.(data, context)
|
|
325
328
|
|
|
326
329
|
if (optimistic) {
|
|
327
|
-
|
|
330
|
+
if (data?.id == null) return
|
|
331
|
+
const previousItem = this.queryKeys.getRetrieveData(data.id)
|
|
328
332
|
|
|
329
333
|
if (!previousItem) return
|
|
330
334
|
|
|
@@ -342,27 +346,27 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
342
346
|
}
|
|
343
347
|
}, [providedOnMutate, optimistic])
|
|
344
348
|
|
|
345
|
-
const onError = useCallback((error: Error, variables: Partial<T>, onMutateResult: UpdateMutationCtx<T
|
|
346
|
-
if (providedOnError) providedOnError?.(error, variables, onMutateResult, context)
|
|
349
|
+
const onError = useCallback((error: Error, variables: Partial<T>, onMutateResult: UpdateMutationCtx<T> | undefined, context: MutationFunctionContext) => {
|
|
350
|
+
if (providedOnError && onMutateResult) providedOnError?.(error, variables, onMutateResult, context)
|
|
347
351
|
|
|
348
352
|
if (!TypeGuards.isNil(onMutateResult?.previousItem?.id)) {
|
|
349
353
|
this.mutations.updateItems(onMutateResult?.previousItem)
|
|
350
354
|
}
|
|
351
355
|
}, [providedOnError])
|
|
352
356
|
|
|
353
|
-
const onSuccess = useCallback((data: T, variables: Partial<T>, onMutateResult: UpdateMutationCtx<T
|
|
354
|
-
if (providedOnSuccess) providedOnSuccess?.(data, variables, onMutateResult, context)
|
|
357
|
+
const onSuccess = useCallback((data: T, variables: Partial<T>, onMutateResult: UpdateMutationCtx<T> | undefined, context: MutationFunctionContext) => {
|
|
358
|
+
if (providedOnSuccess && onMutateResult) providedOnSuccess?.(data, variables, onMutateResult, context)
|
|
355
359
|
|
|
356
360
|
this.mutations.updateItems(data)
|
|
357
361
|
}, [providedOnSuccess])
|
|
358
362
|
|
|
359
|
-
const mutation = useMutation<T, Error, Partial<T>, UpdateMutationCtx<T
|
|
363
|
+
const mutation = useMutation<T, Error, Partial<T>, UpdateMutationCtx<T> | undefined>({
|
|
360
364
|
...mutationOptions,
|
|
361
365
|
|
|
362
366
|
mutationKey: this.queryKeys.keys.update,
|
|
363
367
|
|
|
364
368
|
mutationFn: (data: Partial<T>) => {
|
|
365
|
-
return this.options.updateFn(data)
|
|
369
|
+
return this.options.updateFn!(data)
|
|
366
370
|
},
|
|
367
371
|
|
|
368
372
|
onMutate,
|
|
@@ -407,7 +411,7 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
407
411
|
...mutationOptions
|
|
408
412
|
} = options
|
|
409
413
|
|
|
410
|
-
const onMutate = useCallback(async (id: T['id'], context: MutationFunctionContext) => {
|
|
414
|
+
const onMutate = useCallback(async (id: T['id'], context: MutationFunctionContext): Promise<DeleteMutationCtx<T> | undefined> => {
|
|
411
415
|
if (providedOnMutate) providedOnMutate?.(id, context)
|
|
412
416
|
|
|
413
417
|
if (optimistic) {
|
|
@@ -424,32 +428,32 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
424
428
|
}
|
|
425
429
|
}, [providedOnMutate, optimistic])
|
|
426
430
|
|
|
427
|
-
const onError = useCallback((error: Error, variables: T['id'], onMutateResult: DeleteMutationCtx<T
|
|
428
|
-
if (providedOnError) providedOnError?.(error, variables, onMutateResult, context)
|
|
431
|
+
const onError = useCallback((error: Error, variables: T['id'], onMutateResult: DeleteMutationCtx<T> | undefined, context: MutationFunctionContext) => {
|
|
432
|
+
if (providedOnError && onMutateResult) providedOnError?.(error, variables, onMutateResult, context)
|
|
429
433
|
|
|
430
434
|
if (!TypeGuards.isNil(onMutateResult?.previousItem?.id)) {
|
|
431
435
|
this.mutations.addItem(
|
|
432
436
|
onMutateResult?.previousItem,
|
|
433
|
-
onMutateResult?.removedAt,
|
|
437
|
+
onMutateResult?.removedAt ?? undefined,
|
|
434
438
|
)
|
|
435
439
|
}
|
|
436
440
|
}, [providedOnError])
|
|
437
441
|
|
|
438
|
-
const onSuccess = useCallback((data:
|
|
439
|
-
if (providedOnSuccess) providedOnSuccess?.(data, variables, onMutateResult, context)
|
|
442
|
+
const onSuccess = useCallback((data: unknown, variables: T['id'], onMutateResult: DeleteMutationCtx<T> | undefined, context: MutationFunctionContext) => {
|
|
443
|
+
if (providedOnSuccess && onMutateResult) providedOnSuccess?.(data as T['id'], variables, onMutateResult, context)
|
|
440
444
|
|
|
441
445
|
if (TypeGuards.isNil(onMutateResult?.previousItem?.id)) {
|
|
442
|
-
this.mutations.removeItem(data)
|
|
446
|
+
this.mutations.removeItem(data as T['id'])
|
|
443
447
|
}
|
|
444
448
|
}, [providedOnSuccess])
|
|
445
449
|
|
|
446
|
-
const mutation = useMutation<unknown, Error, T['id'], DeleteMutationCtx<T
|
|
450
|
+
const mutation = useMutation<unknown, Error, T['id'], DeleteMutationCtx<T> | undefined>({
|
|
447
451
|
...mutationOptions,
|
|
448
452
|
|
|
449
453
|
mutationKey: this.queryKeys.keys.delete,
|
|
450
454
|
|
|
451
455
|
mutationFn: async (id: QueryItem['id']) => {
|
|
452
|
-
return this.options.deleteFn(id)
|
|
456
|
+
return this.options.deleteFn!(id)
|
|
453
457
|
},
|
|
454
458
|
|
|
455
459
|
onMutate,
|
|
@@ -481,10 +485,10 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
481
485
|
* ```
|
|
482
486
|
*/
|
|
483
487
|
prefetchRetrieve(id: T['id'], options: Omit<FetchQueryOptions<T, Error, T, QueryKey, never>, 'queryKey' | 'queryFn'> = {}) {
|
|
484
|
-
return this.
|
|
488
|
+
return this.queryClient.prefetchQuery({
|
|
485
489
|
...options,
|
|
486
490
|
queryKey: this.queryKeys.keys.retrieve(id),
|
|
487
|
-
queryFn: () => this.options.retrieveFn(id),
|
|
491
|
+
queryFn: () => this.options.retrieveFn!(id),
|
|
488
492
|
})
|
|
489
493
|
}
|
|
490
494
|
|
|
@@ -515,11 +519,11 @@ export class QueryManager<T extends QueryItem, F> {
|
|
|
515
519
|
) {
|
|
516
520
|
const { initialOffset = 0, ...prefetchOptions } = options
|
|
517
521
|
|
|
518
|
-
return this.
|
|
522
|
+
return this.queryClient.prefetchInfiniteQuery({
|
|
519
523
|
...prefetchOptions as any,
|
|
520
524
|
initialPageParam: initialOffset,
|
|
521
525
|
queryKey: this.queryKeys.listKeyWithFilters(filters),
|
|
522
|
-
queryFn: () => this.options.listFn(this.options.listLimit ?? 10, initialOffset, filters),
|
|
526
|
+
queryFn: () => this.options.listFn!(this.options.listLimit ?? 10, initialOffset, filters as F),
|
|
523
527
|
})
|
|
524
528
|
}
|
|
525
529
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMutation, useQuery, UseQueryOptions, UseMutationOptions, QueryKey, FetchQueryOptions } from '@tanstack/react-query'
|
|
2
2
|
import { QueryOperationsOptions, MutationFn, QueryFn, InferMutationParams, InferMutationReturn, InferQueryParams, InferQueryReturn } from './types'
|
|
3
|
+
import { QueryClientEnhanced } from '../QueryClientEnhanced'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Builder class for creating type-safe query and mutation operations
|
|
@@ -41,6 +42,13 @@ export class QueryOperations<
|
|
|
41
42
|
return this._mutations
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
get queryClient(){
|
|
46
|
+
if(this._options.queryClient instanceof QueryClientEnhanced) {
|
|
47
|
+
return this._options.queryClient.client
|
|
48
|
+
}
|
|
49
|
+
return this._options.queryClient
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
/**
|
|
45
53
|
* Gets all registered query functions
|
|
46
54
|
* @returns Readonly record of query functions
|
|
@@ -297,7 +305,7 @@ export class QueryOperations<
|
|
|
297
305
|
|
|
298
306
|
const queryFn = this._queries[queryKey] as QueryFn
|
|
299
307
|
|
|
300
|
-
return this.
|
|
308
|
+
return this.queryClient.prefetchQuery<InferQueryReturn<TQueries[K]>, Error, T, QueryKey>({
|
|
301
309
|
queryKey: prefetchQueryKey,
|
|
302
310
|
queryFn: queryFn,
|
|
303
311
|
...options,
|
|
@@ -340,12 +348,12 @@ export class QueryOperations<
|
|
|
340
348
|
) {
|
|
341
349
|
const prefetchQueryKey = this.getQueryKey(queryKey, params)
|
|
342
350
|
|
|
343
|
-
const cachedData = this.
|
|
351
|
+
const cachedData = this.queryClient.getQueryData<T, QueryKey>(prefetchQueryKey)
|
|
344
352
|
|
|
345
353
|
if (!cachedData) {
|
|
346
354
|
await this.prefetchQuery(queryKey, params, options)
|
|
347
355
|
}
|
|
348
356
|
|
|
349
|
-
return this.
|
|
357
|
+
return this.queryClient.getQueryData<T, QueryKey>(prefetchQueryKey)
|
|
350
358
|
}
|
|
351
359
|
}
|