UNPKG

@trpc/server

Version:

The tRPC server library

279 lines (276 loc) • 10.4 kB
import { createRecursiveProxy } from './createProxy.mjs'; import { defaultFormatter } from './error/formatter.mjs'; import { TRPCError, getTRPCErrorFromUnknown } from './error/TRPCError.mjs'; import { defaultTransformer } from './transformer.mjs'; import { mergeWithoutOverrides, omitPrototype, isObject, isFunction } from './utils.mjs'; const lazySymbol = Symbol('lazy'); function once(fn) { const uncalled = Symbol(); let result = uncalled; return ()=>{ if (result === uncalled) { result = fn(); } return result; }; } /** * Lazy load a router * @see https://trpc.io/docs/server/merging-routers#lazy-load */ function lazy(importRouter) { async function resolve() { const mod = await importRouter(); // if the module is a router, return it if (isRouter(mod)) { return mod; } const routers = Object.values(mod); if (routers.length !== 1 || !isRouter(routers[0])) { throw new Error("Invalid router module - either define exactly 1 export or return the router directly.\nExample: `lazy(() => import('./slow.js').then((m) => m.slowRouter))`"); } return routers[0]; } resolve[lazySymbol] = true; return resolve; } function isLazy(input) { return typeof input === 'function' && lazySymbol in input; } function isRouter(value) { return isObject(value) && isObject(value['_def']) && 'router' in value['_def']; } const emptyRouter = { _ctx: null, _errorShape: null, _meta: null, 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', /** * `fn.call()` and `fn.apply()` are reserved words because otherwise we can't call a function using `.call` or `.apply` */ 'call', 'apply' ]; /** * @internal */ function createRouterFactory(config) { function createRouterInner(input) { const reservedWordsUsed = new Set(Object.keys(input).filter((v)=>reservedWords.includes(v))); if (reservedWordsUsed.size > 0) { throw new Error('Reserved words used in `router({})` call: ' + Array.from(reservedWordsUsed).join(', ')); } const procedures = omitPrototype({}); const lazy = omitPrototype({}); function createLazyLoader(opts) { return { ref: opts.ref, load: once(async ()=>{ const router = await opts.ref(); const lazyPath = [ ...opts.path, opts.key ]; const lazyKey = lazyPath.join('.'); opts.aggregate[opts.key] = step(router._def.record, lazyPath); delete lazy[lazyKey]; // add lazy loaders for nested routers for (const [nestedKey, nestedItem] of Object.entries(router._def.lazy)){ const nestedRouterKey = [ ...lazyPath, nestedKey ].join('.'); // console.log('adding lazy', nestedRouterKey); lazy[nestedRouterKey] = createLazyLoader({ ref: nestedItem.ref, path: lazyPath, key: nestedKey, aggregate: opts.aggregate[opts.key] }); } }) }; } function step(from, path = []) { const aggregate = omitPrototype({}); for (const [key, item] of Object.entries(from ?? {})){ if (isLazy(item)) { lazy[[ ...path, key ].join('.')] = createLazyLoader({ path, ref: item, key, aggregate }); continue; } if (isRouter(item)) { aggregate[key] = step(item._def.record, [ ...path, key ]); continue; } if (!isProcedure(item)) { // RouterRecord aggregate[key] = step(item, [ ...path, key ]); continue; } const newPath = [ ...path, key ].join('.'); if (procedures[newPath]) { throw new Error(`Duplicate key: ${newPath}`); } procedures[newPath] = item; aggregate[key] = item; } return aggregate; } const record = step(input); const _def = { _config: config, router: true, procedures, lazy, ...emptyRouter, record }; const router = { ...record, _def, createCaller: createCallerFactory()({ _def }) }; return router; } return createRouterInner; } function isProcedure(procedureOrRouter) { return typeof procedureOrRouter === 'function'; } /** * @internal */ async function getProcedureAtPath(router, path) { const { _def } = router; let procedure = _def.procedures[path]; while(!procedure){ const key = Object.keys(_def.lazy).find((key)=>path.startsWith(key)); // console.log(`found lazy: ${key ?? 'NOPE'} (fullPath: ${fullPath})`); if (!key) { return null; } // console.log('loading', key, '.......'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const lazyRouter = _def.lazy[key]; await lazyRouter.load(); procedure = _def.procedures[path]; } return procedure; } /** * @internal */ async function callProcedure(opts) { const { type, path } = opts; const proc = await getProcedureAtPath(opts.router, path); if (!proc || !isProcedure(proc) || proc._def.type !== type && !opts.allowMethodOverride) { throw new TRPCError({ code: 'NOT_FOUND', message: `No "${type}"-procedure on path "${path}"` }); } /* istanbul ignore if -- @preserve */ if (proc._def.type !== type && opts.allowMethodOverride && proc._def.type === 'subscription') { throw new TRPCError({ code: 'METHOD_NOT_SUPPORTED', message: `Method override is not supported for subscriptions` }); } return proc(opts); } function createCallerFactory() { return function createCallerInner(router) { const { _def } = router; return function createCaller(ctxOrCallback, opts) { return createRecursiveProxy(async ({ path, args })=>{ const fullPath = path.join('.'); if (path.length === 1 && path[0] === '_def') { return _def; } const procedure = await getProcedureAtPath(router, fullPath); let ctx = undefined; try { if (!procedure) { throw new TRPCError({ code: 'NOT_FOUND', message: `No procedure found on path "${path}"` }); } ctx = isFunction(ctxOrCallback) ? await Promise.resolve(ctxOrCallback()) : ctxOrCallback; return await procedure({ path: fullPath, getRawInput: async ()=>args[0], ctx, type: procedure._def.type, signal: opts?.signal }); } catch (cause) { opts?.onError?.({ ctx, error: getTRPCErrorFromUnknown(cause), input: args[0], path: fullPath, type: procedure?._def.type ?? 'unknown' }); throw cause; } }); }; }; } function mergeRouters(...routerList) { const record = mergeWithoutOverrides({}, ...routerList.map((r)=>r._def.record)); const errorFormatter = routerList.reduce((currentErrorFormatter, nextRouter)=>{ if (nextRouter._def._config.errorFormatter && nextRouter._def._config.errorFormatter !== defaultFormatter) { if (currentErrorFormatter !== defaultFormatter && currentErrorFormatter !== nextRouter._def._config.errorFormatter) { throw new Error('You seem to have several error formatters'); } return nextRouter._def._config.errorFormatter; } return currentErrorFormatter; }, defaultFormatter); const transformer = routerList.reduce((prev, current)=>{ if (current._def._config.transformer && current._def._config.transformer !== defaultTransformer) { if (prev !== defaultTransformer && prev !== current._def._config.transformer) { throw new Error('You seem to have several transformers'); } return current._def._config.transformer; } return prev; }, defaultTransformer); const router = createRouterFactory({ errorFormatter, transformer, isDev: routerList.every((r)=>r._def._config.isDev), allowOutsideOfServer: routerList.every((r)=>r._def._config.allowOutsideOfServer), isServer: routerList.every((r)=>r._def._config.isServer), $types: routerList[0]?._def._config.$types })(record); return router; } export { callProcedure, createCallerFactory, createRouterFactory, getProcedureAtPath, lazy, mergeRouters };