diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index cf52cafa..60810ad0 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -67,8 +67,8 @@ }, "devDependencies": { "@tanstack/react-query": "catalog:", - "@tanstack/svelte-query": "5.90.2", "@tanstack/vue-query": "5.90.6", + "@tanstack/svelte-query": "5.90.2", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/react": "catalog:", diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index 05962d87..d9e0ca6d 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -36,6 +36,7 @@ import type { GroupByArgs, GroupByResult, ModelResult, + SelectIncludeOmit, SelectSubset, Subset, UpdateArgs, @@ -56,6 +57,7 @@ import { type ExtraMutationOptions, type ExtraQueryOptions, } from './utils/common'; +import type { TrimDelegateModelOperations } from './utils/types'; export type { FetchFn } from './utils/common'; @@ -119,126 +121,144 @@ export type ModelMutationOptions = Omit = UseMutationResult; -export type SchemaHooks = { - [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; -}; - -export type ModelQueryHooks> = { - useFindUnique>( - args: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useSuspenseFindUnique>( - args: SelectSubset>, - options?: ModelSuspenseQueryOptions | null>, - ): ModelSuspenseQueryResult | null>; - - useFindFirst>( - args?: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useSuspenseFindFirst>( - args?: SelectSubset>, - options?: ModelSuspenseQueryOptions | null>, - ): ModelSuspenseQueryResult | null>; - - useFindMany>( - args?: SelectSubset>, - options?: ModelQueryOptions[]>, - ): ModelQueryResult[]>; - - useSuspenseFindMany>( - args?: SelectSubset>, - options?: ModelSuspenseQueryOptions[]>, - ): ModelSuspenseQueryResult[]>; - - useInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, - ): ModelInfiniteQueryResult[]>>; - - useSuspenseInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelSuspenseInfiniteQueryOptions[]>, - ): ModelSuspenseInfiniteQueryResult[]>>; - - useCreate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useCreateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCreateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpdate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useUpdateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useUpdateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpsert>( +export type ModelMutationModelResult< + Schema extends SchemaDef, + Model extends GetModels, + TArgs extends SelectIncludeOmit, + Array extends boolean = false, +> = Omit, TArgs>, 'mutateAsync'> & { + mutateAsync( + args: T, options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; + ): Promise[] : ModelResult>; +}; - useDelete>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useDeleteMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCount>( - args?: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useSuspenseCount>( - args?: Subset>, - options?: ModelSuspenseQueryOptions>, - ): ModelSuspenseQueryResult>; - - useAggregate>( - args: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useSuspenseAggregate>( - args: Subset>, - options?: ModelSuspenseQueryOptions>, - ): ModelSuspenseQueryResult>; - - useGroupBy>( - args: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useSuspenseGroupBy>( - args: Subset>, - options?: ModelSuspenseQueryOptions>, - ): ModelSuspenseQueryResult>; +export type ClientHooks = { + [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; }; +// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems +// to significantly slow down tsc performance ... +export type ModelQueryHooks> = TrimDelegateModelOperations< + Schema, + Model, + { + useFindUnique>( + args: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useSuspenseFindUnique>( + args: SelectSubset>, + options?: ModelSuspenseQueryOptions | null>, + ): ModelSuspenseQueryResult | null>; + + useFindFirst>( + args?: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useSuspenseFindFirst>( + args?: SelectSubset>, + options?: ModelSuspenseQueryOptions | null>, + ): ModelSuspenseQueryResult | null>; + + useFindMany>( + args?: SelectSubset>, + options?: ModelQueryOptions[]>, + ): ModelQueryResult[]>; + + useSuspenseFindMany>( + args?: SelectSubset>, + options?: ModelSuspenseQueryOptions[]>, + ): ModelSuspenseQueryResult[]>; + + useInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelInfiniteQueryOptions[]>, + ): ModelInfiniteQueryResult[]>>; + + useSuspenseInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelSuspenseInfiniteQueryOptions[]>, + ): ModelSuspenseInfiniteQueryResult[]>>; + + useCreate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useCreateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCreateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpdate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useUpdateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useUpdateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpsert>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDelete>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDeleteMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCount>( + args?: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useSuspenseCount>( + args?: Subset>, + options?: ModelSuspenseQueryOptions>, + ): ModelSuspenseQueryResult>; + + useAggregate>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useSuspenseAggregate>( + args: Subset>, + options?: ModelSuspenseQueryOptions>, + ): ModelSuspenseQueryResult>; + + useGroupBy>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useSuspenseGroupBy>( + args: Subset>, + options?: ModelSuspenseQueryOptions>, + ): ModelSuspenseQueryResult>; + } +>; + /** * Gets data query hooks for all models in the schema. */ -export function useClientQueries(schema: Schema): SchemaHooks { +export function useClientQueries(schema: Schema): ClientHooks { return Object.keys(schema.models).reduce((acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema, model as GetModels); return acc; - }, {} as SchemaHooks); + }, {} as ClientHooks); } /** @@ -289,39 +309,39 @@ export function useModelQueries { - return useInternalMutation(schema, modelName, 'POST', 'create', options, true); + return useInternalMutation(schema, modelName, 'POST', 'create', options); }, useCreateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createMany', options, false); + return useInternalMutation(schema, modelName, 'POST', 'createMany', options); }, useCreateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); }, useUpdate: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'update', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'update', options); }, useUpdateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options, false); + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); }, useUpdateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); }, useUpsert: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'upsert', options, true); + return useInternalMutation(schema, modelName, 'POST', 'upsert', options); }, useDelete: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'delete', options, true); + return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); }, useDeleteMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options, false); + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); }, useCount: (options?: any) => { @@ -367,7 +387,7 @@ export function useInternalQuery( queryKey, ...useQuery({ queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), + queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch), ...options, }), }; @@ -390,7 +410,7 @@ export function useInternalSuspenseQuery( queryKey, ...useSuspenseQuery({ queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), + queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch), ...options, }), }; @@ -401,11 +421,14 @@ export function useInternalInfiniteQuery( model: string, operation: string, args: unknown, - options: Omit< - UseInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' - >, + options: + | Omit< + UseInfiniteQueryOptions>, + 'queryKey' | 'initialPageParam' + > + | undefined, ) { + options = options ?? { getNextPageParam: () => undefined }; const { endpoint, fetch } = useHooksContext(); const queryKey = getQueryKey(model, operation, args, { infinite: true, optimisticUpdate: false }); return { @@ -413,12 +436,7 @@ export function useInternalInfiniteQuery( ...useInfiniteQuery({ queryKey, queryFn: ({ pageParam, signal }) => { - return fetcher( - makeUrl(endpoint, model, operation, pageParam ?? args), - { signal }, - fetch, - false, - ); + return fetcher(makeUrl(endpoint, model, operation, pageParam ?? args), { signal }, fetch); }, initialPageParam: args, ...options, @@ -443,12 +461,7 @@ export function useInternalSuspenseInfiniteQuery( ...useSuspenseInfiniteQuery({ queryKey, queryFn: ({ pageParam, signal }) => { - return fetcher( - makeUrl(endpoint, model, operation, pageParam ?? args), - { signal }, - fetch, - false, - ); + return fetcher(makeUrl(endpoint, model, operation, pageParam ?? args), { signal }, fetch); }, initialPageParam: args, ...options, @@ -467,18 +480,12 @@ export function useInternalSuspenseInfiniteQuery( * @param options The react-query options. * @param checkReadBack Whether to check for read back errors and return undefined if found. */ -export function useInternalMutation< - TArgs, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R, ->( +export function useInternalMutation( schema: SchemaDef, model: string, method: 'POST' | 'PUT' | 'DELETE', operation: string, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - checkReadBack?: C, + options?: Omit, 'mutationFn'> & ExtraMutationOptions, ) { const { endpoint, fetch, logging } = useHooksContext(); const queryClient = useQueryClient(); @@ -494,7 +501,7 @@ export function useInternalMutation< body: marshal(data), }), }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; + return fetcher(reqUrl, fetchInit, fetch) as Promise; }; const finalOptions = { ...options, mutationFn }; diff --git a/packages/clients/tanstack-query/src/svelte.ts b/packages/clients/tanstack-query/src/svelte.ts index b2b5961e..29004aee 100644 --- a/packages/clients/tanstack-query/src/svelte.ts +++ b/packages/clients/tanstack-query/src/svelte.ts @@ -32,6 +32,7 @@ import type { GroupByArgs, GroupByResult, ModelResult, + SelectIncludeOmit, SelectSubset, Subset, UpdateArgs, @@ -53,6 +54,7 @@ import { type ExtraMutationOptions, type ExtraQueryOptions, } from './utils/common'; +import type { TrimDelegateModelOperations } from './utils/types'; export type { FetchFn } from './utils/common'; @@ -107,91 +109,111 @@ export type ModelMutationOptions = Omit = CreateMutationResult; -export type SchemaHooks = { +export type ModelMutationModelResult< + Schema extends SchemaDef, + Model extends GetModels, + TArgs extends SelectIncludeOmit, + Array extends boolean = false, +> = Readable< + Omit, TArgs>>, 'mutateAsync'> & { + mutateAsync( + args: T, + options?: ModelMutationOptions, T>, + ): Promise[] : ModelResult>; + } +>; + +export type ClientHooks = { [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; }; -export type ModelQueryHooks> = { - useFindUnique>( - args: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useFindFirst>( - args?: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useFindMany>( - args?: SelectSubset>, - options?: ModelQueryOptions[]>, - ): ModelQueryResult[]>; - - useInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, - ): ModelInfiniteQueryResult[]>>; - - useCreate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useCreateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCreateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpdate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useUpdateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useUpdateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpsert>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useDelete>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useDeleteMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCount>( - args?: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useAggregate>( - args: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useGroupBy>( - args: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; -}; +// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems +// to significantly slow down tsc performance ... +export type ModelQueryHooks> = TrimDelegateModelOperations< + Schema, + Model, + { + useFindUnique>( + args: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindFirst>( + args?: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindMany>( + args?: SelectSubset>, + options?: ModelQueryOptions[]>, + ): ModelQueryResult[]>; + + useInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelInfiniteQueryOptions[]>, + ): ModelInfiniteQueryResult[]>>; + + useCreate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useCreateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCreateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpdate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useUpdateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useUpdateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpsert>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDelete>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDeleteMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCount>( + args?: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useAggregate>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useGroupBy>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + } +>; /** * Gets data query hooks for all models in the schema. */ -export function useClientQueries(schema: Schema): SchemaHooks { +export function useClientQueries(schema: Schema): ClientHooks { return Object.keys(schema.models).reduce((acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema, model as GetModels); return acc; - }, {} as SchemaHooks); + }, {} as ClientHooks); } /** @@ -226,39 +248,39 @@ export function useModelQueries { - return useInternalMutation(schema, modelName, 'POST', 'create', options, true); + return useInternalMutation(schema, modelName, 'POST', 'create', options); }, useCreateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createMany', options, false); + return useInternalMutation(schema, modelName, 'POST', 'createMany', options); }, useCreateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); }, useUpdate: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'update', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'update', options); }, useUpdateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options, false); + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); }, useUpdateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); }, useUpsert: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'upsert', options, true); + return useInternalMutation(schema, modelName, 'POST', 'upsert', options); }, useDelete: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'delete', options, true); + return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); }, useDeleteMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options, false); + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); }, useCount: (options?: any) => { @@ -291,7 +313,7 @@ export function useInternalQuery( optimisticUpdate: optionsValue?.optimisticUpdate !== false, }); const queryFn: QueryFunction = ({ signal }) => - fetcher(reqUrl, { signal }, fetch, false); + fetcher(reqUrl, { signal }, fetch); let mergedOpt: any; if (isStore(options)) { @@ -324,23 +346,21 @@ export function useInternalInfiniteQuery( model: string, operation: string, args: StoreOrVal, - options: StoreOrVal< - Omit< - CreateInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' - > - >, + options: + | StoreOrVal< + Omit< + CreateInfiniteQueryOptions>, + 'queryKey' | 'initialPageParam' + > + > + | undefined, ) { + options = options ?? { getNextPageParam: () => undefined }; const { endpoint, fetch } = getQuerySettings(); const argsValue = unwrapStore(args); const queryKey = getQueryKey(model, operation, argsValue, { infinite: true, optimisticUpdate: false }); const queryFn: QueryFunction = ({ pageParam, signal }) => - fetcher( - makeUrl(endpoint, model, operation, pageParam ?? argsValue), - { signal }, - fetch, - false, - ); + fetcher(makeUrl(endpoint, model, operation, pageParam ?? argsValue), { signal }, fetch); let mergedOpt: StoreOrVal>>; if (isStore(options)) { @@ -381,18 +401,12 @@ export function useInternalInfiniteQuery( * @param options The svelte-query options. * @param checkReadBack Whether to check for read back errors and return undefined if found. */ -export function useInternalMutation< - TArgs, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R, ->( +export function useInternalMutation( schema: SchemaDef, model: string, method: 'POST' | 'PUT' | 'DELETE', operation: string, - options?: StoreOrVal, 'mutationFn'> & ExtraMutationOptions>, - checkReadBack?: C, + options?: StoreOrVal, 'mutationFn'> & ExtraMutationOptions>, ) { const { endpoint, fetch, logging } = getQuerySettings(); const queryClient = useQueryClient(); @@ -409,10 +423,10 @@ export function useInternalMutation< body: marshal(data), }), }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; + return fetcher(reqUrl, fetchInit, fetch) as Promise; }; - let mergedOpt: StoreOrVal>; + let mergedOpt: StoreOrVal>; if (isStore(options)) { mergedOpt = derived([options], ([$opt]) => ({ diff --git a/packages/clients/tanstack-query/src/utils/common.ts b/packages/clients/tanstack-query/src/utils/common.ts index 87fc8fe6..28710d25 100644 --- a/packages/clients/tanstack-query/src/utils/common.ts +++ b/packages/clients/tanstack-query/src/utils/common.ts @@ -125,21 +125,12 @@ export type APIContext = { logging?: boolean; }; -export async function fetcher( - url: string, - options?: RequestInit, - customFetch?: FetchFn, - checkReadBack?: C, -): Promise { +export async function fetcher(url: string, options?: RequestInit, customFetch?: FetchFn): Promise { const _fetch = customFetch ?? fetch; const res = await _fetch(url, options); if (!res.ok) { const errData = unmarshal(await res.text()); - if ( - checkReadBack !== false && - errData.error?.rejectedByPolicy && - errData.error?.rejectReason === 'cannot-read-back' - ) { + if (errData.error?.rejectedByPolicy && errData.error?.rejectReason === 'cannot-read-back') { // policy doesn't allow mutation result to be read back, just return undefined return undefined as any; } diff --git a/packages/clients/tanstack-query/src/utils/types.ts b/packages/clients/tanstack-query/src/utils/types.ts index 1ebd2d25..3c7fdc1d 100644 --- a/packages/clients/tanstack-query/src/utils/types.ts +++ b/packages/clients/tanstack-query/src/utils/types.ts @@ -1,3 +1,6 @@ +import type { OperationsIneligibleForDelegateModels } from '@zenstackhq/orm'; +import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema'; + export type MaybePromise = T | Promise | PromiseLike; export const ORMWriteActions = [ @@ -17,3 +20,9 @@ export const ORMWriteActions = [ ] as const; export type ORMWriteActionType = (typeof ORMWriteActions)[number]; + +export type TrimDelegateModelOperations< + Schema extends SchemaDef, + Model extends GetModels, + T extends Record, +> = IsDelegateModel extends true ? Omit : T; diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts index 990f1bc4..77d3ab34 100644 --- a/packages/clients/tanstack-query/src/vue.ts +++ b/packages/clients/tanstack-query/src/vue.ts @@ -30,6 +30,7 @@ import type { GroupByArgs, GroupByResult, ModelResult, + SelectIncludeOmit, SelectSubset, Subset, UpdateArgs, @@ -51,6 +52,7 @@ import { type ExtraMutationOptions, type ExtraQueryOptions, } from './utils/common'; +import type { TrimDelegateModelOperations } from './utils/types'; export type { FetchFn } from './utils/common'; export const VueQueryContextKey = 'zenstack-vue-query-context'; @@ -80,107 +82,127 @@ function getQuerySettings() { return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; } -export type ModelQueryOptions = Omit, 'queryKey'> & ExtraQueryOptions; +export type ModelQueryOptions = MaybeRefOrGetter< + Omit>, 'queryKey'> & ExtraQueryOptions +>; export type ModelQueryResult = UseQueryReturnType & { queryKey: QueryKey }; -export type ModelInfiniteQueryOptions = Omit< - UseInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' +export type ModelInfiniteQueryOptions = MaybeRefOrGetter< + Omit>>, 'queryKey' | 'initialPageParam'> >; export type ModelInfiniteQueryResult = UseInfiniteQueryReturnType & { queryKey: QueryKey }; -export type ModelMutationOptions = Omit, 'mutationFn'> & - ExtraMutationOptions; +export type ModelMutationOptions = MaybeRefOrGetter< + Omit>, 'mutationFn'> & ExtraMutationOptions +>; export type ModelMutationResult = UseMutationReturnType; -export type SchemaHooks = { - [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; -}; - -export type ModelQueryHooks> = { - useFindUnique>( - args: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useFindFirst>( - args?: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useFindMany>( - args?: SelectSubset>, - options?: ModelQueryOptions[]>, - ): ModelQueryResult[]>; - - useInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, - ): ModelInfiniteQueryResult[]>>; - - useCreate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useCreateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCreateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpdate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useUpdateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useUpdateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpsert>( +export type ModelMutationModelResult< + Schema extends SchemaDef, + Model extends GetModels, + TArgs extends SelectIncludeOmit, + Array extends boolean = false, +> = Omit, TArgs>, 'mutateAsync'> & { + mutateAsync( + args: T, options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; + ): Promise[] : ModelResult>; +}; - useDelete>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useDeleteMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCount>( - args?: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useAggregate>( - args: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useGroupBy>( - args: Subset>, - options?: ModelQueryOptions>, - ): ModelQueryResult>; +export type ClientHooks = { + [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; }; +// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems +// to significantly slow down tsc performance ... +export type ModelQueryHooks> = TrimDelegateModelOperations< + Schema, + Model, + { + useFindUnique>( + args: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindFirst>( + args?: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindMany>( + args?: SelectSubset>, + options?: ModelQueryOptions[]>, + ): ModelQueryResult[]>; + + useInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelInfiniteQueryOptions[]>, + ): ModelInfiniteQueryResult[]>>; + + useCreate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useCreateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCreateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpdate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useUpdateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useUpdateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpsert>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDelete>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDeleteMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCount>( + args?: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useAggregate>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useGroupBy>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + } +>; + /** * Gets data query hooks for all models in the schema. */ -export function useClientQueries(schema: Schema): SchemaHooks { +export function useClientQueries(schema: Schema): ClientHooks { return Object.keys(schema.models).reduce((acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema, model as GetModels); return acc; - }, {} as SchemaHooks); + }, {} as ClientHooks); } /** @@ -215,39 +237,39 @@ export function useModelQueries { - return useInternalMutation(schema, modelName, 'POST', 'create', options, true); + return useInternalMutation(schema, modelName, 'POST', 'create', options); }, useCreateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createMany', options, false); + return useInternalMutation(schema, modelName, 'POST', 'createMany', options); }, useCreateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); }, useUpdate: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'update', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'update', options); }, useUpdateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options, false); + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); }, useUpdateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); }, useUpsert: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'upsert', options, true); + return useInternalMutation(schema, modelName, 'POST', 'upsert', options); }, useDelete: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'delete', options, true); + return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); }, useDeleteMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options, false); + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); }, useCount: (options?: any) => { @@ -286,7 +308,7 @@ export function useInternalQuery( queryFn: ({ queryKey, signal }: any) => { const [_prefix, _model, _op, args] = queryKey; const reqUrl = makeUrl(endpoint, model, operation, args); - return fetcher(reqUrl, { signal }, fetch, false); + return fetcher(reqUrl, { signal }, fetch); }, ...restOptions, }; @@ -298,13 +320,16 @@ export function useInternalInfiniteQuery( model: string, operation: string, args: MaybeRefOrGetter, - options: MaybeRefOrGetter< - Omit< - UnwrapRef>>, - 'queryKey' | 'initialPageParam' - > - >, + options: + | MaybeRefOrGetter< + Omit< + UnwrapRef>>, + 'queryKey' | 'initialPageParam' + > + > + | undefined, ) { + options = options ?? { getNextPageParam: () => undefined }; const { endpoint, fetch } = getQuerySettings(); const argsValue = toValue(args); const optionsValue = toValue(options); @@ -315,7 +340,7 @@ export function useInternalInfiniteQuery( queryFn: ({ queryKey, signal }: any) => { const [_prefix, _model, _op, args] = queryKey; const reqUrl = makeUrl(endpoint, model, operation, args); - return fetcher(reqUrl, { signal }, fetch, false); + return fetcher(reqUrl, { signal }, fetch); }, initialPageParam: argsValue, ...optionsValue, @@ -337,20 +362,14 @@ export function useInternalInfiniteQuery( * @param options The vue-query options. * @param checkReadBack Whether to check for read back errors and return undefined if found. */ -export function useInternalMutation< - TArgs, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R, ->( +export function useInternalMutation( schema: SchemaDef, model: string, method: 'POST' | 'PUT' | 'DELETE', operation: string, options?: MaybeRefOrGetter< - Omit>, 'mutationFn'> & ExtraMutationOptions + Omit>, 'mutationFn'> & ExtraMutationOptions >, - checkReadBack?: C, ) { const { endpoint, fetch, logging } = getQuerySettings(); const queryClient = useQueryClient(); @@ -366,7 +385,7 @@ export function useInternalMutation< body: marshal(data), }), }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; + return fetcher(reqUrl, fetchInit, fetch) as Promise; }; const optionsValue = toValue(options); diff --git a/packages/clients/tanstack-query/test/react-typing-test.ts b/packages/clients/tanstack-query/test/react-typing-test.ts new file mode 100644 index 00000000..b5814dbd --- /dev/null +++ b/packages/clients/tanstack-query/test/react-typing-test.ts @@ -0,0 +1,103 @@ +import { useClientQueries } from '../src/react'; +import { schema } from './schemas/basic/schema-lite'; + +const client = useClientQueries(schema); + +// @ts-expect-error missing args +client.user.useFindUnique(); + +check(client.user.useFindUnique({ where: { id: '1' } }).data?.email); +check(client.user.useFindUnique({ where: { id: '1' } }).queryKey); +check(client.user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true, enabled: false })); + +// @ts-expect-error unselected field +check(client.user.useFindUnique({ select: { email: true } }).data.name); + +check(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } }).data?.posts[0]?.title); + +check(client.user.useFindFirst().data?.email); + +check(client.user.useFindMany().data?.[0]?.email); + +check(client.user.useInfiniteFindMany().data?.pages[0]?.[0]?.email); +check( + client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: () => ({ id: '2' }), + }, + ).data?.pages[1]?.[0]?.email, +); + +check(client.user.useSuspenseFindMany().data[0]?.email); +check(client.user.useSuspenseInfiniteFindMany().data.pages[0]?.[0]?.email); +check(client.user.useCount().data?.toFixed(2)); +check(client.user.useCount({ select: { email: true } }).data?.email.toFixed(2)); + +check(client.user.useAggregate({ _max: { email: true } }).data?._max.email); + +check(client.user.useGroupBy({ by: ['email'], _max: { name: true } }).data?.[0]?._max.name); + +// @ts-expect-error missing args +client.user.useCreate().mutate(); +client.user.useCreate().mutate({ data: { email: 'test@example.com' } }); +client.user + .useCreate({ optimisticUpdate: true, invalidateQueries: false, retry: 3 }) + .mutate({ data: { email: 'test@example.com' } }); + +client.user + .useCreate() + .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) + .then((d) => check(d.posts[0]?.title)); + +client.user + .useCreateMany() + .mutateAsync({ + data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], + skipDuplicates: true, + }) + .then((d) => d.count); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + }) + .then((d) => check(d[0]?.name)); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + select: { email: true }, + }) + // @ts-expect-error unselected field + .then((d) => check(d[0].name)); + +client.user.useUpdate().mutate( + { data: { email: 'updated@example.com' }, where: { id: '1' } }, + { + onSuccess: (d) => { + check(d.email); + }, + }, +); + +client.user.useUpdateMany().mutate({ data: { email: 'updated@example.com' } }); + +client.user + .useUpdateManyAndReturn() + .mutateAsync({ data: { email: 'updated@example.com' } }) + .then((d) => check(d[0]?.email)); + +client.user + .useUpsert() + .mutate({ where: { id: '1' }, create: { email: 'new@example.com' }, update: { email: 'updated@example.com' } }); + +client.user.useDelete().mutate({ where: { id: '1' }, include: { posts: true } }); + +client.user.useDeleteMany().mutate({ where: { email: 'test@example.com' } }); + +function check(_value: unknown) { + // noop +} diff --git a/packages/clients/tanstack-query/test/svelte-typing-test.ts b/packages/clients/tanstack-query/test/svelte-typing-test.ts new file mode 100644 index 00000000..b39c1566 --- /dev/null +++ b/packages/clients/tanstack-query/test/svelte-typing-test.ts @@ -0,0 +1,100 @@ +import { get } from 'svelte/store'; +import { useClientQueries } from '../src/svelte'; +import { schema } from './schemas/basic/schema-lite'; + +const client = useClientQueries(schema); + +// @ts-expect-error missing args +client.user.useFindUnique(); + +check(get(client.user.useFindUnique({ where: { id: '1' } })).data?.email); +check(get(client.user.useFindUnique({ where: { id: '1' } })).queryKey); +check(get(client.user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true, enabled: false }))); + +// @ts-expect-error unselected field +check(get(client.user.useFindUnique({ select: { email: true } })).data.name); + +check(get(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } })).data?.posts[0]?.title); + +check(get(client.user.useFindFirst()).data?.email); + +check(get(client.user.useFindMany()).data?.[0]?.email); +check(get(client.user.useInfiniteFindMany()).data?.pages[0]?.[0]?.email); +check( + get( + client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: () => ({ id: '2' }), + }, + ), + ).data?.pages[1]?.[0]?.email, +); + +check(get(client.user.useCount()).data?.toFixed(2)); +check(get(client.user.useCount({ select: { email: true } })).data?.email.toFixed(2)); + +check(get(client.user.useAggregate({ _max: { email: true } })).data?._max.email); + +check(get(client.user.useGroupBy({ by: ['email'], _max: { name: true } })).data?.[0]?._max.name); + +// @ts-expect-error missing args +client.user.useCreate().mutate(); +get(client.user.useCreate()).mutate({ data: { email: 'test@example.com' } }); +get(client.user.useCreate({ optimisticUpdate: true, invalidateQueries: false, retry: 3 })).mutate({ + data: { email: 'test@example.com' }, +}); + +get(client.user.useCreate()) + .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) + .then((d) => check(d.posts[0]?.title)); + +get(client.user.useCreateMany()) + .mutateAsync({ + data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], + skipDuplicates: true, + }) + .then((d) => d.count); + +get(client.user.useCreateManyAndReturn()) + .mutateAsync({ + data: [{ email: 'test@example.com' }], + }) + .then((d) => check(d[0]?.name)); + +get(client.user.useCreateManyAndReturn()) + .mutateAsync({ + data: [{ email: 'test@example.com' }], + select: { email: true }, + }) + // @ts-expect-error unselected field + .then((d) => check(d[0].name)); + +get(client.user.useUpdate()).mutate( + { data: { email: 'updated@example.com' }, where: { id: '1' } }, + { + onSuccess: (d) => { + check(d.email); + }, + }, +); + +get(client.user.useUpdateMany()).mutate({ data: { email: 'updated@example.com' } }); + +get(client.user.useUpdateManyAndReturn()) + .mutateAsync({ data: { email: 'updated@example.com' } }) + .then((d) => check(d[0]?.email)); + +get(client.user.useUpsert()).mutate({ + where: { id: '1' }, + create: { email: 'new@example.com' }, + update: { email: 'updated@example.com' }, +}); + +get(client.user.useDelete()).mutate({ where: { id: '1' }, include: { posts: true } }); + +get(client.user.useDeleteMany()).mutate({ where: { email: 'test@example.com' } }); + +function check(_value: unknown) { + // noop +} diff --git a/packages/clients/tanstack-query/test/vue-typing-test.ts b/packages/clients/tanstack-query/test/vue-typing-test.ts new file mode 100644 index 00000000..5d99e318 --- /dev/null +++ b/packages/clients/tanstack-query/test/vue-typing-test.ts @@ -0,0 +1,101 @@ +import { useClientQueries } from '../src/vue'; +import { schema } from './schemas/basic/schema-lite'; + +const client = useClientQueries(schema); + +// @ts-expect-error missing args +client.user.useFindUnique(); + +check(client.user.useFindUnique({ where: { id: '1' } }).data.value?.email); +check(client.user.useFindUnique({ where: { id: '1' } }).queryKey); +check(client.user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true, enabled: false })); + +// @ts-expect-error unselected field +check(client.user.useFindUnique({ select: { email: true } }).data.name); + +check(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } }).data.value?.posts[0]?.title); + +check(client.user.useFindFirst().data.value?.email); + +check(client.user.useFindMany().data.value?.[0]?.email); + +check(client.user.useInfiniteFindMany().data.value?.pages[0]?.[0]?.email); +check( + client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: () => ({ id: '2' }), + }, + ).data.value?.pages[1]?.[0]?.email, +); + +check(client.user.useCount().data.value?.toFixed(2)); +check(client.user.useCount({ select: { email: true } }).data.value?.email.toFixed(2)); + +check(client.user.useAggregate({ _max: { email: true } }).data.value?._max.email); + +check(client.user.useGroupBy({ by: ['email'], _max: { name: true } }).data.value?.[0]?._max.name); + +// @ts-expect-error missing args +client.user.useCreate().mutate(); +client.user.useCreate().mutate({ data: { email: 'test@example.com' } }); +client.user + .useCreate({ optimisticUpdate: true, invalidateQueries: false, retry: 3 }) + .mutate({ data: { email: 'test@example.com' } }); + +client.user + .useCreate() + .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) + .then((d) => check(d.posts[0]?.title)); + +client.user + .useCreateMany() + .mutateAsync({ + data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], + skipDuplicates: true, + }) + .then((d) => d.count); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + }) + .then((d) => check(d[0]?.name)); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + select: { email: true }, + }) + // @ts-expect-error unselected field + .then((d) => check(d[0].name)); + +client.user.useUpdate().mutate( + { data: { email: 'updated@example.com' }, where: { id: '1' } }, + { + onSuccess: (d) => { + check(d.email); + }, + }, +); + +client.user.useUpdateMany().mutate({ data: { email: 'updated@example.com' } }); + +client.user + .useUpdateManyAndReturn() + .mutateAsync({ data: { email: 'updated@example.com' } }) + .then((d) => check(d[0]?.email)); + +client.user + .useUpsert() + .mutate({ where: { id: '1' }, create: { email: 'new@example.com' }, update: { email: 'updated@example.com' } }); + +client.user.useDelete().mutate({ where: { id: '1' }, include: { posts: true } }); + +client.user.useDeleteMany().mutate({ where: { email: 'test@example.com' } }); + +function check(_value: unknown) { + // noop +} diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index 81d7c016..8e30795a 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -795,10 +795,12 @@ export type AllModelOperations>>; }; +export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert'; + export type ModelOperations> = Omit< AllModelOperations, // exclude operations not applicable to delegate models - IsDelegateModel extends true ? 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' : never + IsDelegateModel extends true ? OperationsIneligibleForDelegateModels : never >; //#endregion