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