import { defaultFormatter } from './error/formatter'; import { TRPCError } from './error/TRPCError'; import { AnyRootConfig } from './internals/config'; import { omitPrototype } from './internals/omitPrototype'; import { ProcedureCallOptions } from './internals/procedureBuilder'; import { AnyProcedure, ProcedureArgs } from './procedure'; import { createRecursiveProxy } from './shared/createProxy'; import { defaultTransformer } from './transformer'; import { MaybePromise } from './types'; /** @internal **/ export type ProcedureRecord = Record; export interface ProcedureRouterRecord { [key: string]: AnyProcedure | AnyRouter; } export interface RouterDef< TConfig extends AnyRootConfig, TRecord extends ProcedureRouterRecord, > { _config: TConfig; router: true; procedure?: never; procedures: TRecord; record: TRecord; } export type AnyRouterDef = RouterDef; type DecorateProcedure = ( input: ProcedureArgs[0], ) => Promise; /** * @internal */ type DecoratedProcedureRecord = { [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter ? DecoratedProcedureRecord : TProcedures[TKey] extends AnyProcedure ? DecorateProcedure : never; }; /** * @internal */ export type RouterCaller = ( /** * @note * If passing a function, we recommend it's a cached function * e.g. wrapped in `React.cache` to avoid unnecessary computations */ ctx: | TDef['_config']['$types']['ctx'] | (() => MaybePromise), ) => DecoratedProcedureRecord; export interface Router { _def: TDef; /** * @deprecated use `t.createCallerFactory(router)` instead * @see https://trpc.io/docs/server/server-side-calls */ createCaller: RouterCaller; } export type AnyRouter = Router; function isRouter( procedureOrRouter: AnyProcedure | AnyRouter, ): procedureOrRouter is AnyRouter { return 'router' in procedureOrRouter._def; } const emptyRouter = { _ctx: null as any, _errorShape: null as any, _meta: null as any, queries: {}, mutations: {}, subscriptions: {}, errorFormatter: defaultFormatter, transformer: defaultTransformer, }; /** * Reserved words that can't be used as router or procedure names */ const reservedWords = [ /** * Then is a reserved word because otherwise we can't return a promise that returns a Proxy * since JS will think that `.then` is something that exists */ 'then', ]; /** * @internal */ export type CreateRouterInner< TConfig extends AnyRootConfig, TProcRouterRecord extends ProcedureRouterRecord, > = Router> & /** * This adds ability to call procedures directly but is primarily used for quick access in type inference */ TProcRouterRecord; /** * @internal */ export function createRouterFactory( config: TConfig, ) { return function createRouterInner< TProcRouterRecord extends ProcedureRouterRecord, >( procedures: TProcRouterRecord, ): CreateRouterInner { const reservedWordsUsed = new Set( Object.keys(procedures).filter((v) => reservedWords.includes(v)), ); if (reservedWordsUsed.size > 0) { throw new Error( 'Reserved words used in `router({})` call: ' + Array.from(reservedWordsUsed).join(', '), ); } const routerProcedures: ProcedureRecord = omitPrototype({}); function recursiveGetPaths(procedures: ProcedureRouterRecord, path = '') { for (const [key, procedureOrRouter] of Object.entries(procedures ?? {})) { const newPath = `${path}${key}`; if (isRouter(procedureOrRouter)) { recursiveGetPaths(procedureOrRouter._def.procedures, `${newPath}.`); continue; } if (routerProcedures[newPath]) { throw new Error(`Duplicate key: ${newPath}`); } routerProcedures[newPath] = procedureOrRouter; } } recursiveGetPaths(procedures); const _def: AnyRouterDef = { _config: config, router: true, procedures: routerProcedures, ...emptyRouter, record: procedures, }; const router: AnyRouter = { ...procedures, _def, createCaller(ctx) { const proxy = createRecursiveProxy(({ path, args }) => { const fullPath = path.join('.'); const procedure = _def.procedures[fullPath] as AnyProcedure; return procedure({ path: fullPath, getRawInput: async () => args[0], ctx, type: procedure._def.type, }); }); return proxy as ReturnType>; }, }; return router as any; }; } function isProcedure( procedureOrRouter: AnyProcedure | AnyRouter, ): procedureOrRouter is AnyProcedure { return !!procedureOrRouter._def.procedure; } /** * @internal */ export function callProcedure( opts: ProcedureCallOptions & { procedures: ProcedureRouterRecord }, ) { const { type, path } = opts; const proc = opts.procedures[path]; if (!proc || !isProcedure(proc) || proc._def.type !== type) { throw new TRPCError({ code: 'NOT_FOUND', message: `No "${type}"-procedure on path "${path}"`, }); } return proc(opts); } export function createCallerFactory() { return function createCallerInner< TRouter extends Router>, >(router: TRouter): RouterCaller { const _def = router._def; type Context = TConfig['$types']['ctx']; return function createCaller(maybeContext) { const proxy = createRecursiveProxy(({ path, args }) => { const fullPath = path.join('.'); const procedure = _def.procedures[fullPath] as AnyProcedure; const callProc = (ctx: Context) => procedure({ path: fullPath, getRawInput: async () => args[0], ctx, type: procedure._def.type, }); if (typeof maybeContext === 'function') { const context = (maybeContext as () => MaybePromise)(); if (context instanceof Promise) { return context.then(callProc); } return callProc(context); } return callProc(maybeContext); }); return proxy as ReturnType>; }; }; }