tambi / src /lib /forms.ts
mmt
Tambi β€” deploy to Hugging Face Spaces
fc04d53
Raw
History Blame Contribute Delete
2.34 kB
// @polsia:framework-owned β€” DO NOT EDIT. Bridges a route handler's 400 body
// onto react-hook-form so server-side field validation surfaces on the form.
//
// ROUTE 400 CONTRACT this consumes (emit exactly one of these from a handler
// when validation fails):
// A) flat map β€” { errors: { <field>: '<message>' } } (preferred shape;
// this is what the reference route at src/app/api/example/route.ts emits)
// B) zod shape β€” { errors: { fieldErrors: { <field>: ['<message>', ...] } } }
// i.e. the result of `zodError.flatten()`. Useful if a handler forwards a
// flatten() verbatim. The first message per field is used.
// Anything else is ignored (the caller should fall back to a generic toast).
import type { FieldValues, Path, UseFormSetError } from 'react-hook-form';
import { z } from 'zod';
// Shape A: flat field -> message.
const flatErrors = z.record(z.string(), z.string());
// Shape B: zod flatten() β€” formErrors[] + fieldErrors{ field: string[] }.
const flattenErrors = z.object({
formErrors: z.array(z.string()).optional(),
fieldErrors: z.record(z.string(), z.array(z.string())),
});
// The 400 envelope: { errors: A | B }.
const errorBody = z.object({
errors: z.union([flattenErrors, flatErrors]),
});
/**
* Map a route handler's 400 body onto react-hook-form field errors.
*
* @returns true if at least one field error was applied (so the caller can skip
* a generic error toast), false if the body did not match the 400 contract.
*/
export function applyServerErrors<TFieldValues extends FieldValues>(
body: unknown,
setError: UseFormSetError<TFieldValues>,
): boolean {
const parsed = errorBody.safeParse(body);
if (!parsed.success) {
return false;
}
const { errors } = parsed.data;
// Normalise both shapes to a flat field -> first-message map.
const flat: Record<string, string> =
'fieldErrors' in errors
? Object.fromEntries(
Object.entries(errors.fieldErrors)
.map(([field, messages]) => [field, messages[0]])
.filter((entry): entry is [string, string] => entry[1] !== undefined),
)
: errors;
let applied = false;
for (const [field, message] of Object.entries(flat)) {
setError(field as Path<TFieldValues>, { type: 'server', message });
applied = true;
}
return applied;
}