Skip to content

Commit

Permalink
feat(react-query): Generate polymorphic route/mutation/query interfac…
Browse files Browse the repository at this point in the history
…es (#3646)
  • Loading branch information
Nick-Lucas committed Feb 8, 2023
1 parent a4b195f commit 3b11de8
Show file tree
Hide file tree
Showing 11 changed files with 910 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/react-query/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './hooks/createHooksInternal';
export * from './queryClient';
export * from './types';
export * from './hooks/types';
export * from './polymorphism';

export {
/**
Expand Down
4 changes: 4 additions & 0 deletions packages/react-query/src/shared/polymorphism/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './mutationLike';
export * from './queryLike';
export * from './routerLike';
export * from './utilsLike';
31 changes: 31 additions & 0 deletions packages/react-query/src/shared/polymorphism/mutationLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AnyProcedure, inferProcedureInput } from '@trpc/server';
import { inferTransformedProcedureOutput } from '@trpc/server/shared';
import {
InferMutationOptions,
InferMutationResult,
} from '../../utils/inferReactQueryProcedure';

/**
* Use to describe a mutation route which matches a given mutation procedure's interface
*/
export type MutationLike<TProcedure extends AnyProcedure = AnyProcedure> = {
useMutation: (
opts?: InferMutationOptions<TProcedure>,
) => InferMutationResult<TProcedure>;
};

/**
* Use to unwrap a MutationLike's input
*/
export type InferMutationLikeInput<TMutationLike extends MutationLike> =
TMutationLike extends MutationLike<infer TProcedure>
? inferProcedureInput<TProcedure>
: never;

/**
* Use to unwrap a MutationLike's data output
*/
export type InferMutationLikeData<TMutationLike extends MutationLike> =
TMutationLike extends MutationLike<infer TProcedure>
? inferTransformedProcedureOutput<TProcedure>
: never;
32 changes: 32 additions & 0 deletions packages/react-query/src/shared/polymorphism/queryLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AnyProcedure, inferProcedureInput } from '@trpc/server';
import { inferTransformedProcedureOutput } from '@trpc/server/shared';
import {
InferQueryOptions,
InferQueryResult,
} from '../../utils/inferReactQueryProcedure';

/**
* Use to request a query route which matches a given query procedure's interface
*/
export type QueryLike<TProcedure extends AnyProcedure = AnyProcedure> = {
useQuery: (
variables: inferProcedureInput<TProcedure>,
opts?: InferQueryOptions<TProcedure, any>,
) => InferQueryResult<TProcedure>;
};

/**
* Use to unwrap a QueryLike's input
*/
export type InferQueryLikeInput<TQueryLike extends QueryLike> =
TQueryLike extends QueryLike<infer TProcedure>
? inferProcedureInput<TProcedure>
: never;

/**
* Use to unwrap a QueryLike's data output
*/
export type InferQueryLikeData<TQueryLike extends QueryLike> =
TQueryLike extends QueryLike<infer TProcedure>
? inferTransformedProcedureOutput<TProcedure>
: never;
23 changes: 23 additions & 0 deletions packages/react-query/src/shared/polymorphism/routerLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
AnyMutationProcedure,
AnyQueryProcedure,
AnyRouter,
} from '@trpc/server';
import { MutationLike } from './mutationLike';
import { QueryLike } from './queryLike';

/**
* Use to describe a route path which matches a given route's interface
*/
export type RouterLike<
TRouter extends AnyRouter,
TRecord extends TRouter['_def']['record'] = TRouter['_def']['record'],
> = {
[key in keyof TRecord]: TRecord[key] extends AnyRouter
? RouterLike<TRecord[key]>
: TRecord[key] extends AnyQueryProcedure
? QueryLike<TRecord[key]>
: TRecord[key] extends AnyMutationProcedure
? MutationLike<TRecord[key]>
: never;
};
8 changes: 8 additions & 0 deletions packages/react-query/src/shared/polymorphism/utilsLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AnyRouter } from '@trpc/server';
import { DecoratedProcedureUtilsRecord } from '../proxy/utilsProxy';

/**
* Use to describe a Utils/Context path which matches the given route's interface
*/
export type UtilsLike<TRouter extends AnyRouter> =
DecoratedProcedureUtilsRecord<TRouter>;
39 changes: 36 additions & 3 deletions packages/react-query/src/utils/inferReactQueryProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ import {
inferProcedureInput,
} from '@trpc/server';
import { inferTransformedProcedureOutput } from '@trpc/server/shared';
import { UseTRPCMutationOptions, UseTRPCQueryOptions } from '../shared';
import {
UseTRPCMutationOptions,
UseTRPCMutationResult,
UseTRPCQueryOptions,
UseTRPCQueryResult,
} from '../shared';

type InferQueryOptions<
/**
* @internal
*/
export type InferQueryOptions<
TProcedure extends AnyProcedure,
TPath extends string,
> = Omit<
Expand All @@ -23,13 +31,38 @@ type InferQueryOptions<
'select'
>;

type InferMutationOptions<TProcedure extends AnyProcedure> =
/**
* @internal
*/
export type InferMutationOptions<TProcedure extends AnyProcedure> =
UseTRPCMutationOptions<
inferProcedureInput<TProcedure>,
TRPCClientErrorLike<TProcedure>,
inferTransformedProcedureOutput<TProcedure>
>;

/**
* @internal
*/
export type InferQueryResult<TProcedure extends AnyProcedure> =
UseTRPCQueryResult<
inferTransformedProcedureOutput<TProcedure>,
TRPCClientErrorLike<TProcedure>
>;

/**
* @internal
*/
export type InferMutationResult<
TProcedure extends AnyProcedure,
TContext = unknown,
> = UseTRPCMutationResult<
inferTransformedProcedureOutput<TProcedure>,
TRPCClientErrorLike<TProcedure>,
inferProcedureInput<TProcedure>,
TContext
>;

export type inferReactQueryProcedureOptions<
TRouter extends AnyRouter,
TPath extends string = '',
Expand Down
111 changes: 111 additions & 0 deletions packages/tests/server/react/polymorphism.factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// This file contains a useful pattern in tRPC,
// building factories which can produce common functionality over a homologous data source.
//
import { RouterLike, UtilsLike } from '@trpc/react-query/shared';
import { AnyRootConfig, TRPCError } from '@trpc/server';
import { createBuilder } from '@trpc/server/core/internals/procedureBuilder';
import { createRouterFactory } from '@trpc/server/core/router';
import z from 'zod';

//
// 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 RouterFactory<TConfig extends AnyRootConfig> = ReturnType<
typeof createRouterFactory<TConfig>
>;
type BaseProcedure<TConfig extends AnyRootConfig> = ReturnType<
typeof createBuilder<TConfig>
>;

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<
TConfig extends AnyRootConfig,
TRouterFactory extends RouterFactory<TConfig>,
TBaseProcedure extends BaseProcedure<TConfig>,
>(
createRouter: TRouterFactory,
baseProcedure: TBaseProcedure,
dataProvider: DataProvider,
) {
return createRouter({
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 = RouterLike<ExportRouteType>;

export type ExportUtilsLike = UtilsLike<ExportRouteType>;
Loading

3 comments on commit 3b11de8

@vercel
Copy link

@vercel vercel bot commented on 3b11de8 Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

www – ./www

www.trpc.io
www-trpc.vercel.app
www-git-main-trpc.vercel.app
trpc.io
beta.trpc.io
alpha.trpc.io

@vercel
Copy link

@vercel vercel bot commented on 3b11de8 Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

og-image – ./www/og-image

og-image-three-neon.vercel.app
og-image-trpc.vercel.app
og-image-git-main-trpc.vercel.app
og-image.trpc.io

@vercel
Copy link

@vercel vercel bot commented on 3b11de8 Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-prisma-starter – ./examples/next-prisma-starter

next-prisma-starter-trpc.vercel.app
nextjs.trpc.io
next-prisma-starter-git-main-trpc.vercel.app

Please sign in to comment.