Skip to content

Commit

Permalink
fix: generate suspense queries in tanstack-query plugin (#996)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Feb 12, 2024
1 parent 541cd97 commit 43eb615
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 76 deletions.
171 changes: 95 additions & 76 deletions packages/plugins/tanstack-query/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TArgs, ${argsType}>`;

let defaultReturnType = `Prisma.${model}GetPayload<TArgs>`;
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<TArgs, ${argsType}>`;

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<TArgs>`;
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'
}<TQueryFnData, TData, TError>('${model}', \`\${endpoint}/${lowerCaseFirst(
model
)}/${operation}\`, args, options, fetch${optimisticUpdate ? ', optimisticUpdate' : ''});`,
]);
func.addStatements([
makeGetContext(target),
`return use${generateMode}ModelQuery<TQueryFnData, TData, TError>('${model}', \`\${endpoint}/${lowerCaseFirst(
model
)}/${operation}\`, args, options, fetch${optimistic ? ', optimisticUpdate' : ''});`,
]);
}
}

function generateMutationHook(
Expand Down Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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';`,
Expand All @@ -587,6 +602,7 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) {
`import { getHooksContext } from '${runtimeImportBase}/${target}';`,
...shared,
];
}
default:
throw new PluginError(name, `Unsupported target: ${target}`);
}
Expand All @@ -597,15 +613,18 @@ function makeQueryOptions(
returnType: string,
dataType: string,
infinite: boolean,
suspense: boolean,
version: TanStackVersion
) {
switch (target) {
case 'react':
return infinite
? version === 'v4'
? `Omit<UseInfiniteQueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>`
: `Omit<UseInfiniteQueryOptions<${returnType}, TError, InfiniteData<${dataType}>>, 'queryKey'>`
: `Omit<UseQueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>`;
: `Omit<Use${
suspense ? 'Suspense' : ''
}InfiniteQueryOptions<${returnType}, TError, InfiniteData<${dataType}>>, 'queryKey'>`
: `Omit<Use${suspense ? 'Suspense' : ''}QueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>`;
case 'vue':
return `Omit<Use${infinite ? 'Infinite' : ''}QueryOptions<${returnType}, TError, ${dataType}>, 'queryKey'>`;
case 'svelte':
Expand Down
57 changes: 57 additions & 0 deletions packages/plugins/tanstack-query/src/runtime-v5/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,6 +75,33 @@ export function useModelQuery<TQueryFnData, TData, TError>(
});
}

/**
* 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<TQueryFnData, TData, TError>(
model: string,
url: string,
args?: unknown,
options?: Omit<UseSuspenseQueryOptions<TQueryFnData, TError, TData>, 'queryKey'>,
fetch?: FetchFn,
optimisticUpdate = false
) {
const reqUrl = makeUrl(url, args);
return useSuspenseQuery({
queryKey: getQueryKey(model, url, args, false, optimisticUpdate),
queryFn: () => fetcher<TQueryFnData, false>(reqUrl, undefined, fetch, false),
...options,
});
}

/**
* Creates a react-query infinite query.
*
Expand All @@ -97,6 +128,32 @@ export function useInfiniteModelQuery<TQueryFnData, TData, TError>(
});
}

/**
* 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<TQueryFnData, TData, TError>(
model: string,
url: string,
args: unknown,
options: Omit<UseSuspenseInfiniteQueryOptions<TQueryFnData, TError, InfiniteData<TData>>, 'queryKey'>,
fetch?: FetchFn
) {
return useSuspenseInfiniteQuery({
queryKey: getQueryKey(model, url, args, true),
queryFn: ({ pageParam }) => {
return fetcher<TQueryFnData, false>(makeUrl(url, pageParam ?? args), undefined, fetch, false);
},
...options,
});
}

/**
* Creates a react-query mutation
*
Expand Down

0 comments on commit 43eb615

Please sign in to comment.