import { HttpError } from "@/lib/errors"; import { readJson, route } from "@/lib/http"; import { categoryCounts, mutatePosts, readPosts } from "@/lib/store"; import { SLUG_RE } from "@/lib/validate"; export const dynamic = "force-dynamic"; export const GET = route(async () => { const { posts, revision } = await readPosts(); return Response.json({ categories: categoryCounts(posts), revision }); }); /** * Body: { from, to, revision? }. Moves every post in `from` to `to`; if `to` * is already in use this is a merge. Categories are derived from posts, so a * category disappears once no post uses it. */ export const PATCH = route(async (req) => { const { from, to, revision: expected } = await readJson<{ from?: string; to?: string; revision?: string }>(req); const target = to?.trim() ?? ""; if (!from) throw new HttpError(400, "from is required"); if (!SLUG_RE.test(target)) throw new HttpError(400, "category must be lowercase kebab-case"); const { result, revision } = await mutatePosts(expected, (posts) => { let changed = 0; for (const post of posts) { if (post.category !== from) continue; post.category = target; changed++; } if (!changed) throw new HttpError(404, `no posts in category "${from}"`); return changed; }); return Response.json({ changed: result, revision }); });