-
-
Notifications
You must be signed in to change notification settings - Fork 17
feat: rpc custom api methods #549
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -55,6 +55,7 @@ import { getQueryKey } from './common/query-key'; | |||||||||
| import type { | ||||||||||
| ExtraMutationOptions, | ||||||||||
| ExtraQueryOptions, | ||||||||||
| CustomOperationDefinition, | ||||||||||
| QueryContext, | ||||||||||
| TrimDelegateModelOperations, | ||||||||||
| WithOptimistic, | ||||||||||
|
|
@@ -131,8 +132,32 @@ export type ModelMutationModelResult< | |||||||||
| ): Promise<SimplifiedResult<Schema, Model, T, Options, false, Array>>; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export type ClientHooks<Schema extends SchemaDef, Options extends QueryOptions<Schema> = QueryOptions<Schema>> = { | ||||||||||
| [Model in GetModels<Schema> as `${Uncapitalize<Model>}`]: ModelQueryHooks<Schema, Model, Options>; | ||||||||||
| type CustomOperationHooks<CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}> = { | ||||||||||
| [K in keyof CustomOperations as `use${Capitalize<string & K>}`]: CustomOperations[K] extends CustomOperationDefinition< | ||||||||||
| infer TArgs, | ||||||||||
| infer TResult | ||||||||||
| > | ||||||||||
| ? CustomOperations[K]['kind'] extends 'mutation' | ||||||||||
| ? (options?: ModelMutationOptions<TResult, TArgs>) => ModelMutationResult<TResult, TArgs> | ||||||||||
| : CustomOperations[K]['kind'] extends 'query' | ||||||||||
| ? (args?: TArgs, options?: ModelQueryOptions<TResult>) => ModelQueryResult<TResult> | ||||||||||
| : CustomOperations[K]['kind'] extends 'suspenseQuery' | ||||||||||
| ? (args?: TArgs, options?: ModelSuspenseQueryOptions<TResult>) => ModelSuspenseQueryResult<TResult> | ||||||||||
| : CustomOperations[K]['kind'] extends 'infiniteQuery' | ||||||||||
| ? (args?: TArgs, options?: ModelInfiniteQueryOptions<TResult>) => ModelInfiniteQueryResult< | ||||||||||
| InfiniteData<TResult> | ||||||||||
| > | ||||||||||
| : (args?: TArgs, options?: ModelSuspenseInfiniteQueryOptions<TResult>) => | ||||||||||
| ModelSuspenseInfiniteQueryResult<InfiniteData<TResult>> | ||||||||||
| : never; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export type ClientHooks< | ||||||||||
| Schema extends SchemaDef, | ||||||||||
| Options extends QueryOptions<Schema> = QueryOptions<Schema>, | ||||||||||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||||||||||
| > = { | ||||||||||
| [Model in GetModels<Schema> as `${Uncapitalize<Model>}`]: ModelQueryHooks<Schema, Model, Options, CustomOperations>; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| // Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems | ||||||||||
|
|
@@ -141,6 +166,7 @@ export type ModelQueryHooks< | |||||||||
| Schema extends SchemaDef, | ||||||||||
| Model extends GetModels<Schema>, | ||||||||||
| Options extends QueryOptions<Schema> = QueryOptions<Schema>, | ||||||||||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||||||||||
| > = TrimDelegateModelOperations< | ||||||||||
| Schema, | ||||||||||
| Model, | ||||||||||
|
|
@@ -250,7 +276,7 @@ export type ModelQueryHooks< | |||||||||
| args: Subset<T, GroupByArgs<Schema, Model>>, | ||||||||||
| options?: ModelSuspenseQueryOptions<GroupByResult<Schema, Model, T>>, | ||||||||||
| ): ModelSuspenseQueryResult<GroupByResult<Schema, Model, T>>; | ||||||||||
| } | ||||||||||
| } & CustomOperationHooks<CustomOperations> | ||||||||||
| >; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
|
|
@@ -259,20 +285,27 @@ export type ModelQueryHooks< | |||||||||
| * @param schema The schema. | ||||||||||
| * @param options Options for all queries originated from this hook. | ||||||||||
| */ | ||||||||||
| export function useClientQueries<Schema extends SchemaDef, Options extends QueryOptions<Schema> = QueryOptions<Schema>>( | ||||||||||
| schema: Schema, | ||||||||||
| options?: QueryContext, | ||||||||||
| ): ClientHooks<Schema, Options> { | ||||||||||
| export function useClientQueries< | ||||||||||
| Schema extends SchemaDef, | ||||||||||
| Options extends QueryOptions<Schema> = QueryOptions<Schema>, | ||||||||||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||||||||||
| >(schema: Schema, options?: QueryContext, customOperations?: CustomOperations): ClientHooks<Schema, Options, CustomOperations> { | ||||||||||
| return Object.keys(schema.models).reduce( | ||||||||||
| (acc, model) => { | ||||||||||
| (acc as any)[lowerCaseFirst(model)] = useModelQueries<Schema, GetModels<Schema>, Options>( | ||||||||||
| (acc as any)[lowerCaseFirst(model)] = useModelQueries< | ||||||||||
| Schema, | ||||||||||
| GetModels<Schema>, | ||||||||||
| Options, | ||||||||||
| CustomOperations | ||||||||||
| >( | ||||||||||
| schema, | ||||||||||
| model as GetModels<Schema>, | ||||||||||
| options, | ||||||||||
| customOperations, | ||||||||||
| ); | ||||||||||
| return acc; | ||||||||||
| }, | ||||||||||
| {} as ClientHooks<Schema, Options>, | ||||||||||
| {} as ClientHooks<Schema, Options, CustomOperations>, | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -283,15 +316,21 @@ export function useModelQueries< | |||||||||
| Schema extends SchemaDef, | ||||||||||
| Model extends GetModels<Schema>, | ||||||||||
| Options extends QueryOptions<Schema>, | ||||||||||
| >(schema: Schema, model: Model, rootOptions?: QueryContext): ModelQueryHooks<Schema, Model, Options> { | ||||||||||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||||||||||
| >( | ||||||||||
| schema: Schema, | ||||||||||
| model: Model, | ||||||||||
| rootOptions?: QueryContext, | ||||||||||
| customOperations?: CustomOperations, | ||||||||||
| ): ModelQueryHooks<Schema, Model, Options, CustomOperations> { | ||||||||||
| const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); | ||||||||||
| if (!modelDef) { | ||||||||||
| throw new Error(`Model "${model}" not found in schema`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const modelName = modelDef.name; | ||||||||||
|
|
||||||||||
| return { | ||||||||||
| const builtInHooks = { | ||||||||||
| useFindUnique: (args: any, options?: any) => { | ||||||||||
| return useInternalQuery(schema, modelName, 'findUnique', args, { ...rootOptions, ...options }); | ||||||||||
| }, | ||||||||||
|
|
@@ -390,6 +429,78 @@ export function useModelQueries< | |||||||||
| return useInternalSuspenseQuery(schema, modelName, 'groupBy', args, { ...rootOptions, ...options }); | ||||||||||
| }, | ||||||||||
| } as ModelQueryHooks<Schema, Model, Options>; | ||||||||||
|
|
||||||||||
| const customHooks = createCustomOperationHooks(schema, modelName, rootOptions, customOperations); | ||||||||||
|
|
||||||||||
| return { ...builtInHooks, ...customHooks } as ModelQueryHooks<Schema, Model, Options, CustomOperations>; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function createCustomOperationHooks< | ||||||||||
| Schema extends SchemaDef, | ||||||||||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||||||||||
| >( | ||||||||||
| schema: Schema, | ||||||||||
| modelName: string, | ||||||||||
| rootOptions: QueryContext | undefined, | ||||||||||
| customOperations?: CustomOperations, | ||||||||||
| ) { | ||||||||||
| if (!customOperations) { | ||||||||||
| return {} as CustomOperationHooks<CustomOperations>; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const hooks: Record<string, unknown> = {}; | ||||||||||
| for (const [name, def] of Object.entries(customOperations)) { | ||||||||||
| const hookName = `use${name.charAt(0).toUpperCase()}${name.slice(1)}`; | ||||||||||
|
Comment on lines
+452
to
+453
|
||||||||||
| switch (def.kind) { | ||||||||||
| case 'query': | ||||||||||
| hooks[hookName] = (args?: unknown, options?: unknown) => | ||||||||||
| useInternalQuery(schema, modelName, name, args, { | ||||||||||
| ...(rootOptions ?? {}), | ||||||||||
| ...((options as object) ?? {}), | ||||||||||
| }); | ||||||||||
| break; | ||||||||||
| case 'suspenseQuery': | ||||||||||
| hooks[hookName] = (args?: unknown, options?: unknown) => | ||||||||||
| useInternalSuspenseQuery(schema, modelName, name, args, { | ||||||||||
| ...(rootOptions ?? {}), | ||||||||||
| ...((options as object) ?? {}), | ||||||||||
| }); | ||||||||||
| break; | ||||||||||
| case 'infiniteQuery': | ||||||||||
| hooks[hookName] = (args?: unknown, options?: unknown) => | ||||||||||
| useInternalInfiniteQuery(schema, modelName, name, args, buildInfiniteOptions(rootOptions, options)); | ||||||||||
| break; | ||||||||||
| case 'suspenseInfiniteQuery': | ||||||||||
| hooks[hookName] = (args?: unknown, options?: unknown) => | ||||||||||
| useInternalSuspenseInfiniteQuery( | ||||||||||
| schema, | ||||||||||
| modelName, | ||||||||||
| name, | ||||||||||
| args, | ||||||||||
| buildInfiniteOptions(rootOptions, options) as any, | ||||||||||
| ); | ||||||||||
| break; | ||||||||||
| case 'mutation': | ||||||||||
| hooks[hookName] = (options?: unknown) => | ||||||||||
| useInternalMutation(schema, modelName, (def.method ?? 'POST') as any, name, { | ||||||||||
| ...(rootOptions ?? {}), | ||||||||||
| ...((options as object) ?? {}), | ||||||||||
| }); | ||||||||||
| break; | ||||||||||
| default: | ||||||||||
| break; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return hooks as CustomOperationHooks<CustomOperations>; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function buildInfiniteOptions(rootOptions: QueryContext | undefined, options: unknown) { | ||||||||||
| const merged = { ...(rootOptions ?? {}), ...((options as object) ?? {}) } as Record<string, unknown>; | ||||||||||
| if (typeof merged.getNextPageParam !== 'function') { | ||||||||||
| merged.getNextPageParam = () => undefined; | ||||||||||
|
||||||||||
| merged.getNextPageParam = () => undefined; | |
| throw new Error( | |
| 'Infinite custom operations require a getNextPageParam function to be provided in the query options.', | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,7 @@ import { getContext, setContext } from 'svelte'; | |
| import { getAllQueries, invalidateQueriesMatchingPredicate } from '../common/client'; | ||
| import { getQueryKey } from '../common/query-key'; | ||
| import type { | ||
| CustomOperationDefinition, | ||
| ExtraMutationOptions, | ||
| ExtraQueryOptions, | ||
| QueryContext, | ||
|
|
@@ -120,8 +121,30 @@ export type ModelMutationModelResult< | |
| ): Promise<SimplifiedResult<Schema, Model, T, Options, false, Array>>; | ||
| }; | ||
|
|
||
| export type ClientHooks<Schema extends SchemaDef, Options extends QueryOptions<Schema> = QueryOptions<Schema>> = { | ||
| [Model in GetModels<Schema> as `${Uncapitalize<Model>}`]: ModelQueryHooks<Schema, Model, Options>; | ||
| type CustomOperationHooks<CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}> = { | ||
| [K in keyof CustomOperations as `use${Capitalize<string & K>}`]: CustomOperations[K] extends CustomOperationDefinition< | ||
| infer TArgs, | ||
| infer TResult | ||
| > | ||
| ? CustomOperations[K]['kind'] extends 'mutation' | ||
| ? (options?: ModelMutationOptions<TResult, TArgs>) => ModelMutationResult<TResult, TArgs> | ||
| : CustomOperations[K]['kind'] extends 'infiniteQuery' | 'suspenseInfiniteQuery' | ||
| ? (args?: TArgs, options?: ModelInfiniteQueryOptions<TResult>) => ModelInfiniteQueryResult<TResult> | ||
| : (args?: TArgs, options?: ModelQueryOptions<TResult>) => ModelQueryResult<TResult> | ||
| : never; | ||
| }; | ||
|
|
||
| export type ClientHooks< | ||
| Schema extends SchemaDef, | ||
| Options extends QueryOptions<Schema> = QueryOptions<Schema>, | ||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||
| > = { | ||
| [Model in GetModels<Schema> as `${Uncapitalize<Model>}`]: ModelQueryHooks< | ||
| Schema, | ||
| Model, | ||
| Options, | ||
| CustomOperations | ||
| >; | ||
| }; | ||
|
|
||
| // Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems | ||
|
|
@@ -130,6 +153,7 @@ export type ModelQueryHooks< | |
| Schema extends SchemaDef, | ||
| Model extends GetModels<Schema>, | ||
| Options extends QueryOptions<Schema> = QueryOptions<Schema>, | ||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||
| > = TrimDelegateModelOperations< | ||
| Schema, | ||
| Model, | ||
|
|
@@ -202,26 +226,37 @@ export type ModelQueryHooks< | |
| args: Accessor<Subset<T, GroupByArgs<Schema, Model>>>, | ||
| options?: Accessor<ModelQueryOptions<GroupByResult<Schema, Model, T>>>, | ||
| ): ModelQueryResult<GroupByResult<Schema, Model, T>>; | ||
| } | ||
| } & CustomOperationHooks<CustomOperations> | ||
| >; | ||
|
|
||
| /** | ||
| * Gets data query hooks for all models in the schema. | ||
| */ | ||
| export function useClientQueries<Schema extends SchemaDef, Options extends QueryOptions<Schema> = QueryOptions<Schema>>( | ||
| export function useClientQueries< | ||
| Schema extends SchemaDef, | ||
| Options extends QueryOptions<Schema> = QueryOptions<Schema>, | ||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||
| >( | ||
| schema: Schema, | ||
| options?: Accessor<QueryContext>, | ||
| ): ClientHooks<Schema, Options> { | ||
| customOperations?: CustomOperations, | ||
| ): ClientHooks<Schema, Options, CustomOperations> { | ||
| return Object.keys(schema.models).reduce( | ||
| (acc, model) => { | ||
| (acc as any)[lowerCaseFirst(model)] = useModelQueries<Schema, GetModels<Schema>, Options>( | ||
| (acc as any)[lowerCaseFirst(model)] = useModelQueries< | ||
| Schema, | ||
| GetModels<Schema>, | ||
| Options, | ||
| CustomOperations | ||
| >( | ||
| schema, | ||
| model as GetModels<Schema>, | ||
| options, | ||
| customOperations, | ||
| ); | ||
| return acc; | ||
| }, | ||
| {} as ClientHooks<Schema, Options>, | ||
| {} as ClientHooks<Schema, Options, CustomOperations>, | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -232,7 +267,13 @@ export function useModelQueries< | |
| Schema extends SchemaDef, | ||
| Model extends GetModels<Schema>, | ||
| Options extends QueryOptions<Schema>, | ||
| >(schema: Schema, model: Model, rootOptions?: Accessor<QueryContext>): ModelQueryHooks<Schema, Model, Options> { | ||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||
| >( | ||
| schema: Schema, | ||
| model: Model, | ||
| rootOptions?: Accessor<QueryContext>, | ||
| customOperations?: CustomOperations, | ||
| ): ModelQueryHooks<Schema, Model, Options, CustomOperations> { | ||
| const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); | ||
| if (!modelDef) { | ||
| throw new Error(`Model "${model}" not found in schema`); | ||
|
|
@@ -248,7 +289,7 @@ export function useModelQueries< | |
| }; | ||
| }; | ||
|
|
||
| return { | ||
| const builtIns = { | ||
| useFindUnique: (args: any, options?: any) => { | ||
| return useInternalQuery(schema, modelName, 'findUnique', args, merge(rootOptions, options)); | ||
| }, | ||
|
|
@@ -313,6 +354,67 @@ export function useModelQueries< | |
| return useInternalQuery(schema, modelName, 'groupBy', args, options); | ||
| }, | ||
| } as unknown as ModelQueryHooks<Schema, Model, Options>; | ||
|
|
||
| const custom = createCustomOperationHooks(schema, modelName, rootOptions, customOperations, merge); | ||
|
|
||
| return { ...builtIns, ...custom } as ModelQueryHooks<Schema, Model, Options, CustomOperations>; | ||
| } | ||
|
|
||
| function createCustomOperationHooks< | ||
| Schema extends SchemaDef, | ||
| CustomOperations extends Record<string, CustomOperationDefinition<any, any>> = {}, | ||
| >( | ||
| schema: Schema, | ||
| modelName: string, | ||
| rootOptions: Accessor<QueryContext> | undefined, | ||
| customOperations: CustomOperations | undefined, | ||
| mergeOptions: (rootOpt: unknown, opt: unknown) => Accessor<any>, | ||
| ) { | ||
| if (!customOperations) { | ||
| return {} as CustomOperationHooks<CustomOperations>; | ||
| } | ||
|
|
||
| const hooks: Record<string, unknown> = {}; | ||
| for (const [name, def] of Object.entries(customOperations)) { | ||
| const hookName = `use${name.charAt(0).toUpperCase()}${name.slice(1)}`; | ||
|
Comment on lines
+378
to
+379
|
||
| const merged = (options?: unknown) => mergeOptions(rootOptions, options); | ||
|
|
||
| switch (def.kind) { | ||
| case 'query': | ||
| case 'suspenseQuery': | ||
| hooks[hookName] = (args?: unknown, options?: unknown) => | ||
| useInternalQuery(schema, modelName, name, args, merged(options as Accessor<unknown> | undefined)); | ||
| break; | ||
| case 'infiniteQuery': | ||
| case 'suspenseInfiniteQuery': | ||
| hooks[hookName] = (args?: unknown, options?: unknown) => { | ||
| const mergedOptions = merged(options as Accessor<unknown> | undefined); | ||
| const withDefault = () => { | ||
| const value = mergedOptions?.() as any; | ||
| if (value && typeof value.getNextPageParam !== 'function') { | ||
| value.getNextPageParam = () => undefined; | ||
| } | ||
| return value; | ||
| }; | ||
| return useInternalInfiniteQuery(schema, modelName, name, args, withDefault as any); | ||
| }; | ||
| break; | ||
| case 'mutation': | ||
| hooks[hookName] = (options?: unknown) => | ||
| useInternalMutation( | ||
| schema, | ||
| modelName, | ||
| (def.method ?? 'POST') as any, | ||
| name, | ||
| merged(options as Accessor<unknown> | undefined) as any, | ||
| ); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| return hooks as CustomOperationHooks<CustomOperations>; | ||
| } | ||
|
|
||
| export function useInternalQuery<TQueryFnData, TData>( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CustomOperationDefinition type allows specifying an HTTP method for non-mutation kinds, but this is only meaningful for mutations. Query-type operations ('query', 'suspenseQuery', 'infiniteQuery', 'suspenseInfiniteQuery') should always use GET method and don't need a method field. Consider either making the method field conditional (only allowed when kind is 'mutation'), or add validation/documentation clarifying that method is ignored for query-type operations.