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}\`` }