import { and, eq, inArray } from "drizzle-orm"; import { hostAccess, roles, userRoles, users } from "../db/schema.js"; import type { DatabaseContext } from "./database-context.js"; import { rowsAffected } from "./mutation-result.js"; import { deleteReturning, insertReturning } from "./returning.js"; export type RoleRecord = typeof roles.$inferSelect; export type NewRoleRecord = typeof roles.$inferInsert; export type RoleUpdate = Pick< Partial, "displayName" | "description" | "permissions" | "updatedAt" >; export type UserRoleWithRole = { id: number; roleId: number; roleName: string; roleDisplayName: string; description: string | null; isSystem: boolean; grantedAt: string; }; export type RoleMemberRecord = { userId: string; username: string; grantedAt: string; grantedBy: string | null; }; export type UserRolePermissionRecord = { permissions: string | null; }; export type UserRoleNameSwitchResult = { added: boolean; removed: boolean; }; export class RoleRepository { constructor( private readonly context: DatabaseContext, private readonly onWrite?: () => void | Promise, ) {} async listRoles(): Promise { return this.context.drizzle .select() .from(roles) .orderBy(roles.isSystem, roles.name); } async findRoleById(id: number): Promise { const rows = await this.context.drizzle .select() .from(roles) .where(eq(roles.id, id)) .limit(1); return rows[0] ?? null; } async findRoleByName(name: string): Promise { const rows = await this.context.drizzle .select() .from(roles) .where(eq(roles.name, name)) .limit(1); return rows[0] ?? null; } async createRole(role: NewRoleRecord): Promise { const [created] = await insertReturning(this.context, roles, role); await this.afterWrite(); return created.id; } async updateRole(id: number, update: RoleUpdate): Promise { const result = await this.context.drizzle .update(roles) .set(update) .where(eq(roles.id, id)); await this.afterWrite(); return rowsAffected(result) > 0; } async deleteRole(id: number): Promise<{ deletedUserIds: string[] }> { const deletedUserRoles = await deleteReturning( this.context, userRoles, eq(userRoles.roleId, id), ); await this.context.drizzle .delete(hostAccess) .where(eq(hostAccess.roleId, id)); await this.context.drizzle.delete(roles).where(eq(roles.id, id)); await this.afterWrite(); return { deletedUserIds: deletedUserRoles.map((row) => row.userId), }; } async findUserRole( userId: string, roleId: number, ): Promise { const rows = await this.context.drizzle .select() .from(userRoles) .where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId))) .limit(1); return rows[0] ?? null; } async assignRoleToUser(input: { userId: string; roleId: number; grantedBy: string; }): Promise { await this.context.drizzle.insert(userRoles).values(input); await this.afterWrite(); } async assignRoleNameToUser(input: { userId: string; roleName: string; grantedBy: string; }): Promise { const role = await this.findRoleByName(input.roleName); if (!role) { return false; } await this.context.drizzle.insert(userRoles).values({ userId: input.userId, roleId: role.id, grantedBy: input.grantedBy, }); await this.afterWrite(); return true; } async switchUserRoleName(input: { userId: string; addRoleName: string; removeRoleName: string; grantedBy: string; }): Promise { const [addRole, removeRole] = await Promise.all([ this.findRoleByName(input.addRoleName), this.findRoleByName(input.removeRoleName), ]); let added = false; let removed = false; if (addRole) { await this.context.drizzle .delete(userRoles) .where( and( eq(userRoles.userId, input.userId), eq(userRoles.roleId, addRole.id), ), ); await this.context.drizzle.insert(userRoles).values({ userId: input.userId, roleId: addRole.id, grantedBy: input.grantedBy, }); added = true; } if (removeRole) { const result = await this.context.drizzle .delete(userRoles) .where( and( eq(userRoles.userId, input.userId), eq(userRoles.roleId, removeRole.id), ), ); removed = rowsAffected(result) > 0; } if (added || removed) { await this.afterWrite(); } return { added, removed }; } async removeRoleFromUser(userId: string, roleId: number): Promise { await this.context.drizzle .delete(userRoles) .where(and(eq(userRoles.userId, userId), eq(userRoles.roleId, roleId))); await this.afterWrite(); } async removeAllRolesFromUser(userId: string): Promise { const result = await this.context.drizzle .delete(userRoles) .where(eq(userRoles.userId, userId)); if (rowsAffected(result) > 0) { await this.afterWrite(); } return rowsAffected(result); } async listUserRoleIds(userId: string): Promise { const rows = await this.context.drizzle .select({ roleId: userRoles.roleId }) .from(userRoles) .where(eq(userRoles.userId, userId)); return rows.map((row) => row.roleId); } async listRoleUserIds(roleId: number): Promise { const rows = await this.context.drizzle .select({ userId: userRoles.userId }) .from(userRoles) .where(eq(userRoles.roleId, roleId)); return rows.map((row) => row.userId); } async listRoleMembers(roleId: number): Promise { return this.context.drizzle .select({ userId: userRoles.userId, username: users.username, grantedAt: userRoles.grantedAt, grantedBy: userRoles.grantedBy, }) .from(userRoles) .innerJoin(users, eq(userRoles.userId, users.id)) .where(eq(userRoles.roleId, roleId)) .orderBy(users.username); } async listUserRolePermissions( userId: string, ): Promise { return this.context.drizzle .select({ permissions: roles.permissions }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where(eq(userRoles.userId, userId)); } async userHasAnyRoleName( userId: string, roleNames: string[], ): Promise { if (roleNames.length === 0) { return false; } const rows = await this.context.drizzle .select({ roleName: roles.name }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where(and(eq(userRoles.userId, userId), inArray(roles.name, roleNames))) .limit(1); return rows.length > 0; } async listUserRoles(userId: string): Promise { return this.context.drizzle .select({ id: userRoles.id, roleId: roles.id, roleName: roles.name, roleDisplayName: roles.displayName, description: roles.description, isSystem: roles.isSystem, grantedAt: userRoles.grantedAt, }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where(eq(userRoles.userId, userId)); } private async afterWrite(): Promise { await this.onWrite?.(); } }