spb/autoscholar
Public
1import "server-only";2import { AppError } from "@autoscholar/core";3import { NextResponse } from "next/server";4import type { ZodType } from "zod";5import { log } from "./db";67export function json<T>(data: T, init?: ResponseInit): NextResponse {8 return NextResponse.json(data, {9 ...init,10 headers: { "cache-control": "no-store", ...(init?.headers ?? {}) },11 });12}1314export function errorResponse(e: unknown): NextResponse {15 if (e instanceof AppError)16 return NextResponse.json(e.toJSON(), { status: e.status, headers: { "cache-control": "no-store" } });17 if (e && typeof e === "object" && "issues" in e) {18 return NextResponse.json(19 {20 error: { code: "invalid", message: "validation failed", details: (e as { issues: unknown }).issues },21 },22 { status: 400 },23 );24 }25 const msg = (e as Error)?.message ?? "internal error";26 log.error({ err: msg, stack: (e as Error)?.stack?.split("\n").slice(0, 4) }, "unhandled API error");27 return NextResponse.json(28 { error: { code: "internal", message: process.env.NODE_ENV === "production" ? "internal error" : msg } },29 { status: 500 },30 );31}3233export async function parseBody<T>(req: Request, schema: ZodType<T>, maxBytes = 1024 * 1024): Promise<T> {34 const len = Number(req.headers.get("content-length") ?? 0);35 if (len > maxBytes) throw new AppError("too_large", `body exceeds ${maxBytes} bytes`, 413);36 const text = await req.text();37 if (text.length > maxBytes) throw new AppError("too_large", `body exceeds ${maxBytes} bytes`, 413);38 let raw: unknown;39 try {40 raw = text ? JSON.parse(text) : {};41 } catch {42 throw new AppError("invalid_json", "body is not valid JSON", 400);43 }44 const r = schema.safeParse(raw);45 if (!r.success) throw new AppError("invalid", "validation failed", 400, r.error.issues);46 return r.data;47}4849export function parseQuery<T>(url: string, schema: ZodType<T>): T {50 const u = new URL(url);51 const obj: Record<string, string> = {};52 u.searchParams.forEach((v, k) => {53 obj[k] = v;54 });55 const r = schema.safeParse(obj);56 if (!r.success) throw new AppError("invalid", "invalid query", 400, r.error.issues);57 return r.data;58}5960export const handler =61 (fn: (req: Request, ctx: { params: Promise<Record<string, string | string[]>> }) => Promise<Response>) =>62 async (req: Request, ctx: { params: Promise<Record<string, string | string[]>> }) => {63 try {64 return await fn(req, ctx);65 } catch (e) {66 return errorResponse(e);67 }68 };69