DRY up ZodError → FieldError mapping
Five journal API route handlers had the same inline lambda:
parsed.error.issues.map((i) => ({
field: i.path.join("."),
message: i.message,
}))
Pull it into `zodIssuesToFieldErrors(error)` next to FieldError in
`@trails-cool/api/errors.ts` and re-export it so all five handlers
share the same implementation. The path-flattening rule now lives in
one place, and new routes get it for free.
Public error-response shape is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
079c3de90c
commit
edd2cabf12
7 changed files with 23 additions and 8 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
PaginationQuerySchema,
|
||||
CreateActivityRequestSchema,
|
||||
ERROR_CODES,
|
||||
zodIssuesToFieldErrors,
|
||||
} from "@trails-cool/api";
|
||||
|
||||
/** GET /api/v1/activities — paginated activity list */
|
||||
|
|
@ -59,7 +60,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
const parsed = CreateActivityRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
||||
zodIssuesToFieldErrors(parsed.error));
|
||||
}
|
||||
|
||||
const id = await createActivity(user.id, {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Route } from "./+types/api.v1.routes.$id";
|
||||
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
||||
import { getRouteWithVersions, updateRoute, deleteRoute } from "~/lib/routes.server";
|
||||
import { UpdateRouteRequestSchema, ERROR_CODES } from "@trails-cool/api";
|
||||
import { UpdateRouteRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api";
|
||||
|
||||
/** GET /api/v1/routes/:id — full route detail */
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
|
|
@ -45,7 +45,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
const parsed = UpdateRouteRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
||||
zodIssuesToFieldErrors(parsed.error));
|
||||
}
|
||||
|
||||
await updateRoute(params.id, user.id, parsed.data);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
PaginationQuerySchema,
|
||||
CreateRouteRequestSchema,
|
||||
ERROR_CODES,
|
||||
zodIssuesToFieldErrors,
|
||||
} from "@trails-cool/api";
|
||||
|
||||
/** GET /api/v1/routes — paginated route list */
|
||||
|
|
@ -58,7 +59,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
const parsed = CreateRouteRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
||||
zodIssuesToFieldErrors(parsed.error));
|
||||
}
|
||||
|
||||
const id = await createRoute(user.id, parsed.data);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Route } from "./+types/api.v1.routes.compute";
|
||||
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
||||
import { ComputeRouteRequestSchema, ERROR_CODES } from "@trails-cool/api";
|
||||
import { ComputeRouteRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api";
|
||||
|
||||
const PLANNER_URL = process.env.PLANNER_URL ?? "http://localhost:3001";
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
const parsed = ComputeRouteRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
||||
zodIssuesToFieldErrors(parsed.error));
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Route } from "./+types/api.v1.uploads";
|
||||
import { requireApiUser, apiError } from "~/lib/api-guard.server";
|
||||
import { PresignedUploadRequestSchema, ERROR_CODES } from "@trails-cool/api";
|
||||
import { PresignedUploadRequestSchema, ERROR_CODES, zodIssuesToFieldErrors } from "@trails-cool/api";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const S3_ENDPOINT = process.env.S3_ENDPOINT ?? "http://localhost:3902";
|
||||
|
|
@ -16,7 +16,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
const parsed = PresignedUploadRequestSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(400, ERROR_CODES.VALIDATION_ERROR, "Validation failed",
|
||||
parsed.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })));
|
||||
zodIssuesToFieldErrors(parsed.error));
|
||||
}
|
||||
|
||||
const { filename, resourceType, resourceId } = parsed.data;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,18 @@ export const ApiErrorResponseSchema = z.object({
|
|||
export type FieldError = z.infer<typeof FieldErrorSchema>;
|
||||
export type ApiErrorResponse = z.infer<typeof ApiErrorResponseSchema>;
|
||||
|
||||
/**
|
||||
* Map a ZodError's issues to the FieldError[] shape used in API
|
||||
* validation responses. Keeps the path-flattening rule in one place so
|
||||
* every route returns validation errors in the same shape.
|
||||
*/
|
||||
export function zodIssuesToFieldErrors(error: z.ZodError): FieldError[] {
|
||||
return error.issues.map((i) => ({
|
||||
field: i.path.join("."),
|
||||
message: i.message,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Standard error codes */
|
||||
export const ERROR_CODES = {
|
||||
VALIDATION_ERROR: "VALIDATION_ERROR",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export { ENDPOINTS } from "./endpoints.ts";
|
|||
// Error schemas
|
||||
export {
|
||||
ApiErrorResponseSchema, FieldErrorSchema, ERROR_CODES,
|
||||
zodIssuesToFieldErrors,
|
||||
type ApiErrorResponse, type FieldError,
|
||||
} from "./errors.ts";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue