import { Prisma } from "@/generated/prisma/client"; const VALID_KEY = /^[a-z][a-z0-9_]{0,63}$/; export const RANK_GENERAL_FIELDS = new Set([ "rank_name", "hidden_rank", "badge", "job_description", "staff_color", "staff_background", "level", "room_effect", "log_commands", "prefix", "prefix_color", "auto_credits_amount", "auto_pixels_amount", "auto_gotw_amount", "auto_points_amount", ]); export interface EmulatorRankSummary { id: number; rank_name: string; badge: string; level: number; prefix: string; prefix_color: string; hidden_rank: string; } export interface EmulatorRankForEdit extends EmulatorRankSummary { log_commands: string; room_effect: number; auto_credits_amount: number; auto_pixels_amount: number; auto_gotw_amount: number; auto_points_amount: number; permissions: Record; permissionMaxValues: Record; } type RawDb = { $queryRaw: (query: Prisma.Sql) => Promise; $executeRaw: (query: Prisma.Sql) => Promise; $transaction: (queries: Promise[]) => Promise; }; export function getRankPermissionColumn(rankId: number) { if (!Number.isInteger(rankId) || rankId <= 0 || rankId > 255) { throw new Error("Invalid rank id"); } return `rank_${rankId}`; } export function toPermissionValue(value: string | number) { const parsed = typeof value === "number" ? value : Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed < 0) return 0; return Math.min(Math.trunc(parsed), 255); } export function splitRankUpdateFields(fields: Record) { const rankFields: Record = {}; const permissionFields: Record = {}; for (const [key, value] of Object.entries(fields)) { if (key === "id" || !VALID_KEY.test(key)) continue; if (RANK_GENERAL_FIELDS.has(key)) { rankFields[key] = value; } else { permissionFields[key] = toPermissionValue(value); } } return { rankFields, permissionFields }; } export async function fetchEmulatorRankSummaries(db: RawDb): Promise { const rows = await db.$queryRaw[]>( Prisma.sql` SELECT id, rank_name, badge, level, prefix, prefix_color, hidden_rank FROM permission_ranks ORDER BY id ASC `, ); return rows.map((row) => ({ id: Number(row.id), rank_name: String(row.rank_name ?? ""), badge: String(row.badge ?? ""), level: Number(row.level ?? 1), prefix: String(row.prefix ?? ""), prefix_color: String(row.prefix_color ?? ""), hidden_rank: String(row.hidden_rank ?? "0"), })); } export async function fetchEmulatorRankForEdit( db: RawDb, rankId: number, ): Promise { const rankRows = await db.$queryRaw[]>( Prisma.sql` SELECT id, rank_name, badge, level, prefix, prefix_color, hidden_rank, log_commands, room_effect, auto_credits_amount, auto_pixels_amount, auto_gotw_amount, auto_points_amount FROM permission_ranks WHERE id = ${rankId} `, ); if (rankRows.length === 0) return null; const rankColumn = getRankPermissionColumn(rankId); const permissionRows = await db.$queryRaw< { permission_key: string; max_value: number; value: number | string | null }[] >( Prisma.sql` SELECT permission_key, max_value, ${Prisma.raw(quoteIdentifier(rankColumn))} AS value FROM permission_definitions ORDER BY permission_key ASC `, ); const permissions: Record = {}; const permissionMaxValues: Record = {}; for (const row of permissionRows) { permissions[row.permission_key] = String(row.value ?? 0); permissionMaxValues[row.permission_key] = Number(row.max_value ?? 1); } const row = rankRows[0]; return { id: Number(row.id), rank_name: String(row.rank_name ?? ""), badge: String(row.badge ?? ""), level: Number(row.level ?? 1), prefix: String(row.prefix ?? ""), prefix_color: String(row.prefix_color ?? ""), hidden_rank: String(row.hidden_rank ?? "0"), log_commands: String(row.log_commands ?? "0"), room_effect: Number(row.room_effect ?? 0), auto_credits_amount: Number(row.auto_credits_amount ?? 0), auto_pixels_amount: Number(row.auto_pixels_amount ?? 0), auto_gotw_amount: Number(row.auto_gotw_amount ?? 0), auto_points_amount: Number(row.auto_points_amount ?? 0), permissions, permissionMaxValues, }; } export async function createEmulatorRank( db: RawDb, data: { rank_name: string; level: number }, ): Promise { const rows = await db.$queryRaw<{ next_id: number }[]>( Prisma.sql`SELECT COALESCE(MAX(id), 0) + 1 AS next_id FROM permission_ranks`, ); const id = Number(rows[0]?.next_id ?? 1); await ensurePermissionRankColumn(db, id); await resetPermissionRankColumn(db, id); await db.$transaction([ db.$executeRaw( Prisma.sql` INSERT INTO permission_ranks (id, rank_name, level) VALUES (${id}, ${data.rank_name}, ${data.level}) `, ), db.$executeRaw( Prisma.sql` INSERT INTO permissions (id, rank_name, level) VALUES (${id}, ${data.rank_name}, ${data.level}) ON DUPLICATE KEY UPDATE rank_name = VALUES(rank_name), level = VALUES(level) `, ), ]); return id; } export async function deleteEmulatorRank(db: RawDb, rankId: number) { await resetPermissionRankColumn(db, rankId); await db.$transaction([ db.$executeRaw(Prisma.sql`DELETE FROM permission_ranks WHERE id = ${rankId}`), db.$executeRaw(Prisma.sql`DELETE FROM permissions WHERE id = ${rankId}`), ]); } export async function updateEmulatorRank(db: RawDb, rankId: number, fields: Record) { const { rankFields, permissionFields } = splitRankUpdateFields(fields); if (Object.keys(rankFields).length > 0) { await updateTableFields(db, "permission_ranks", rankId, rankFields); await updateLegacyPermissionFields(db, rankId, rankFields); } if (Object.keys(permissionFields).length > 0) { await updatePermissionDefinitionValues(db, rankId, permissionFields); await updateLegacyPermissionFields(db, rankId, permissionFields); } } async function updateTableFields( db: RawDb, table: string, rankId: number, fields: Record, ) { const assignments = Object.entries(fields).map( ([key, value]) => Prisma.sql`${Prisma.raw(quoteIdentifier(key))} = ${value}`, ); return db.$executeRaw( Prisma.sql` UPDATE ${Prisma.raw(quoteIdentifier(table))} SET ${Prisma.join(assignments, ", ")} WHERE id = ${rankId} `, ); } async function updatePermissionDefinitionValues( db: RawDb, rankId: number, permissionFields: Record, ) { const rankColumn = getRankPermissionColumn(rankId); const entries = Object.entries(permissionFields); const cases = entries.map(([key, value]) => Prisma.sql`WHEN ${key} THEN LEAST(${value}, max_value)`); const keys = entries.map(([key]) => key); return db.$executeRaw( Prisma.sql` UPDATE permission_definitions SET ${Prisma.raw(quoteIdentifier(rankColumn))} = CASE permission_key ${Prisma.join(cases, " ")} ELSE ${Prisma.raw(quoteIdentifier(rankColumn))} END WHERE permission_key IN (${Prisma.join(keys)}) `, ); } async function updateLegacyPermissionFields( db: RawDb, rankId: number, fields: Record, ) { const columns = await getTableColumns(db, "permissions"); const existingFields = Object.fromEntries(Object.entries(fields).filter(([key]) => columns.has(key))); if (Object.keys(existingFields).length === 0) return 0; return updateTableFields(db, "permissions", rankId, existingFields); } async function ensurePermissionRankColumn(db: RawDb, rankId: number) { const rankColumn = getRankPermissionColumn(rankId); const exists = await db.$queryRaw<{ c: bigint | number }[]>( Prisma.sql` SELECT COUNT(*) AS c FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'permission_definitions' AND column_name = ${rankColumn} `, ); if (Number(exists[0]?.c ?? 0) > 0) return; await db.$executeRaw( Prisma.sql` ALTER TABLE permission_definitions ADD COLUMN ${Prisma.raw(quoteIdentifier(rankColumn))} TINYINT UNSIGNED NOT NULL DEFAULT 0 `, ); } async function resetPermissionRankColumn(db: RawDb, rankId: number) { const rankColumn = getRankPermissionColumn(rankId); await db.$executeRaw( Prisma.sql` UPDATE permission_definitions SET ${Prisma.raw(quoteIdentifier(rankColumn))} = 0 `, ); } async function getTableColumns(db: RawDb, table: string) { const rows = await db.$queryRaw<{ column_name: string }[]>( Prisma.sql` SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ${table} `, ); return new Set(rows.map((row) => row.column_name)); } function quoteIdentifier(identifier: string) { if (!VALID_KEY.test(identifier)) throw new Error("Invalid identifier"); return `\`${identifier}\``; }