diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index afb86f9c7..63a1046db 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,14 +84,16 @@ function generateQueryHook( model: string, operation: string, returnArray: boolean, + returnNullable: boolean, optionalInput: boolean, overrideReturnType?: string, overrideInputType?: string, overrideTypeParameters?: string[], supportInfinite = false, - supportOptimistic = false + supportOptimistic = false, + generatePrefetch = false ) { - const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite')[] = ['']; + const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite' | 'Prefetch' | 'PrefetchInfinite')[] = ['']; if (supportInfinite) { generateModes.push('Infinite'); } @@ -91,19 +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 getArgsType = () => { + return overrideInputType ?? `Prisma.${model}${upperCaseFirst(operation)}Args`; + }; - const infinite = generateMode.includes('Infinite'); - const suspense = generateMode.includes('Suspense'); - 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 }'; @@ -111,8 +122,29 @@ function generateQueryHook( if (returnArray) { defaultReturnType = `Array<${defaultReturnType}>`; } + 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({ @@ -148,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([ + `const endpoint = options?.endpoint ?? DEFAULT_QUERY_ENDPOINT;`, + `return ${mode}${ + infinite ? 'Infinite' : '' + }ModelQuery(queryClient, '${model}', \`\${endpoint}/${lowerCaseFirst( + model + )}/${operation}\`, args, options, options?.fetch);`, + ]); + } + } } function generateMutationHook( @@ -334,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 @@ -365,12 +451,14 @@ function generateModelHooks( model.name, 'findMany', true, + false, true, undefined, undefined, undefined, true, - true + true, + generatePrefetch ); } @@ -383,12 +471,14 @@ function generateModelHooks( model.name, 'findUnique', false, + true, false, undefined, undefined, undefined, false, - true + true, + generatePrefetch ); } @@ -402,11 +492,13 @@ function generateModelHooks( 'findFirst', false, true, + true, undefined, undefined, undefined, false, - true + true, + generatePrefetch ); } @@ -451,7 +543,13 @@ function generateModelHooks( 'aggregate', false, false, - `Prisma.Get${modelNameCap}AggregateType` + false, + `Prisma.Get${modelNameCap}AggregateType`, + undefined, + undefined, + false, + false, + generatePrefetch ); } @@ -535,9 +633,13 @@ function generateModelHooks( 'groupBy', false, false, + false, returnType, `Prisma.SubsetIntersection & InputErrors`, - typeParameters + typeParameters, + false, + false, + generatePrefetch ); } @@ -550,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 ); } @@ -598,15 +706,24 @@ 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 } 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;`, ]; + + if (version === 'v5' && generatePrefetch) { + shared.push( + `import { fetchModelQuery, prefetchModelQuery, fetchInfiniteModelQuery, prefetchInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, + `import type { QueryClient, FetchQueryOptions, FetchInfiniteQueryOptions } from '@tanstack/${target}-query';`, + `import type { ExtraPrefetchOptions } from '${runtimeImportBase}';` + ); + } + switch (target) { case 'react': { const suspense = @@ -627,7 +744,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';`, + `import { toValue } from 'vue';`, ...shared, ]; } @@ -647,10 +765,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; } @@ -703,6 +825,18 @@ function makeQueryOptions( return result; } +function makePrefetchQueryOptions(_target: string, returnType: string, dataType: string, infinite: boolean) { + let extraOptions = 'ExtraPrefetchOptions'; + if (!infinite) { + // non-infinite queries support extra options like optimistic updates + extraOptions += ' & ExtraQueryOptions'; + } + + return infinite + ? `Omit, 'queryKey' | 'initialPageParam'> & ${extraOptions}` + : `Omit, 'queryKey'> & ${extraOptions}`; +} + 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/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/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index e8017befa..de912bc35 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -1,17 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - UseSuspenseInfiniteQueryOptions, - UseSuspenseQueryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient, useSuspenseInfiniteQuery, useSuspenseQuery, + 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'; @@ -78,6 +81,62 @@ export function useModelQuery( }); } +/** + * 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 + */ +export function prefetchModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn +) { + return queryClient.prefetchQuery({ + queryKey: getQueryKey(model, url, args, { + infinite: false, + optimisticUpdate: options?.optimisticUpdate !== 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, + }); +} + /** * Creates a react-query suspense query. * @@ -133,6 +192,62 @@ export function useInfiniteModelQuery( }); } +/** + * 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 + */ +export function prefetchInfiniteModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args: unknown, + options?: Omit, 'queryKey' | 'initialPageParam'>, + fetch?: FetchFn +) { + 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); +} + /** * 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..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, @@ -57,7 +60,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 +97,66 @@ export function useModelQuery( return createQuery(mergedOpt); } +/** + * 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 + */ +export function prefetchModelQuery( + 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.prefetchQuery({ + queryKey, + queryFn, + ...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 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, + }); +} + /** * Creates a svelte-query infinite query. * @@ -101,7 +164,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 +207,64 @@ export function useInfiniteModelQuery( return createInfiniteQuery>(mergedOpt); } +/** + * 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 + */ +export function prefetchInfiniteModelQuery( + 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.prefetchInfiniteQuery({ + queryKey, + queryFn, + 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 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 { return typeof (opt as any)?.subscribe === 'function'; } @@ -155,7 +277,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..0f275fd88 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,18 @@ export { APIContext as RequestHandlerContext } from '../runtime/common'; export const VueQueryContextKey = 'zenstack-vue-query-context'; +// #region from "@tanstack/vue-query" +export type MaybeRefDeep = MaybeRef< + T extends Function + ? T + : T extends object + ? { + [Property in keyof T]: MaybeRefDeep; + } + : T +>; +// #endregion + /** * Provide context for the generated TanStack Query hooks. */ @@ -89,6 +105,70 @@ export function useModelQuery( return useQuery(queryOptions); } +/** + * 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 + */ +export function prefetchModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: MaybeRef, + options?: Omit, 'queryKey'> & ExtraQueryOptions, + fetch?: FetchFn +) { + 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, + } as MaybeRefDeep>); +} + +/** + * 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?: Omit, '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, + } as MaybeRefDeep>); +} + /** * Creates a vue-query infinite query. * @@ -127,6 +207,68 @@ export function useInfiniteModelQuery( return useInfiniteQuery>(queryOptions); } +/** + * 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 + */ +export function prefetchInfiniteModelQuery( + queryClient: QueryClient, + model: string, + url: string, + args?: MaybeRef, + options?: Omit, 'queryKey' | 'initialPageParam'>, + fetch?: FetchFn +) { + 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; + return fetcher(makeUrl(url, pageParam ?? _args), undefined, fetch, false); + }, + initialPageParam: argsValue, + ...optValue, + } as MaybeRefDeep>); +} + +/** + * 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?: 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>); +} + /** * Creates a mutation with vue-query. * @@ -137,7 +279,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, 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, 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')], } ); });