// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team // file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts // repo link: https://github.com/trpc/next-13 // code is / will continue to be adapted for our usage import { dehydrate, QueryClient } from "@tanstack/query-core"; import type { DehydratedState, QueryKey } from "@tanstack/react-query"; import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc"; import { callProcedure, type AnyProcedure, type AnyQueryProcedure, type AnyRouter, type DataTransformer, type inferProcedureInput, type inferProcedureOutput, type inferRouterContext, type MaybePromise, type ProcedureRouterRecord, } from "@calcom/trpc/server"; import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared"; export function getArrayQueryKey( queryKey: string | [string] | [string, ...unknown[]] | unknown[], type: string ): QueryKey { const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey]; const [arrayPath, input] = queryKeyArrayed; if (!input && (!type || type === "any")) { return Array.isArray(arrayPath) && arrayPath.length !== 0 ? [arrayPath] : ([] as unknown as QueryKey); } return [ arrayPath, { ...(typeof input !== "undefined" && { input: input }), ...(type && type !== "any" && { type: type }), }, ]; } // copy starts // copied from trpc/trpc repo // ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58 function transformQueryOrMutationCacheErrors< TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0] >(result: TState): TState { const error = result.state.error as Maybe>; if (error instanceof Error && error.name === "TRPCClientError") { const newError: TRPCClientErrorLike = { message: error.message, data: error.data, shape: error.shape, }; return { ...result, state: { ...result.state, error: newError, }, }; } return result; } // copy ends interface CreateTRPCNextLayoutOptions { router: TRouter; createContext: () => MaybePromise>; transformer?: DataTransformer; } /** * @internal */ export type DecorateProcedure = TProcedure extends AnyQueryProcedure ? { fetch(input: inferProcedureInput): Promise>; fetchInfinite(input: inferProcedureInput): Promise>; prefetch(input: inferProcedureInput): Promise>; prefetchInfinite(input: inferProcedureInput): Promise>; } : never; type OmitNever = Pick< TType, { [K in keyof TType]: TType[K] extends never ? never : K; }[keyof TType] >; /** * @internal */ export type DecoratedProcedureRecord< TProcedures extends ProcedureRouterRecord, TPath extends string = "" > = OmitNever<{ [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter ? DecoratedProcedureRecord : TProcedures[TKey] extends AnyQueryProcedure ? DecorateProcedure : never; }>; type CreateTRPCNextLayout = DecoratedProcedureRecord & { dehydrate(): Promise; queryClient: QueryClient; }; const getStateContainer = (opts: CreateTRPCNextLayoutOptions) => { let _trpc: { queryClient: QueryClient; context: inferRouterContext; } | null = null; return () => { if (_trpc === null) { _trpc = { context: opts.createContext(), queryClient: new QueryClient(), }; } return _trpc; }; }; export function createTRPCNextLayout( opts: CreateTRPCNextLayoutOptions ): CreateTRPCNextLayout { const getState = getStateContainer(opts); const transformer = opts.transformer ?? { serialize: (v) => v, deserialize: (v) => v, }; return createFlatProxy((key) => { const state = getState(); const { queryClient } = state; if (key === "queryClient") { return queryClient; } if (key === "dehydrate") { // copy starts // copied from trpc/trpc repo // ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229 const dehydratedCache = dehydrate(queryClient, { shouldDehydrateQuery() { // makes sure errors are also dehydrated return true; }, }); // since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects const dehydratedCacheWithErrors = { ...dehydratedCache, queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors), mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors), }; return () => transformer.serialize(dehydratedCacheWithErrors); } // copy ends return createRecursiveProxy(async (callOpts) => { const path = [key, ...callOpts.path]; const utilName = path.pop(); const ctx = await state.context; const caller = opts.router.createCaller(ctx); const pathStr = path.join("."); const input = callOpts.args[0]; if (utilName === "fetchInfinite") { return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () => caller.query(pathStr, input) ); } if (utilName === "prefetch") { return queryClient.prefetchQuery({ queryKey: getArrayQueryKey([path, input], "query"), queryFn: async () => { const res = await callProcedure({ procedures: opts.router._def.procedures, path: pathStr, rawInput: input, ctx, type: "query", }); return res; }, }); } if (utilName === "prefetchInfinite") { return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () => caller.query(pathStr, input) ); } return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () => caller.query(pathStr, input) ); }) as CreateTRPCNextLayout; }); }