@codeleap/query 4.3.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/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './QueryManager'
2
+ export * from './queryClient'
3
+ export * from './types'
4
+
5
+ import * as ReactQuery from '@tanstack/react-query'
6
+
7
+ export {
8
+ ReactQuery
9
+ }
@@ -0,0 +1,260 @@
1
+ import * as ReactQuery from '@tanstack/react-query'
2
+ import { waitFor } from '@codeleap/utils'
3
+ import { QueryManagerOptions, QueryManagerItem } from './types'
4
+ import { QueryManager } from './QueryManager'
5
+
6
+ export type QueryKeyBuilder<Args extends any[] = any[]> = (...args:Args) => ReactQuery.QueryKey
7
+
8
+ type PollingResult<T> = {
9
+ stop: boolean
10
+ data: T
11
+ }
12
+
13
+ type PollingCallback<T, R> = (query: ReactQuery.Query<T>, count: number, prev?: R) => Promise<PollingResult<R>>
14
+
15
+ type PollQueryOptions<T, R> = {
16
+ interval: number
17
+ callback: PollingCallback<T, R>
18
+ leading?: boolean
19
+ initialData?: R
20
+ }
21
+
22
+ interface EnhancedQuery<T> extends ReactQuery.Query<T> {
23
+ waitForRefresh(): Promise<ReactQuery.Query<T>>
24
+ listen(callback: (e: ReactQuery.QueryCacheNotifyEvent) => void): () => void
25
+ refresh(): Promise<T>
26
+ poll<R>(
27
+ options: PollQueryOptions<T, R>
28
+ ): Promise<R>
29
+ getData(): T
30
+ ensureData(options?: Partial<ReactQuery.EnsureQueryDataOptions<T, Error, T, ReactQuery.QueryKey, never>>): Promise<T>
31
+ key: ReactQuery.QueryKey
32
+ }
33
+
34
+ type DynamicEnhancedQuery<T, BuilderArgs extends any[]> = {
35
+ [P in keyof EnhancedQuery<T>]: (...args: BuilderArgs) => EnhancedQuery<T>[P]
36
+ }
37
+
38
+ export class CodeleapQueryClient {
39
+ constructor(public client: ReactQuery.QueryClient) {
40
+ }
41
+
42
+ listenToQuery(key: ReactQuery.QueryKey, callback: (e: ReactQuery.QueryCacheNotifyEvent) => void) {
43
+ const cache = this.client.getQueryCache()
44
+
45
+ const query = cache.find({ exact: true, queryKey: key })
46
+
47
+ if (!query) {
48
+ return
49
+ }
50
+
51
+ const removeListener = cache.subscribe((e) => {
52
+ const matches = ReactQuery.matchQuery({ exact: true, queryKey: key }, e.query)
53
+
54
+ if (matches) {
55
+ callback(e)
56
+
57
+ }
58
+ })
59
+
60
+ return removeListener
61
+ }
62
+
63
+ async pollQuery<T, R>(
64
+ key: ReactQuery.QueryKey,
65
+ options: PollQueryOptions<T, R>,
66
+ ) {
67
+ const { interval, callback, initialData, leading = false } = options
68
+ const cache = this.client.getQueryCache()
69
+
70
+ const initialQuery = cache.find({ exact: true, queryKey: key })
71
+
72
+ if (!initialQuery) {
73
+ return Promise.reject(new Error('Query not found'))
74
+ }
75
+
76
+ let count = 0
77
+ let result: PollingResult<R> = {
78
+ stop: false,
79
+ data: initialData,
80
+ }
81
+
82
+ while (!result?.stop) {
83
+ const shouldWait = count > 0 || leading
84
+
85
+ if (shouldWait) {
86
+ await waitFor(interval)
87
+ }
88
+
89
+ this.client.refetchQueries({
90
+ exact: true,
91
+ queryKey: key,
92
+ })
93
+
94
+ const newQuery = await this.waitForRefresh<T>(key)
95
+
96
+ const newResult = await callback(newQuery, count, result?.data)
97
+
98
+ count += 1
99
+ result = newResult
100
+ }
101
+
102
+ return result?.data
103
+
104
+ }
105
+
106
+ queryProxy<T>(key: ReactQuery.QueryKey) {
107
+ const getClient = () => this
108
+
109
+ return new Proxy<EnhancedQuery<T>>({} as EnhancedQuery<T>, {
110
+ get(target, p, receiver) {
111
+
112
+ const client = getClient()
113
+
114
+ // these don't need the actual query
115
+ switch (p) {
116
+ case 'key':
117
+ return key
118
+ case 'getData':
119
+ return () => {
120
+ return client.client.getQueryData<T>(key)
121
+ }
122
+ default:
123
+ break
124
+ }
125
+
126
+ const cache = client.client.getQueryCache()
127
+
128
+ const query = cache.find({ exact: true, queryKey: key })
129
+
130
+ if (!query) {
131
+ console.warn(`Attempt to access property ${String(p)} on undefined query with key`, key)
132
+ return undefined
133
+ }
134
+
135
+ switch (p) {
136
+
137
+ case 'waitForRefresh':
138
+ return () => {
139
+ return client.waitForRefresh<T>(key)
140
+ }
141
+ case 'listen':
142
+ return (callback: (e: ReactQuery.QueryCacheNotifyEvent) => void) => {
143
+ return client.listenToQuery(key, callback)
144
+ }
145
+ case 'ensureData':
146
+ return (options) => {
147
+ return client.client.ensureQueryData<T>({
148
+ queryKey: key,
149
+ ...options
150
+ })
151
+ }
152
+ case 'refresh':
153
+ return async () => {
154
+ client.client.refetchQueries({
155
+ exact: true,
156
+ queryKey: key,
157
+ })
158
+ const newQuery = await client.waitForRefresh<T>(key)
159
+ return newQuery.state.data
160
+ }
161
+ case 'poll':
162
+ return (options: PollQueryOptions<T, any>) => {
163
+ return client.pollQuery(key, options)
164
+ }
165
+ default:
166
+ return Reflect.get(query, p, receiver)
167
+ }
168
+ },
169
+
170
+ })
171
+ }
172
+
173
+ waitForRefresh<T>(key: ReactQuery.QueryKey) {
174
+ const initialQuery = this.client.getQueryCache().find({ exact: true, queryKey: key })
175
+
176
+ if (!initialQuery) {
177
+ return Promise.reject(new Error('Query not found'))
178
+ }
179
+
180
+ const updateTime = initialQuery.state.dataUpdatedAt
181
+ const errorTime = initialQuery.state.errorUpdatedAt
182
+
183
+ return new Promise<ReactQuery.Query<T>>((resolve, reject) => {
184
+ const removeListener = this.listenToQuery(key, (e) => {
185
+ const query = e.query
186
+
187
+ const isNewer = query.state.dataUpdatedAt > updateTime || query.state.errorUpdatedAt > errorTime
188
+
189
+ const isIdle = query.state.fetchStatus === 'idle'
190
+
191
+ const isSuccess = query.state.status === 'success'
192
+ const isError = query.state.status === 'error'
193
+
194
+ const isResolved = isSuccess || isError
195
+
196
+ if (isNewer && isIdle && isResolved) {
197
+ if (isSuccess) {
198
+ resolve(query)
199
+ } else {
200
+ reject()
201
+ }
202
+
203
+ removeListener()
204
+ }
205
+ })
206
+ })
207
+
208
+ }
209
+
210
+ queryKey<Data>(k: ReactQuery.QueryKey, options?: ReactQuery.QueryOptions<Data>) {
211
+
212
+ if(options){
213
+
214
+ this.client.setQueryDefaults(k, options)
215
+
216
+ const cache = this.client.getQueryCache()
217
+
218
+ const q = new ReactQuery.Query({
219
+ cache,
220
+ queryKey: k,
221
+ queryHash: ReactQuery.hashKey(k),
222
+ ...options,
223
+ })
224
+
225
+ cache.add(q)
226
+ }
227
+
228
+ return this.queryProxy<Data>(k)
229
+ }
230
+
231
+ dynamicQueryKey<Data, BuilderArgs extends any[] = any[]>(k: QueryKeyBuilder<BuilderArgs>) {
232
+
233
+ const getClient = () => this
234
+
235
+ return new Proxy<DynamicEnhancedQuery<Data, BuilderArgs>>({} as DynamicEnhancedQuery<Data, BuilderArgs>, {
236
+ get(target, p, receiver) {
237
+ return (...params:BuilderArgs) => {
238
+ const key = k(...params)
239
+
240
+ const proxy = getClient().queryProxy<Data>(key)
241
+
242
+ return Reflect.get(proxy, p, receiver)
243
+ }
244
+ },
245
+ })
246
+ }
247
+
248
+ queryManager<T extends QueryManagerItem, Args>(name:string, options: Partial<QueryManagerOptions<T, Args>>) {
249
+ // @ts-expect-error
250
+ const m = new QueryManager<T, Args>({
251
+ name,
252
+ queryClient: this.client,
253
+ ...options,
254
+
255
+ })
256
+
257
+ return m
258
+ }
259
+
260
+ }
package/src/types.ts ADDED
@@ -0,0 +1,199 @@
1
+ import { DefinedInitialDataInfiniteOptions, InfiniteData, QueryKey, UseInfiniteQueryResult, UseMutationOptions, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
2
+ import { QueryManager } from './QueryManager'
3
+
4
+ export type PageParam = {limit: number; offset: number}
5
+
6
+ export type PaginationResponse<T> = {
7
+ count: number
8
+ next: string | null
9
+ previous: string | null
10
+ results: T[]
11
+ }
12
+
13
+ type OmitMutationKeys<O> = Omit<O, 'mutationFn'|'mutationKey'>
14
+
15
+ export type QueryManagerMeta = Record<string, any>
16
+
17
+ export type CreateOptions<T extends QueryManagerItem> = {
18
+ appendTo?: 'start' | 'end' | [number, number] | Record<string, [number, number]>
19
+ optimistic?: boolean
20
+ mutationOptions?: Partial<OmitMutationKeys<UseMutationOptions<T, unknown, Partial<T>, MutationCtx<T>>>>
21
+ onListsWithFilters?: any
22
+ }
23
+
24
+ export type UpdateOptions<T extends QueryManagerItem> = {
25
+ optimistic?: boolean
26
+
27
+ mutationOptions?: Partial<OmitMutationKeys<UseMutationOptions<T, unknown, Partial<T>, MutationCtx<T>>>>
28
+
29
+ }
30
+
31
+ export type DeleteOptions<T extends QueryManagerItem> = {
32
+ optimistic?: boolean
33
+
34
+ mutationOptions?: Partial<OmitMutationKeys<UseMutationOptions<T, unknown, T, MutationCtx<T>>>>
35
+
36
+ }
37
+
38
+ export type RetrieveOptions<T extends QueryManagerItem> = {
39
+ queryOptions?: Partial<UseQueryOptions<T, unknown, T>>
40
+ id?: T['id']
41
+ }
42
+
43
+ export type ListOptions<T extends QueryManagerItem, ExtraArgs = any> = {
44
+ queryOptions?: Partial<
45
+ DefinedInitialDataInfiniteOptions<PaginationResponse<T>, Error, UseListSelector<T>, QueryKey, PageParam>
46
+ >
47
+ filter?: ExtraArgs
48
+ limit?: number
49
+ }
50
+
51
+ export type QueryManagerAction<
52
+ T extends QueryManagerItem,
53
+ ExtraArgs = any,
54
+ Meta extends QueryManagerMeta = QueryManagerMeta,
55
+ Args extends any[] = any[]
56
+ > = (
57
+ manager: QueryManager<T, ExtraArgs, Meta>, ...args: Args
58
+ ) => any
59
+
60
+ export type QueryManagerActions<
61
+ T extends QueryManagerItem,
62
+ ExtraArgs = any,
63
+ Meta extends QueryManagerMeta = QueryManagerMeta
64
+ > = Record<
65
+ string, QueryManagerAction<T, ExtraArgs, Meta>
66
+ >
67
+
68
+ export type UseListEffect<T extends QueryManagerItem = any> = (
69
+ listQuery: {
70
+ query: UseInfiniteQueryResult<UseListSelector<T>, Error>
71
+ refreshQuery: (silent?: boolean) => void
72
+ cancelQuery: () => void
73
+ }
74
+ ) => void
75
+
76
+ export type QueryManagerOptions<
77
+ T extends QueryManagerItem,
78
+ ExtraArgs = any,
79
+ Meta extends QueryManagerMeta = QueryManagerMeta,
80
+ Actions extends QueryManagerActions<T, ExtraArgs, Meta> = QueryManagerActions<T, ExtraArgs, Meta>
81
+ > = {
82
+ name: string
83
+ itemType: T
84
+ queryClient: ReturnType<typeof useQueryClient>
85
+
86
+ listItems?: (limit: number, offset: number, args?: ExtraArgs) => Promise<PaginationResponse<T>>
87
+ createItem?: (data: Partial<T>, args?: ExtraArgs) => Promise<T>
88
+ updateItem?: (data: Partial<T>, args?: ExtraArgs) => Promise<T>
89
+ deleteItem?: (data: T, args?: ExtraArgs) => Promise<T>
90
+ retrieveItem?: (id: T['id']) => Promise<T>
91
+
92
+ useListEffect?: UseListEffect<T>
93
+
94
+ limit?: number
95
+ creation?: CreateOptions<T>
96
+ update?: UpdateOptions<T>
97
+ deletion?: DeleteOptions<T>
98
+ generateId?: () => T['id']
99
+ actions?: Actions
100
+ keyExtractor?: (item: T) => string
101
+ initialMeta?: Meta
102
+ }
103
+
104
+ export type QueryManagerActionTrigger<
105
+ A extends QueryManagerAction<any, any, any>,
106
+ Args extends any[] = A extends QueryManagerAction<any, any, any, infer _Args> ? _Args : any[]
107
+ > = (...args: Args) => any
108
+
109
+ export type QueryManagerActionTriggers<
110
+ Actions extends QueryManagerActions<any, any, any>
111
+ > = {
112
+ [K in keyof Actions]: QueryManagerActionTrigger<Actions[K]>
113
+ }
114
+
115
+ export type InfinitePaginationData<T> = InfiniteData<PaginationResponse<T>, PageParam>
116
+
117
+ export type UseManagerArgs<T extends QueryManagerItem, ExtraArgs = any> = {
118
+ filter?: ExtraArgs
119
+ limit?: number
120
+ offset?: number
121
+
122
+ creation?: CreateOptions<T>
123
+ update?: UpdateOptions<T>
124
+ deletion?: DeleteOptions<T>
125
+
126
+ listOptions?: Pick<ListOptions<T, ExtraArgs>, 'queryOptions'>
127
+ }
128
+
129
+ export type QueryManagerItem = {
130
+ id: string | number
131
+ }
132
+
133
+ export type AppendToPaginationParams<TItem extends QueryManagerItem, Filters = any> = {
134
+ item: TItem|TItem[]
135
+ to?: CreateOptions<TItem>['appendTo']
136
+ refreshKey?: QueryKey
137
+ onListsWithFilters?: Filters
138
+ }
139
+
140
+ export type AppendToPaginationReturn<TItem = any> = InfiniteData<TItem>
141
+
142
+ export type AppendToPagination<TItem extends QueryManagerItem, ExtraArgs = any> = (params: AppendToPaginationParams<TItem, ExtraArgs>) => Promise<void>
143
+
144
+ export type MutationCtx<T extends QueryManagerItem> = null | {
145
+ previousData?: InfinitePaginationData<T>
146
+ addedId?: T['id']
147
+ previousItem?: T
148
+ optimisticItem?: T
149
+ prevItemPages?:Record<string, [number, number]>
150
+ }
151
+
152
+ export const isInfiniteQueryData = <T>(data: any): data is InfinitePaginationData<T> => {
153
+ return !!data?.pages && !!data?.pageParams
154
+ }
155
+
156
+ export type QueryStateValue<T extends QueryManagerItem> = {
157
+ pagesById: Record<T['id'], [number, number]>
158
+ itemIndexes: Record<T['id'], number>
159
+ key: QueryKey
160
+ }
161
+
162
+ export type QueryStateSubscriber<T extends QueryManagerItem> = (data: QueryStateValue<T>) => void
163
+
164
+ export type FilterKeyOrder = string[]
165
+
166
+ export type GetItemOptions<T extends QueryManagerItem> = {
167
+ forceRefetch?: boolean
168
+ fetchOnNotFoud?: boolean
169
+ }
170
+
171
+ export type SettableOptions<O extends QueryManagerOptions<any, any, any, any>> = Partial<
172
+ Pick<
173
+ O,
174
+ 'limit' |
175
+ 'creation' |
176
+ 'update' |
177
+ 'deletion'
178
+ > & {
179
+ meta: O['initialMeta']
180
+ }
181
+ >
182
+
183
+ export type OptionChangeListener<O extends QueryManagerOptions<any, any, any, any>> = (
184
+ options: O,
185
+ meta: O['initialMeta'],
186
+ ) => any
187
+
188
+ export type UseActionOptions<T extends QueryManagerAction<any, any, any>> = UseMutationOptions<
189
+ Awaited<ReturnType<T>>,
190
+ unknown,
191
+ Parameters<T>[1]
192
+ >
193
+
194
+ export type UseListSelector<T> = {
195
+ pageParams: PageParam[]
196
+ pages: PaginationResponse<T>[]
197
+ flatItems: T[]
198
+ }
199
+