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< string, (value: string) => 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, }; }