import "server-only"; import { PERMS } from "@/lib/permission-slugs"; import { authorizeHousekeeping } from "../../../foundation/authorization"; import { anyCapability, fail, type HousekeepingQuery, ok, } from "../../../foundation/contracts"; import { createPeoplePage, type ListInput, normalizePeopleListInput, type Page, type PeopleModerationTeamMember, type PeopleStaffApplication, type PeopleTeam, peopleUserHref, toPeopleIsoDate, } from "../models"; interface StaffRows { readonly rows: readonly T[]; readonly total: number; } export interface PeopleStaffAdapters { loadApplications( input: ReturnType, ): Promise>; loadTeams( input: ReturnType, ): Promise>; loadModerationTeam( input: ReturnType, ): Promise>; } export type PeopleStaffQueryInput = | { readonly routeId: "people.staff.applications"; readonly list: ListInput } | { readonly routeId: "people.staff.teams"; readonly list: ListInput } | { readonly routeId: "people.staff.moderation-team"; readonly list: ListInput; }; export type PeopleStaffQueryData = | { readonly kind: "applications"; readonly page: Page; } | { readonly kind: "teams"; readonly page: Page } | { readonly kind: "moderation-team"; readonly page: Page; }; export function createPeopleStaffQuery( adapters: PeopleStaffAdapters, ): HousekeepingQuery { return { id: "people.staff.query", owner: "people", capability: anyCapability( PERMS.USERS_VIEW, PERMS.MODERATION_VIEW, PERMS.MOD_TEAM_VIEW, ), async run(context, input) { const requirement = input.routeId === "people.staff.moderation-team" ? anyCapability(PERMS.MODERATION_VIEW, PERMS.MOD_TEAM_VIEW) : anyCapability(PERMS.USERS_VIEW); const authorization = authorizeHousekeeping(context, requirement); if (!authorization.ok) return authorization; const correlationId = authorization.correlationId; const allowedSorts = input.routeId === "people.staff.applications" ? ["id", "username", "createdAt"] : input.routeId === "people.staff.teams" ? ["id", "name"] : ["id", "username"]; const list = normalizePeopleListInput(input.list, allowedSorts, "id"); try { if (input.routeId === "people.staff.applications") { const result = await adapters.loadApplications(list); return ok( { kind: "applications" as const, page: createPeoplePage(result.rows, result.total, list), }, correlationId, ); } if (input.routeId === "people.staff.teams") { const result = await adapters.loadTeams(list); return ok( { kind: "teams" as const, page: createPeoplePage(result.rows, result.total, list), }, correlationId, ); } const result = await adapters.loadModerationTeam(list); return ok( { kind: "moderation-team" as const, page: createPeoplePage(result.rows, result.total, list), }, correlationId, ); } catch { return fail( "DEPENDENCY_UNAVAILABLE", "errors.housekeeping.dependencyUnavailable", correlationId, ); } }, }; } function resultRows(result: unknown): T[] { if (!Array.isArray(result) || !Array.isArray(result[0])) { throw new Error("invalid People driver result"); } return result[0] as T[]; } const peopleStaffAdapters: PeopleStaffAdapters = { async loadApplications(input) { const [{ sql }, { db }] = await Promise.all([ import("drizzle-orm"), import("@/lib/db"), ]); const pattern = `%${input.search}%`; const where = input.search ? sql`WHERE u.username LIKE ${pattern} OR a.content LIKE ${pattern}` : sql``; const [rowsResult, countResult] = await Promise.all([ db.execute(sql` SELECT a.id, a.user_id AS userId, u.username, a.rank_id AS rankId, a.content, a.created_at AS createdAt FROM website_staff_applications a LEFT JOIN users u ON u.id = a.user_id ${where} ORDER BY ${input.sort === "username" ? sql`u.username` : input.sort === "createdAt" ? sql`a.created_at` : sql`a.id`} ${ input.order === "asc" ? sql`ASC` : sql`DESC` }, a.id ASC LIMIT ${input.pageSize} OFFSET ${input.offset} `), db.execute(sql` SELECT COUNT(*) AS total FROM website_staff_applications a LEFT JOIN users u ON u.id = a.user_id ${where} `), ]); const rows = resultRows<{ id: bigint | number; userId: number; username: string | null; rankId: number; content: string; createdAt: Date | string | null; }>(rowsResult).map((row) => ({ id: String(row.id), userId: Number(row.userId), username: row.username, rankId: Number(row.rankId), content: row.content, createdAt: toPeopleIsoDate(row.createdAt), })); return { rows, total: Number(resultRows<{ total: number }>(countResult)[0]?.total ?? 0), }; }, async loadTeams(input) { const [{ sql }, { db }] = await Promise.all([ import("drizzle-orm"), import("@/lib/db"), ]); const pattern = `%${input.search}%`; const where = input.search ? sql`WHERE rank_name LIKE ${pattern} OR job_description LIKE ${pattern}` : sql``; const [rowsResult, countResult] = await Promise.all([ db.execute(sql` SELECT id, rank_name AS name, hidden_rank AS hidden, badge, job_description AS jobDescription FROM website_teams ${where} ORDER BY ${input.sort === "name" ? sql`rank_name` : sql`id`} ${ input.order === "desc" ? sql`DESC` : sql`ASC` }, id ASC LIMIT ${input.pageSize} OFFSET ${input.offset} `), db.execute(sql`SELECT COUNT(*) AS total FROM website_teams ${where}`), ]); const rows = resultRows<{ id: bigint | number; name: string; hidden: boolean | number; badge: string | null; jobDescription: string | null; }>(rowsResult).map((row) => ({ id: String(row.id), name: row.name, rank: String(row.id), hidden: Boolean(row.hidden), badge: row.badge, jobDescription: row.jobDescription, })); return { rows, total: Number(resultRows<{ total: number }>(countResult)[0]?.total ?? 0), }; }, async loadModerationTeam(input) { const [{ sql }, { db }, { getMinStaffRank }] = await Promise.all([ import("drizzle-orm"), import("@/lib/db"), import("@/lib/admin/min-staff-rank"), ]); const minStaffRank = await getMinStaffRank(); const pattern = `%${input.search}%`; const search = input.search ? sql`AND u.username LIKE ${pattern}` : sql``; const [rowsResult, countResult] = await Promise.all([ db.execute(sql` SELECT u.id, u.username, u.rank, (SELECT COUNT(*) FROM support_tickets c WHERE c.mod_id = u.id) AS openCfh, (SELECT COUNT(*) FROM website_tickets t WHERE t.assignee_id = u.id) AS openTickets, (SELECT COUNT(*) FROM admin_audit_log a WHERE a.user_id = u.id AND a.action LIKE 'mod_%') AS actionCount FROM users u WHERE u.rank >= ${minStaffRank} ${search} ORDER BY ${input.sort === "username" ? sql`u.username` : sql`u.id`} ${ input.order === "desc" ? sql`DESC` : sql`ASC` }, u.id ASC LIMIT ${input.pageSize} OFFSET ${input.offset} `), db.execute(sql` SELECT COUNT(*) AS total FROM users u WHERE u.rank >= ${minStaffRank} ${search} `), ]); const rows = resultRows<{ id: number; username: string; rank: number; openCfh: number; openTickets: number; actionCount: number; }>(rowsResult).map((row) => ({ id: Number(row.id), username: row.username, rank: Number(row.rank), openCfh: Number(row.openCfh), openTickets: Number(row.openTickets), actionCount: Number(row.actionCount), href: peopleUserHref(Number(row.id)), })); return { rows, total: Number(resultRows<{ total: number }>(countResult)[0]?.total ?? 0), }; }, }; export const peopleStaffQuery = createPeopleStaffQuery(peopleStaffAdapters);