From f2e50c2fea8b0a4babf6f9abfda18b4dc4ff1ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Frison?= Date: Wed, 18 Sep 2024 18:12:08 +0200 Subject: [PATCH 1/4] feat: create prefetch functions --- .../plugins/tanstack-query/src/generator.ts | 32 ++++- .../tanstack-query/src/runtime-v5/react.ts | 57 +++++++++ .../tanstack-query/src/runtime-v5/svelte.ts | 114 +++++++++++++++++- .../tanstack-query/src/runtime-v5/vue.ts | 88 +++++++++++++- 4 files changed, 284 insertions(+), 7 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index afb86f9c7..efe7a4305 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -76,9 +76,10 @@ function generateQueryHook( overrideInputType?: string, overrideTypeParameters?: string[], supportInfinite = false, - supportOptimistic = false + supportOptimistic = false, + supportPrefetching = false, ) { - const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite')[] = ['']; + const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite' | 'Prefetch' | 'PrefetchInfinite')[] = ['']; if (supportInfinite) { generateModes.push('Infinite'); } @@ -89,6 +90,22 @@ function generateQueryHook( if (supportInfinite) { generateModes.push('SuspenseInfinite'); } + + if (supportPrefetching) { + generateModes.push('Prefetch'); + + if (supportInfinite) { + generateModes.push('PrefetchInfinite'); + } + } + } + + if (target === 'svelte' && supportPrefetching) { + generateModes.push('Prefetch'); + + if (supportInfinite) { + generateModes.push('PrefetchInfinite'); + } } for (const generateMode of generateModes) { @@ -99,6 +116,9 @@ function generateQueryHook( const infinite = generateMode.includes('Infinite'); const suspense = generateMode.includes('Suspense'); + const prefetch = generateMode.includes('Prefetch'); + const prefetchInfinite = generateMode.includes('PrefetchInfinite'); + const optimistic = supportOptimistic && // infinite queries are not subject to optimistic updates @@ -111,6 +131,9 @@ function generateQueryHook( if (returnArray) { defaultReturnType = `Array<${defaultReturnType}>`; } + if (prefetch || prefetchInfinite) { + defaultReturnType = `Promise`; + } const returnType = overrideReturnType ?? defaultReturnType; const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, suspense, version); @@ -370,6 +393,7 @@ function generateModelHooks( undefined, undefined, true, + true, true ); } @@ -388,6 +412,7 @@ function generateModelHooks( undefined, undefined, false, + true, true ); } @@ -406,6 +431,7 @@ function generateModelHooks( undefined, undefined, false, + true, true ); } @@ -601,7 +627,7 @@ function makeGetContext(target: TargetFramework) { function makeBaseImports(target: TargetFramework, version: TanStackVersion) { const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ - `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, + `import { useModelQuery, useInfiniteModelQuery, useModelMutation, usePrefetchModelQuery, usePrefetchInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, `import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '${runtimeImportBase}';`, `import type { PolicyCrudKind } from '${RUNTIME_PACKAGE}'`, `import metadata from './__model_meta';`, diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index e8017befa..3ff876f11 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -8,6 +8,8 @@ import { useQueryClient, useSuspenseInfiniteQuery, useSuspenseQuery, + usePrefetchQuery, + usePrefetchInfiniteQuery, type InfiniteData, type UseInfiniteQueryOptions, type UseMutationOptions, @@ -78,6 +80,34 @@ export function useModelQuery( }); } +/** + * Creates a react-query prefetch query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The request args object, URL-encoded and appended as "?q=" parameter + * @param options The react-query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns usePrefetchQuery hook + */ +export function usePrefetchModelQuery( + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn +) { + const reqUrl = makeUrl(url, args); + return usePrefetchQuery({ + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }), + queryFn: () => fetcher(reqUrl, undefined, fetch, false), + ...options, + }); +} + /** * Creates a react-query suspense query. * @@ -133,6 +163,33 @@ export function useInfiniteModelQuery( }); } +/** + * Creates a react-query prefetch infinite query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The react-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns usePrefetchInfiniteQuery hook + */ +export function usePrefetchInfiniteModelQuery( + model: string, + url: string, + args: unknown, + options: Omit>, 'queryKey' | 'initialPageParam'>, + fetch?: FetchFn +) { + return usePrefetchInfiniteQuery({ + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), + queryFn: ({ pageParam }) => { + return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + }, + initialPageParam: args, + ...options, + }); +} + /** * Creates a react-query infinite suspense query. * diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts index 2c554aec9..053abcf58 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts @@ -57,7 +57,7 @@ export function getHooksContext() { * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The svelte-query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook + * @returns createQuery hook */ export function useModelQuery( model: string, @@ -94,6 +94,56 @@ export function useModelQuery( return createQuery(mergedOpt); } +/** + * Creates a svelte-query prefetch query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The request args object, URL-encoded and appended as "?q=" parameter + * @param options The svelte-query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns createPrefetchQuery hook + */ +export function usePrefetchModelQuery( + model: string, + url: string, + args?: unknown, + options?: StoreOrVal, 'queryKey'>> & ExtraQueryOptions, + fetch?: FetchFn +) { + const reqUrl = makeUrl(url, args); + const queryKey = getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }); + const queryFn = () => fetcher(reqUrl, undefined, fetch, false); + + let mergedOpt: any; + if (isStore(options)) { + // options is store + mergedOpt = derived([options], ([$opt]) => { + return { + queryKey, + queryFn, + ...($opt as object), + }; + }); + } else { + // options is value + mergedOpt = { + queryKey, + queryFn, + ...options, + }; + } + + // Todo : When createPrefetchQuery is available in svelte-query, use it + const queryClient = useQueryClient(); + + return queryClient.prefetchQuery(mergedOpt); + // return createPrefetchQuery(mergedOpt); +} + /** * Creates a svelte-query infinite query. * @@ -101,7 +151,8 @@ export function useModelQuery( * @param url The request URL. * @param args The initial request args object, URL-encoded and appended as "?q=" parameter * @param options The svelte-query infinite query options object - * @returns useQuery hook + * @param fetch The fetch function to use for sending the HTTP request + * @returns createInfiniteQuery hook */ export function useInfiniteModelQuery( model: string, @@ -143,6 +194,61 @@ export function useInfiniteModelQuery( return createInfiniteQuery>(mergedOpt); } +/** + * Creates a svelte-query prefetch infinite query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The svelte-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns createPrefetchInfiniteQuery hook + */ +export function usePrefetchInfiniteModelQuery( + model: string, + url: string, + args: unknown, + options: StoreOrVal< + Omit>, 'queryKey' | 'initialPageParam'> + >, + fetch?: FetchFn +) { + const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); + const queryFn = ({ pageParam }: { pageParam: unknown }) => + fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + + let mergedOpt: StoreOrVal; + if ( + isStore< + Omit>, 'queryKey' | 'initialPageParam'> + >(options) + ) { + // options is store + mergedOpt = derived([options], ([$opt]) => { + return { + queryKey, + queryFn, + initialPageParam: args, + ...$opt, + }; + }); + } else { + // options is value + mergedOpt = { + queryKey, + queryFn, + initialPageParam: args, + ...options, + }; + } + + // Todo : When createPrefetchInfiniteQuery is available in svelte-query, use it + const queryClient = useQueryClient(); + + return queryClient.prefetchInfiniteQuery(mergedOpt); + // return createPrefetchInfiniteQuery>(mergedOpt); +} + function isStore(opt: unknown): opt is Readable { return typeof (opt as any)?.subscribe === 'function'; } @@ -155,7 +261,9 @@ function isStore(opt: unknown): opt is Readable { * @param modelMeta The model metadata. * @param url The request URL. * @param options The svelte-query options. - * @returns useMutation hooks + * @param fetch The fetch function to use for sending the HTTP request + * @param checkReadBack Whether to check for read back errors and return undefined if found. + * @returns createMutation hook */ export function useModelMutation< TArgs, diff --git a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts index f62fd78c9..7ff391725 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts @@ -89,6 +89,50 @@ export function useModelQuery( return useQuery(queryOptions); } +/** + * Creates a vue-query prefetch query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The request args object, URL-encoded and appended as "?q=" parameter + * @param options The vue-query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns useQuery hook + */ +export function usePrefetchModelQuery( + model: string, + url: string, + args?: MaybeRefOrGetter | ComputedRef, + options?: + | MaybeRefOrGetter, 'queryKey'> & ExtraQueryOptions> + | ComputedRef, 'queryKey'> & ExtraQueryOptions>, + fetch?: FetchFn +) { + const queryOptions: any = computed(() => { + const optionsValue = toValue< + (Omit, 'queryKey'> & ExtraQueryOptions) | undefined + >(options); + return { + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: optionsValue?.optimisticUpdate !== false, + }), + queryFn: ({ queryKey }: { queryKey: QueryKey }) => { + const [_prefix, _model, _op, args] = queryKey; + const reqUrl = makeUrl(url, toValue(args)); + return fetcher(reqUrl, undefined, fetch, false); + }, + ...optionsValue, + }; + }); + + // Todo : When usePrefetchQuery is available in vue-query, use it + const queryClient = useQueryClient(); + + return queryClient.prefetchQuery(queryOptions); + // return usePrefetchQuery(queryOptions); +} + /** * Creates a vue-query infinite query. * @@ -127,6 +171,48 @@ export function useInfiniteModelQuery( return useInfiniteQuery>(queryOptions); } +/** + * Creates a vue-query prefetch infinite query. + * + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The vue-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + * @returns usePrefetchInfiniteQuery hook + */ +export function usePrefetchInfiniteModelQuery( + model: string, + url: string, + args?: MaybeRefOrGetter | ComputedRef, + options?: + | MaybeRefOrGetter< + Omit>, 'queryKey' | 'initialPageParam'> + > + | ComputedRef< + Omit>, 'queryKey' | 'initialPageParam'> + >, + fetch?: FetchFn +) { + // CHECKME: vue-query's `useInfiniteQuery`'s input typing seems wrong + const queryOptions: any = computed(() => ({ + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), + queryFn: ({ queryKey, pageParam }: { queryKey: QueryKey; pageParam?: unknown }) => { + const [_prefix, _model, _op, args] = queryKey; + const reqUrl = makeUrl(url, pageParam ?? toValue(args)); + return fetcher(reqUrl, undefined, fetch, false); + }, + initialPageParam: toValue(args), + ...toValue(options) + })); + + // Todo : When usePrefetchInfiniteQuery is available in vue-query, use it + const queryClient = useQueryClient(); + + return queryClient.prefetchQuery(queryOptions); + // return usePrefetchInfiniteQuery>(queryOptions); +} + /** * Creates a mutation with vue-query. * @@ -137,7 +223,7 @@ export function useInfiniteModelQuery( * @param options The vue-query options. * @param fetch The fetch function to use for sending the HTTP request * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @returns useMutation hooks + * @returns useMutation hook */ export function useModelMutation< TArgs, From f4cbaaf0f3a0f131f481e815af6f9b97459522b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Frison?= Date: Wed, 18 Sep 2024 18:23:18 +0200 Subject: [PATCH 2/4] fix: support all three frameworks --- .../plugins/tanstack-query/src/generator.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index efe7a4305..ea121ba3c 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -84,27 +84,19 @@ function generateQueryHook( generateModes.push('Infinite'); } - if (target === 'react' && version === 'v5') { - // react-query v5 supports suspense query - generateModes.push('Suspense'); - if (supportInfinite) { - generateModes.push('SuspenseInfinite'); - } - - if (supportPrefetching) { - generateModes.push('Prefetch'); + if (supportPrefetching) { + generateModes.push('Prefetch'); - if (supportInfinite) { - generateModes.push('PrefetchInfinite'); - } + if (supportInfinite) { + generateModes.push('PrefetchInfinite'); } } - if (target === 'svelte' && supportPrefetching) { - generateModes.push('Prefetch'); - + if (target === 'react' && version === 'v5') { + // react-query v5 supports suspense query + generateModes.push('Suspense'); if (supportInfinite) { - generateModes.push('PrefetchInfinite'); + generateModes.push('SuspenseInfinite'); } } From 44567cce9c47c271d88080106d719ad446f586fa Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:23:44 -0700 Subject: [PATCH 3/4] feat(tanstack-query): generate fetch and prefetch functions --- .../plugins/tanstack-query/src/generator.ts | 208 ++++++++++++++---- .../tanstack-query/src/runtime-v5/react.ts | 92 ++++++-- .../tanstack-query/src/runtime-v5/svelte.ts | 150 +++++++------ .../tanstack-query/src/runtime-v5/vue.ts | 165 +++++++++----- .../tanstack-query/tests/plugin.test.ts | 34 ++- 5 files changed, 473 insertions(+), 176 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index ea121ba3c..fc09b0833 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -25,6 +25,8 @@ const supportedTargets = ['react', 'vue', 'svelte']; type TargetFramework = (typeof supportedTargets)[number]; type TanStackVersion = 'v4' | 'v5'; +// TODO: turn it into a class to simplify parameter passing + export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { const project = createProject(); const warnings: string[] = []; @@ -40,6 +42,17 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. throw new PluginError(name, `Unsupported version "${version}": use "v4" or "v5"`); } + if (options.generatePrefetch !== undefined && typeof options.generatePrefetch !== 'boolean') { + throw new PluginError( + name, + `Invalid "generatePrefetch" option: expected boolean, got ${options.generatePrefetch}` + ); + } + + if (options.generatePrefetch === true && version === 'v4') { + throw new PluginError(name, `"generatePrefetch" is not supported for version "v4"`); + } + let outDir = requireOption(options, 'output', name); outDir = resolvePath(outDir, options); ensureEmptyDir(outDir); @@ -71,27 +84,20 @@ function generateQueryHook( model: string, operation: string, returnArray: boolean, + returnNullable: boolean, optionalInput: boolean, overrideReturnType?: string, overrideInputType?: string, overrideTypeParameters?: string[], supportInfinite = false, supportOptimistic = false, - supportPrefetching = false, + generatePrefetch = false ) { const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite' | 'Prefetch' | 'PrefetchInfinite')[] = ['']; if (supportInfinite) { generateModes.push('Infinite'); } - if (supportPrefetching) { - generateModes.push('Prefetch'); - - if (supportInfinite) { - generateModes.push('PrefetchInfinite'); - } - } - if (target === 'react' && version === 'v5') { // react-query v5 supports suspense query generateModes.push('Suspense'); @@ -100,22 +106,15 @@ function generateQueryHook( } } - for (const generateMode of generateModes) { - const capOperation = upperCaseFirst(operation); - - const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; - const inputType = makeQueryArgsType(target, argsType); - - const infinite = generateMode.includes('Infinite'); - const suspense = generateMode.includes('Suspense'); - const prefetch = generateMode.includes('Prefetch'); - const prefetchInfinite = generateMode.includes('PrefetchInfinite'); + const getArgsType = () => { + return overrideInputType ?? `Prisma.${model}${upperCaseFirst(operation)}Args`; + }; - const optimistic = - supportOptimistic && - // infinite queries are not subject to optimistic updates - !infinite; + const getInputType = (prefetch: boolean) => { + return makeQueryArgsType(target, getArgsType(), prefetch); + }; + const getReturnType = (optimistic: boolean) => { let defaultReturnType = `Prisma.${model}GetPayload`; if (optimistic) { defaultReturnType += '& { $optimistic?: boolean }'; @@ -123,11 +122,29 @@ function generateQueryHook( if (returnArray) { defaultReturnType = `Array<${defaultReturnType}>`; } - if (prefetch || prefetchInfinite) { - defaultReturnType = `Promise`; + if (returnNullable) { + defaultReturnType = `(${defaultReturnType}) | null`; } const returnType = overrideReturnType ?? defaultReturnType; + return returnType; + }; + + const capOperation = upperCaseFirst(operation); + + for (const generateMode of generateModes) { + const argsType = getArgsType(); + const inputType = getInputType(false); + + const infinite = generateMode.includes('Infinite'); + const suspense = generateMode.includes('Suspense'); + + const optimistic = + supportOptimistic && + // infinite queries are not subject to optimistic updates + !infinite; + + const returnType = getReturnType(optimistic); const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, suspense, version); const func = sf.addFunction({ @@ -163,6 +180,58 @@ function generateQueryHook( )}/${operation}\`, args, options, fetch);`, ]); } + + if (generatePrefetch) { + const argsType = getArgsType(); + const inputType = getInputType(true); + const returnType = getReturnType(false); + + const modes = [ + { mode: 'prefetch', infinite: false }, + { mode: 'fetch', infinite: false }, + ]; + if (supportInfinite) { + modes.push({ mode: 'prefetch', infinite: true }, { mode: 'fetch', infinite: true }); + } + + for (const { mode, infinite } of modes) { + const optionsType = makePrefetchQueryOptions(target, 'TQueryFnData', 'TData', infinite); + + const func = sf.addFunction({ + name: `${mode}${infinite ? 'Infinite' : ''}${capOperation}${model}`, + typeParameters: overrideTypeParameters ?? [ + `TArgs extends ${argsType}`, + `TQueryFnData = ${returnType} `, + 'TData = TQueryFnData', + 'TError = DefaultError', + ], + parameters: [ + { + name: 'queryClient', + type: 'QueryClient', + }, + { + name: optionalInput ? 'args?' : 'args', + type: inputType, + }, + { + name: 'options?', + type: optionsType, + }, + ], + isExported: true, + }); + + func.addStatements([ + makeGetContext(target), + `return ${mode}${ + infinite ? 'Infinite' : '' + }ModelQuery(queryClient, '${model}', \`\${endpoint}/${lowerCaseFirst( + model + )}/${operation}\`, args, options, fetch);`, + ]); + } + } } function generateMutationHook( @@ -349,13 +418,15 @@ function generateModelHooks( sf.addStatements('/* eslint-disable */'); + const generatePrefetch = options.generatePrefetch === true; + const prismaImport = getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma', model.name], isTypeOnly: true, moduleSpecifier: prismaImport, }); - sf.addStatements(makeBaseImports(target, version)); + sf.addStatements(makeBaseImports(target, version, generatePrefetch)); // Note: delegate models don't support create and upsert operations @@ -380,13 +451,14 @@ function generateModelHooks( model.name, 'findMany', true, + false, true, undefined, undefined, undefined, true, true, - true + generatePrefetch ); } @@ -399,13 +471,14 @@ function generateModelHooks( model.name, 'findUnique', false, + true, false, undefined, undefined, undefined, false, true, - true + generatePrefetch ); } @@ -419,12 +492,13 @@ function generateModelHooks( 'findFirst', false, true, + true, undefined, undefined, undefined, false, true, - true + generatePrefetch ); } @@ -469,7 +543,13 @@ function generateModelHooks( 'aggregate', false, false, - `Prisma.Get${modelNameCap}AggregateType` + false, + `Prisma.Get${modelNameCap}AggregateType`, + undefined, + undefined, + false, + false, + generatePrefetch ); } @@ -553,9 +633,13 @@ function generateModelHooks( 'groupBy', false, false, + false, returnType, `Prisma.SubsetIntersection & InputErrors`, - typeParameters + typeParameters, + false, + false, + generatePrefetch ); } @@ -568,8 +652,14 @@ function generateModelHooks( model.name, 'count', false, + false, true, - `TArgs extends { select: any; } ? TArgs['select'] extends true ? number : Prisma.GetScalarType : number` + `TArgs extends { select: any; } ? TArgs['select'] extends true ? number : Prisma.GetScalarType : number`, + undefined, + undefined, + false, + false, + generatePrefetch ); } @@ -616,15 +706,25 @@ function makeGetContext(target: TargetFramework) { } } -function makeBaseImports(target: TargetFramework, version: TanStackVersion) { +function makeBaseImports(target: TargetFramework, version: TanStackVersion, generatePrefetch: boolean) { const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ - `import { useModelQuery, useInfiniteModelQuery, useModelMutation, usePrefetchModelQuery, usePrefetchInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, + `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, `import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '${runtimeImportBase}';`, `import type { PolicyCrudKind } from '${RUNTIME_PACKAGE}'`, `import metadata from './__model_meta';`, `type DefaultError = QueryError;`, ]; + + if (version === 'v5' && generatePrefetch) { + shared.push( + `import { fetchModelQuery, prefetchModelQuery, fetchInfiniteModelQuery, prefetchInfiniteModelQuery } from '${runtimeImportBase}/${target}';` + ); + shared.push( + `import type { QueryClient, FetchQueryOptions, FetchInfiniteQueryOptions } from '@tanstack/${target}-query';` + ); + } + switch (target) { case 'react': { const suspense = @@ -645,7 +745,8 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { return [ `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, - `import type { MaybeRefOrGetter, ComputedRef, UnwrapRef } from 'vue';`, + `import type { MaybeRef, MaybeRefOrGetter, ComputedRef, UnwrapRef } from 'vue';`, + ...(generatePrefetch ? [`import { type MaybeRefDeep } from '${runtimeImportBase}/${target}';`] : []), ...shared, ]; } @@ -665,10 +766,14 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { } } -function makeQueryArgsType(target: string, argsType: string) { +function makeQueryArgsType(target: string, argsType: string, prefetch: boolean) { const type = `Prisma.SelectSubset`; if (target === 'vue') { - return `MaybeRefOrGetter<${type}> | ComputedRef<${type}>`; + if (prefetch) { + return `MaybeRef<${type}>`; + } else { + return `MaybeRefOrGetter<${type}> | ComputedRef<${type}>`; + } } else { return type; } @@ -721,6 +826,35 @@ function makeQueryOptions( return result; } +function makePrefetchQueryOptions(target: string, returnType: string, dataType: string, infinite: boolean) { + let result = match(target) + .with('react', () => + infinite + ? `Omit, 'queryKey' | 'initialPageParam'>` + : `Omit, 'queryKey'>` + ) + .with('vue', () => + infinite + ? `MaybeRefDeep, 'queryKey' | 'initialPageParam'>>` + : `MaybeRefDeep, 'queryKey'>>` + ) + .with('svelte', () => + infinite + ? `Omit, 'queryKey' | 'initialPageParam'>` + : `Omit, 'queryKey'>` + ) + .otherwise(() => { + throw new PluginError(name, `Unsupported target: ${target}`); + }); + + if (!infinite) { + // non-infinite queries support extra options like optimistic updates + result = `(${result} & ExtraQueryOptions)`; + } + + return result; +} + function makeMutationOptions(target: string, returnType: string, argsType: string) { let result = match(target) .with('react', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`) diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index 3ff876f11..de912bc35 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -1,19 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - UseSuspenseInfiniteQueryOptions, - UseSuspenseQueryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient, useSuspenseInfiniteQuery, useSuspenseQuery, - usePrefetchQuery, - usePrefetchInfiniteQuery, + type FetchInfiniteQueryOptions, + type FetchQueryOptions, type InfiniteData, + type QueryClient, type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, + type UseSuspenseInfiniteQueryOptions, + type UseSuspenseQueryOptions, } from '@tanstack/react-query-v5'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; @@ -81,29 +82,57 @@ export function useModelQuery( } /** - * Creates a react-query prefetch query. + * Prefetches a query. * + * @param queryClient The query client instance. * @param model The name of the model under query. * @param url The request URL. * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The react-query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns usePrefetchQuery hook */ -export function usePrefetchModelQuery( +export function prefetchModelQuery( + queryClient: QueryClient, model: string, url: string, args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, + options?: Omit, 'queryKey'> & ExtraQueryOptions, fetch?: FetchFn ) { - const reqUrl = makeUrl(url, args); - return usePrefetchQuery({ + return queryClient.prefetchQuery({ queryKey: getQueryKey(model, url, args, { infinite: false, optimisticUpdate: options?.optimisticUpdate !== false, }), - queryFn: () => fetcher(reqUrl, undefined, fetch, false), + queryFn: () => fetcher(makeUrl(url, args), undefined, fetch, false), + ...options, + }); +} + +/** + * Fetches a query. + * + * @param queryClient The query client instance. + * @param model The name of the model under query. + * @param url The request URL. + * @param args The request args object, URL-encoded and appended as "?q=" parameter + * @param options The react-query options object + * @param fetch The fetch function to use for sending the HTTP request + */ +export function fetchModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn +) { + return queryClient.fetchQuery({ + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }), + queryFn: () => fetcher(makeUrl(url, args), undefined, fetch, false), ...options, }); } @@ -164,30 +193,59 @@ export function useInfiniteModelQuery( } /** - * Creates a react-query prefetch infinite query. + * Prefetches an infinite query. * + * @param queryClient The query client instance. * @param model The name of the model under query. * @param url The request URL. * @param args The initial request args object, URL-encoded and appended as "?q=" parameter * @param options The react-query infinite query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns usePrefetchInfiniteQuery hook */ -export function usePrefetchInfiniteModelQuery( +export function prefetchInfiniteModelQuery( + queryClient: QueryClient, model: string, url: string, args: unknown, - options: Omit>, 'queryKey' | 'initialPageParam'>, + options?: Omit, 'queryKey' | 'initialPageParam'>, fetch?: FetchFn ) { - return usePrefetchInfiniteQuery({ + return queryClient.prefetchInfiniteQuery({ queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), queryFn: ({ pageParam }) => { return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); }, initialPageParam: args, ...options, - }); + } as FetchInfiniteQueryOptions); +} + +/** + * Fetches an infinite query. + * + * @param queryClient The query client instance. + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The react-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + */ +export function fetchInfiniteModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args: unknown, + options?: Omit, 'queryKey' | 'initialPageParam'>, + fetch?: FetchFn +) { + return queryClient.fetchInfiniteQuery({ + queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), + queryFn: ({ pageParam }) => { + return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + }, + initialPageParam: args, + ...options, + } as FetchInfiniteQueryOptions); } /** diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts index 053abcf58..e72d4668d 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts @@ -6,13 +6,16 @@ import { useQueryClient, type CreateInfiniteQueryOptions, type CreateQueryOptions, + type FetchInfiniteQueryOptions, + type FetchQueryOptions, type InfiniteData, type MutationOptions, + type QueryClient, type StoreOrVal, } from '@tanstack/svelte-query-v5'; import { ModelMeta } from '@zenstackhq/runtime/cross'; import { getContext, setContext } from 'svelte'; -import { Readable, derived } from 'svelte/store'; +import { derived, Readable } from 'svelte/store'; import { APIContext, DEFAULT_QUERY_ENDPOINT, @@ -95,53 +98,63 @@ export function useModelQuery( } /** - * Creates a svelte-query prefetch query. + * Prefetches a query. * + * @param queryClient The query client instance. * @param model The name of the model under query. * @param url The request URL. * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The svelte-query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns createPrefetchQuery hook */ -export function usePrefetchModelQuery( +export function prefetchModelQuery( + queryClient: QueryClient, model: string, url: string, args?: unknown, - options?: StoreOrVal, 'queryKey'>> & ExtraQueryOptions, + options?: Omit, 'queryKey'> & ExtraQueryOptions, fetch?: FetchFn ) { - const reqUrl = makeUrl(url, args); const queryKey = getQueryKey(model, url, args, { infinite: false, optimisticUpdate: options?.optimisticUpdate !== false, }); - const queryFn = () => fetcher(reqUrl, undefined, fetch, false); - - let mergedOpt: any; - if (isStore(options)) { - // options is store - mergedOpt = derived([options], ([$opt]) => { - return { - queryKey, - queryFn, - ...($opt as object), - }; - }); - } else { - // options is value - mergedOpt = { - queryKey, - queryFn, - ...options, - }; - } - - // Todo : When createPrefetchQuery is available in svelte-query, use it - const queryClient = useQueryClient(); + const queryFn = () => fetcher(makeUrl(url, args), undefined, fetch, false); + return queryClient.prefetchQuery({ + queryKey, + queryFn, + ...options, + }); +} - return queryClient.prefetchQuery(mergedOpt); - // return createPrefetchQuery(mergedOpt); +/** + * Fetches a query. + * + * @param queryClient The query client instance. + * @param model The name of the model under query. + * @param url The request URL. + * @param args The request args object, URL-encoded and appended as "?q=" parameter + * @param options The svelte-query options object + * @param fetch The fetch function to use for sending the HTTP request + */ +export function fetchModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn +) { + const queryKey = getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== false, + }); + const queryFn = () => fetcher(makeUrl(url, args), undefined, fetch, false); + return queryClient.fetchQuery({ + queryKey, + queryFn, + ...options, + }); } /** @@ -195,58 +208,61 @@ export function useInfiniteModelQuery( } /** - * Creates a svelte-query prefetch infinite query. + * Prefetches an infinite query. * + * @param queryClient The query client instance. * @param model The name of the model under query. * @param url The request URL. * @param args The initial request args object, URL-encoded and appended as "?q=" parameter * @param options The svelte-query infinite query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns createPrefetchInfiniteQuery hook */ -export function usePrefetchInfiniteModelQuery( +export function prefetchInfiniteModelQuery( + queryClient: QueryClient, model: string, url: string, args: unknown, - options: StoreOrVal< - Omit>, 'queryKey' | 'initialPageParam'> - >, + options?: Omit, 'queryKey' | 'initialPageParam'>, fetch?: FetchFn ) { const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); const queryFn = ({ pageParam }: { pageParam: unknown }) => fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + return queryClient.prefetchInfiniteQuery({ + queryKey, + queryFn, + initialPageParam: args, + ...options, + } as FetchInfiniteQueryOptions); +} - let mergedOpt: StoreOrVal; - if ( - isStore< - Omit>, 'queryKey' | 'initialPageParam'> - >(options) - ) { - // options is store - mergedOpt = derived([options], ([$opt]) => { - return { - queryKey, - queryFn, - initialPageParam: args, - ...$opt, - }; - }); - } else { - // options is value - mergedOpt = { - queryKey, - queryFn, - initialPageParam: args, - ...options, - }; - } - - // Todo : When createPrefetchInfiniteQuery is available in svelte-query, use it - const queryClient = useQueryClient(); - - return queryClient.prefetchInfiniteQuery(mergedOpt); - // return createPrefetchInfiniteQuery>(mergedOpt); +/** + * Fetches an infinite query. + * + * @param queryClient The query client instance. + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The svelte-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + */ +export function fetchInfiniteModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args: unknown, + options?: Omit, 'queryKey' | 'initialPageParam'>, + fetch?: FetchFn +) { + const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); + const queryFn = ({ pageParam }: { pageParam: unknown }) => + fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + return queryClient.fetchInfiniteQuery({ + queryKey, + queryFn, + initialPageParam: args, + ...options, + } as FetchInfiniteQueryOptions); } function isStore(opt: unknown): opt is Readable { diff --git a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts index 7ff391725..f250cb09c 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts @@ -5,14 +5,18 @@ import { useMutation, useQuery, useQueryClient, + type FetchInfiniteQueryOptions, + type FetchQueryOptions, type InfiniteData, + type QueryClient, type QueryKey, type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, } from '@tanstack/vue-query'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { computed, inject, provide, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue'; +import { computed, inject, provide, toValue, type ComputedRef, type MaybeRef, type MaybeRefOrGetter } from 'vue'; + import { APIContext, DEFAULT_QUERY_ENDPOINT, @@ -31,6 +35,17 @@ export { APIContext as RequestHandlerContext } from '../runtime/common'; export const VueQueryContextKey = 'zenstack-vue-query-context'; +// from "@tanstack/vue-query" +export type MaybeRefDeep = MaybeRef< + T extends Function + ? T + : T extends object + ? { + [Property in keyof T]: MaybeRefDeep; + } + : T +>; + /** * Provide context for the generated TanStack Query hooks. */ @@ -90,47 +105,67 @@ export function useModelQuery( } /** - * Creates a vue-query prefetch query. + * Prefetches a query. * + * @param queryClient The query client instance. * @param model The name of the model under query. * @param url The request URL. * @param args The request args object, URL-encoded and appended as "?q=" parameter * @param options The vue-query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook */ -export function usePrefetchModelQuery( +export function prefetchModelQuery( + queryClient: QueryClient, model: string, url: string, - args?: MaybeRefOrGetter | ComputedRef, - options?: - | MaybeRefOrGetter, 'queryKey'> & ExtraQueryOptions> - | ComputedRef, 'queryKey'> & ExtraQueryOptions>, + args?: MaybeRef, + options?: MaybeRefDeep, 'queryKey'> & ExtraQueryOptions>, fetch?: FetchFn ) { - const queryOptions: any = computed(() => { - const optionsValue = toValue< - (Omit, 'queryKey'> & ExtraQueryOptions) | undefined - >(options); - return { - queryKey: getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: optionsValue?.optimisticUpdate !== false, - }), - queryFn: ({ queryKey }: { queryKey: QueryKey }) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(url, toValue(args)); - return fetcher(reqUrl, undefined, fetch, false); - }, - ...optionsValue, - }; + const optValue = toValue(options); + return queryClient.prefetchQuery({ + queryKey: getQueryKey(model, url, toValue(args), { + infinite: false, + optimisticUpdate: optValue?.optimisticUpdate !== false, + }), + queryFn: ({ queryKey }: { queryKey: QueryKey }) => { + const [_prefix, _model, _op, _args] = queryKey; + return fetcher(makeUrl(url, _args), undefined, fetch, false); + }, + ...optValue, }); +} - // Todo : When usePrefetchQuery is available in vue-query, use it - const queryClient = useQueryClient(); - - return queryClient.prefetchQuery(queryOptions); - // return usePrefetchQuery(queryOptions); +/** + * Fetches a query. + * + * @param queryClient The query client instance. + * @param model The name of the model under query. + * @param url The request URL. + * @param args The request args object, URL-encoded and appended as "?q=" parameter + * @param options The vue-query options object + * @param fetch The fetch function to use for sending the HTTP request + */ +export function fetchModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: MaybeRef, + options?: MaybeRefDeep, 'queryKey'> & ExtraQueryOptions>, + fetch?: FetchFn +) { + const optValue = toValue(options); + return queryClient.fetchQuery({ + queryKey: getQueryKey(model, url, toValue(args), { + infinite: false, + optimisticUpdate: optValue?.optimisticUpdate !== false, + }), + queryFn: ({ queryKey }: { queryKey: QueryKey }) => { + const [_prefix, _model, _op, _args] = queryKey; + return fetcher(makeUrl(url, _args), undefined, fetch, false); + }, + ...optValue, + }); } /** @@ -172,45 +207,69 @@ export function useInfiniteModelQuery( } /** - * Creates a vue-query prefetch infinite query. + * Prefetches an infinite query. * + * @param queryClient The query client instance. * @param model The name of the model under query. * @param url The request URL. * @param args The initial request args object, URL-encoded and appended as "?q=" parameter * @param options The vue-query infinite query options object * @param fetch The fetch function to use for sending the HTTP request - * @returns usePrefetchInfiniteQuery hook */ -export function usePrefetchInfiniteModelQuery( +export function prefetchInfiniteModelQuery( + queryClient: QueryClient, model: string, url: string, - args?: MaybeRefOrGetter | ComputedRef, - options?: - | MaybeRefOrGetter< - Omit>, 'queryKey' | 'initialPageParam'> - > - | ComputedRef< - Omit>, 'queryKey' | 'initialPageParam'> + args?: MaybeRef, + options?: MaybeRefDeep< + Omit, 'queryKey' | 'initialPageParam'> >, fetch?: FetchFn ) { - // CHECKME: vue-query's `useInfiniteQuery`'s input typing seems wrong - const queryOptions: any = computed(() => ({ - queryKey: getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }), + const optValue = toValue(options); + const argsValue = toValue(args); + return queryClient.prefetchInfiniteQuery({ + queryKey: getQueryKey(model, url, argsValue, { infinite: true, optimisticUpdate: false }), queryFn: ({ queryKey, pageParam }: { queryKey: QueryKey; pageParam?: unknown }) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(url, pageParam ?? toValue(args)); - return fetcher(reqUrl, undefined, fetch, false); + const [_prefix, _model, _op, _args] = queryKey; + return fetcher(makeUrl(url, pageParam ?? _args), undefined, fetch, false); }, - initialPageParam: toValue(args), - ...toValue(options) - })); - - // Todo : When usePrefetchInfiniteQuery is available in vue-query, use it - const queryClient = useQueryClient(); + initialPageParam: argsValue, + ...optValue, + } as MaybeRefDeep>); +} - return queryClient.prefetchQuery(queryOptions); - // return usePrefetchInfiniteQuery>(queryOptions); +/** + * Fetches an infinite query. + * + * @param queryClient The query client instance. + * @param model The name of the model under query. + * @param url The request URL. + * @param args The initial request args object, URL-encoded and appended as "?q=" parameter + * @param options The vue-query infinite query options object + * @param fetch The fetch function to use for sending the HTTP request + */ +export function fetchInfiniteModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: MaybeRef, + options?: MaybeRefDeep< + Omit, 'queryKey' | 'initialPageParam'> + >, + fetch?: FetchFn +) { + const optValue = toValue(options); + const argsValue = toValue(args); + return queryClient.fetchInfiniteQuery({ + queryKey: getQueryKey(model, url, argsValue, { infinite: true, optimisticUpdate: false }), + queryFn: ({ queryKey, pageParam }: { queryKey: QueryKey; pageParam?: unknown }) => { + const [_prefix, _model, _op, _args] = queryKey; + return fetcher(makeUrl(url, pageParam ?? _args), undefined, fetch, false); + }, + initialPageParam: argsValue, + ...optValue, + } as MaybeRefDeep>); } /** diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts index 7fc7a18b3..63ee55c7e 100644 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ b/packages/plugins/tanstack-query/tests/plugin.test.ts @@ -76,6 +76,32 @@ model Foo { `, }; + const makePrefetchSource = (target: string) => { + return { + name: 'prefetch.ts', + content: ` + import { QueryClient } from '@tanstack/${target}-query'; + import { prefetchFindUniquepost_Item, fetchFindUniquepost_Item, prefetchInfiniteFindManypost_Item, fetchInfiniteFindManypost_Item } from './hooks'; + + async function prefetch() { + const queryClient = new QueryClient(); + await prefetchFindUniquepost_Item(queryClient, { where: { id: '1' } }); + const r1 = await fetchFindUniquepost_Item(queryClient, { where: { id: '1' }, include: { author: true } }); + console.log(r1?.author?.email); + + await prefetchInfiniteFindManypost_Item(queryClient, { + where: { published: true }, + }); + const r2 = await fetchInfiniteFindManypost_Item(queryClient, { + where: { published: true }, + include: { author: true }, + }); + console.log(r2.pages[0][0].author?.email); + } + `, + }; + }; + it('react-query run plugin v4', async () => { await loadSchema( ` @@ -106,6 +132,7 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'react' + generatePrefetch = true } ${sharedModel} @@ -132,6 +159,7 @@ ${sharedModel} } `, }, + makePrefetchSource('react'), ], } ); @@ -195,6 +223,7 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'vue' + generatePrefetch = true } ${sharedModel} @@ -205,7 +234,7 @@ ${sharedModel} extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@latest'], copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, - extraSourceFiles: [vueAppSource], + extraSourceFiles: [vueAppSource, makePrefetchSource('vue')], } ); }); @@ -269,6 +298,7 @@ plugin tanstack { provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' output = '$projectRoot/hooks' target = 'svelte' + generatePrefetch = true } ${sharedModel} @@ -279,7 +309,7 @@ ${sharedModel} extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@^5.0.0'], copyDependencies: [path.resolve(__dirname, '../dist')], compile: true, - extraSourceFiles: [svelteAppSource], + extraSourceFiles: [svelteAppSource, makePrefetchSource('svelte')], } ); }); From c374642df6ae185f0446786f1024eea6b75801a9 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:57:14 -0700 Subject: [PATCH 4/4] avoid accessing context during prefetch --- .../plugins/tanstack-query/src/generator.ts | 44 ++++++------------- .../tanstack-query/src/runtime-v5/index.ts | 2 + .../tanstack-query/src/runtime-v5/vue.ts | 19 ++++---- .../tanstack-query/src/runtime/common.ts | 5 +++ .../tanstack-query/src/runtime/index.ts | 2 + 5 files changed, 30 insertions(+), 42 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index fc09b0833..63a1046db 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -223,12 +223,12 @@ function generateQueryHook( }); func.addStatements([ - makeGetContext(target), + `const endpoint = options?.endpoint ?? DEFAULT_QUERY_ENDPOINT;`, `return ${mode}${ infinite ? 'Infinite' : '' }ModelQuery(queryClient, '${model}', \`\${endpoint}/${lowerCaseFirst( model - )}/${operation}\`, args, options, fetch);`, + )}/${operation}\`, args, options, options?.fetch);`, ]); } } @@ -710,7 +710,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion, gene const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, - `import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '${runtimeImportBase}';`, + `import { type PickEnumerable, type CheckSelect, type QueryError, type ExtraQueryOptions, type ExtraMutationOptions, DEFAULT_QUERY_ENDPOINT } from '${runtimeImportBase}';`, `import type { PolicyCrudKind } from '${RUNTIME_PACKAGE}'`, `import metadata from './__model_meta';`, `type DefaultError = QueryError;`, @@ -718,10 +718,9 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion, gene if (version === 'v5' && generatePrefetch) { shared.push( - `import { fetchModelQuery, prefetchModelQuery, fetchInfiniteModelQuery, prefetchInfiniteModelQuery } from '${runtimeImportBase}/${target}';` - ); - shared.push( - `import type { QueryClient, FetchQueryOptions, FetchInfiniteQueryOptions } from '@tanstack/${target}-query';` + `import { fetchModelQuery, prefetchModelQuery, fetchInfiniteModelQuery, prefetchInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, + `import type { QueryClient, FetchQueryOptions, FetchInfiniteQueryOptions } from '@tanstack/${target}-query';`, + `import type { ExtraPrefetchOptions } from '${runtimeImportBase}';` ); } @@ -746,7 +745,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion, gene `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, `import type { MaybeRef, MaybeRefOrGetter, ComputedRef, UnwrapRef } from 'vue';`, - ...(generatePrefetch ? [`import { type MaybeRefDeep } from '${runtimeImportBase}/${target}';`] : []), + `import { toValue } from 'vue';`, ...shared, ]; } @@ -826,33 +825,16 @@ function makeQueryOptions( return result; } -function makePrefetchQueryOptions(target: string, returnType: string, dataType: string, infinite: boolean) { - let result = match(target) - .with('react', () => - infinite - ? `Omit, 'queryKey' | 'initialPageParam'>` - : `Omit, 'queryKey'>` - ) - .with('vue', () => - infinite - ? `MaybeRefDeep, 'queryKey' | 'initialPageParam'>>` - : `MaybeRefDeep, 'queryKey'>>` - ) - .with('svelte', () => - infinite - ? `Omit, 'queryKey' | 'initialPageParam'>` - : `Omit, 'queryKey'>` - ) - .otherwise(() => { - throw new PluginError(name, `Unsupported target: ${target}`); - }); - +function makePrefetchQueryOptions(_target: string, returnType: string, dataType: string, infinite: boolean) { + let extraOptions = 'ExtraPrefetchOptions'; if (!infinite) { // non-infinite queries support extra options like optimistic updates - result = `(${result} & ExtraQueryOptions)`; + extraOptions += ' & ExtraQueryOptions'; } - return result; + return infinite + ? `Omit, 'queryKey' | 'initialPageParam'> & ${extraOptions}` + : `Omit, 'queryKey'> & ${extraOptions}`; } function makeMutationOptions(target: string, returnType: string, argsType: string) { diff --git a/packages/plugins/tanstack-query/src/runtime-v5/index.ts b/packages/plugins/tanstack-query/src/runtime-v5/index.ts index ee494ca7d..deb6167a7 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/index.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/index.ts @@ -1,6 +1,8 @@ export { + DEFAULT_QUERY_ENDPOINT, getQueryKey, type ExtraMutationOptions, + type ExtraPrefetchOptions, type ExtraQueryOptions, type FetchFn, type QueryError, diff --git a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts index f250cb09c..0f275fd88 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts @@ -35,7 +35,7 @@ export { APIContext as RequestHandlerContext } from '../runtime/common'; export const VueQueryContextKey = 'zenstack-vue-query-context'; -// from "@tanstack/vue-query" +// #region from "@tanstack/vue-query" export type MaybeRefDeep = MaybeRef< T extends Function ? T @@ -45,6 +45,7 @@ export type MaybeRefDeep = MaybeRef< } : T >; +// #endregion /** * Provide context for the generated TanStack Query hooks. @@ -119,7 +120,7 @@ export function prefetchModelQuery( model: string, url: string, args?: MaybeRef, - options?: MaybeRefDeep, 'queryKey'> & ExtraQueryOptions>, + options?: Omit, 'queryKey'> & ExtraQueryOptions, fetch?: FetchFn ) { const optValue = toValue(options); @@ -133,7 +134,7 @@ export function prefetchModelQuery( return fetcher(makeUrl(url, _args), undefined, fetch, false); }, ...optValue, - }); + } as MaybeRefDeep>); } /** @@ -151,7 +152,7 @@ export function fetchModelQuery( model: string, url: string, args?: MaybeRef, - options?: MaybeRefDeep, 'queryKey'> & ExtraQueryOptions>, + options?: Omit, 'queryKey'> & ExtraQueryOptions, fetch?: FetchFn ) { const optValue = toValue(options); @@ -165,7 +166,7 @@ export function fetchModelQuery( return fetcher(makeUrl(url, _args), undefined, fetch, false); }, ...optValue, - }); + } as MaybeRefDeep>); } /** @@ -221,9 +222,7 @@ export function prefetchInfiniteModelQuery( model: string, url: string, args?: MaybeRef, - options?: MaybeRefDeep< - Omit, 'queryKey' | 'initialPageParam'> - >, + options?: Omit, 'queryKey' | 'initialPageParam'>, fetch?: FetchFn ) { const optValue = toValue(options); @@ -254,9 +253,7 @@ export function fetchInfiniteModelQuery( model: string, url: string, args?: MaybeRef, - options?: MaybeRefDeep< - Omit, 'queryKey' | 'initialPageParam'> - >, + options?: Omit, 'queryKey' | 'initialPageParam'>, fetch?: FetchFn ) { const optValue = toValue(options); diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts index 2d6793c8a..c9005ee69 100644 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ b/packages/plugins/tanstack-query/src/runtime/common.ts @@ -110,6 +110,11 @@ export type ExtraQueryOptions = { optimisticUpdate?: boolean; }; +/** + * Extra prefetch options. + */ +export type ExtraPrefetchOptions = Pick; + /** * Context type for configuring the hooks. */ diff --git a/packages/plugins/tanstack-query/src/runtime/index.ts b/packages/plugins/tanstack-query/src/runtime/index.ts index 085fd5bf3..7e41db34e 100644 --- a/packages/plugins/tanstack-query/src/runtime/index.ts +++ b/packages/plugins/tanstack-query/src/runtime/index.ts @@ -1,6 +1,8 @@ export { + DEFAULT_QUERY_ENDPOINT, getQueryKey, type ExtraMutationOptions, + type ExtraPrefetchOptions, type ExtraQueryOptions, type FetchFn, type QueryError,