import type { DehydratedState, DehydrateOptions, QueryClient, } from '@tanstack/react-query'; import { dehydrate } from '@tanstack/react-query'; import type { TRPCClient } from '@trpc/client'; import { getUntypedClient, TRPCUntypedClient } from '@trpc/client'; import type { CoercedTransformerParameters } from '@trpc/client/unstable-internals'; import { getTransformer, type TransformerOptions, } from '@trpc/client/unstable-internals'; import type { AnyQueryProcedure, AnyRootTypes, AnyRouter, inferClientTypes, inferRouterContext, Maybe, ProtectedIntersection, RouterRecord, } from '@trpc/server/unstable-core-do-not-import'; import { callProcedure, createFlatProxy, createRecursiveProxy, run, } from '@trpc/server/unstable-core-do-not-import'; import { getQueryKeyInternal } from '../internals/getQueryKey'; import type { CreateTRPCReactQueryClientConfig, DecorateQueryProcedure, TRPCFetchInfiniteQueryOptions, TRPCFetchQueryOptions, } from '../shared'; import { getQueryClient, getQueryType } from '../shared'; type CreateSSGHelpersInternal = { router: TRouter; ctx: inferRouterContext; } & TransformerOptions>; interface CreateSSGHelpersExternal { client: TRPCClient | TRPCUntypedClient; } type CreateServerSideHelpersOptions = CreateTRPCReactQueryClientConfig & (CreateSSGHelpersExternal | CreateSSGHelpersInternal); type SSGFns = | 'queryOptions' | 'infiniteQueryOptions' | 'fetch' | 'fetchInfinite' | 'prefetch' | 'prefetchInfinite'; /** * @internal */ type DecoratedProcedureSSGRecord< TRoot extends AnyRootTypes, TRecord extends RouterRecord, > = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyQueryProcedure ? Pick, SSGFns> : $Value extends RouterRecord ? DecoratedProcedureSSGRecord : never : never; }; type AnyDecoratedProcedure = Pick, SSGFns>; /** * Create functions you can use for server-side rendering / static generation * @see https://trpc.io/docs/v11/client/nextjs/server-side-helpers */ export function createServerSideHelpers( opts: CreateServerSideHelpersOptions, ) { const queryClient = getQueryClient(opts); const transformer = getTransformer( (opts as CoercedTransformerParameters).transformer, ); const resolvedOpts: { serialize: (obj: unknown) => any; query: (queryOpts: { path: string; input: unknown }) => Promise; } = (() => { if ('router' in opts) { const { ctx, router } = opts; return { serialize: (obj) => transformer.output.serialize(obj), query: (queryOpts) => { return callProcedure({ router, path: queryOpts.path, getRawInput: async () => queryOpts.input, ctx, type: 'query', signal: undefined, batchIndex: 0, }); }, }; } const { client } = opts; const untypedClient = client instanceof TRPCUntypedClient ? client : getUntypedClient(client); return { query: (queryOpts) => untypedClient.query(queryOpts.path, queryOpts.input), serialize: (obj) => transformer.output.serialize(obj), }; })(); function _dehydrate( opts: DehydrateOptions = { shouldDehydrateQuery(query) { if (query.state.status === 'pending') { return false; } // makes sure to serialize errors return true; }, }, ): DehydratedState { const before = run(() => { const dehydrated = dehydrate(queryClient, opts); return { ...dehydrated, queries: dehydrated.queries.map((query) => { if (query.promise) { const { promise: _, ...rest } = query; return rest; } return query; }), }; }); const after = resolvedOpts.serialize(before); return after; } type CreateSSGHelpers = ProtectedIntersection< { queryClient: QueryClient; dehydrate: (opts?: DehydrateOptions) => DehydratedState; }, DecoratedProcedureSSGRecord< TRouter['_def']['_config']['$types'], TRouter['_def']['record'] > >; const proxy = createRecursiveProxy((opts) => { const args = opts.args; const input = args[0]; const arrayPath = [...opts.path]; const utilName = arrayPath.pop() as keyof AnyDecoratedProcedure; const queryFn = () => resolvedOpts.query({ path: arrayPath.join('.'), input }); const queryKey = getQueryKeyInternal( arrayPath, input, getQueryType(utilName), ); const helperMap: Record unknown> = { queryOptions: () => { const args1 = args[1] as Maybe; return { ...args1, queryKey, queryFn }; }, infiniteQueryOptions: () => { const args1 = args[1] as Maybe; return { ...args1, queryKey, queryFn }; }, fetch: () => { const args1 = args[1] as Maybe>; return queryClient.fetchQuery({ ...args1, queryKey, queryFn }); }, fetchInfinite: () => { const args1 = args[1] as Maybe< TRPCFetchInfiniteQueryOptions >; return queryClient.fetchInfiniteQuery({ ...args1, queryKey, queryFn, initialPageParam: args1?.initialCursor ?? null, }); }, prefetch: () => { const args1 = args[1] as Maybe>; return queryClient.prefetchQuery({ ...args1, queryKey, queryFn }); }, prefetchInfinite: () => { const args1 = args[1] as Maybe< TRPCFetchInfiniteQueryOptions >; return queryClient.prefetchInfiniteQuery({ ...args1, queryKey, queryFn, initialPageParam: args1?.initialCursor ?? null, }); }, }; return helperMap[utilName](); }); return createFlatProxy((key) => { if (key === 'queryClient') return queryClient; if (key === 'dehydrate') return _dehydrate; return proxy[key]; }); }