/** * Helper functions for org procedures * Provides org lookup, membership verification, and role checks */ import type { DB, OrgRole } from "@reviq/db-schema"; import type { Kysely } from "kysely"; import { ORPCError } from "@orpc/server"; // ===== Types ===== /** Org info returned from lookup */ export interface OrgInfo { id: number; slug: string; displayName: string; logoUrl: string | null; createdAt: Date; } /** User's membership in an org */ export interface OrgMembership { id: number; role: OrgRole; createdAt: Date; } // ===== Role Hierarchy ===== const ROLE_LEVELS: Record = { owner: 3, admin: 2, member: 1, }; // ===== Helper Functions ===== /** * Look up an org by slug * @throws NOT_FOUND if org doesn't exist */ export async function lookupOrgBySlug( db: Kysely, slug: string, ): Promise { const org = await db .selectFrom("orgs") .select(["id", "slug", "display_name", "logo_url", "created_at"]) .where("slug", "=", slug) .executeTakeFirst(); if (!org) { throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); } return { id: org.id, slug: org.slug, displayName: org.display_name, logoUrl: org.logo_url, createdAt: org.created_at, }; } /** * Get a user's membership in an org * @throws FORBIDDEN if user is not a member */ export async function getMembership( db: Kysely, orgId: number, userId: number, ): Promise { const membership = await db .selectFrom("org_members") .select(["id", "role", "created_at"]) .where("org_id", "=", orgId) .where("user_id", "=", userId) .executeTakeFirst(); if (!membership) { throw new ORPCError("FORBIDDEN", { message: "You are not a member of this organization", }); } return { id: membership.id, role: membership.role, createdAt: membership.created_at, }; } /** * Verify user has the required role or higher * Role hierarchy: owner > admin > member * @throws FORBIDDEN if user lacks required role */ export function requireRole( membership: OrgMembership, requiredRole: OrgRole, ): void { if (ROLE_LEVELS[membership.role] < ROLE_LEVELS[requiredRole]) { throw new ORPCError("FORBIDDEN", { message: "Insufficient permissions" }); } } /** * Count the number of owners in an org * Used to prevent removing/demoting the last owner */ export async function countOwners( db: Kysely, orgId: number, ): Promise { const result = await db .selectFrom("org_members") .select((eb) => eb.fn.countAll().as("count")) .where("org_id", "=", orgId) .where("role", "=", "owner") .executeTakeFirstOrThrow(); // PostgreSQL COUNT returns bigint (string), convert to number return Number(result.count); }