Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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