1
1
/* eslint-disable @typescript-eslint/no-explicit-any */
2
2
import { deserialize , serialize } from '@zenstackhq/runtime/browser' ;
3
- import { createContext } from 'react' ;
3
+ import { getMutatedModels , getReadModels , type ModelMeta , type PrismaWriteActionType } from '@zenstackhq/runtime/cross' ;
4
+ import * as crossFetch from 'cross-fetch' ;
5
+ import { lowerCaseFirst } from 'lower-case-first' ;
6
+ import { createContext , useContext } from 'react' ;
4
7
import type { Fetcher , MutatorCallback , MutatorOptions , SWRConfiguration , SWRResponse } from 'swr' ;
5
8
import useSWR , { useSWRConfig } from 'swr' ;
6
9
import useSWRInfinite , { SWRInfiniteConfiguration , SWRInfiniteFetcher , SWRInfiniteResponse } from 'swr/infinite' ;
@@ -18,19 +21,26 @@ export type RequestHandlerContext = {
18
21
/**
19
22
* The endpoint to use for the queries.
20
23
*/
21
- endpoint : string ;
24
+ endpoint ? : string ;
22
25
23
26
/**
24
27
* A custom fetch function for sending the HTTP requests.
25
28
*/
26
29
fetch ?: FetchFn ;
30
+
31
+ /**
32
+ * If logging is enabled.
33
+ */
34
+ logging ?: boolean ;
27
35
} ;
28
36
37
+ const DEFAULT_QUERY_ENDPOINT = '/api/model' ;
38
+
29
39
/**
30
40
* Context for configuring react hooks.
31
41
*/
32
42
export const RequestHandlerContext = createContext < RequestHandlerContext > ( {
33
- endpoint : '/api/model' ,
43
+ endpoint : DEFAULT_QUERY_ENDPOINT ,
34
44
fetch : undefined ,
35
45
} ) ;
36
46
@@ -39,6 +49,14 @@ export const RequestHandlerContext = createContext<RequestHandlerContext>({
39
49
*/
40
50
export const Provider = RequestHandlerContext . Provider ;
41
51
52
+ /**
53
+ * Hooks context.
54
+ */
55
+ export function useHooksContext ( ) {
56
+ const { endpoint, ...rest } = useContext ( RequestHandlerContext ) ;
57
+ return { endpoint : endpoint ?? DEFAULT_QUERY_ENDPOINT , ...rest } ;
58
+ }
59
+
42
60
/**
43
61
* Client request options for regular query.
44
62
*/
@@ -69,6 +87,29 @@ export type InfiniteRequestOptions<Result, Error = any> = {
69
87
initialData ?: Result [ ] ;
70
88
} & SWRInfiniteConfiguration < Result , Error , SWRInfiniteFetcher < Result > > ;
71
89
90
+ export const QUERY_KEY_PREFIX = 'zenstack' ;
91
+
92
+ type QueryKey = { prefix : typeof QUERY_KEY_PREFIX ; model : string ; operation : string ; args : unknown } ;
93
+
94
+ export function getQueryKey ( model : string , operation : string , args ?: unknown ) {
95
+ return JSON . stringify ( { prefix : QUERY_KEY_PREFIX , model, operation, args } ) ;
96
+ }
97
+
98
+ export function parseQueryKey ( key : unknown ) {
99
+ if ( typeof key !== 'string' ) {
100
+ return undefined ;
101
+ }
102
+ try {
103
+ const parsed = JSON . parse ( key ) ;
104
+ if ( ! parsed || parsed . prefix !== QUERY_KEY_PREFIX ) {
105
+ return undefined ;
106
+ }
107
+ return parsed as QueryKey ;
108
+ } catch {
109
+ return undefined ;
110
+ }
111
+ }
112
+
72
113
/**
73
114
* Makes a GET request with SWR.
74
115
*
@@ -79,14 +120,17 @@ export type InfiniteRequestOptions<Result, Error = any> = {
79
120
* @returns SWR response
80
121
*/
81
122
// eslint-disable-next-line @typescript-eslint/no-explicit-any
82
- export function get < Result , Error = any > (
83
- url : string | null ,
123
+ export function useGet < Result , Error = any > (
124
+ model : string ,
125
+ operation : string ,
126
+ endpoint : string ,
84
127
args ?: unknown ,
85
128
options ?: RequestOptions < Result , Error > ,
86
129
fetch ?: FetchFn
87
130
) : SWRResponse < Result , Error > {
88
- const reqUrl = options ?. disabled ? null : url ? makeUrl ( url , args ) : null ;
89
- return useSWR < Result , Error > ( reqUrl , ( url ) => fetcher < Result , false > ( url , undefined , fetch , false ) , {
131
+ const key = options ?. disabled ? null : getQueryKey ( model , operation , args ) ;
132
+ const url = makeUrl ( `${ endpoint } /${ lowerCaseFirst ( model ) } /${ operation } ` , args ) ;
133
+ return useSWR < Result , Error > ( key , ( ) => fetcher < Result , false > ( url , undefined , fetch , false ) , {
90
134
...options ,
91
135
fallbackData : options ?. initialData ?? options ?. fallbackData ,
92
136
} ) ;
@@ -107,26 +151,40 @@ export type GetNextArgs<Args, Result> = (pageIndex: number, previousPageData: Re
107
151
* @returns SWR infinite query response
108
152
*/
109
153
// eslint-disable-next-line @typescript-eslint/no-explicit-any
110
- export function infiniteGet < Args , Result , Error = any > (
111
- url : string | null ,
154
+ export function useInfiniteGet < Args , Result , Error = any > (
155
+ model : string ,
156
+ operation : string ,
157
+ endpoint : string ,
112
158
getNextArgs : GetNextArgs < Args , any > ,
113
159
options ?: InfiniteRequestOptions < Result , Error > ,
114
160
fetch ?: FetchFn
115
161
) : SWRInfiniteResponse < Result , Error > {
116
162
const getKey = ( pageIndex : number , previousPageData : Result | null ) => {
117
- if ( options ?. disabled || ! url ) {
163
+ if ( options ?. disabled ) {
118
164
return null ;
119
165
}
120
166
const nextArgs = getNextArgs ( pageIndex , previousPageData ) ;
121
167
return nextArgs !== null // null means reached the end
122
- ? makeUrl ( url , nextArgs )
168
+ ? getQueryKey ( model , operation , nextArgs )
123
169
: null ;
124
170
} ;
125
171
126
- return useSWRInfinite < Result , Error > ( getKey , ( url ) => fetcher < Result , false > ( url , undefined , fetch , false ) , {
127
- ...options ,
128
- fallbackData : options ?. initialData ?? options ?. fallbackData ,
129
- } ) ;
172
+ return useSWRInfinite < Result , Error > (
173
+ getKey ,
174
+ ( key ) => {
175
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
176
+ const parsedKey = parseQueryKey ( key ) ! ;
177
+ const url = makeUrl (
178
+ `${ endpoint } /${ lowerCaseFirst ( parsedKey . model ) } /${ parsedKey . operation } ` ,
179
+ parsedKey . args
180
+ ) ;
181
+ return fetcher < Result , false > ( url , undefined , fetch , false ) ;
182
+ } ,
183
+ {
184
+ ...options ,
185
+ fallbackData : options ?. initialData ?? options ?. fallbackData ,
186
+ }
187
+ ) ;
130
188
}
131
189
132
190
/**
@@ -155,7 +213,7 @@ export async function post<Result, C extends boolean = boolean>(
155
213
fetch ,
156
214
checkReadBack
157
215
) ;
158
- mutate ( ) ;
216
+ mutate ( getOperationFromUrl ( url ) , data ) ;
159
217
return r ;
160
218
}
161
219
@@ -185,7 +243,7 @@ export async function put<Result, C extends boolean = boolean>(
185
243
fetch ,
186
244
checkReadBack
187
245
) ;
188
- mutate ( ) ;
246
+ mutate ( getOperationFromUrl ( url ) , data ) ;
189
247
return r ;
190
248
}
191
249
@@ -212,29 +270,42 @@ export async function del<Result, C extends boolean = boolean>(
212
270
fetch ,
213
271
checkReadBack
214
272
) ;
215
- const path = url . split ( '/' ) ;
216
- path . pop ( ) ;
217
- mutate ( ) ;
273
+ mutate ( getOperationFromUrl ( url ) , args ) ;
218
274
return r ;
219
275
}
220
276
221
277
type Mutator = (
278
+ operation : string ,
222
279
data ?: unknown | Promise < unknown > | MutatorCallback ,
223
280
opts ?: boolean | MutatorOptions
224
281
) => Promise < unknown [ ] > ;
225
282
226
- export function getMutate ( prefixes : string [ ] ) : Mutator {
283
+ export function useMutate ( model : string , modelMeta : ModelMeta , logging ?: boolean ) : Mutator {
227
284
// https://swr.vercel.app/docs/advanced/cache#mutate-multiple-keys-from-regex
228
285
const { cache, mutate } = useSWRConfig ( ) ;
229
- return ( data ?: unknown | Promise < unknown > | MutatorCallback , opts ?: boolean | MutatorOptions ) => {
286
+ return async ( operation : string , args : unknown , opts ?: boolean | MutatorOptions ) => {
230
287
if ( ! ( cache instanceof Map ) ) {
231
288
throw new Error ( 'mutate requires the cache provider to be a Map instance' ) ;
232
289
}
233
290
234
- const keys = Array . from ( cache . keys ( ) ) . filter (
235
- ( k ) => typeof k === 'string' && prefixes . some ( ( prefix ) => k . startsWith ( prefix ) )
236
- ) as string [ ] ;
237
- const mutations = keys . map ( ( key ) => mutate ( key , data , opts ) ) ;
291
+ const mutatedModels = await getMutatedModels ( model , operation as PrismaWriteActionType , args , modelMeta ) ;
292
+
293
+ const keys = Array . from ( cache . keys ( ) ) . filter ( ( key ) => {
294
+ const parsedKey = parseQueryKey ( key ) ;
295
+ if ( ! parsedKey ) {
296
+ return false ;
297
+ }
298
+ const modelsRead = getReadModels ( parsedKey . model , modelMeta , parsedKey . args ) ;
299
+ return modelsRead . some ( ( m ) => mutatedModels . includes ( m ) ) ;
300
+ } ) ;
301
+
302
+ if ( logging ) {
303
+ keys . forEach ( ( key ) => {
304
+ console . log ( `Invalidating query ${ key } due to mutation "${ model } .${ operation } "` ) ;
305
+ } ) ;
306
+ }
307
+
308
+ const mutations = keys . map ( ( key ) => mutate ( key , undefined , opts ) ) ;
238
309
return Promise . all ( mutations ) ;
239
310
} ;
240
311
}
@@ -245,7 +316,7 @@ export async function fetcher<R, C extends boolean>(
245
316
fetch ?: FetchFn ,
246
317
checkReadBack ?: C
247
318
) : Promise < C extends true ? R | undefined : R > {
248
- const _fetch = fetch ?? window . fetch ;
319
+ const _fetch = fetch ?? crossFetch . fetch ;
249
320
const res = await _fetch ( url , options ) ;
250
321
if ( ! res . ok ) {
251
322
const errData = unmarshal ( await res . text ( ) ) ;
@@ -306,3 +377,13 @@ function makeUrl(url: string, args: unknown) {
306
377
}
307
378
return result ;
308
379
}
380
+
381
+ function getOperationFromUrl ( url : string ) {
382
+ const parts = url . split ( '/' ) ;
383
+ const r = parts . pop ( ) ;
384
+ if ( ! r ) {
385
+ throw new Error ( `Invalid URL: ${ url } ` ) ;
386
+ } else {
387
+ return r ;
388
+ }
389
+ }
0 commit comments