From db69b0010759bce84c743ceb60b60a3ddea609e3 Mon Sep 17 00:00:00 2001 From: Tom Lienard Date: Fri, 23 Dec 2022 02:22:29 +0100 Subject: [PATCH] feat: require transformers on frontend & backend when present (#3289) --- packages/client/src/createTRPCClient.ts | 2 +- packages/client/src/internals/TRPCClient.ts | 62 ++++++++++++++----- packages/server/src/core/initTRPC.ts | 5 +- packages/tests/server/___testHelpers.ts | 4 +- packages/tests/server/initTRPC.test.ts | 3 +- .../__legacyRouterToServerAndClient.ts | 11 ++-- packages/tests/server/transformer.test.ts | 50 +++++++++++++++ 7 files changed, 107 insertions(+), 30 deletions(-) diff --git a/packages/client/src/createTRPCClient.ts b/packages/client/src/createTRPCClient.ts index efe2b7e4607..36dfc8c16c6 100644 --- a/packages/client/src/createTRPCClient.ts +++ b/packages/client/src/createTRPCClient.ts @@ -18,7 +18,7 @@ export function createTRPCClient( return [httpBatchLink(opts)]; }; const client = new Client({ - transformer: opts.transformer, + ...opts, links: getLinks(), }); return client; diff --git a/packages/client/src/internals/TRPCClient.ts b/packages/client/src/internals/TRPCClient.ts index 523485c8f13..aa03667ce28 100644 --- a/packages/client/src/internals/TRPCClient.ts +++ b/packages/client/src/internals/TRPCClient.ts @@ -1,7 +1,10 @@ import { AnyRouter, ClientDataTransformerOptions, + CombinedDataTransformer, DataTransformer, + DataTransformerOptions, + DefaultDataTransformer, inferProcedureInput, inferProcedureOutput, inferSubscriptionOutput, @@ -22,13 +25,38 @@ import { TRPCLink, } from '../links/types'; -interface CreateTRPCClientBaseOptions { - /** - * Data transformer - * @link https://trpc.io/docs/data-transformers - **/ - transformer?: ClientDataTransformerOptions; -} +type CreateTRPCClientBaseOptions = + TRouter['_def']['_config']['transformer'] extends DefaultDataTransformer + ? { + /** + * Data transformer + * + * You must use the same transformer on the backend and frontend + * @link https://trpc.io/docs/data-transformers + **/ + transformer?: 'You must set a transformer on the backend router'; + } + : TRouter['_def']['_config']['transformer'] extends DataTransformerOptions + ? { + /** + * Data transformer + * + * You must use the same transformer on the backend and frontend + * @link https://trpc.io/docs/data-transformers + **/ + transformer: TRouter['_def']['_config']['transformer'] extends CombinedDataTransformer + ? DataTransformerOptions + : TRouter['_def']['_config']['transformer']; + } + : { + /** + * Data transformer + * + * You must use the same transformer on the backend and frontend + * @link https://trpc.io/docs/data-transformers + **/ + transformer?: ClientDataTransformerOptions; + }; type TRPCType = 'subscription' | 'query' | 'mutation'; export interface TRPCRequestOptions { @@ -49,7 +77,7 @@ export interface TRPCSubscriptionObserver { /** @internal */ export type CreateTRPCClientOptions = - | CreateTRPCClientBaseOptions & { + | CreateTRPCClientBaseOptions & { links: TRPCLink[]; }; @@ -62,16 +90,22 @@ export class TRPCClient { this.requestId = 0; function getTransformer(): DataTransformer { - if (!opts.transformer) + if (!opts.transformer || typeof opts.transformer === 'string') return { serialize: (data) => data, deserialize: (data) => data, }; - if ('input' in opts.transformer) - return { - serialize: opts.transformer.input.serialize, - deserialize: opts.transformer.output.deserialize, - }; + // Type guard for `opts.transformer` because it can be `any` + function isTransformer(obj: any): obj is ClientDataTransformerOptions { + return true; + } + if (isTransformer(opts.transformer)) { + if ('input' in opts.transformer) + return { + serialize: opts.transformer.input.serialize, + deserialize: opts.transformer.output.deserialize, + }; + } return opts.transformer; } diff --git a/packages/server/src/core/initTRPC.ts b/packages/server/src/core/initTRPC.ts index 616035c7e33..fc1d2ef0349 100644 --- a/packages/server/src/core/initTRPC.ts +++ b/packages/server/src/core/initTRPC.ts @@ -7,7 +7,6 @@ import { } from '../error/formatter'; import { createFlatProxy } from '../shared'; import { - CombinedDataTransformer, DataTransformerOptions, DefaultDataTransformer, defaultTransformer, @@ -86,9 +85,7 @@ function createTRPCInner() { ErrorFormatter<$Context, DefaultErrorShape> >; type $Transformer = TOptions['transformer'] extends DataTransformerOptions - ? TOptions['transformer'] extends DataTransformerOptions - ? CombinedDataTransformer - : DefaultDataTransformer + ? TOptions['transformer'] : DefaultDataTransformer; type $ErrorShape = ErrorFormatterShape<$Formatter>; diff --git a/packages/tests/server/___testHelpers.ts b/packages/tests/server/___testHelpers.ts index e2ac4e5201d..1c9407b827a 100644 --- a/packages/tests/server/___testHelpers.ts +++ b/packages/tests/server/___testHelpers.ts @@ -70,14 +70,14 @@ export function routerToServerAndClientNew( url: wssUrl, ...opts?.wsClient, }); - const trpcClientOptions: WithTRPCConfig = { + const trpcClientOptions = { links: [httpBatchLink({ url: httpUrl })], ...(opts?.client ? typeof opts.client === 'function' ? opts.client({ httpUrl, wssUrl, wsClient }) : opts.client : {}), - }; + } as WithTRPCConfig; const client = createTRPCClient(trpcClientOptions); const proxy = createTRPCClientProxy(client); diff --git a/packages/tests/server/initTRPC.test.ts b/packages/tests/server/initTRPC.test.ts index da73f71f04d..7e321a75fde 100644 --- a/packages/tests/server/initTRPC.test.ts +++ b/packages/tests/server/initTRPC.test.ts @@ -1,5 +1,4 @@ import { - CombinedDataTransformer, DataTransformerOptions, DefaultDataTransformer, initTRPC, @@ -34,7 +33,7 @@ test('custom transformer', () => { const router = t.router({}); expectTypeOf( router._def._config.transformer, - ).toMatchTypeOf(); + ).toMatchTypeOf(); expectTypeOf( router._def._config.transformer, ).not.toMatchTypeOf(); diff --git a/packages/tests/server/interop/__legacyRouterToServerAndClient.ts b/packages/tests/server/interop/__legacyRouterToServerAndClient.ts index 3730ab169b4..52ae7c110a6 100644 --- a/packages/tests/server/interop/__legacyRouterToServerAndClient.ts +++ b/packages/tests/server/interop/__legacyRouterToServerAndClient.ts @@ -1,9 +1,6 @@ import { routerToServerAndClientNew } from '../___testHelpers'; -import { - CreateTRPCClientOptions, - TRPCWebSocketClient, - WebSocketClientOptions, -} from '@trpc/client/src'; +import { TRPCWebSocketClient, WebSocketClientOptions } from '@trpc/client/src'; +import { WithTRPCConfig } from '@trpc/next'; import { CreateHTTPHandlerOptions } from '@trpc/server/src/adapters/standalone'; import { WSSHandlerOptions } from '@trpc/server/src/adapters/ws'; import { MigrateOldRouter } from '@trpc/server/src/deprecated/interop'; @@ -20,12 +17,12 @@ export function legacyRouterToServerAndClient( wssServer?: Partial>>; wsClient?: Partial; client?: - | Partial>> + | Partial>> | ((opts: { httpUrl: string; wssUrl: string; wsClient: TRPCWebSocketClient; - }) => Partial>>); + }) => Partial>>); }, ) { const router = _router.interop() as MigrateOldRouter; diff --git a/packages/tests/server/transformer.test.ts b/packages/tests/server/transformer.test.ts index df306263494..400da4f55fa 100644 --- a/packages/tests/server/transformer.test.ts +++ b/packages/tests/server/transformer.test.ts @@ -2,6 +2,7 @@ import { routerToServerAndClientNew, waitError } from './___testHelpers'; import { TRPCClientError, + createTRPCProxyClient, createWSClient, httpBatchLink, httpLink, @@ -495,3 +496,52 @@ Object { close(); }); + +describe('required tranformers', () => { + test('works without transformer', () => { + const t = initTRPC.create({}); + const router = t.router({}); + + createTRPCProxyClient({ + links: [httpBatchLink({ url: '' })], + }); + }); + + test('works with transformer', () => { + const transformer = superjson; + const t = initTRPC.create({ + transformer, + }); + const router = t.router({}); + + createTRPCProxyClient({ + links: [httpBatchLink({ url: '' })], + transformer, + }); + }); + + test('errors with transformer set on backend but not on frontend', () => { + const transformer = superjson; + const t = initTRPC.create({ + transformer, + }); + const router = t.router({}); + + // @ts-expect-error missing transformer on frontend + createTRPCProxyClient({ + links: [httpBatchLink({ url: '' })], + }); + }); + + test('errors with transformer set on frontend but not on backend', () => { + const transformer = superjson; + const t = initTRPC.create({}); + const router = t.router({}); + + createTRPCProxyClient({ + links: [httpBatchLink({ url: '' })], + // @ts-expect-error missing transformer on backend + transformer, + }); + }); +});