Skip to content

Commit

Permalink
feat: automatic optimistic update for tanstack hooks (zenstackhq#830)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Nov 15, 2023
1 parent 9c75c6c commit 93dc7df
Show file tree
Hide file tree
Showing 23 changed files with 1,295 additions and 128 deletions.
81 changes: 72 additions & 9 deletions packages/plugins/tanstack-query/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,23 @@ function generateQueryHook(
overrideReturnType?: string,
overrideInputType?: string,
overrideTypeParameters?: string[],
infinite = false
infinite = false,
optimisticUpdate = false
) {
const capOperation = upperCaseFirst(operation);

const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType =
overrideReturnType ?? (returnArray ? `Array<Prisma.${model}GetPayload<T>>` : `Prisma.${model}GetPayload<T>`);

let defaultReturnType = `Prisma.${model}GetPayload<T>`;
if (optimisticUpdate) {
defaultReturnType += '& { $optimistic?: boolean }';
}
if (returnArray) {
defaultReturnType = `Array<${defaultReturnType}>`;
}

const returnType = overrideReturnType ?? defaultReturnType;
const optionsType = makeQueryOptions(target, returnType, infinite, version);

const func = sf.addFunction({
Expand All @@ -100,6 +109,15 @@ function generateQueryHook(
name: 'options?',
type: optionsType,
},
...(optimisticUpdate
? [
{
name: 'optimisticUpdate',
type: 'boolean',
initializer: 'true',
},
]
: []),
],
isExported: true,
});
Expand All @@ -113,7 +131,7 @@ function generateQueryHook(
makeGetContext(target),
`return ${infinite ? 'useInfiniteModelQuery' : 'useModelQuery'}('${model}', \`\${endpoint}/${lowerCaseFirst(
model
)}/${operation}\`, args, options, fetch);`,
)}/${operation}\`, args, options, fetch${optimisticUpdate ? ', optimisticUpdate' : ''});`,
]);
}

Expand Down Expand Up @@ -154,6 +172,11 @@ function generateMutationHook(
type: 'boolean',
initializer: 'true',
},
{
name: 'optimisticUpdate',
type: 'boolean',
initializer: 'false',
},
],
});

Expand All @@ -170,7 +193,7 @@ function generateMutationHook(
overrideReturnType ?? model
}, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst(
model
)}/${operation}\`, metadata, options, fetch, invalidateQueries, ${checkReadBack})
)}/${operation}\`, metadata, options, fetch, invalidateQueries, ${checkReadBack}, optimisticUpdate)
`,
},
],
Expand Down Expand Up @@ -272,8 +295,6 @@ function generateModelHooks(
// findMany
if (mapping.findMany) {
// regular findMany
generateQueryHook(target, version, sf, model.name, 'findMany', true, true);
// infinite findMany
generateQueryHook(
target,
version,
Expand All @@ -285,18 +306,60 @@ function generateModelHooks(
undefined,
undefined,
undefined,
false,
true
);
// infinite findMany
generateQueryHook(
target,
version,
sf,
model.name,
'findMany',
true,
true,
undefined,
undefined,
undefined,
true,
false
);
}

// findUnique
if (mapping.findUnique) {
generateQueryHook(target, version, sf, model.name, 'findUnique', false, false);
generateQueryHook(
target,
version,
sf,
model.name,
'findUnique',
false,
false,
undefined,
undefined,
undefined,
false,
true
);
}

// findFirst
if (mapping.findFirst) {
generateQueryHook(target, version, sf, model.name, 'findFirst', false, true);
generateQueryHook(
target,
version,
sf,
model.name,
'findFirst',
false,
true,
undefined,
undefined,
undefined,
false,
true
);
}

// update
Expand Down
47 changes: 40 additions & 7 deletions packages/plugins/tanstack-query/src/runtime-v5/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
makeUrl,
marshal,
setupInvalidation,
setupOptimisticUpdate,
type APIContext,
} from '../runtime/common';

Expand Down Expand Up @@ -50,18 +51,21 @@ export const Provider = RequestHandlerContext.Provider;
* @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 useQuery hook
*/
export function useModelQuery<R>(
model: string,
url: string,
args?: unknown,
options?: Omit<UseQueryOptions<R>, 'queryKey'>,
fetch?: FetchFn
fetch?: FetchFn,
optimisticUpdate = false
) {
const reqUrl = makeUrl(url, args);
return useQuery({
queryKey: getQueryKey(model, url, args),
queryKey: getQueryKey(model, url, args, false, optimisticUpdate),
queryFn: () => fetcher<R, false>(reqUrl, undefined, fetch, false),
...options,
});
Expand All @@ -74,6 +78,7 @@ export function useModelQuery<R>(
* @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 useInfiniteQuery hook
*/
export function useInfiniteModelQuery<R>(
Expand All @@ -84,14 +89,27 @@ export function useInfiniteModelQuery<R>(
fetch?: FetchFn
) {
return useInfiniteQuery({
queryKey: getQueryKey(model, url, args),
queryKey: getQueryKey(model, url, args, true),
queryFn: ({ pageParam }) => {
return fetcher<R, false>(makeUrl(url, pageParam ?? args), undefined, fetch, false);
},
...options,
});
}

/**
* Creates a react-query mutation
*
* @param model The name of the model under mutation.
* @param method The HTTP method.
* @param url The request URL.
* @param modelMeta The model metadata.
* @param options The react-query options.
* @param fetch The fetch function to use for sending the HTTP request
* @param invalidateQueries Whether to invalidate queries after mutation.
* @param checkReadBack Whether to check for read back errors and return undefined if found.
* @param optimisticUpdate Whether to enable automatic optimistic update
*/
export function useModelMutation<T, R = any, C extends boolean = boolean, Result = C extends true ? R | undefined : R>(
model: string,
method: 'POST' | 'PUT' | 'DELETE',
Expand All @@ -100,7 +118,8 @@ export function useModelMutation<T, R = any, C extends boolean = boolean, Result
options?: Omit<UseMutationOptions<Result, unknown, T>, 'mutationFn'>,
fetch?: FetchFn,
invalidateQueries = true,
checkReadBack?: C
checkReadBack?: C,
optimisticUpdate = false
) {
const queryClient = useQueryClient();
const mutationFn = (data: any) => {
Expand All @@ -118,10 +137,11 @@ export function useModelMutation<T, R = any, C extends boolean = boolean, Result
};

const finalOptions = { ...options, mutationFn };
if (invalidateQueries) {
const operation = url.split('/').pop();

if (operation) {
const { logging } = useContext(RequestHandlerContext);
const operation = url.split('/').pop();
if (operation) {
if (invalidateQueries) {
setupInvalidation(
model,
operation,
Expand All @@ -131,6 +151,19 @@ export function useModelMutation<T, R = any, C extends boolean = boolean, Result
logging
);
}

if (optimisticUpdate) {
setupOptimisticUpdate(
model,
operation,
modelMeta,
finalOptions,
queryClient.getQueryCache().getAll(),
(queryKey, data) => queryClient.setQueryData<unknown>(queryKey, data),
invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined,
logging
);
}
}

return useMutation(finalOptions);
Expand Down
33 changes: 26 additions & 7 deletions packages/plugins/tanstack-query/src/runtime-v5/svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
makeUrl,
marshal,
setupInvalidation,
setupOptimisticUpdate,
} from '../runtime/common';

export { APIContext as RequestHandlerContext } from '../runtime/common';
Expand Down Expand Up @@ -53,17 +54,20 @@ export function getHooksContext() {
* @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
* @param optimisticUpdate Whether to enable automatic optimistic update
* @returns useQuery hook
*/
export function useModelQuery<R>(
model: string,
url: string,
args?: unknown,
options?: StoreOrVal<Omit<QueryOptions<R>, 'queryKey'>>,
fetch?: FetchFn
fetch?: FetchFn,
optimisticUpdate = false
) {
const reqUrl = makeUrl(url, args);
const queryKey = getQueryKey(model, url, args);
const queryKey = getQueryKey(model, url, args, false, optimisticUpdate);
const queryFn = () => fetcher<R, false>(reqUrl, undefined, fetch, false);

let mergedOpt: any;
Expand Down Expand Up @@ -103,7 +107,7 @@ export function useInfiniteModelQuery<R>(
options: StoreOrVal<Omit<CreateInfiniteQueryOptions<R, unknown, InfiniteData<R>>, 'queryKey'>>,
fetch?: FetchFn
) {
const queryKey = getQueryKey(model, url, args);
const queryKey = getQueryKey(model, url, args, true);
const queryFn = ({ pageParam }: { pageParam: unknown }) =>
fetcher<R, false>(makeUrl(url, pageParam ?? args), undefined, fetch, false);

Expand Down Expand Up @@ -151,7 +155,8 @@ export function useModelMutation<T, R = any, C extends boolean = boolean, Result
options?: Omit<MutationOptions<Result, unknown, T>, 'mutationFn'>,
fetch?: FetchFn,
invalidateQueries = true,
checkReadBack?: C
checkReadBack?: C,
optimisticUpdate = false
) {
const queryClient = useQueryClient();
const mutationFn = (data: any) => {
Expand All @@ -169,10 +174,11 @@ export function useModelMutation<T, R = any, C extends boolean = boolean, Result
};

const finalOptions = { ...options, mutationFn };
if (invalidateQueries) {
const operation = url.split('/').pop();

if (operation) {
const { logging } = getContext<APIContext>(SvelteQueryContextKey);
const operation = url.split('/').pop();
if (operation) {
if (invalidateQueries) {
setupInvalidation(
model,
operation,
Expand All @@ -182,6 +188,19 @@ export function useModelMutation<T, R = any, C extends boolean = boolean, Result
logging
);
}

if (optimisticUpdate) {
setupOptimisticUpdate(
model,
operation,
modelMeta,
finalOptions,
queryClient.getQueryCache().getAll(),
(queryKey, data) => queryClient.setQueryData<unknown>(queryKey, data),
invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined,
logging
);
}
}

return createMutation(finalOptions);
Expand Down
Loading

0 comments on commit 93dc7df

Please sign in to comment.