import { redirect } from 'next/navigation' import { calcPagination, parseListParams } from './admin-helpers' import { canAccess, getAdminContext, type PermissionSet } from './permissions' import { prisma } from './prisma' // biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code type PrismaModel = any interface AdminListConfig { /** Permission slug required to view this page */ permission: string /** Prisma model name (e.g. 'user', 'ban', 'room') */ model: string /** Search field paths for OR filter. Use dot notation for nested relations (e.g. 'owner.username') */ searchFields: string[] /** Default sort when no sort param provided. Default: { id: 'desc' } */ defaultSort?: Record /** Default items per page. Default: 20 */ defaultPerPage?: number /** Prisma include clause for relations */ include?: Record /** Prisma select clause (mutually exclusive with include) */ select?: Record /** Fixed WHERE conditions merged with search (e.g. { online: '1' }) */ baseWhere?: Record /** * Map URL ?filter_* params to Prisma WHERE conditions. Each entry receives * the raw string value from the URL and returns the WHERE fragment to merge. * Returning null/undefined drops the filter. * * @example * filterMap: { * filter_rank: (v) => ({ rank: { gte: Number(v) } }), * filter_online: (v) => ({ online: v }), * } */ filterMap?: Record Record | null | undefined> /** Transform raw Prisma row to the shape needed by the table component */ mapRow?: (row: PrismaModel) => TRow } interface AdminListResult { rows: TRow[] total: number page: number perPage: number lastPage: number locale: string permissions: PermissionSet rank: number } /** * Build a nested Prisma WHERE condition from a dot-notation field path. * e.g. 'owner.username' => { owner: { username: { contains: search } } } * e.g. 'username' => { username: { contains: search } } */ function buildFieldCondition(fieldPath: string, search: string): Record { const parts = fieldPath.split('.') if (parts.length === 1) { return { [parts[0]]: { contains: search } } } // Build nested object from right to left let result: Record = { [parts[parts.length - 1]]: { contains: search } } for (let i = parts.length - 2; i >= 0; i--) { result = { [parts[i]]: result } } return result } /** * Generic admin list data fetcher. * Centralizes the common pattern: auth check, param parsing, WHERE/orderBy construction, * Prisma query with pagination, and row mapping. */ export async function fetchAdminList( config: AdminListConfig, paramsPromise: Promise<{ locale: string }>, searchParamsPromise: Promise>, ): Promise> { const { locale } = await paramsPromise const { session, permissions } = await getAdminContext() if (!canAccess(permissions, config.permission, session.user.rank)) { redirect('/admin') } const rawParams = await searchParamsPromise const sp = new URLSearchParams(rawParams) const parsed = parseListParams(sp) const perPage = config.defaultPerPage ? Number(rawParams.perPage) || config.defaultPerPage : parsed.perPage const { search, page, sort, order } = parsed // Build WHERE clause const searchWhere = search && config.searchFields.length > 0 ? { OR: config.searchFields.map((field) => buildFieldCondition(field, search)) } : {} // Apply URL filter_* params via filterMap const filterWhere: Record = {} if (config.filterMap) { for (const [paramKey, build] of Object.entries(config.filterMap)) { const value = rawParams[paramKey] if (!value) continue const fragment = build(value) if (fragment) Object.assign(filterWhere, fragment) } } const where = { ...(config.baseWhere ?? {}), ...filterWhere, ...searchWhere, } // Build orderBy const orderBy = sort ? { [sort]: order } : (config.defaultSort ?? { id: 'desc' as const }) // Access Prisma model dynamically const delegate = (prisma as PrismaModel)[config.model] const queryArgs: PrismaModel = { where, orderBy, skip: (page - 1) * perPage, take: perPage, } if (config.include) queryArgs.include = config.include if (config.select) queryArgs.select = config.select const [rawRows, total] = await Promise.all([ delegate.findMany(queryArgs), delegate.count({ where }), ]) const pagination = calcPagination(total, page, perPage) const rows = config.mapRow ? rawRows.map(config.mapRow) : rawRows return { rows, ...pagination, locale, permissions, rank: session.user.rank, } }