WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import { Hono } from "hono";
2import type { AppContext } from "../lib/app-context.js";
3import { categories, boards } from "@atbb/db";
4import { eq, asc } from "drizzle-orm";
5import { serializeCategory, serializeBoard, parseBigIntParam } from "./helpers.js";
6import { handleRouteError } from "../lib/route-errors.js";
7
8/**
9 * Factory function that creates category routes with access to app context.
10 *
11 * Note: GET /api/categories/:id/topics endpoint removed.
12 * Reason: posts table has no categoryUri/categoryId field to filter by.
13 * posts.forumUri stores forum URIs, not category URIs.
14 * This endpoint would always return empty arrays.
15 * TODO: Add categoryUri field to posts schema + update indexer (ATB-12 or later)
16 */
17export function createCategoriesRoutes(ctx: AppContext) {
18 return new Hono()
19 .get("/", async (c) => {
20 try {
21 const allCategories = await ctx.db
22 .select()
23 .from(categories)
24 .orderBy(categories.sortOrder)
25 .limit(1000); // Defensive limit
26
27 return c.json({
28 categories: allCategories.map(serializeCategory),
29 });
30 } catch (error) {
31 return handleRouteError(c, error, "Failed to retrieve categories", {
32 operation: "GET /api/categories",
33 logger: ctx.logger,
34 });
35 }
36 })
37 .get("/:id", async (c) => {
38 const raw = c.req.param("id");
39 const id = parseBigIntParam(raw);
40 if (id === null) {
41 return c.json({ error: "Invalid category ID" }, 400);
42 }
43
44 try {
45 const [category] = await ctx.db
46 .select()
47 .from(categories)
48 .where(eq(categories.id, id))
49 .limit(1);
50
51 if (!category) {
52 return c.json({ error: "Category not found" }, 404);
53 }
54
55 return c.json(serializeCategory(category));
56 } catch (error) {
57 return handleRouteError(c, error, "Failed to retrieve category", {
58 operation: "GET /api/categories/:id",
59 logger: ctx.logger,
60 categoryId: raw,
61 });
62 }
63 })
64 .get("/:id/boards", async (c) => {
65 const { id } = c.req.param();
66
67 const categoryId = parseBigIntParam(id);
68 if (categoryId === null) {
69 return c.json({ error: "Invalid category ID format" }, 400);
70 }
71
72 try {
73 // Check if category exists
74 const [category] = await ctx.db
75 .select()
76 .from(categories)
77 .where(eq(categories.id, categoryId))
78 .limit(1);
79
80 if (!category) {
81 return c.json({ error: "Category not found" }, 404);
82 }
83
84 const categoryBoards = await ctx.db
85 .select()
86 .from(boards)
87 .where(eq(boards.categoryId, categoryId))
88 .orderBy(asc(boards.sortOrder))
89 .limit(1000); // Defensive limit
90
91 return c.json({
92 boards: categoryBoards.map(serializeBoard),
93 });
94 } catch (error) {
95 return handleRouteError(c, error, "Failed to retrieve boards", {
96 operation: "GET /api/categories/:id/boards",
97 logger: ctx.logger,
98 categoryId: id,
99 });
100 }
101 });
102}