SPB Git

spb/autoscholar

Public
2.4 KB · 69 lines typescript
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
Files · cf174810baa9fec822f36c2d1dff7efc3e0bec61
Menu

Theme