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
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(appview): DELETE /api/admin/boards/:id delete endpoint (ATB-45)

Pre-flight refuses with 409 if any posts reference the board (via posts.boardId).
Also fixes test error messages to use "Database connection lost" (matching isDatabaseError keywords) for consistent 503 classification.

Malpercio 2e5a0fd0 a0601e3f

+83 -3
+2 -2
apps/appview/src/routes/__tests__/admin.test.ts
··· 2006 2006 2007 2007 it("returns 503 when board lookup query fails", async () => { 2008 2008 const dbSelectSpy = vi.spyOn(ctx.db, "select").mockImplementationOnce(() => { 2009 - throw new Error("DB connection lost"); 2009 + throw new Error("Database connection lost"); 2010 2010 }); 2011 2011 2012 2012 const res = await app.request(`/api/admin/boards/${boardId}`, { ··· 2031 2031 return (originalSelect as any)(...args); 2032 2032 } 2033 2033 // Second call: post count preflight — throw DB error 2034 - throw new Error("DB connection lost"); 2034 + throw new Error("Database connection lost"); 2035 2035 }); 2036 2036 2037 2037 const res = await app.request(`/api/admin/boards/${boardId}`, {
+81 -1
apps/appview/src/routes/admin.ts
··· 3 3 import type { Variables } from "../types.js"; 4 4 import { requireAuth } from "../middleware/auth.js"; 5 5 import { requirePermission, getUserRole } from "../middleware/permissions.js"; 6 - import { memberships, roles, rolePermissions, users, forums, backfillProgress, backfillErrors, categories, boards } from "@atbb/db"; 6 + import { memberships, roles, rolePermissions, users, forums, backfillProgress, backfillErrors, categories, boards, posts } from "@atbb/db"; 7 7 import { eq, and, sql, asc, count } from "drizzle-orm"; 8 8 import { isProgrammingError } from "../lib/errors.js"; 9 9 import { BackfillStatus } from "../lib/backfill-manager.js"; ··· 893 893 } catch (error) { 894 894 return handleRouteError(c, error, "Failed to update board", { 895 895 operation: "PUT /api/admin/boards/:id", 896 + logger: ctx.logger, 897 + id: idParam, 898 + }); 899 + } 900 + } 901 + ); 902 + 903 + /** 904 + * DELETE /api/admin/boards/:id 905 + * 906 + * Delete a board. Pre-flight: refuses with 409 if any posts have boardId 907 + * pointing to this board. If clear, calls deleteRecord on the Forum DID's PDS. 908 + * The firehose indexer removes the DB row asynchronously. 909 + */ 910 + app.delete( 911 + "/boards/:id", 912 + requireAuth(ctx), 913 + requirePermission(ctx, "space.atbb.permission.manageCategories"), 914 + async (c) => { 915 + const idParam = c.req.param("id"); 916 + const id = parseBigIntParam(idParam); 917 + if (id === null) { 918 + return c.json({ error: "Invalid board ID" }, 400); 919 + } 920 + 921 + let board: typeof boards.$inferSelect; 922 + try { 923 + const [row] = await ctx.db 924 + .select() 925 + .from(boards) 926 + .where(and(eq(boards.id, id), eq(boards.did, ctx.config.forumDid))) 927 + .limit(1); 928 + 929 + if (!row) { 930 + return c.json({ error: "Board not found" }, 404); 931 + } 932 + board = row; 933 + } catch (error) { 934 + return handleRouteError(c, error, "Failed to look up board", { 935 + operation: "DELETE /api/admin/boards/:id", 936 + logger: ctx.logger, 937 + id: idParam, 938 + }); 939 + } 940 + 941 + // Pre-flight: refuse if any posts reference this board 942 + try { 943 + const [postCount] = await ctx.db 944 + .select({ count: count() }) 945 + .from(posts) 946 + .where(eq(posts.boardId, id)); 947 + 948 + if (postCount && postCount.count > 0) { 949 + return c.json( 950 + { error: "Cannot delete board with posts. Remove all posts first." }, 951 + 409 952 + ); 953 + } 954 + } catch (error) { 955 + return handleRouteError(c, error, "Failed to check board posts", { 956 + operation: "DELETE /api/admin/boards/:id", 957 + logger: ctx.logger, 958 + id: idParam, 959 + }); 960 + } 961 + 962 + const { agent, error: agentError } = getForumAgentOrError(ctx, c, "DELETE /api/admin/boards/:id"); 963 + if (agentError) return agentError; 964 + 965 + try { 966 + await agent.com.atproto.repo.deleteRecord({ 967 + repo: ctx.config.forumDid, 968 + collection: "space.atbb.forum.board", 969 + rkey: board.rkey, 970 + }); 971 + 972 + return c.json({ success: true }); 973 + } catch (error) { 974 + return handleRouteError(c, error, "Failed to delete board", { 975 + operation: "DELETE /api/admin/boards/:id", 896 976 logger: ctx.logger, 897 977 id: idParam, 898 978 });