From 43eb61508fbde4431831343566dd637dff7a6d49 Mon Sep 17 00:00:00 2001 From: Yiming Date: Mon, 12 Feb 2024 21:49:37 +0800 Subject: [PATCH] fix: generate suspense queries in tanstack-query plugin (#996) --- .../plugins/tanstack-query/src/generator.ts | 171 ++++++++++-------- .../tanstack-query/src/runtime-v5/react.ts | 57 ++++++ 2 files changed, 152 insertions(+), 76 deletions(-) diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index bf0c88e0a..10852e826 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -78,68 +78,88 @@ function generateQueryHook( overrideReturnType?: string, overrideInputType?: string, overrideTypeParameters?: string[], - infinite = false, - optimisticUpdate = false + supportInfinite = false, + supportOptimistic = false ) { - const capOperation = upperCaseFirst(operation); - - const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; - const inputType = `Prisma.SelectSubset`; - - let defaultReturnType = `Prisma.${model}GetPayload`; - if (optimisticUpdate) { - defaultReturnType += '& { $optimistic?: boolean }'; + const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite')[] = ['']; + if (supportInfinite) { + generateModes.push('Infinite'); } - if (returnArray) { - defaultReturnType = `Array<${defaultReturnType}>`; + + if (target === 'react' && version === 'v5') { + // react-query v5 supports suspense query + generateModes.push('Suspense'); + if (supportInfinite) { + generateModes.push('SuspenseInfinite'); + } } - const returnType = overrideReturnType ?? defaultReturnType; - const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, version); + for (const generateMode of generateModes) { + const capOperation = upperCaseFirst(operation); - const func = sf.addFunction({ - name: `use${infinite ? 'Infinite' : ''}${capOperation}${model}`, - typeParameters: overrideTypeParameters ?? [ - `TArgs extends ${argsType}`, - `TQueryFnData = ${returnType} `, - 'TData = TQueryFnData', - 'TError = DefaultError', - ], - parameters: [ - { - name: optionalInput ? 'args?' : 'args', - type: inputType, - }, - { - name: 'options?', - type: optionsType, - }, - ...(optimisticUpdate - ? [ - { - name: 'optimisticUpdate', - type: 'boolean', - initializer: 'true', - }, - ] - : []), - ], - isExported: true, - }); + const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; + const inputType = `Prisma.SelectSubset`; - if (version === 'v5' && infinite && ['react', 'svelte'].includes(target)) { - // initialPageParam and getNextPageParam options are required in v5 - func.addStatements([`options = options ?? { initialPageParam: undefined, getNextPageParam: () => null };`]); - } + const infinite = generateMode.includes('Infinite'); + const suspense = generateMode.includes('Suspense'); + const optimistic = + supportOptimistic && + // infinite queries are not subject to optimistic updates + !infinite; + + let defaultReturnType = `Prisma.${model}GetPayload`; + if (optimistic) { + defaultReturnType += '& { $optimistic?: boolean }'; + } + if (returnArray) { + defaultReturnType = `Array<${defaultReturnType}>`; + } + + const returnType = overrideReturnType ?? defaultReturnType; + const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, suspense, version); + + const func = sf.addFunction({ + name: `use${generateMode}${capOperation}${model}`, + typeParameters: overrideTypeParameters ?? [ + `TArgs extends ${argsType}`, + `TQueryFnData = ${returnType} `, + 'TData = TQueryFnData', + 'TError = DefaultError', + ], + parameters: [ + { + name: optionalInput ? 'args?' : 'args', + type: inputType, + }, + { + name: 'options?', + type: optionsType, + }, + ...(optimistic + ? [ + { + name: 'optimisticUpdate', + type: 'boolean', + initializer: 'true', + }, + ] + : []), + ], + isExported: true, + }); + + if (version === 'v5' && infinite && ['react', 'svelte'].includes(target)) { + // initialPageParam and getNextPageParam options are required in v5 + func.addStatements([`options = options ?? { initialPageParam: undefined, getNextPageParam: () => null };`]); + } - func.addStatements([ - makeGetContext(target), - `return ${ - infinite ? 'useInfiniteModelQuery' : 'useModelQuery' - }('${model}', \`\${endpoint}/${lowerCaseFirst( - model - )}/${operation}\`, args, options, fetch${optimisticUpdate ? ', optimisticUpdate' : ''});`, - ]); + func.addStatements([ + makeGetContext(target), + `return use${generateMode}ModelQuery('${model}', \`\${endpoint}/${lowerCaseFirst( + model + )}/${operation}\`, args, options, fetch${optimistic ? ', optimisticUpdate' : ''});`, + ]); + } } function generateMutationHook( @@ -313,23 +333,8 @@ function generateModelHooks( undefined, undefined, undefined, - false, - true - ); - // infinite findMany - generateQueryHook( - target, - version, - sf, - model.name, - 'findMany', - true, true, - undefined, - undefined, - undefined, - true, - false + true ); } @@ -565,19 +570,29 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { `type DefaultError = Error;`, ]; switch (target) { - case 'react': + case 'react': { + const suspense = + version === 'v5' + ? [ + `import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, + `import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query';`, + ] + : []; return [ `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, + ...suspense, ]; - case 'vue': + } + case 'vue': { return [ `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; - case 'svelte': + } + case 'svelte': { return [ `import { derived } from 'svelte/store';`, `import type { MutationOptions, CreateQueryOptions, CreateInfiniteQueryOptions } from '@tanstack/svelte-query';`, @@ -587,6 +602,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { `import { getHooksContext } from '${runtimeImportBase}/${target}';`, ...shared, ]; + } default: throw new PluginError(name, `Unsupported target: ${target}`); } @@ -597,6 +613,7 @@ function makeQueryOptions( returnType: string, dataType: string, infinite: boolean, + suspense: boolean, version: TanStackVersion ) { switch (target) { @@ -604,8 +621,10 @@ function makeQueryOptions( return infinite ? version === 'v4' ? `Omit, 'queryKey'>` - : `Omit>, 'queryKey'>` - : `Omit, 'queryKey'>`; + : `Omit>, 'queryKey'>` + : `Omit, 'queryKey'>`; case 'vue': return `Omit, 'queryKey'>`; case 'svelte': diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index 4871e8229..375cb2676 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -4,10 +4,14 @@ import { useMutation, useQuery, useQueryClient, + useSuspenseInfiniteQuery, + useSuspenseQuery, type InfiniteData, type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, + UseSuspenseInfiniteQueryOptions, + UseSuspenseQueryOptions, } from '@tanstack/react-query-v5'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; @@ -71,6 +75,33 @@ export function useModelQuery( }); } +/** + * Creates a react-query suspense 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 + * @param optimisticUpdate Whether to enable automatic optimistic update + * @returns useSuspenseQuery hook + */ +export function useSuspenseModelQuery( + model: string, + url: string, + args?: unknown, + options?: Omit, 'queryKey'>, + fetch?: FetchFn, + optimisticUpdate = false +) { + const reqUrl = makeUrl(url, args); + return useSuspenseQuery({ + queryKey: getQueryKey(model, url, args, false, optimisticUpdate), + queryFn: () => fetcher(reqUrl, undefined, fetch, false), + ...options, + }); +} + /** * Creates a react-query infinite query. * @@ -97,6 +128,32 @@ export function useInfiniteModelQuery( }); } +/** + * Creates a react-query infinite suspense 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 useSuspenseInfiniteQuery hook + */ +export function useSuspenseInfiniteModelQuery( + model: string, + url: string, + args: unknown, + options: Omit>, 'queryKey'>, + fetch?: FetchFn +) { + return useSuspenseInfiniteQuery({ + queryKey: getQueryKey(model, url, args, true), + queryFn: ({ pageParam }) => { + return fetcher(makeUrl(url, pageParam ?? args), undefined, fetch, false); + }, + ...options, + }); +} + /** * Creates a react-query mutation *