diff --git a/bun.lock b/bun.lock index e66aa34..d60e9c5 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "jsonwebtoken": "^9.0.3", "pg": "^8.16.3", "sharp": "^0.34.5", + "zod": "^3.23.8", }, "devDependencies": { "@types/bcrypt": "^6.0.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index d09df43..4c3a03c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -33,6 +33,7 @@ "drizzle-orm": "^0.45.0", "jsonwebtoken": "^9.0.3", "pg": "^8.16.3", - "sharp": "^0.34.5" + "sharp": "^0.34.5", + "zod": "^3.23.8" } } diff --git a/packages/backend/src/validation.ts b/packages/backend/src/validation.ts new file mode 100644 index 0000000..84cf414 --- /dev/null +++ b/packages/backend/src/validation.ts @@ -0,0 +1,60 @@ +import type { ApiError } from "@issue/shared"; +import type { z } from "zod"; + +type ZodSchema = z.ZodSchema; +type ZodError = z.ZodError; + +export function formatZodError(error: ZodError): ApiError { + const details: Record = {}; + for (const issue of error.issues) { + const path = issue.path.join(".") || "_root"; + if (!details[path]) details[path] = []; + details[path].push(issue.message); + } + return { + error: "validation failed", + code: "VALIDATION_ERROR", + details, + }; +} + +export function errorResponse(error: string, code?: string, status = 400): Response { + const body: ApiError = { error, code }; + return Response.json(body, { status }); +} + +export async function parseJsonBody( + req: Request, + schema: ZodSchema, +): Promise<{ data: T } | { error: Response }> { + let body: unknown; + try { + body = await req.json(); + } catch { + return { + error: Response.json({ error: "invalid JSON", code: "INVALID_JSON" } satisfies ApiError, { + status: 400, + }), + }; + } + + const result = schema.safeParse(body); + if (!result.success) { + return { + error: Response.json(formatZodError(result.error), { status: 400 }), + }; + } + + return { data: result.data }; +} + +export function parseQueryParams(url: URL, schema: ZodSchema): { data: T } | { error: Response } { + const params = Object.fromEntries(url.searchParams); + const result = schema.safeParse(params); + if (!result.success) { + return { + error: Response.json(formatZodError(result.error), { status: 400 }), + }; + } + return { data: result.data }; +}