36 lines
1.3 KiB
TypeScript
36 lines
1.3 KiB
TypeScript
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 });
|
|
});
|