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): PUT /api/admin/boards/:id update endpoint (ATB-45)

Malpercio 2810dc8c c22b6ac8

+115
+115
apps/appview/src/routes/admin.ts
··· 785 785 } 786 786 ); 787 787 788 + /** 789 + * PUT /api/admin/boards/:id 790 + * 791 + * Update an existing board's name, description, and sortOrder. 792 + * Fetches existing rkey + categoryUri from DB, then fetches category CID, 793 + * then putRecord with updated fields preserving the original categoryRef and createdAt. 794 + * Category cannot be changed on edit (no reparenting). 795 + * The firehose indexer updates the DB row asynchronously. 796 + */ 797 + app.put( 798 + "/boards/:id", 799 + requireAuth(ctx), 800 + requirePermission(ctx, "space.atbb.permission.manageCategories"), 801 + async (c) => { 802 + const idParam = c.req.param("id"); 803 + const id = parseBigIntParam(idParam); 804 + if (id === null) { 805 + return c.json({ error: "Invalid board ID" }, 400); 806 + } 807 + 808 + const { body, error: parseError } = await safeParseJsonBody(c); 809 + if (parseError) return parseError; 810 + 811 + const { name, description, sortOrder } = body; 812 + 813 + if (typeof name !== "string" || name.trim().length === 0) { 814 + return c.json({ error: "name is required and must be a non-empty string" }, 400); 815 + } 816 + 817 + let board: typeof boards.$inferSelect; 818 + try { 819 + const [row] = await ctx.db 820 + .select() 821 + .from(boards) 822 + .where(and(eq(boards.id, id), eq(boards.did, ctx.config.forumDid))) 823 + .limit(1); 824 + 825 + if (!row) { 826 + return c.json({ error: "Board not found" }, 404); 827 + } 828 + board = row; 829 + } catch (error) { 830 + return handleRouteError(c, error, "Failed to look up board", { 831 + operation: "PUT /api/admin/boards/:id", 832 + logger: ctx.logger, 833 + id: idParam, 834 + }); 835 + } 836 + 837 + // Fetch category CID to rebuild the categoryRef strongRef. 838 + // Always fetch fresh — the category's CID can change after category edits. 839 + let categoryCid: string; 840 + try { 841 + const categoryRkey = board.categoryUri.split("/").pop() ?? ""; 842 + const [cat] = await ctx.db 843 + .select({ cid: categories.cid }) 844 + .from(categories) 845 + .where( 846 + and( 847 + eq(categories.did, ctx.config.forumDid), 848 + eq(categories.rkey, categoryRkey) 849 + ) 850 + ) 851 + .limit(1); 852 + 853 + if (!cat) { 854 + return c.json({ error: "Category not found" }, 404); 855 + } 856 + categoryCid = cat.cid; 857 + } catch (error) { 858 + return handleRouteError(c, error, "Failed to look up category", { 859 + operation: "PUT /api/admin/boards/:id", 860 + logger: ctx.logger, 861 + id: idParam, 862 + }); 863 + } 864 + 865 + const { agent, error: agentError } = getForumAgentOrError(ctx, c, "PUT /api/admin/boards/:id"); 866 + if (agentError) return agentError; 867 + 868 + // putRecord is a full replacement — fall back to existing values for 869 + // optional fields not provided in the request body, to avoid data loss. 870 + const resolvedDescription = typeof description === "string" 871 + ? description.trim() 872 + : board.description; 873 + const resolvedSortOrder = (Number.isInteger(sortOrder) && sortOrder >= 0) 874 + ? sortOrder 875 + : board.sortOrder; 876 + 877 + try { 878 + const result = await agent.com.atproto.repo.putRecord({ 879 + repo: ctx.config.forumDid, 880 + collection: "space.atbb.forum.board", 881 + rkey: board.rkey, 882 + record: { 883 + $type: "space.atbb.forum.board", 884 + name: name.trim(), 885 + ...(resolvedDescription != null && { description: resolvedDescription }), 886 + ...(resolvedSortOrder != null && { sortOrder: resolvedSortOrder }), 887 + category: { category: { uri: board.categoryUri, cid: categoryCid } }, 888 + createdAt: board.createdAt.toISOString(), 889 + }, 890 + }); 891 + 892 + return c.json({ uri: result.data.uri, cid: result.data.cid }); 893 + } catch (error) { 894 + return handleRouteError(c, error, "Failed to update board", { 895 + operation: "PUT /api/admin/boards/:id", 896 + logger: ctx.logger, 897 + id: idParam, 898 + }); 899 + } 900 + } 901 + ); 902 + 788 903 return app; 789 904 }