11/* eslint-disable @typescript-eslint/no-explicit-any */
22import { 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' ;
47import type { Fetcher , MutatorCallback , MutatorOptions , SWRConfiguration , SWRResponse } from 'swr' ;
58import useSWR , { useSWRConfig } from 'swr' ;
69import useSWRInfinite , { SWRInfiniteConfiguration , SWRInfiniteFetcher , SWRInfiniteResponse } from 'swr/infinite' ;
@@ -18,19 +21,26 @@ export type RequestHandlerContext = {
1821 /**
1922 * The endpoint to use for the queries.
2023 */
21- endpoint : string ;
24+ endpoint ? : string ;
2225
2326 /**
2427 * A custom fetch function for sending the HTTP requests.
2528 */
2629 fetch ?: FetchFn ;
30+
31+ /**
32+ * If logging is enabled.
33+ */
34+ logging ?: boolean ;
2735} ;
2836
37+ const DEFAULT_QUERY_ENDPOINT = '/api/model' ;
38+
2939/**
3040 * Context for configuring react hooks.
3141 */
3242export const RequestHandlerContext = createContext < RequestHandlerContext > ( {
33- endpoint : '/api/model' ,
43+ endpoint : DEFAULT_QUERY_ENDPOINT ,
3444 fetch : undefined ,
3545} ) ;
3646
@@ -39,6 +49,14 @@ export const RequestHandlerContext = createContext<RequestHandlerContext>({
3949 */
4050export const Provider = RequestHandlerContext . Provider ;
4151
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+
4260/**
4361 * Client request options for regular query.
4462 */
@@ -69,6 +87,29 @@ export type InfiniteRequestOptions<Result, Error = any> = {
6987 initialData ?: Result [ ] ;
7088} & SWRInfiniteConfiguration < Result , Error , SWRInfiniteFetcher < Result > > ;
7189
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+
72113/**
73114 * Makes a GET request with SWR.
74115 *
@@ -79,14 +120,17 @@ export type InfiniteRequestOptions<Result, Error = any> = {
79120 * @returns SWR response
80121 */
81122// 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 ,
84127 args ?: unknown ,
85128 options ?: RequestOptions < Result , Error > ,
86129 fetch ?: FetchFn
87130) : 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 ) , {
90134 ...options ,
91135 fallbackData : options ?. initialData ?? options ?. fallbackData ,
92136 } ) ;
@@ -107,26 +151,40 @@ export type GetNextArgs<Args, Result> = (pageIndex: number, previousPageData: Re
107151 * @returns SWR infinite query response
108152 */
109153// 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 ,
112158 getNextArgs : GetNextArgs < Args , any > ,
113159 options ?: InfiniteRequestOptions < Result , Error > ,
114160 fetch ?: FetchFn
115161) : SWRInfiniteResponse < Result , Error > {
116162 const getKey = ( pageIndex : number , previousPageData : Result | null ) => {
117- if ( options ?. disabled || ! url ) {
163+ if ( options ?. disabled ) {
118164 return null ;
119165 }
120166 const nextArgs = getNextArgs ( pageIndex , previousPageData ) ;
121167 return nextArgs !== null // null means reached the end
122- ? makeUrl ( url , nextArgs )
168+ ? getQueryKey ( model , operation , nextArgs )
123169 : null ;
124170 } ;
125171
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+ ) ;
130188}
131189
132190/**
@@ -155,7 +213,7 @@ export async function post<Result, C extends boolean = boolean>(
155213 fetch ,
156214 checkReadBack
157215 ) ;
158- mutate ( ) ;
216+ mutate ( getOperationFromUrl ( url ) , data ) ;
159217 return r ;
160218}
161219
@@ -185,7 +243,7 @@ export async function put<Result, C extends boolean = boolean>(
185243 fetch ,
186244 checkReadBack
187245 ) ;
188- mutate ( ) ;
246+ mutate ( getOperationFromUrl ( url ) , data ) ;
189247 return r ;
190248}
191249
@@ -212,29 +270,42 @@ export async function del<Result, C extends boolean = boolean>(
212270 fetch ,
213271 checkReadBack
214272 ) ;
215- const path = url . split ( '/' ) ;
216- path . pop ( ) ;
217- mutate ( ) ;
273+ mutate ( getOperationFromUrl ( url ) , args ) ;
218274 return r ;
219275}
220276
221277type Mutator = (
278+ operation : string ,
222279 data ?: unknown | Promise < unknown > | MutatorCallback ,
223280 opts ?: boolean | MutatorOptions
224281) => Promise < unknown [ ] > ;
225282
226- export function getMutate ( prefixes : string [ ] ) : Mutator {
283+ export function useMutate ( model : string , modelMeta : ModelMeta , logging ?: boolean ) : Mutator {
227284 // https://swr.vercel.app/docs/advanced/cache#mutate-multiple-keys-from-regex
228285 const { cache, mutate } = useSWRConfig ( ) ;
229- return ( data ?: unknown | Promise < unknown > | MutatorCallback , opts ?: boolean | MutatorOptions ) => {
286+ return async ( operation : string , args : unknown , opts ?: boolean | MutatorOptions ) => {
230287 if ( ! ( cache instanceof Map ) ) {
231288 throw new Error ( 'mutate requires the cache provider to be a Map instance' ) ;
232289 }
233290
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 ) ) ;
238309 return Promise . all ( mutations ) ;
239310 } ;
240311}
@@ -245,7 +316,7 @@ export async function fetcher<R, C extends boolean>(
245316 fetch ?: FetchFn ,
246317 checkReadBack ?: C
247318) : Promise < C extends true ? R | undefined : R > {
248- const _fetch = fetch ?? window . fetch ;
319+ const _fetch = fetch ?? crossFetch . fetch ;
249320 const res = await _fetch ( url , options ) ;
250321 if ( ! res . ok ) {
251322 const errData = unmarshal ( await res . text ( ) ) ;
@@ -306,3 +377,13 @@ function makeUrl(url: string, args: unknown) {
306377 }
307378 return result ;
308379}
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