-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Polymorphism types and tests for Query/Mutation Options Client, more Utility Factory methods #6234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
06fb2d7
a20fa7c
21b548d
2f4a642
5057974
73a440f
6dea4c2
039f1a4
e0817fd
7ffa1d9
9d99878
da66346
d99ad12
caac08f
2a5e5b3
da84aa9
267c75c
635922d
0e31958
20bfef7
68ed1e4
397ed66
fa6811c
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 |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| export { createTRPCContext } from './internals/Context'; | ||
| export type { TRPCOptionsProxy } from './internals/createOptionsProxy'; | ||
| export type { | ||
| TRPCOptionsProxy, | ||
| InferInput, | ||
| InferOutput, | ||
| } from './internals/createOptionsProxy'; | ||
| export { createTRPCOptionsProxy } from './internals/createOptionsProxy'; | ||
| export { useSubscription } from './internals/subscriptionOptions'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { type QueryClient } from '@tanstack/react-query'; | ||
| import { type QueryClient, type QueryFilters } from '@tanstack/react-query'; | ||
| import { | ||
| getUntypedClient, | ||
| TRPCUntypedClient, | ||
|
|
@@ -33,10 +33,48 @@ import { | |
| trpcSubscriptionOptions, | ||
| type TRPCSubscriptionOptions, | ||
| } from './subscriptionOptions'; | ||
| import type { QueryType, ResolverDef } from './types'; | ||
| import type { QueryType, ResolverDef, TRPCQueryKey } from './types'; | ||
| import { getQueryKeyInternal } from './utils'; | ||
|
|
||
| export interface DecorateQueryKeyable { | ||
| /** | ||
| * Calculate the Tanstack Query Key for a Route | ||
| * | ||
| * @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys | ||
| */ | ||
| queryKey: () => TRPCQueryKey; | ||
|
|
||
| /** | ||
| * Calculate a Tanstack Query Filter for a Route | ||
| * | ||
| * @see https://tanstack.com/query/latest/docs/framework/react/guides/filters | ||
| */ | ||
| queryFilter: () => QueryFilters; | ||
| } | ||
|
|
||
| export type InferInput< | ||
| TProcedure extends | ||
| | DecorateQueryProcedure<any> | ||
| | DecorateMutationProcedure<any>, | ||
| > = TProcedure['~input']; | ||
|
|
||
| export type InferOutput< | ||
| TProcedure extends | ||
| | DecorateQueryProcedure<any> | ||
| | DecorateMutationProcedure<any>, | ||
| > = TProcedure['~output']; | ||
|
|
||
| export interface DecorateQueryProcedure<TDef extends ResolverDef> { | ||
| /** | ||
| * @internal prefer using InferInput to access input type | ||
| */ | ||
| '~input': TDef['input']; | ||
|
|
||
| /** | ||
| * @internal prefer using InferOutput to access input type | ||
| */ | ||
| '~output': TDef['output']; | ||
|
|
||
| /** | ||
| * @see https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions#queryoptions | ||
| */ | ||
|
|
@@ -46,9 +84,33 @@ export interface DecorateQueryProcedure<TDef extends ResolverDef> { | |
| * @see https://tanstack.com/query/latest/docs/framework/react/reference/infiniteQueryOptions#infinitequeryoptions | ||
| */ | ||
| infiniteQueryOptions: TRPCInfiniteQueryOptions<TDef>; | ||
|
|
||
| /** | ||
| * Calculate the Tanstack Query Key for a Query Procedure | ||
| * | ||
| * @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys | ||
| */ | ||
| queryKey: (input?: TDef['input']) => TRPCQueryKey; | ||
|
|
||
| /** | ||
| * Calculate a Tanstack Query Filter for a Query Procedure | ||
| * | ||
| * @see https://tanstack.com/query/latest/docs/framework/react/guides/filters | ||
| */ | ||
| queryFilter: (input?: TDef['input']) => QueryFilters; | ||
| } | ||
|
|
||
| export interface DecorateMutationProcedure<TDef extends ResolverDef> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's a different user story to queryKey really, I'm not very familiar with mutationKey so wouldn't want to rush into adding that without knowing the story, but it seems to be more about remotely executing a mutation which wouldn't make a lot of sense with tRPC
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's needed for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, are you okay if we implement that as additional work into your branch? The scope of this PR already expanded a lot and it was all purely in service of polymorphism support. mutationKey isn't needed for that case
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes sure 👍 seems like this is targeting some other intermediate branch though 🤨
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, what's the correct branch? I thought I had it from your PR?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is Alex's R19 branch which I think isn't working in some areas 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Crap. Well that threw me for a loop and into git hell: #6244
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Closing this in favour of the other MR |
||
| /** | ||
| * @internal prefer using InferInput to access input type | ||
| */ | ||
| '~input': TDef['input']; | ||
|
|
||
| /** | ||
| * @internal prefer using InferOutput to access input type | ||
| */ | ||
| '~output': TDef['output']; | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Putting the type on the object makes for a very cheap way of accessing type later, but providing some Infer helpers instead suggesting users utilise this directly. Prefixing with ~ forces autocomplete to sort it to the end |
||
| /** | ||
| * @see | ||
| */ | ||
|
|
@@ -82,7 +144,7 @@ export type DecoratedProcedureUtilsRecord< | |
| > = { | ||
| [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value | ||
| ? $Value extends RouterRecord | ||
| ? DecoratedProcedureUtilsRecord<TRoot, $Value> | ||
| ? DecoratedProcedureUtilsRecord<TRoot, $Value> & DecorateQueryKeyable | ||
| : $Value extends AnyProcedure | ||
| ? DecorateProcedure< | ||
| $Value['_def']['type'], | ||
|
|
@@ -101,7 +163,8 @@ export type TRPCOptionsProxy<TRouter extends AnyRouter> = | |
| DecoratedProcedureUtilsRecord< | ||
| TRouter['_def']['_config']['$types'], | ||
| TRouter['_def']['record'] | ||
| >; | ||
| > & | ||
| DecorateQueryKeyable; | ||
|
|
||
| export interface TRPCOptionsProxyOptionsBase { | ||
| queryClient: QueryClient; | ||
|
|
@@ -132,12 +195,14 @@ type UtilsMethods = | |
| | keyof DecorateSubscriptionProcedure<any>; | ||
|
|
||
| function getQueryType(method: UtilsMethods) { | ||
| return { | ||
| const map: Partial<Record<UtilsMethods, QueryType>> = { | ||
| queryOptions: 'query', | ||
| infiniteQueryOptions: 'infinite', | ||
| subscriptionOptions: 'any', | ||
| mutationOptions: 'any', | ||
| }[method] as QueryType; | ||
| }; | ||
|
|
||
| return map[method]; | ||
| } | ||
|
|
||
| export function createTRPCOptionsProxy<TRouter extends AnyRouter>( | ||
|
|
@@ -172,23 +237,32 @@ export function createTRPCOptionsProxy<TRouter extends AnyRouter>( | |
| return createRecursiveProxy(({ args, path: _path }) => { | ||
| const path = [..._path]; | ||
| const utilName = path.pop() as UtilsMethods; | ||
| const [input, userOptions] = args as any[]; | ||
| const [arg1, arg2] = args as any[]; | ||
|
|
||
| const queryType = getQueryType(utilName); | ||
| const queryKey = getQueryKeyInternal(path, input, queryType); | ||
| const queryKey = getQueryKeyInternal(path, arg1, queryType ?? 'any'); | ||
|
|
||
| const contextMap: Record<UtilsMethods, () => unknown> = { | ||
| infiniteQueryOptions: () => | ||
| trpcInfiniteQueryOptions({ | ||
| opts: userOptions, | ||
| '~input': undefined as any, | ||
| '~output': undefined as any, | ||
| queryKey: () => queryKey, | ||
| queryFilter: (): QueryFilters => { | ||
| return { | ||
| queryKey: queryKey, | ||
|
Nick-Lucas marked this conversation as resolved.
|
||
| }; | ||
| }, | ||
| infiniteQueryOptions: () => { | ||
| return trpcInfiniteQueryOptions({ | ||
| opts: arg2, | ||
| path, | ||
| queryClient: opts.queryClient, | ||
| queryKey, | ||
| queryKey: queryKey, | ||
| query: callIt('query'), | ||
| }), | ||
| }); | ||
| }, | ||
| queryOptions: () => { | ||
| return trpcQueryOptions({ | ||
| opts: userOptions, | ||
| opts: arg2, | ||
| path, | ||
| queryClient: opts.queryClient, | ||
| queryKey: queryKey, | ||
|
|
@@ -197,7 +271,7 @@ export function createTRPCOptionsProxy<TRouter extends AnyRouter>( | |
| }, | ||
| mutationOptions: () => { | ||
| return trpcMutationOptions({ | ||
| opts: userOptions, | ||
| opts: arg1, | ||
| path, | ||
| queryClient: opts.queryClient, | ||
| mutate: callIt('mutation'), | ||
|
|
@@ -206,9 +280,9 @@ export function createTRPCOptionsProxy<TRouter extends AnyRouter>( | |
| }, | ||
| subscriptionOptions: () => { | ||
| return trpcSubscriptionOptions({ | ||
| opts: userOptions, | ||
| opts: arg2, | ||
| path, | ||
| queryKey, | ||
| queryKey: queryKey, | ||
| subscribe: callIt('subscription'), | ||
| }); | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { initTRPC } from '@trpc/server'; | ||
|
|
||
| export const t = initTRPC.create(); | ||
|
|
||
| export type $RootTypes = (typeof t)['_config']['$types']; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // | ||
| // This file contains a useful pattern in tRPC, | ||
| // building factories which can produce common functionality over a homologous data source. | ||
| // | ||
| import { TRPCError } from '@trpc/server'; | ||
| import type { | ||
| AnyRootTypes, | ||
| createBuilder, | ||
| } from '@trpc/server/unstable-core-do-not-import'; | ||
| import z from 'zod'; | ||
| import type { TRPCOptionsProxy } from '../src'; | ||
| import type { $RootTypes } from './polymorphism.common'; | ||
| import { t } from './polymorphism.common'; | ||
|
|
||
| // | ||
| // DTOs | ||
| // | ||
|
|
||
| export const FileExportRequest = z.object({ | ||
| name: z.string().min(0), | ||
| filter: z.string().min(0), | ||
| }); | ||
|
|
||
| export const FileExportStatus = z.object({ | ||
| id: z.number().min(0), | ||
| name: z.string().min(0), | ||
| downloadUri: z.string().optional(), | ||
| createdAt: z.date(), | ||
| }); | ||
| export type FileExportStatusType = z.infer<typeof FileExportStatus>; | ||
|
|
||
| // | ||
| // Dependencies | ||
| // | ||
|
|
||
| type BaseProcedure<TRoot extends AnyRootTypes> = ReturnType< | ||
| typeof createBuilder<TRoot['ctx'], TRoot['meta']> | ||
| >; | ||
|
|
||
| export type DataProvider = FileExportStatusType[]; | ||
|
|
||
| // | ||
| // Set up a route factory which can be re-used for different data sources. | ||
| // In this case just with a simple array data source a POC | ||
| // | ||
|
|
||
| let COUNTER = 1; | ||
|
|
||
| export function createExportRoute< | ||
| TBaseProcedure extends BaseProcedure<$RootTypes>, | ||
| >(baseProcedure: TBaseProcedure, dataProvider: DataProvider) { | ||
| return t.router({ | ||
| start: baseProcedure | ||
| .input(FileExportRequest) | ||
| .output(FileExportStatus) | ||
| .mutation(async (opts) => { | ||
| const exportInstance: FileExportStatusType = { | ||
| id: COUNTER++, | ||
| name: opts.input.name, | ||
| createdAt: new Date(), | ||
| downloadUri: undefined, | ||
| }; | ||
|
|
||
| dataProvider.push(exportInstance); | ||
|
|
||
| return exportInstance; | ||
| }), | ||
| list: baseProcedure.output(z.array(FileExportStatus)).query(async () => { | ||
| return dataProvider; | ||
| }), | ||
| status: baseProcedure | ||
| .input(z.object({ id: z.number().min(0) })) | ||
| .output(FileExportStatus) | ||
| .query(async (opts) => { | ||
| const index = dataProvider.findIndex( | ||
| (item) => item.id === opts.input.id, | ||
| ); | ||
|
|
||
| const exportInstance = dataProvider[index]; | ||
|
|
||
| if (!exportInstance) { | ||
| throw new TRPCError({ | ||
| code: 'NOT_FOUND', | ||
| }); | ||
| } | ||
|
|
||
| // When status is polled a second time the download should be ready | ||
| dataProvider[index] = { | ||
| ...exportInstance, | ||
| downloadUri: `example.com/export-${exportInstance.name}.csv`, | ||
| }; | ||
|
|
||
| return exportInstance; | ||
| }), | ||
| }); | ||
| } | ||
|
|
||
| // | ||
| // Generate abstract types which can be used by the client | ||
| // | ||
|
|
||
| type ExportRouteType = ReturnType<typeof createExportRoute>; | ||
|
|
||
| export type ExportRouteLike = TRPCOptionsProxy<ExportRouteType>; |
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.
I'm still not convinced this is a very useful method, especially since it doesn't take any of the other query filter args (predicate functions etc). why not just use
{ queryKey: trpc.foo.queryKey() }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.
I think the idea would be that we have full control, so if we do want to set some of the other filters internally we can without breaking anybody's apps
I'm open to not having it, just felt like it's appropriate for a React Query factory to provide
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.
Could also add options to override some bits, but I didn't want to dive straight into that world, worth analysing some use cases
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.
I'd say until we have identified these use cases and added support for the other filter options just the queryKey suffices? Not a hill I'd die on so if you still see value in it go ahead and merge 👍