@@ -25,6 +25,8 @@ const supportedTargets = ['react', 'vue', 'svelte'];
2525type TargetFramework = ( typeof supportedTargets ) [ number ] ;
2626type TanStackVersion = 'v4' | 'v5' ;
2727
28+ // TODO: turn it into a class to simplify parameter passing
29+
2830export async function generate ( model : Model , options : PluginOptions , dmmf : DMMF . Document ) {
2931 const project = createProject ( ) ;
3032 const warnings : string [ ] = [ ] ;
@@ -40,6 +42,17 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
4042 throw new PluginError ( name , `Unsupported version "${ version } ": use "v4" or "v5"` ) ;
4143 }
4244
45+ if ( options . generatePrefetch !== undefined && typeof options . generatePrefetch !== 'boolean' ) {
46+ throw new PluginError (
47+ name ,
48+ `Invalid "generatePrefetch" option: expected boolean, got ${ options . generatePrefetch } `
49+ ) ;
50+ }
51+
52+ if ( options . generatePrefetch === true && version === 'v4' ) {
53+ throw new PluginError ( name , `"generatePrefetch" is not supported for version "v4"` ) ;
54+ }
55+
4356 let outDir = requireOption < string > ( options , 'output' , name ) ;
4457 outDir = resolvePath ( outDir , options ) ;
4558 ensureEmptyDir ( outDir ) ;
@@ -71,27 +84,20 @@ function generateQueryHook(
7184 model : string ,
7285 operation : string ,
7386 returnArray : boolean ,
87+ returnNullable : boolean ,
7488 optionalInput : boolean ,
7589 overrideReturnType ?: string ,
7690 overrideInputType ?: string ,
7791 overrideTypeParameters ?: string [ ] ,
7892 supportInfinite = false ,
7993 supportOptimistic = false ,
80- supportPrefetching = false ,
94+ generatePrefetch = false
8195) {
8296 const generateModes : ( '' | 'Infinite' | 'Suspense' | 'SuspenseInfinite' | 'Prefetch' | 'PrefetchInfinite' ) [ ] = [ '' ] ;
8397 if ( supportInfinite ) {
8498 generateModes . push ( 'Infinite' ) ;
8599 }
86100
87- if ( supportPrefetching ) {
88- generateModes . push ( 'Prefetch' ) ;
89-
90- if ( supportInfinite ) {
91- generateModes . push ( 'PrefetchInfinite' ) ;
92- }
93- }
94-
95101 if ( target === 'react' && version === 'v5' ) {
96102 // react-query v5 supports suspense query
97103 generateModes . push ( 'Suspense' ) ;
@@ -100,34 +106,45 @@ function generateQueryHook(
100106 }
101107 }
102108
103- for ( const generateMode of generateModes ) {
104- const capOperation = upperCaseFirst ( operation ) ;
105-
106- const argsType = overrideInputType ?? `Prisma.${ model } ${ capOperation } Args` ;
107- const inputType = makeQueryArgsType ( target , argsType ) ;
108-
109- const infinite = generateMode . includes ( 'Infinite' ) ;
110- const suspense = generateMode . includes ( 'Suspense' ) ;
111- const prefetch = generateMode . includes ( 'Prefetch' ) ;
112- const prefetchInfinite = generateMode . includes ( 'PrefetchInfinite' ) ;
109+ const getArgsType = ( ) => {
110+ return overrideInputType ?? `Prisma.${ model } ${ upperCaseFirst ( operation ) } Args` ;
111+ } ;
113112
114- const optimistic =
115- supportOptimistic &&
116- // infinite queries are not subject to optimistic updates
117- ! infinite ;
113+ const getInputType = ( prefetch : boolean ) => {
114+ return makeQueryArgsType ( target , getArgsType ( ) , prefetch ) ;
115+ } ;
118116
117+ const getReturnType = ( optimistic : boolean ) => {
119118 let defaultReturnType = `Prisma.${ model } GetPayload<TArgs>` ;
120119 if ( optimistic ) {
121120 defaultReturnType += '& { $optimistic?: boolean }' ;
122121 }
123122 if ( returnArray ) {
124123 defaultReturnType = `Array<${ defaultReturnType } >` ;
125124 }
126- if ( prefetch || prefetchInfinite ) {
127- defaultReturnType = `Promise<void> ` ;
125+ if ( returnNullable ) {
126+ defaultReturnType = `( ${ defaultReturnType } ) | null ` ;
128127 }
129128
130129 const returnType = overrideReturnType ?? defaultReturnType ;
130+ return returnType ;
131+ } ;
132+
133+ const capOperation = upperCaseFirst ( operation ) ;
134+
135+ for ( const generateMode of generateModes ) {
136+ const argsType = getArgsType ( ) ;
137+ const inputType = getInputType ( false ) ;
138+
139+ const infinite = generateMode . includes ( 'Infinite' ) ;
140+ const suspense = generateMode . includes ( 'Suspense' ) ;
141+
142+ const optimistic =
143+ supportOptimistic &&
144+ // infinite queries are not subject to optimistic updates
145+ ! infinite ;
146+
147+ const returnType = getReturnType ( optimistic ) ;
131148 const optionsType = makeQueryOptions ( target , 'TQueryFnData' , 'TData' , infinite , suspense , version ) ;
132149
133150 const func = sf . addFunction ( {
@@ -163,6 +180,58 @@ function generateQueryHook(
163180 ) } /${ operation } \`, args, options, fetch);`,
164181 ] ) ;
165182 }
183+
184+ if ( generatePrefetch ) {
185+ const argsType = getArgsType ( ) ;
186+ const inputType = getInputType ( true ) ;
187+ const returnType = getReturnType ( false ) ;
188+
189+ const modes = [
190+ { mode : 'prefetch' , infinite : false } ,
191+ { mode : 'fetch' , infinite : false } ,
192+ ] ;
193+ if ( supportInfinite ) {
194+ modes . push ( { mode : 'prefetch' , infinite : true } , { mode : 'fetch' , infinite : true } ) ;
195+ }
196+
197+ for ( const { mode, infinite } of modes ) {
198+ const optionsType = makePrefetchQueryOptions ( target , 'TQueryFnData' , 'TData' , infinite ) ;
199+
200+ const func = sf . addFunction ( {
201+ name : `${ mode } ${ infinite ? 'Infinite' : '' } ${ capOperation } ${ model } ` ,
202+ typeParameters : overrideTypeParameters ?? [
203+ `TArgs extends ${ argsType } ` ,
204+ `TQueryFnData = ${ returnType } ` ,
205+ 'TData = TQueryFnData' ,
206+ 'TError = DefaultError' ,
207+ ] ,
208+ parameters : [
209+ {
210+ name : 'queryClient' ,
211+ type : 'QueryClient' ,
212+ } ,
213+ {
214+ name : optionalInput ? 'args?' : 'args' ,
215+ type : inputType ,
216+ } ,
217+ {
218+ name : 'options?' ,
219+ type : optionsType ,
220+ } ,
221+ ] ,
222+ isExported : true ,
223+ } ) ;
224+
225+ func . addStatements ( [
226+ makeGetContext ( target ) ,
227+ `return ${ mode } ${
228+ infinite ? 'Infinite' : ''
229+ } ModelQuery<TQueryFnData, TData, TError>(queryClient, '${ model } ', \`\${endpoint}/${ lowerCaseFirst (
230+ model
231+ ) } /${ operation } \`, args, options, fetch);`,
232+ ] ) ;
233+ }
234+ }
166235}
167236
168237function generateMutationHook (
@@ -349,13 +418,15 @@ function generateModelHooks(
349418
350419 sf . addStatements ( '/* eslint-disable */' ) ;
351420
421+ const generatePrefetch = options . generatePrefetch === true ;
422+
352423 const prismaImport = getPrismaClientImportSpec ( outDir , options ) ;
353424 sf . addImportDeclaration ( {
354425 namedImports : [ 'Prisma' , model . name ] ,
355426 isTypeOnly : true ,
356427 moduleSpecifier : prismaImport ,
357428 } ) ;
358- sf . addStatements ( makeBaseImports ( target , version ) ) ;
429+ sf . addStatements ( makeBaseImports ( target , version , generatePrefetch ) ) ;
359430
360431 // Note: delegate models don't support create and upsert operations
361432
@@ -380,13 +451,14 @@ function generateModelHooks(
380451 model . name ,
381452 'findMany' ,
382453 true ,
454+ false ,
383455 true ,
384456 undefined ,
385457 undefined ,
386458 undefined ,
387459 true ,
388460 true ,
389- true
461+ generatePrefetch
390462 ) ;
391463 }
392464
@@ -399,13 +471,14 @@ function generateModelHooks(
399471 model . name ,
400472 'findUnique' ,
401473 false ,
474+ true ,
402475 false ,
403476 undefined ,
404477 undefined ,
405478 undefined ,
406479 false ,
407480 true ,
408- true
481+ generatePrefetch
409482 ) ;
410483 }
411484
@@ -419,12 +492,13 @@ function generateModelHooks(
419492 'findFirst' ,
420493 false ,
421494 true ,
495+ true ,
422496 undefined ,
423497 undefined ,
424498 undefined ,
425499 false ,
426500 true ,
427- true
501+ generatePrefetch
428502 ) ;
429503 }
430504
@@ -469,7 +543,13 @@ function generateModelHooks(
469543 'aggregate' ,
470544 false ,
471545 false ,
472- `Prisma.Get${ modelNameCap } AggregateType<TArgs>`
546+ false ,
547+ `Prisma.Get${ modelNameCap } AggregateType<TArgs>` ,
548+ undefined ,
549+ undefined ,
550+ false ,
551+ false ,
552+ generatePrefetch
473553 ) ;
474554 }
475555
@@ -553,9 +633,13 @@ function generateModelHooks(
553633 'groupBy' ,
554634 false ,
555635 false ,
636+ false ,
556637 returnType ,
557638 `Prisma.SubsetIntersection<TArgs, Prisma.${ useName } GroupByArgs, OrderByArg> & InputErrors` ,
558- typeParameters
639+ typeParameters ,
640+ false ,
641+ false ,
642+ generatePrefetch
559643 ) ;
560644 }
561645
@@ -568,8 +652,14 @@ function generateModelHooks(
568652 model . name ,
569653 'count' ,
570654 false ,
655+ false ,
571656 true ,
572- `TArgs extends { select: any; } ? TArgs['select'] extends true ? number : Prisma.GetScalarType<TArgs['select'], Prisma.${ modelNameCap } CountAggregateOutputType> : number`
657+ `TArgs extends { select: any; } ? TArgs['select'] extends true ? number : Prisma.GetScalarType<TArgs['select'], Prisma.${ modelNameCap } CountAggregateOutputType> : number` ,
658+ undefined ,
659+ undefined ,
660+ false ,
661+ false ,
662+ generatePrefetch
573663 ) ;
574664 }
575665
@@ -616,15 +706,25 @@ function makeGetContext(target: TargetFramework) {
616706 }
617707}
618708
619- function makeBaseImports ( target : TargetFramework , version : TanStackVersion ) {
709+ function makeBaseImports ( target : TargetFramework , version : TanStackVersion , generatePrefetch : boolean ) {
620710 const runtimeImportBase = makeRuntimeImportBase ( version ) ;
621711 const shared = [
622- `import { useModelQuery, useInfiniteModelQuery, useModelMutation, usePrefetchModelQuery, usePrefetchInfiniteModelQuery } from '${ runtimeImportBase } /${ target } ';` ,
712+ `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${ runtimeImportBase } /${ target } ';` ,
623713 `import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '${ runtimeImportBase } ';` ,
624714 `import type { PolicyCrudKind } from '${ RUNTIME_PACKAGE } '` ,
625715 `import metadata from './__model_meta';` ,
626716 `type DefaultError = QueryError;` ,
627717 ] ;
718+
719+ if ( version === 'v5' && generatePrefetch ) {
720+ shared . push (
721+ `import { fetchModelQuery, prefetchModelQuery, fetchInfiniteModelQuery, prefetchInfiniteModelQuery } from '${ runtimeImportBase } /${ target } ';`
722+ ) ;
723+ shared . push (
724+ `import type { QueryClient, FetchQueryOptions, FetchInfiniteQueryOptions } from '@tanstack/${ target } -query';`
725+ ) ;
726+ }
727+
628728 switch ( target ) {
629729 case 'react' : {
630730 const suspense =
@@ -645,7 +745,8 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) {
645745 return [
646746 `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';` ,
647747 `import { getHooksContext } from '${ runtimeImportBase } /${ target } ';` ,
648- `import type { MaybeRefOrGetter, ComputedRef, UnwrapRef } from 'vue';` ,
748+ `import type { MaybeRef, MaybeRefOrGetter, ComputedRef, UnwrapRef } from 'vue';` ,
749+ ...( generatePrefetch ? [ `import { type MaybeRefDeep } from '${ runtimeImportBase } /${ target } ';` ] : [ ] ) ,
649750 ...shared ,
650751 ] ;
651752 }
@@ -665,10 +766,14 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) {
665766 }
666767}
667768
668- function makeQueryArgsType ( target : string , argsType : string ) {
769+ function makeQueryArgsType ( target : string , argsType : string , prefetch : boolean ) {
669770 const type = `Prisma.SelectSubset<TArgs, ${ argsType } >` ;
670771 if ( target === 'vue' ) {
671- return `MaybeRefOrGetter<${ type } > | ComputedRef<${ type } >` ;
772+ if ( prefetch ) {
773+ return `MaybeRef<${ type } >` ;
774+ } else {
775+ return `MaybeRefOrGetter<${ type } > | ComputedRef<${ type } >` ;
776+ }
672777 } else {
673778 return type ;
674779 }
@@ -721,6 +826,35 @@ function makeQueryOptions(
721826 return result ;
722827}
723828
829+ function makePrefetchQueryOptions ( target : string , returnType : string , dataType : string , infinite : boolean ) {
830+ let result = match ( target )
831+ . with ( 'react' , ( ) =>
832+ infinite
833+ ? `Omit<FetchInfiniteQueryOptions<${ returnType } , TError, ${ dataType } >, 'queryKey' | 'initialPageParam'>`
834+ : `Omit<FetchQueryOptions<${ returnType } , TError, ${ dataType } >, 'queryKey'>`
835+ )
836+ . with ( 'vue' , ( ) =>
837+ infinite
838+ ? `MaybeRefDeep<Omit<FetchInfiniteQueryOptions<${ returnType } , TError, ${ dataType } >, 'queryKey' | 'initialPageParam'>>`
839+ : `MaybeRefDeep<Omit<FetchQueryOptions<${ returnType } , TError, ${ dataType } >, 'queryKey'>>`
840+ )
841+ . with ( 'svelte' , ( ) =>
842+ infinite
843+ ? `Omit<FetchInfiniteQueryOptions<${ returnType } , TError, ${ dataType } >, 'queryKey' | 'initialPageParam'>`
844+ : `Omit<FetchQueryOptions<${ returnType } , TError, ${ dataType } >, 'queryKey'>`
845+ )
846+ . otherwise ( ( ) => {
847+ throw new PluginError ( name , `Unsupported target: ${ target } ` ) ;
848+ } ) ;
849+
850+ if ( ! infinite ) {
851+ // non-infinite queries support extra options like optimistic updates
852+ result = `(${ result } & ExtraQueryOptions)` ;
853+ }
854+
855+ return result ;
856+ }
857+
724858function makeMutationOptions ( target : string , returnType : string , argsType : string ) {
725859 let result = match ( target )
726860 . with ( 'react' , ( ) => `UseMutationOptions<${ returnType } , DefaultError, ${ argsType } >` )
0 commit comments