import type { AtpAgent } from "@atproto/api"; import type { Database } from "@atbb/db"; import { roles, rolePermissions } from "@atbb/db"; import { eq } from "drizzle-orm"; interface DefaultRole { name: string; description: string; permissions: string[]; priority: number; } export const DEFAULT_ROLES: DefaultRole[] = [ { name: "Owner", description: "Forum owner with full control", permissions: ["*"], priority: 0, }, { name: "Admin", description: "Can manage forum structure and users", permissions: [ "space.atbb.permission.manageCategories", "space.atbb.permission.manageRoles", "space.atbb.permission.manageMembers", "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", "space.atbb.permission.pinTopics", "space.atbb.permission.lockTopics", "space.atbb.permission.createTopics", "space.atbb.permission.createPosts", ], priority: 10, }, { name: "Moderator", description: "Can moderate content and users", permissions: [ "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", "space.atbb.permission.pinTopics", "space.atbb.permission.lockTopics", "space.atbb.permission.createTopics", "space.atbb.permission.createPosts", ], priority: 20, }, { name: "Member", description: "Regular forum member", permissions: [ "space.atbb.permission.createTopics", "space.atbb.permission.createPosts", ], priority: 30, }, ]; export interface SeededRole { name: string; uri: string; cid: string; } interface SeedRolesResult { created: number; skipped: number; roles: SeededRole[]; } /** * Seed default roles to Forum DID's PDS and database. * Idempotent: checks for existing roles by name before creating. * Returns role data (URI + CID) for downstream steps. */ export async function seedDefaultRoles( db: Database, agent: AtpAgent, forumDid: string ): Promise { let created = 0; let skipped = 0; const seededRoles: SeededRole[] = []; for (const defaultRole of DEFAULT_ROLES) { // Check if role already exists by name const [existingRole] = await db .select() .from(roles) .where(eq(roles.name, defaultRole.name)) .limit(1); if (existingRole) { skipped++; seededRoles.push({ name: existingRole.name, uri: `at://${existingRole.did}/space.atbb.forum.role/${existingRole.rkey}`, cid: existingRole.cid, }); continue; } // Create role record on Forum DID's PDS const response = await agent.com.atproto.repo.createRecord({ repo: forumDid, collection: "space.atbb.forum.role", record: { $type: "space.atbb.forum.role", name: defaultRole.name, description: defaultRole.description, permissions: defaultRole.permissions, priority: defaultRole.priority, createdAt: new Date().toISOString(), }, }); // Extract rkey from the returned URI (at://did/collection/rkey) const rkey = response.data.uri.split("/").pop()!; // Insert into database so downstream steps can query it const [insertedRole] = await db.insert(roles).values({ did: forumDid, rkey, cid: response.data.cid, name: defaultRole.name, description: defaultRole.description, priority: defaultRole.priority, createdAt: new Date(), indexedAt: new Date(), }).returning({ id: roles.id }); if (defaultRole.permissions.length > 0) { await db.insert(rolePermissions).values( defaultRole.permissions.map((permission) => ({ roleId: insertedRole.id, permission, })) ); } seededRoles.push({ name: defaultRole.name, uri: response.data.uri, cid: response.data.cid, }); created++; } return { created, skipped, roles: seededRoles }; }