UNPKG

@trpc/server

Version:

The tRPC server library

134 lines (124 loc) 4.19 kB
import type { CreateContextCallback } from '../../@trpc/server'; import { getTRPCErrorFromUnknown, TRPCError } from '../../@trpc/server'; // eslint-disable-next-line no-restricted-imports import { formDataToObject } from '../../unstable-core-do-not-import'; // FIXME: fix lint rule, this is ok // eslint-disable-next-line no-restricted-imports import type { ErrorHandlerOptions } from '../../unstable-core-do-not-import/procedure'; // FIXME: fix lint rule, this is ok // eslint-disable-next-line no-restricted-imports import type { CallerOverride } from '../../unstable-core-do-not-import/procedureBuilder'; // FIXME: fix lint rule, this is ok // eslint-disable-next-line no-restricted-imports import type { MaybePromise, Simplify, } from '../../unstable-core-do-not-import/types'; import { TRPCRedirectError } from './redirect'; import { rethrowNextErrors } from './rethrowNextErrors'; /** * Create a caller that works with Next.js React Server Components & Server Actions */ export function nextAppDirCaller<TContext, TMeta>( config: Simplify< { /** * Extract the path from the procedure metadata */ pathExtractor?: (opts: { meta: TMeta }) => string; /** * Transform form data to a `Record` before passing it to the procedure * @default true */ normalizeFormData?: boolean; /** * Called when an error occurs in the handler */ onError?: (opts: ErrorHandlerOptions<TContext>) => void; } & CreateContextCallback<TContext, () => MaybePromise<TContext>> >, ): CallerOverride<TContext> { const { normalizeFormData = true, // rethrowNextErrors = true } = config; const createContext = async (): Promise<TContext> => { return config?.createContext?.() ?? ({} as TContext); }; return async (opts) => { const path = config.pathExtractor?.({ meta: opts._def.meta as TMeta }) ?? ''; const ctx: TContext = await createContext().catch((cause) => { const error = new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to create context', cause, }); throw error; }); const handleError = (cause: unknown) => { const error = getTRPCErrorFromUnknown(cause); config.onError?.({ ctx, error, input: opts.args[0], path, type: opts._def.type, }); rethrowNextErrors(error); throw error; }; switch (opts._def.type) { case 'mutation': { /** * When you wrap an action with useFormState, it gets an extra argument as its first argument. * The submitted form data is therefore its second argument instead of its first as it would usually be. * The new first argument that gets added is the current state of the form. * @see https://react.dev/reference/react-dom/hooks/useFormState#my-action-can-no-longer-read-the-submitted-form-data */ let input = opts.args.length === 1 ? opts.args[0] : opts.args[1]; if (normalizeFormData && input instanceof FormData) { input = formDataToObject(input); } return await opts .invoke({ type: opts._def.type, ctx, getRawInput: async () => input, path, input, signal: undefined, }) .then((data) => { if (data instanceof TRPCRedirectError) throw data; return data; }) .catch(handleError); } case 'query': { const input = opts.args[0]; return await opts .invoke({ type: opts._def.type, ctx, getRawInput: async () => input, path, input, signal: undefined, }) .then((data) => { if (data instanceof TRPCRedirectError) throw data; return data; }) .catch(handleError); } case 'subscription': default: { throw new TRPCError({ code: 'NOT_IMPLEMENTED', message: `Not implemented for type ${opts._def.type}`, }); } } }; }