@codeleap/query 5.8.2 → 5.8.4

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.
@@ -0,0 +1,319 @@
1
+ import { CancelOptions, InfiniteData, InvalidateOptions, InvalidateQueryFilters, Query, QueryFilters, QueryKey, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'
2
+ import { useMemo } from 'react'
3
+ import { TypeGuards } from '@codeleap/types'
4
+ import { ListPaginationResponse, ListSelector, PageParam, QueryClient, QueryItem } from '../types'
5
+ import deepEqual from 'fast-deep-equal'
6
+
7
+ /**
8
+ * Class for managing React Query keys and operations for a specific query type
9
+ * @template T - The query item type that extends QueryItem
10
+ * @template F - The filter type used for list queries
11
+ */
12
+ export class QueryKeys<T extends QueryItem, F> {
13
+ /**
14
+ * Creates a new QueryKeys instance
15
+ * @param queryName - The name of the query used as base for all keys
16
+ * @param queryClient - The React Query client instance
17
+ */
18
+ constructor(
19
+ private queryName: string,
20
+ private queryClient: QueryClient
21
+ ) { }
22
+
23
+ /**
24
+ * Gets the base query keys for different operations
25
+ * @returns Object containing base query keys for list, retrieve, create, update, and delete operations
26
+ */
27
+ get keys() {
28
+ return {
29
+ // queries
30
+ list: [this.queryName, 'list'] as QueryKey,
31
+ retrieve: (id: QueryItem['id']) => [this.queryName, 'retrieve', id] as QueryKey,
32
+
33
+ // mutations
34
+ create: [this.queryName, 'create'] as QueryKey,
35
+ update: [this.queryName, 'update'] as QueryKey,
36
+ delete: [this.queryName, 'delete'] as QueryKey,
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Generates a list query key with optional filters
42
+ * @param filters - Optional filters to include in the query key
43
+ * @returns Query key array with or without filters
44
+ */
45
+ listKeyWithFilters(filters?: F): QueryKey {
46
+ const hasValidFilters = filters != null && (
47
+ typeof filters !== 'object'
48
+ ? Boolean(filters)
49
+ : Object.values(filters).some(value =>
50
+ value != null && (typeof value !== 'string' || value.trim() !== '')
51
+ )
52
+ )
53
+
54
+ return hasValidFilters ? [...this.keys.list, filters] : this.keys.list
55
+ }
56
+
57
+ /**
58
+ * React hook that returns a memoized list query key with filters
59
+ * @param filters - Optional filters to include in the query key
60
+ * @returns Memoized query key array
61
+ */
62
+ useListKeyWithFilters(filters?: F) {
63
+ return useMemo(() => {
64
+ return this.listKeyWithFilters(filters)
65
+ }, [filters])
66
+ }
67
+
68
+ /**
69
+ * React hook that returns a memoized retrieve query key
70
+ * @param id - The ID of the item to retrieve
71
+ * @returns Memoized query key array for retrieve operation
72
+ */
73
+ useRetrieveKey(id: QueryItem['id']) {
74
+ return useMemo(() => {
75
+ return this.keys.retrieve(id)
76
+ }, [id])
77
+ }
78
+
79
+ /**
80
+ * Predicate function to check if a query belongs to this query name (all operations)
81
+ * @private
82
+ * @param queryName - The query name to match against
83
+ * @param query - The query object to check
84
+ * @returns True if the query matches the query name
85
+ */
86
+ private predicateQueryKeyAll(queryName: string, query: Query<unknown, Error, unknown, QueryKey>) {
87
+ const queryKey = query?.queryKey?.join('/')
88
+
89
+ return queryKey?.includes(queryName)
90
+ }
91
+
92
+ /**
93
+ * Predicate function to check if a query is a list query for this query name
94
+ * @private
95
+ * @param queryName - The query name to match against
96
+ * @param query - The query object to check
97
+ * @param toIgnoreQueryKeys - Query keys to ignore in the matching
98
+ * @returns True if the query is a list query and not in the ignore list
99
+ */
100
+ private predicateQueryKeyList(queryName: string, query: Query<unknown, Error, unknown, QueryKey>, toIgnoreQueryKeys?: QueryKey | QueryKey[]) {
101
+ const queryKey = query?.queryKey?.join('/')
102
+
103
+ if (!TypeGuards.isNil(toIgnoreQueryKeys)) {
104
+ const ignoreQueryKeys = Array.isArray(toIgnoreQueryKeys?.[0]) ? toIgnoreQueryKeys : [toIgnoreQueryKeys]
105
+ if (ignoreQueryKeys.some(key => deepEqual(query?.queryKey, key))) {
106
+ return false
107
+ }
108
+ }
109
+
110
+ const isListQueryKey = queryKey?.includes(queryName) && queryKey?.includes('list')
111
+
112
+ return isListQueryKey
113
+ }
114
+
115
+ /**
116
+ * Invalidates all queries for this query name
117
+ * @param filters - Optional filters to apply to the invalidation
118
+ * @param options - Optional invalidation options
119
+ * @returns Promise that resolves when invalidation is complete
120
+ */
121
+ async invalidateAll(filters?: InvalidateQueryFilters<QueryKey>, options?: InvalidateOptions) {
122
+ return this.queryClient.invalidateQueries({
123
+ ...filters,
124
+ predicate: (query) => this.predicateQueryKeyAll(this.queryName, query),
125
+ }, options)
126
+ }
127
+
128
+ /**
129
+ * Invalidates list queries, optionally with specific filters
130
+ * @param listFilters - Optional filters to target specific list queries
131
+ * @param ignoreQueryKeys - Query keys to ignore during invalidation
132
+ * @param filters - Optional filters to apply to the invalidation
133
+ * @param options - Optional invalidation options
134
+ * @returns Promise that resolves when invalidation is complete
135
+ */
136
+ async invalidateList(listFilters?: F, ignoreQueryKeys?: QueryKey | QueryKey[], filters?: InvalidateQueryFilters<QueryKey>, options?: InvalidateOptions) {
137
+ if (!!listFilters) {
138
+ const queryKey = this.listKeyWithFilters(listFilters)
139
+
140
+ return this.queryClient.invalidateQueries({ ...filters, queryKey }, options)
141
+ }
142
+
143
+ return this.queryClient.invalidateQueries({
144
+ ...filters,
145
+ predicate: (query) => this.predicateQueryKeyList(this.queryName, query, ignoreQueryKeys),
146
+ }, options)
147
+ }
148
+
149
+ /**
150
+ * Invalidates a specific retrieve query by ID
151
+ * @param id - The ID of the item to invalidate
152
+ * @param filters - Optional filters to apply to the invalidation
153
+ * @param options - Optional invalidation options
154
+ * @returns Promise that resolves when invalidation is complete
155
+ */
156
+ async invalidateRetrieve(id: QueryItem['id'], filters?: InvalidateQueryFilters<QueryKey>, options?: InvalidateOptions) {
157
+ const queryKey = this.keys.retrieve(id)
158
+
159
+ return this.queryClient.invalidateQueries({
160
+ ...filters,
161
+ queryKey,
162
+ }, options)
163
+ }
164
+
165
+ /**
166
+ * Refetches all queries for this query name
167
+ * @param filters - Optional filters to apply to the refetch
168
+ * @param options - Optional refetch options
169
+ * @returns Promise that resolves when refetch is complete
170
+ */
171
+ async refetchAll(filters?: RefetchQueryFilters<QueryKey>, options?: RefetchOptions) {
172
+ return this.queryClient.refetchQueries({
173
+ ...filters,
174
+ predicate: (query) => this.predicateQueryKeyAll(this.queryName, query),
175
+ }, options)
176
+ }
177
+
178
+ /**
179
+ * Refetches list queries, optionally with specific filters
180
+ * @param listFilters - Optional filters to target specific list queries
181
+ * @param ignoreQueryKeys - Query keys to ignore during refetch
182
+ * @param filters - Optional filters to apply to the refetch
183
+ * @param options - Optional refetch options
184
+ * @returns Promise that resolves when refetch is complete
185
+ */
186
+ async refetchList(listFilters?: F, ignoreQueryKeys?: QueryKey | QueryKey[], filters?: RefetchQueryFilters<QueryKey>, options?: RefetchOptions) {
187
+ if (!!listFilters) {
188
+ const queryKey = this.listKeyWithFilters(listFilters)
189
+
190
+ return this.queryClient.refetchQueries({ ...filters, queryKey }, options)
191
+ }
192
+
193
+ return this.queryClient.refetchQueries({
194
+ ...filters,
195
+ predicate: (query) => this.predicateQueryKeyList(this.queryName, query, ignoreQueryKeys),
196
+ }, options)
197
+ }
198
+
199
+ /**
200
+ * Refetches a specific retrieve query by ID
201
+ * @param id - The ID of the item to refetch
202
+ * @param filters - Optional filters to apply to the refetch
203
+ * @param options - Optional refetch options
204
+ * @returns Promise that resolves when refetch is complete
205
+ */
206
+ async refetchRetrieve(id: QueryItem['id'], filters?: RefetchQueryFilters<QueryKey>, options?: RefetchOptions) {
207
+ const queryKey = this.keys.retrieve(id)
208
+
209
+ return this.queryClient.refetchQueries({
210
+ ...filters,
211
+ queryKey,
212
+ }, options)
213
+ }
214
+
215
+ /**
216
+ * Removes a specific retrieve query data from the cache
217
+ * @param id - The ID of the item to remove from cache
218
+ * @param filters - Optional filters to apply to the removal
219
+ * @returns Promise that resolves when removal is complete
220
+ */
221
+ async removeRetrieveQueryData(id: QueryItem['id'], filters?: QueryFilters<QueryKey>) {
222
+ const queryKey = this.keys.retrieve(id)
223
+
224
+ return this.queryClient.removeQueries({
225
+ ...filters,
226
+ queryKey,
227
+ })
228
+ }
229
+
230
+ /**
231
+ * Cancels list queries that are currently in flight
232
+ * @param listFilters - Optional filters to target specific list queries
233
+ * @param ignoreQueryKeys - Query keys to ignore during cancellation
234
+ * @param filters - Optional filters to apply to the cancellation
235
+ * @param options - Optional cancellation options
236
+ * @returns Promise that resolves when cancellation is complete
237
+ */
238
+ async cancelListQueries(listFilters?: F, ignoreQueryKeys?: QueryKey | QueryKey[], filters?: QueryFilters<QueryKey>, options?: CancelOptions) {
239
+ if (!!listFilters) {
240
+ const queryKey = this.listKeyWithFilters(listFilters)
241
+
242
+ return this.queryClient.cancelQueries({ ...filters, queryKey }, options)
243
+ }
244
+
245
+ return this.queryClient.cancelQueries({
246
+ ...filters,
247
+ predicate: (query) => this.predicateQueryKeyList(this.queryName, query, ignoreQueryKeys),
248
+ }, options)
249
+ }
250
+
251
+ /**
252
+ * Gets list data from the cache, including both items array and item map
253
+ * @param filters - Optional filters to target specific list data
254
+ * @returns Object containing items array and itemMap (keyed by ID)
255
+ */
256
+ getListData(filters?: F) {
257
+ const queryKey = this.listKeyWithFilters(filters)
258
+
259
+ const data = this.queryClient.getQueryData<InfiniteData<ListPaginationResponse<T>, PageParam>>(queryKey)
260
+
261
+ const items = (data?.pages ?? []).flat()
262
+
263
+ const itemMap = items.reduce((acc, item) => {
264
+ acc[item?.id] = item
265
+ return acc
266
+ }, {} as Record<QueryItem['id'], T>)
267
+
268
+ return {
269
+ items,
270
+ itemMap,
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Gets retrieve data from cache, with fallback to list data
276
+ * @param id - The ID of the item to retrieve
277
+ * @param onlyQueryData - If true, only returns data from the specific retrieve query, not from list data
278
+ * @returns The item data or undefined if not found
279
+ */
280
+ getRetrieveData(id: QueryItem['id'], onlyQueryData = false): T | undefined {
281
+ if (TypeGuards.isNil(id)) return undefined
282
+
283
+ const queryKey = this.keys.retrieve(id)
284
+
285
+ const queryData = this.queryClient.getQueryData<T>(queryKey)
286
+
287
+ if (!queryData?.id && !onlyQueryData) {
288
+ const { itemMap } = this.getListData()
289
+
290
+ return itemMap?.[id]
291
+ }
292
+
293
+ return queryData
294
+ }
295
+
296
+ /**
297
+ * Gets all list queries from the query cache
298
+ * @returns Array of list queries for this query name
299
+ */
300
+ getAllListQueries() {
301
+ const queries = this.queryClient.getQueryCache().findAll({
302
+ predicate: (query) => this.predicateQueryKeyList(this.queryName, query),
303
+ })
304
+
305
+ return queries as Query<ListPaginationResponse<T>, Error, Omit<ListSelector<T>, 'allItems'>, QueryKey>[]
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Factory function to create a new QueryKeys instance
311
+ * @template T - The query item type that extends QueryItem
312
+ * @template F - The filter type used for list queries
313
+ * @param name - The name of the query used as base for all keys
314
+ * @param queryClient - The React Query client instance
315
+ * @returns New QueryKeys instance
316
+ */
317
+ export const createQueryKeys = <T extends QueryItem, F>(name: string, queryClient: QueryClient) => {
318
+ return new QueryKeys<T, F>(name, queryClient)
319
+ }