@trpc/next
Version:
216 lines (195 loc) • 5.89 kB
text/typescript
/// <reference types="next" />
import {
clientCallTypeToProcedureType,
createTRPCUntypedClient,
} from '@trpc/client';
import type { CreateContextCallback } from '@trpc/server';
import { rethrowNextErrors } from '@trpc/server/adapters/next-app-dir';
import type {
AnyProcedure,
AnyRootTypes,
AnyRouter,
ErrorHandlerOptions,
inferClientTypes,
inferProcedureInput,
MaybePromise,
RootConfig,
Simplify,
TRPCResponse,
} from '@trpc/server/unstable-core-do-not-import';
import {
createRecursiveProxy,
formDataToObject,
getErrorShape,
getTRPCErrorFromUnknown,
transformTRPCResponse,
TRPCError,
} from '@trpc/server/unstable-core-do-not-import';
import { revalidateTag } from 'next/cache';
import { cache } from 'react';
import type {
ActionHandlerDef,
CreateTRPCNextAppRouterOptions,
inferActionDef,
} from './shared';
import { generateCacheTag, isFormData } from './shared';
import type { NextAppDirDecorateRouterRecord } from './types';
export type { ActionHandlerDef };
// ts-prune-ignore-next
export function experimental_createTRPCNextAppDirServer<
TRouter extends AnyRouter,
>(opts: CreateTRPCNextAppRouterOptions<TRouter>) {
const getClient = cache(() => {
const config = opts.config();
return createTRPCUntypedClient(config);
});
return createRecursiveProxy<
NextAppDirDecorateRouterRecord<
TRouter['_def']['_config']['$types'],
TRouter['_def']['record']
>
>((callOpts) => {
// lazily initialize client
const client = getClient();
const pathCopy = [...callOpts.path];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const action = pathCopy.pop()!;
const procedurePath = pathCopy.join('.');
const procedureType = clientCallTypeToProcedureType(action);
const cacheTag = generateCacheTag(procedurePath, callOpts.args[0]);
if (action === 'revalidate') {
revalidateTag(cacheTag);
return;
}
return (client[procedureType] as any)(procedurePath, ...callOpts.args);
});
}
/**
* @internal
*/
export type TRPCActionHandler<TDef extends ActionHandlerDef> = (
input: FormData | TDef['input'],
) => Promise<TRPCResponse<TDef['output'], TDef['errorShape']>>;
export function experimental_createServerActionHandler<
TInstance extends {
_config: RootConfig<AnyRootTypes>;
},
>(
t: TInstance,
opts: CreateContextCallback<
TInstance['_config']['$types']['ctx'],
() => MaybePromise<TInstance['_config']['$types']['ctx']>
> & {
/**
* 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<TInstance['_config']['$types']['ctx']>,
) => void;
/**
* Rethrow errors that should be handled by Next.js
* @default true
*/
rethrowNextErrors?: boolean;
},
) {
const config = t._config;
const {
normalizeFormData = true,
createContext,
rethrowNextErrors: shouldRethrowNextErrors = true,
} = opts;
const transformer = config.transformer;
// TODO allow this to take a `TRouter` in addition to a `AnyProcedure`
return function createServerAction<TProc extends AnyProcedure>(
proc: TProc,
): TRPCActionHandler<
Simplify<inferActionDef<inferClientTypes<TInstance>, TProc>>
> {
return async function actionHandler(
rawInput: FormData | inferProcedureInput<TProc>,
) {
let ctx: TInstance['_config']['$types']['ctx'] | undefined = undefined;
try {
ctx = (await createContext?.()) ?? {};
if (normalizeFormData && isFormData(rawInput)) {
// Normalizes FormData so we can use `z.object({})` etc on the server
try {
rawInput = formDataToObject(rawInput);
} catch {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to convert FormData to an object',
});
}
} else if (rawInput && !isFormData(rawInput)) {
rawInput = transformer.input.deserialize(rawInput);
}
const data = proc._def.experimental_caller
? await proc(rawInput as any)
: await proc({
input: undefined,
ctx,
path: '',
getRawInput: async () => rawInput,
type: proc._def.type,
// is it possible to get the AbortSignal from the request?
signal: undefined,
});
const transformedJSON = transformTRPCResponse(config, {
result: {
data,
},
});
return transformedJSON;
} catch (cause) {
const error = getTRPCErrorFromUnknown(cause);
opts.onError?.({
ctx,
error,
input: rawInput,
path: '',
type: proc._def.type,
});
if (shouldRethrowNextErrors) {
rethrowNextErrors(error);
}
const shape = getErrorShape({
config,
ctx,
error,
input: rawInput,
path: '',
type: proc._def.type,
});
return transformTRPCResponse(t._config, {
error: shape,
});
}
} as TRPCActionHandler<
inferActionDef<TInstance['_config']['$types'], TProc>
>;
};
}
// ts-prune-ignore-next
export async function experimental_revalidateEndpoint(req: Request) {
const { cacheTag } = await req.json();
if (typeof cacheTag !== 'string') {
return new Response(
JSON.stringify({
revalidated: false,
error: 'cacheTag must be a string',
}),
{ status: 400 },
);
}
revalidateTag(cacheTag);
return new Response(JSON.stringify({ revalidated: true, now: Date.now() }), {
status: 200,
});
}