/** * Serves the categories dumps are filed under and their feeds, and changes them * for those with `category:manage`. * * - `GET /api/categories` — public. Answers every category, in order. * - `GET /api/categories/:slug` — public. Answers one category. * - `GET /api/categories/:slug/dumps?page=&limit=` — optional sign-in. Answers * a page of its dumps, newest first, with the caller's private ones. * - `POST /api/categories` — permission `category:manage`. Creates a category. * - `PUT /api/categories/:id` — permission `category:manage`. Updates a * category. * - `DELETE /api/categories/:id` — permission `category:manage`. Deletes a * category; its dumps stay. * * @module */ import { Router } from "@oak/oak"; import { type APIResponse, type PaginatedData } from "../../shared/api.ts"; import { APIException } from "../lib/api-exception.ts"; import type { Category } from "../../shared/category.ts"; import type { Dump } from "../../shared/dump.ts"; import { isCreateCategoryRequest, isUpdateCategoryRequest, } from "../validators/category.ts"; import { authMiddleware, requirePermission } from "../middleware/auth.ts"; import { parseOptionalAuth } from "../lib/auth.ts"; import { parsePagination } from "../lib/pagination.ts"; import { createCategory, deleteCategory, getCategoryBySlug, listCategories, listDumpsByCategory, updateCategory, } from "../services/category-service.ts"; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ const router: Router = new Router({ prefix: "/api/categories" }); router.get("/", (ctx) => { const responseBody: APIResponse = { success: true, data: listCategories(), }; ctx.response.body = responseBody; }); router.get("/:slug", (ctx) => { const category = getCategoryBySlug(ctx.params.slug); const responseBody: APIResponse = { success: true, data: category, }; ctx.response.body = responseBody; }); router.get("/:slug/dumps", async (ctx) => { const requestingUserId = await parseOptionalAuth(ctx) ?? undefined; const { page, limit } = parsePagination(ctx.request.url.searchParams); const { items, hasMore } = listDumpsByCategory( ctx.params.slug, page, limit, requestingUserId, ); const responseBody: APIResponse> = { success: true, data: { items, hasMore }, }; ctx.response.body = responseBody; }); router.post( "/", authMiddleware, requirePermission("category:manage"), async (ctx) => { const body: unknown = await ctx.request.body.json(); if (!isCreateCategoryRequest(body)) { throw new APIException(400, "Invalid category data"); } const category = createCategory(body); const responseBody: APIResponse = { success: true, data: category, }; ctx.response.status = 201; ctx.response.body = responseBody; }, ); router.put( "/:id", authMiddleware, requirePermission("category:manage"), async (ctx) => { const body: unknown = await ctx.request.body.json(); if (!isUpdateCategoryRequest(body)) { throw new APIException(400, "Invalid category data"); } const category = updateCategory(ctx.params.id, body); const responseBody: APIResponse = { success: true, data: category, }; ctx.response.body = responseBody; }, ); router.delete( "/:id", authMiddleware, requirePermission("category:manage"), (ctx) => { deleteCategory(ctx.params.id); const responseBody: APIResponse = { success: true, data: null }; ctx.response.body = responseBody; }, ); export default router;