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 type { AtpAgent } from "@atproto/api";
2import type { Database } from "@atbb/db";
3import { categories, forums } from "@atbb/db";
4import { eq, and } from "drizzle-orm";
5import { deriveSlug } from "../slug.js";
6
7interface CreateCategoryInput {
8 name: string;
9 description?: string;
10 slug?: string;
11 sortOrder?: number;
12}
13
14interface CreateCategoryResult {
15 created: boolean;
16 skipped: boolean;
17 uri?: string;
18 cid?: string;
19 existingName?: string;
20}
21
22/**
23 * Create a space.atbb.forum.category record on the Forum DID's PDS
24 * and insert it into the database.
25 * Idempotent: skips if a category with the same name already exists.
26 */
27export async function createCategory(
28 db: Database,
29 agent: AtpAgent,
30 forumDid: string,
31 input: CreateCategoryInput
32): Promise<CreateCategoryResult> {
33 // Check if category with this name already exists
34 const [existing] = await db
35 .select()
36 .from(categories)
37 .where(and(eq(categories.did, forumDid), eq(categories.name, input.name)))
38 .limit(1);
39
40 if (existing) {
41 return {
42 created: false,
43 skipped: true,
44 uri: `at://${existing.did}/space.atbb.forum.category/${existing.rkey}`,
45 cid: existing.cid,
46 existingName: existing.name,
47 };
48 }
49
50 // Look up forum row for FK reference (optional — null if forum not yet in DB)
51 const [forum] = await db
52 .select()
53 .from(forums)
54 .where(and(eq(forums.did, forumDid), eq(forums.rkey, "self")))
55 .limit(1);
56
57 if (!forum?.id) {
58 console.warn(
59 "Forum record not found in DB — inserting category with forumId: null",
60 JSON.stringify({ forumDid })
61 );
62 }
63
64 const slug = input.slug ?? deriveSlug(input.name);
65 const now = new Date();
66
67 const response = await agent.com.atproto.repo.createRecord({
68 repo: forumDid,
69 collection: "space.atbb.forum.category",
70 record: {
71 $type: "space.atbb.forum.category",
72 name: input.name,
73 ...(input.description && { description: input.description }),
74 slug,
75 ...(input.sortOrder !== undefined && { sortOrder: input.sortOrder }),
76 createdAt: now.toISOString(),
77 },
78 });
79
80 const rkey = response.data.uri.split("/").pop()!;
81
82 await db.insert(categories).values({
83 did: forumDid,
84 rkey,
85 cid: response.data.cid,
86 name: input.name,
87 description: input.description ?? null,
88 slug,
89 sortOrder: input.sortOrder ?? null,
90 forumId: forum?.id ?? null,
91 createdAt: now,
92 indexedAt: now,
93 });
94
95 return {
96 created: true,
97 skipped: false,
98 uri: response.data.uri,
99 cid: response.data.cid,
100 };
101}