From 580972c0a03f6fa4c229a0ed13ff4dcbb9e810dd Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 1 Aug 2026 14:15:32 +0200 Subject: [PATCH] refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (3) Co-authored-by: Cursor --- src/app/admin/guilds/[id]/page.tsx | 93 +++++----- src/app/admin/guilds/page.tsx | 80 ++++----- src/app/admin/help-questions/[id]/page.tsx | 16 +- src/app/admin/help-questions/page.tsx | 19 ++- src/app/admin/help-tickets/[id]/page.tsx | 110 +++++++----- src/app/admin/help-tickets/page.tsx | 171 +++++++++++-------- src/app/admin/housekeeping/page.tsx | 62 ++++--- src/app/admin/ip/page.tsx | 21 ++- src/app/admin/items/[id]/page.tsx | 9 +- src/app/admin/layout.tsx | 14 +- src/app/admin/logs/_lib/load-ignored-logs.ts | 68 ++++---- src/app/admin/logs/page.tsx | 93 ++++++---- src/app/admin/maintenance/page.tsx | 11 +- src/app/admin/marketplace/page.tsx | 46 +++-- src/app/admin/moderation/cfh/[id]/page.tsx | 59 ++++--- src/app/admin/moderation/cfh/page.tsx | 119 +++++++------ src/app/admin/moderation/page.tsx | 87 ++++++---- src/app/admin/moderation/team/page.tsx | 96 +++++++---- src/app/admin/navigation/page.tsx | 64 ++++--- src/app/admin/page.tsx | 66 ++++--- src/app/admin/photos/page.tsx | 31 ++-- src/app/admin/polls/[id]/page.tsx | 13 +- src/app/admin/radio/api-keys/page.tsx | 32 ++-- src/app/admin/radio/autodj/page.tsx | 35 ++-- src/app/admin/radio/banners/page.tsx | 29 ++-- src/app/admin/radio/history/page.tsx | 37 ++-- src/app/admin/radio/moderation/page.tsx | 25 +-- src/app/admin/radio/page.tsx | 72 ++++---- src/app/admin/radio/ranks/page.tsx | 23 +-- src/app/admin/radio/settings/page.tsx | 12 +- src/app/admin/rare-values/page.tsx | 27 +-- src/app/admin/rooms/[id]/furni/page.tsx | 33 ++-- src/app/admin/rooms/_lib/load-room.ts | 26 ++- src/app/admin/settings/page.tsx | 12 +- src/app/admin/shop/[id]/page.tsx | 15 +- src/app/admin/shop/page.tsx | 36 ++-- src/app/admin/sounds/page.tsx | 40 +++-- src/app/admin/subscriptions/page.tsx | 39 +++-- src/app/admin/tags/page.tsx | 31 ++-- src/app/admin/teams/page.tsx | 25 +-- 40 files changed, 1089 insertions(+), 808 deletions(-) diff --git a/src/app/admin/guilds/[id]/page.tsx b/src/app/admin/guilds/[id]/page.tsx index e1c93934..61bac825 100644 --- a/src/app/admin/guilds/[id]/page.tsx +++ b/src/app/admin/guilds/[id]/page.tsx @@ -1,11 +1,12 @@ +import { asc, count, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { disbandGuild } from "@/actions/admin-guilds"; import { Button } from "@/components/ui/button"; +import { db, Guilds, GuildsForumsThreads, GuildsMembers, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function AdminGuildDetailPage({ params, @@ -23,49 +24,65 @@ export default async function AdminGuildDetailPage({ const id = Number(rawId); if (!Number.isFinite(id) || id <= 0) notFound(); - const guild = await prisma.guilds.findUnique({ - where: { id }, - select: { - id: true, - name: true, - description: true, - userId: true, - roomId: true, - badge: true, - dateCreated: true, - state: true, - forum: true, - }, - }); + const [guild] = await db + .select({ + id: Guilds.id, + name: Guilds.name, + description: Guilds.description, + userId: Guilds.userId, + roomId: Guilds.roomId, + badge: Guilds.badge, + dateCreated: Guilds.dateCreated, + state: Guilds.state, + forum: Guilds.forum, + }) + .from(Guilds) + .where(eq(Guilds.id, id)) + .limit(1); if (!guild) notFound(); - const [owner, members, memberCount, threadCount] = await Promise.all([ - prisma.user - .findUnique({ - where: { id: guild.userId }, - select: { id: true, username: true }, - }) - .catch(() => null), - prisma.guildsMembers - .findMany({ - where: { guildId: id }, - orderBy: [{ levelId: "asc" }, { memberSince: "asc" }], - take: 100, - select: { userId: true, levelId: true, memberSince: true }, - }) - .catch(() => []), - prisma.guildsMembers.count({ where: { guildId: id } }).catch(() => 0), - prisma.guildsForumsThreads.count({ where: { guildId: id } }).catch(() => 0), - ]); + const [ownerRows, members, memberCountRows, threadCountRows] = + await Promise.all([ + db + .select({ id: User.id, username: User.username }) + .from(User) + .where(eq(User.id, guild.userId)) + .limit(1) + .catch(() => []), + db + .select({ + userId: GuildsMembers.userId, + levelId: GuildsMembers.levelId, + memberSince: GuildsMembers.memberSince, + }) + .from(GuildsMembers) + .where(eq(GuildsMembers.guildId, id)) + .orderBy(asc(GuildsMembers.levelId), asc(GuildsMembers.memberSince)) + .limit(100) + .catch(() => []), + db + .select({ total: count() }) + .from(GuildsMembers) + .where(eq(GuildsMembers.guildId, id)) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(GuildsForumsThreads) + .where(eq(GuildsForumsThreads.guildId, id)) + .catch(() => [{ total: 0 }]), + ]); + + const owner = ownerRows[0] ?? null; + const memberCount = Number(memberCountRows[0]?.total ?? 0); + const threadCount = Number(threadCountRows[0]?.total ?? 0); const memberUserIds = members.map((m) => m.userId).filter((uid) => uid > 0); const memberUsers = memberUserIds.length > 0 - ? await prisma.user - .findMany({ - where: { id: { in: memberUserIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, memberUserIds)) .catch(() => []) : []; const usernameById = new Map(memberUsers.map((u) => [u.id, u.username])); diff --git a/src/app/admin/guilds/page.tsx b/src/app/admin/guilds/page.tsx index b6c5d676..10cc2997 100644 --- a/src/app/admin/guilds/page.tsx +++ b/src/app/admin/guilds/page.tsx @@ -1,3 +1,4 @@ +import { count, desc, eq, inArray, like, or } from "drizzle-orm"; import { Shield } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -7,9 +8,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db, Guilds, GuildsMembers, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function AdminGuildsPage({ searchParams, @@ -40,34 +41,36 @@ export default async function AdminGuildsPage({ try { const where = q - ? { - OR: [ - { name: { contains: q } }, - { description: { contains: q } }, - ...(Number.isFinite(Number(q)) && Number(q) > 0 - ? [{ id: Number(q) }] - : []), - ], - } - : {}; + ? or( + like(Guilds.name, `%${q}%`), + like(Guilds.description, `%${q}%`), + ...(Number.isFinite(Number(q)) && Number(q) > 0 + ? [eq(Guilds.id, Number(q))] + : []), + ) + : undefined; - total = await prisma.guilds.count({ where }); + const [totalRow] = await db + .select({ total: count() }) + .from(Guilds) + .where(where); + total = Number(totalRow?.total ?? 0); const pagination = calcPagination(total, parsed.page, parsed.perPage); - rows = await prisma.guilds.findMany({ - where, - select: { - id: true, - name: true, - description: true, - userId: true, - badge: true, - dateCreated: true, - roomId: true, - }, - orderBy: { id: "desc" }, - skip: pagination.offset, - take: pagination.perPage, - }); + rows = await db + .select({ + id: Guilds.id, + name: Guilds.name, + description: Guilds.description, + userId: Guilds.userId, + badge: Guilds.badge, + dateCreated: Guilds.dateCreated, + roomId: Guilds.roomId, + }) + .from(Guilds) + .where(where) + .orderBy(desc(Guilds.id)) + .limit(pagination.perPage) + .offset(pagination.offset); } catch { loadError = true; } @@ -80,27 +83,28 @@ export default async function AdminGuildsPage({ const [owners, memberGroups] = await Promise.all([ ownerIds.length - ? prisma.user - .findMany({ - where: { id: { in: ownerIds } }, - select: { id: true, username: true }, - }) + ? db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, ownerIds)) .catch(() => []) : Promise.resolve([]), guildIds.length - ? prisma.guildsMembers - .groupBy({ - by: ["guildId"], - where: { guildId: { in: guildIds } }, - _count: { _all: true }, + ? db + .select({ + guildId: GuildsMembers.guildId, + total: count(), }) + .from(GuildsMembers) + .where(inArray(GuildsMembers.guildId, guildIds)) + .groupBy(GuildsMembers.guildId) .catch(() => []) : Promise.resolve([]), ]); const ownerById = new Map(owners.map((u) => [u.id, u.username])); const membersByGuild = new Map( - memberGroups.map((g) => [g.guildId, g._count._all]), + memberGroups.map((g) => [g.guildId, Number(g.total)]), ); return ( diff --git a/src/app/admin/help-questions/[id]/page.tsx b/src/app/admin/help-questions/[id]/page.tsx index 306945e4..f88f3648 100644 --- a/src/app/admin/help-questions/[id]/page.tsx +++ b/src/app/admin/help-questions/[id]/page.tsx @@ -1,10 +1,11 @@ +import { eq } from "drizzle-orm"; import { HelpCircle } from "lucide-react"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help"; import { Button } from "@/components/ui/button"; +import { db, WebsiteHelpCenterCategories } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function EditHelpQuestion({ params, @@ -20,13 +21,14 @@ export default async function EditHelpQuestion({ const { id } = await params; const { error } = await searchParams; - let entry: Awaited< - ReturnType - > = null; + let entry: typeof WebsiteHelpCenterCategories.$inferSelect | null = null; try { - entry = await prisma.websiteHelpCenterCategories.findUnique({ - where: { id: BigInt(id) }, - }); + const [row] = await db + .select() + .from(WebsiteHelpCenterCategories) + .where(eq(WebsiteHelpCenterCategories.id, BigInt(id))) + .limit(1); + entry = row ?? null; } catch { notFound(); } diff --git a/src/app/admin/help-questions/page.tsx b/src/app/admin/help-questions/page.tsx index 381e6c1d..409fd14f 100644 --- a/src/app/admin/help-questions/page.tsx +++ b/src/app/admin/help-questions/page.tsx @@ -1,10 +1,11 @@ +import { asc } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, WebsiteHelpCenterCategories } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function AdminHelpQuestions() { const { session, permissions } = await getAdminContext(); @@ -13,14 +14,16 @@ export default async function AdminHelpQuestions() { } const t = await getTranslations("pages.admin.helpCenter"); - let entries: Awaited< - ReturnType - > = []; + let entries: (typeof WebsiteHelpCenterCategories.$inferSelect)[] = []; try { - entries = await prisma.websiteHelpCenterCategories.findMany({ - orderBy: [{ position: "asc" }, { id: "asc" }], - take: 500, - }); + entries = await db + .select() + .from(WebsiteHelpCenterCategories) + .orderBy( + asc(WebsiteHelpCenterCategories.position), + asc(WebsiteHelpCenterCategories.id), + ) + .limit(500); } catch { entries = []; } diff --git a/src/app/admin/help-tickets/[id]/page.tsx b/src/app/admin/help-tickets/[id]/page.tsx index 7c8551e8..663a7947 100644 --- a/src/app/admin/help-tickets/[id]/page.tsx +++ b/src/app/admin/help-tickets/[id]/page.tsx @@ -1,11 +1,19 @@ +import { and, asc, eq, gt, inArray, or } from "drizzle-orm"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; import { positiveBigInt } from "@/lib/api"; -import { activeBanWhere } from "@/lib/bans"; +import { unixNow } from "@/lib/bans"; +import { + Ban, + db, + User, + WebsiteHelpCenterCategories, + WebsiteHelpCenterTicketReplies, + WebsiteHelpCenterTickets, +} from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { AdminHelpTicketDetail } from "./admin-help-ticket-detail"; export default async function AdminHelpTicketDetailPage({ @@ -23,32 +31,33 @@ export default async function AdminHelpTicketDetailPage({ const ticketId = positiveBigInt(id); if (!ticketId) notFound(); - const ticket = await prisma.websiteHelpCenterTickets.findUnique({ - where: { id: ticketId }, - select: { - id: true, - userId: true, - categoryId: true, - title: true, - content: true, - open: true, - createdAt: true, - updatedAt: true, - }, - }); + const [ticket] = await db + .select({ + id: WebsiteHelpCenterTickets.id, + userId: WebsiteHelpCenterTickets.userId, + categoryId: WebsiteHelpCenterTickets.categoryId, + title: WebsiteHelpCenterTickets.title, + content: WebsiteHelpCenterTickets.content, + open: WebsiteHelpCenterTickets.open, + createdAt: WebsiteHelpCenterTickets.createdAt, + updatedAt: WebsiteHelpCenterTickets.updatedAt, + }) + .from(WebsiteHelpCenterTickets) + .where(eq(WebsiteHelpCenterTickets.id, ticketId)) + .limit(1); if (!ticket) notFound(); - const replies = await prisma.websiteHelpCenterTicketReplies.findMany({ - where: { ticketId: ticket.id }, - orderBy: { id: "asc" }, - select: { - id: true, - userId: true, - content: true, - createdAt: true, - }, - }); + const replies = await db + .select({ + id: WebsiteHelpCenterTicketReplies.id, + userId: WebsiteHelpCenterTicketReplies.userId, + content: WebsiteHelpCenterTicketReplies.content, + createdAt: WebsiteHelpCenterTicketReplies.createdAt, + }) + .from(WebsiteHelpCenterTicketReplies) + .where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticket.id)) + .orderBy(asc(WebsiteHelpCenterTicketReplies.id)); const authorIds = [ ...new Set([ @@ -57,32 +66,43 @@ export default async function AdminHelpTicketDetailPage({ ]), ]; - const [users, category, activeBan] = await Promise.all([ + const now = unixNow(); + const [users, categoryRows, activeBanRows] = await Promise.all([ authorIds.length > 0 - ? prisma.user.findMany({ - where: { id: { in: authorIds } }, - select: { - id: true, - username: true, - rank: true, - mail: true, - }, - }) + ? db + .select({ + id: User.id, + username: User.username, + rank: User.rank, + mail: User.mail, + }) + .from(User) + .where(inArray(User.id, authorIds)) : Promise.resolve([]), ticket.categoryId != null - ? prisma.websiteHelpCenterCategories.findUnique({ - where: { id: ticket.categoryId }, - select: { name: true }, - }) - : Promise.resolve(null), + ? db + .select({ name: WebsiteHelpCenterCategories.name }) + .from(WebsiteHelpCenterCategories) + .where(eq(WebsiteHelpCenterCategories.id, ticket.categoryId)) + .limit(1) + : Promise.resolve([]), ticket.userId != null - ? prisma.ban.findFirst({ - where: { userId: ticket.userId, ...activeBanWhere() }, - select: { id: true }, - }) - : Promise.resolve(null), + ? db + .select({ id: Ban.id }) + .from(Ban) + .where( + and( + eq(Ban.userId, ticket.userId), + or(eq(Ban.banExpire, 0), gt(Ban.banExpire, now)), + ), + ) + .limit(1) + : Promise.resolve([]), ]); + const category = categoryRows[0] ?? null; + const activeBan = activeBanRows[0] ?? null; + const userById = new Map(users.map((u) => [u.id, u])); const openerId = ticket.userId; const minStaffRank = await getMinStaffRank(); diff --git a/src/app/admin/help-tickets/page.tsx b/src/app/admin/help-tickets/page.tsx index bf246cb2..966acf29 100644 --- a/src/app/admin/help-tickets/page.tsx +++ b/src/app/admin/help-tickets/page.tsx @@ -1,12 +1,28 @@ +import { + and, + asc, + count, + desc, + eq, + inArray, + like, + or, + type SQL, +} from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { + db, + User, + WebsiteHelpCenterTicketReplies, + WebsiteHelpCenterTickets, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { type HelpTicketRow, HelpTicketsTable } from "./help-tickets-table"; export default async function AdminHelpTicketsPage({ @@ -24,87 +40,95 @@ export default async function AdminHelpTicketsPage({ const parsed = parseListParams(new URLSearchParams(raw)); const statusFilter = raw.filter_status || "open"; - const conditions: any[] = []; + const conditions: SQL[] = []; if (statusFilter === "open") { - conditions.push({ open: true }); + conditions.push(eq(WebsiteHelpCenterTickets.open, true)); } else if (statusFilter === "closed") { - conditions.push({ open: false }); + conditions.push(eq(WebsiteHelpCenterTickets.open, false)); } if (parsed.search.trim()) { const q = parsed.search.trim(); const asId = /^\d+$/.test(q) ? BigInt(q) : null; - const matchingUsers = await prisma.user - .findMany({ - where: { username: { contains: q } }, - select: { id: true }, - take: 50, - }) + const matchingUsers = await db + .select({ id: User.id }) + .from(User) + .where(like(User.username, `%${q}%`)) + .limit(50) .catch(() => []); - conditions.push({ - OR: [ - { title: { contains: q } }, - { content: { contains: q } }, - ...(matchingUsers.length > 0 - ? [{ userId: { in: matchingUsers.map((u) => u.id) } }] - : []), - ...(asId !== null ? [{ id: asId }] : []), - ], - }); + const searchOr = [ + like(WebsiteHelpCenterTickets.title, `%${q}%`), + like(WebsiteHelpCenterTickets.content, `%${q}%`), + ...(matchingUsers.length > 0 + ? [ + inArray( + WebsiteHelpCenterTickets.userId, + matchingUsers.map((u) => u.id), + ), + ] + : []), + ...(asId !== null ? [eq(WebsiteHelpCenterTickets.id, asId)] : []), + ]; + conditions.push(or(...searchOr)!); } - const where: any = - conditions.length === 0 - ? {} - : conditions.length === 1 - ? conditions[0] - : { AND: conditions }; + const where = conditions.length > 0 ? and(...conditions) : undefined; - const SORT_MAP: Record = { - title: { title: "asc" }, - open: { open: "desc" }, - user: { userId: "asc" }, - date: { createdAt: "desc" }, - updated: { updatedAt: "desc" }, - id: { id: "desc" }, - }; + const orderCol = + parsed.sort === "title" + ? WebsiteHelpCenterTickets.title + : parsed.sort === "open" + ? WebsiteHelpCenterTickets.open + : parsed.sort === "user" + ? WebsiteHelpCenterTickets.userId + : parsed.sort === "date" + ? WebsiteHelpCenterTickets.createdAt + : parsed.sort === "id" + ? WebsiteHelpCenterTickets.id + : WebsiteHelpCenterTickets.updatedAt; + const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol); - const baseOrder = SORT_MAP[parsed.sort ?? "updated"] ?? { updatedAt: "desc" }; - const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; - const orderBy = { - [orderField]: parsed.order, - } as any; - - const [total, openCount, closedCount, queues] = await Promise.all([ - prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0), - prisma.websiteHelpCenterTickets - .count({ where: { open: true } }) - .catch(() => 0), - prisma.websiteHelpCenterTickets - .count({ where: { open: false } }) - .catch(() => 0), + const [totalRows, openRows, closedRows, queues] = await Promise.all([ + db + .select({ total: count() }) + .from(WebsiteHelpCenterTickets) + .where(where) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(WebsiteHelpCenterTickets) + .where(eq(WebsiteHelpCenterTickets.open, true)) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(WebsiteHelpCenterTickets) + .where(eq(WebsiteHelpCenterTickets.open, false)) + .catch(() => [{ total: 0 }]), fetchTicketQueueOpenCounts(), ]); + const total = Number(totalRows[0]?.total ?? 0); + const openCount = Number(openRows[0]?.total ?? 0); + const closedCount = Number(closedRows[0]?.total ?? 0); + const pagination = calcPagination(total, parsed.page, parsed.perPage); - const tickets = await prisma.websiteHelpCenterTickets - .findMany({ - where, - orderBy, - skip: pagination.offset, - take: pagination.perPage, - select: { - id: true, - userId: true, - title: true, - open: true, - createdAt: true, - updatedAt: true, - }, + const tickets = await db + .select({ + id: WebsiteHelpCenterTickets.id, + userId: WebsiteHelpCenterTickets.userId, + title: WebsiteHelpCenterTickets.title, + open: WebsiteHelpCenterTickets.open, + createdAt: WebsiteHelpCenterTickets.createdAt, + updatedAt: WebsiteHelpCenterTickets.updatedAt, }) + .from(WebsiteHelpCenterTickets) + .where(where) + .orderBy(orderBy) + .limit(pagination.perPage) + .offset(pagination.offset) .catch(() => []); const ticketIds = tickets.map((ticket) => ticket.id); @@ -118,26 +142,27 @@ export default async function AdminHelpTicketsPage({ const [replyGroups, users] = await Promise.all([ ticketIds.length > 0 - ? prisma.websiteHelpCenterTicketReplies - .groupBy({ - by: ["ticketId"], - where: { ticketId: { in: ticketIds } }, - _count: true, + ? db + .select({ + ticketId: WebsiteHelpCenterTicketReplies.ticketId, + total: count(), }) + .from(WebsiteHelpCenterTicketReplies) + .where(inArray(WebsiteHelpCenterTicketReplies.ticketId, ticketIds)) + .groupBy(WebsiteHelpCenterTicketReplies.ticketId) .catch(() => []) : Promise.resolve([]), userIds.length > 0 - ? prisma.user - .findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) + ? db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) .catch(() => []) : Promise.resolve([]), ]); const replyCountByTicket = new Map( - replyGroups.map((g) => [String(g.ticketId), g._count]), + replyGroups.map((g) => [String(g.ticketId), Number(g.total)]), ); const usernameById = new Map(users.map((u) => [u.id, u.username])); diff --git a/src/app/admin/housekeeping/page.tsx b/src/app/admin/housekeeping/page.tsx index 91ea6853..33828691 100644 --- a/src/app/admin/housekeeping/page.tsx +++ b/src/app/admin/housekeeping/page.tsx @@ -1,8 +1,15 @@ +import { asc, eq } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; +import { + AclModelPermission, + AclPermission, + AclRole, + db, + WebsiteHousekeepingPermissions, +} from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { getAuditLogs } from "@/lib/services/audit"; import { AuditSection, ExportSection } from "./client"; @@ -31,12 +38,14 @@ export default async function AdminHousekeeping({ const sp = await searchParams; const tab = sp.tab ?? "overview"; - type HKPerm = Awaited< - ReturnType - >[0]; - const allPermissions: HKPerm[] = await prisma.websiteHousekeepingPermissions - .findMany({ orderBy: [{ groupName: "asc" }, { permission: "asc" }] }) - .catch(() => []); + const allPermissions = await db + .select() + .from(WebsiteHousekeepingPermissions) + .orderBy( + asc(WebsiteHousekeepingPermissions.groupName), + asc(WebsiteHousekeepingPermissions.permission), + ) + .catch((): (typeof WebsiteHousekeepingPermissions.$inferSelect)[] => []); const ranks = allPermissions.map((p) => p.minRank); const maxRank = ranks.length ? Math.max(...ranks) : 0; @@ -48,34 +57,43 @@ export default async function AdminHousekeeping({ groupCounts[g] = (groupCounts[g] || 0) + 1; } - const rankGroups: Record = {}; + const rankGroups: Record< + string, + (typeof WebsiteHousekeepingPermissions.$inferSelect)[] + > = {}; for (const p of allPermissions) { - if (!rankGroups[p.minRank]) rankGroups[p.minRank] = []; - rankGroups[p.minRank].push(p); + const rk = String(p.minRank); + if (!rankGroups[rk]) rankGroups[rk] = []; + rankGroups[rk].push(p); } - const aclSlugs = await prisma.aclPermission - .findMany({ select: { slug: true } }) + const aclSlugs = await db + .select({ slug: AclPermission.slug }) + .from(AclPermission) .catch(() => [] as { slug: string }[]); const aclSlugSet = new Set(aclSlugs.map((p) => p.slug)); - const aclRolePerms = await prisma.aclModelPermission - .findMany({ - where: { modelType: "Role" }, - include: { - permission: { select: { slug: true } }, - role: { select: { slug: true } }, - }, + const aclRolePerms = await db + .select({ + roleSlug: AclRole.slug, + permissionSlug: AclPermission.slug, }) - .catch(() => []); + .from(AclModelPermission) + .innerJoin( + AclPermission, + eq(AclModelPermission.permissionId, AclPermission.id), + ) + .innerJoin(AclRole, eq(AclModelPermission.modelId, AclRole.id)) + .where(eq(AclModelPermission.modelType, "Role")) + .catch((): { roleSlug: string; permissionSlug: string }[] => []); const aclMinRankBySlug = new Map(); for (const row of aclRolePerms) { - const roleSlug = row.role.slug; + const roleSlug = row.roleSlug; if (!roleSlug.startsWith("rank_")) continue; const rank = Number(roleSlug.slice(5)); if (!Number.isFinite(rank)) continue; - const slug = row.permission.slug; + const slug = row.permissionSlug; const prev = aclMinRankBySlug.get(slug); if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank); } diff --git a/src/app/admin/ip/page.tsx b/src/app/admin/ip/page.tsx index 8c32a610..974f4bd7 100644 --- a/src/app/admin/ip/page.tsx +++ b/src/app/admin/ip/page.tsx @@ -1,3 +1,4 @@ +import { desc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { @@ -8,9 +9,9 @@ import { } from "@/actions/admin-ip"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, WebsiteIpBlacklist, WebsiteIpWhitelist } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type IpRow = { id: bigint; @@ -30,19 +31,21 @@ export default async function AdminIp() { let blacklist: IpRow[] = []; try { - whitelist = await prisma.websiteIpWhitelist.findMany({ - orderBy: { id: "desc" }, - take: 200, - }); + whitelist = await db + .select() + .from(WebsiteIpWhitelist) + .orderBy(desc(WebsiteIpWhitelist.id)) + .limit(200); } catch { whitelist = []; } try { - blacklist = await prisma.websiteIpBlacklist.findMany({ - orderBy: { id: "desc" }, - take: 200, - }); + blacklist = await db + .select() + .from(WebsiteIpBlacklist) + .orderBy(desc(WebsiteIpBlacklist.id)) + .limit(200); } catch { blacklist = []; } diff --git a/src/app/admin/items/[id]/page.tsx b/src/app/admin/items/[id]/page.tsx index 0370e721..64312f2d 100644 --- a/src/app/admin/items/[id]/page.tsx +++ b/src/app/admin/items/[id]/page.tsx @@ -1,9 +1,10 @@ +import { eq } from "drizzle-orm"; import { Package } from "lucide-react"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AdminPageShell } from "@/components/admin/admin-page-shell"; +import { db, ItemsBase } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { ItemsBaseEditForm } from "./items-base-edit-form"; export default async function ItemsBaseDetailPage({ @@ -21,7 +22,11 @@ export default async function ItemsBaseDetailPage({ const id = Number(rawId); if (!Number.isFinite(id) || id <= 0) notFound(); - const item = await prisma.itemsBase.findUnique({ where: { id } }); + const [item] = await db + .select() + .from(ItemsBase) + .where(eq(ItemsBase.id, id)) + .limit(1); if (!item) notFound(); const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 41762864..2d706320 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,3 +1,4 @@ +import { eq } from "drizzle-orm"; import { LogOut } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -15,9 +16,9 @@ import { ADMIN_NAV_CONFIG_KEY, parseAdminNavConfig, } from "@/lib/admin-nav-config"; +import { db, User } from "@/lib/db"; import { setCsrfCookie } from "@/lib/foundation/security"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; // Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be @@ -37,11 +38,12 @@ export default async function AdminLayout({ csrfToken = ""; } if (await siteSettings.getBool("force_staff_2fa", false)) { - const u = await prisma.user - .findUnique({ - where: { id: staff.id }, - select: { twoFactorConfirmedAt: true }, - }) + const u = await db + .select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt }) + .from(User) + .where(eq(User.id, staff.id)) + .limit(1) + .then((rows) => rows[0] ?? null) .catch(() => null); if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired"); } diff --git a/src/app/admin/logs/_lib/load-ignored-logs.ts b/src/app/admin/logs/_lib/load-ignored-logs.ts index 2abe8af7..348985e0 100644 --- a/src/app/admin/logs/_lib/load-ignored-logs.ts +++ b/src/app/admin/logs/_lib/load-ignored-logs.ts @@ -1,7 +1,8 @@ +import { sql } from "drizzle-orm"; import { redirect } from "next/navigation"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type ListParamsInput = { searchParams: Promise>; @@ -25,6 +26,11 @@ function parsePageParams( return { search: parsed.search.trim(), page: parsed.page, perPage }; } +async function rawRows(query: ReturnType): Promise { + const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; + return rows ?? []; +} + export type ChatLogListRow = { id: number; username: string; @@ -51,35 +57,35 @@ export async function loadChatLogList({ username: string | null; }; - const [rawRows, countRows] = search + const [rawRowsResult, countRows] = search ? await Promise.all([ - prisma.$queryRaw` + rawRows(sql` SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username FROM chatlogs_room c LEFT JOIN users u ON u.id = c.user_from_id WHERE c.message LIKE ${like} OR u.username LIKE ${like} ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`, - prisma.$queryRaw<{ count: bigint }[]>` + LIMIT ${perPage} OFFSET ${offset}`), + rawRows<{ count: bigint }>(sql` SELECT COUNT(*) AS count FROM chatlogs_room c LEFT JOIN users u ON u.id = c.user_from_id - WHERE c.message LIKE ${like} OR u.username LIKE ${like}`, + WHERE c.message LIKE ${like} OR u.username LIKE ${like}`), ]) : await Promise.all([ - prisma.$queryRaw` + rawRows(sql` SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username FROM chatlogs_room c LEFT JOIN users u ON u.id = c.user_from_id ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`, - prisma.$queryRaw<{ count: bigint }[]>` - SELECT COUNT(*) AS count FROM chatlogs_room`, + LIMIT ${perPage} OFFSET ${offset}`), + rawRows<{ count: bigint }>(sql` + SELECT COUNT(*) AS count FROM chatlogs_room`), ]); const total = Number(countRows[0]?.count ?? 0); const pagination = calcPagination(total, page, perPage); - const rows: ChatLogListRow[] = rawRows.map((row, index) => ({ + const rows: ChatLogListRow[] = rawRowsResult.map((row, index) => ({ id: pagination.offset + index + 1, username: row.username ?? "-", message: row.message, @@ -116,9 +122,9 @@ export async function loadCommandLogList({ username: string | null; }; - const [rawRows, countRows] = search + const [rawRowsResult, countRows] = search ? await Promise.all([ - prisma.$queryRaw` + rawRows(sql` SELECT c.command, c.params, c.timestamp, u.username AS username FROM commandlogs c LEFT JOIN users u ON u.id = c.user_id @@ -126,29 +132,29 @@ export async function loadCommandLogList({ OR c.params LIKE ${like} OR u.username LIKE ${like} ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`, - prisma.$queryRaw<{ count: bigint }[]>` + LIMIT ${perPage} OFFSET ${offset}`), + rawRows<{ count: bigint }>(sql` SELECT COUNT(*) AS count FROM commandlogs c LEFT JOIN users u ON u.id = c.user_id WHERE c.command LIKE ${like} OR c.params LIKE ${like} - OR u.username LIKE ${like}`, + OR u.username LIKE ${like}`), ]) : await Promise.all([ - prisma.$queryRaw` + rawRows(sql` SELECT c.command, c.params, c.timestamp, u.username AS username FROM commandlogs c LEFT JOIN users u ON u.id = c.user_id ORDER BY c.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`, - prisma.$queryRaw<{ count: bigint }[]>` - SELECT COUNT(*) AS count FROM commandlogs`, + LIMIT ${perPage} OFFSET ${offset}`), + rawRows<{ count: bigint }>(sql` + SELECT COUNT(*) AS count FROM commandlogs`), ]); const total = Number(countRows[0]?.count ?? 0); const pagination = calcPagination(total, page, perPage); - const rows: CommandLogListRow[] = rawRows.map((row, index) => ({ + const rows: CommandLogListRow[] = rawRowsResult.map((row, index) => ({ id: pagination.offset + index + 1, username: row.username ?? "-", command: row.command, @@ -185,9 +191,9 @@ export async function loadTradeLogList({ userTwoName: string | null; }; - const [rawRows, countRows] = search + const [rawRowsResult, countRows] = search ? await Promise.all([ - prisma.$queryRaw` + rawRows(sql` SELECT t.id, t.timestamp, u1.username AS userOneName, u2.username AS userTwoName FROM room_trade_log t @@ -195,30 +201,30 @@ export async function loadTradeLogList({ LEFT JOIN users u2 ON u2.id = t.user_two_id WHERE u1.username LIKE ${like} OR u2.username LIKE ${like} ORDER BY t.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`, - prisma.$queryRaw<{ count: bigint }[]>` + LIMIT ${perPage} OFFSET ${offset}`), + rawRows<{ count: bigint }>(sql` SELECT COUNT(*) AS count FROM room_trade_log t LEFT JOIN users u1 ON u1.id = t.user_one_id LEFT JOIN users u2 ON u2.id = t.user_two_id - WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`, + WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`), ]) : await Promise.all([ - prisma.$queryRaw` + rawRows(sql` SELECT t.id, t.timestamp, u1.username AS userOneName, u2.username AS userTwoName FROM room_trade_log t LEFT JOIN users u1 ON u1.id = t.user_one_id LEFT JOIN users u2 ON u2.id = t.user_two_id ORDER BY t.timestamp DESC - LIMIT ${perPage} OFFSET ${offset}`, - prisma.$queryRaw<{ count: bigint }[]>` - SELECT COUNT(*) AS count FROM room_trade_log`, + LIMIT ${perPage} OFFSET ${offset}`), + rawRows<{ count: bigint }>(sql` + SELECT COUNT(*) AS count FROM room_trade_log`), ]); const total = Number(countRows[0]?.count ?? 0); const pagination = calcPagination(total, page, perPage); - const rows: TradeLogListRow[] = rawRows.map((row) => ({ + const rows: TradeLogListRow[] = rawRowsResult.map((row) => ({ id: row.id, userOneName: row.userOneName ?? "-", userTwoName: row.userTwoName ?? "-", diff --git a/src/app/admin/logs/page.tsx b/src/app/admin/logs/page.tsx index 9209124f..06154689 100644 --- a/src/app/admin/logs/page.tsx +++ b/src/app/admin/logs/page.tsx @@ -1,20 +1,29 @@ +import { + and, + asc, + count, + desc, + inArray, + like, + type SQL, + sql, +} from "drizzle-orm"; import { ClipboardList } from "lucide-react"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { StatusCard } from "@/components/admin/dashboard"; -import { buildStaffActivityWhere } from "@/lib/admin/log-filters"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db, StaffActivities, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { StaffActivitiesTable } from "./staff-activities-table"; -const SORT_MAP: Record = { - when: "createdAt", - staff: "userId", - action: "action", -}; +const SORT_MAP = { + when: StaffActivities.createdAt, + staff: StaffActivities.userId, + action: StaffActivities.action, +} as const; export default async function AdminStaffActivities({ searchParams, @@ -30,36 +39,49 @@ export default async function AdminStaffActivities({ const raw = await searchParams; const parsed = parseListParams(new URLSearchParams(raw)); const authorizationOnly = raw.filter_authorization === "1"; - const whereClause = buildStaffActivityWhere({ - q: parsed.search, - authorizationOnly, - }); - const sortField = SORT_MAP[parsed.sort ?? ""] ?? "createdAt"; + const conditions: SQL[] = []; + if (parsed.search.trim()) { + const q = parsed.search.trim(); + conditions.push( + sql`(${like(StaffActivities.action, `%${q}%`)} OR ${like(StaffActivities.description, `%${q}%`)} OR ${like(StaffActivities.ipAddress, `%${q}%`)})`, + ); + } + if (authorizationOnly) { + conditions.push(like(StaffActivities.action, "permission.%")); + } + const where = conditions.length > 0 ? and(...conditions) : undefined; - const total = await prisma.staffActivities - .count({ where: whereClause }) - .catch(() => 0); + const sortCol = + SORT_MAP[(parsed.sort ?? "") as keyof typeof SORT_MAP] ?? + StaffActivities.createdAt; + const orderBy = parsed.order === "asc" ? asc(sortCol) : desc(sortCol); + + const [totalRows] = await db + .select({ total: count() }) + .from(StaffActivities) + .where(where) + .catch(() => [{ total: 0 }]); + const total = Number(totalRows?.total ?? 0); const pagination = calcPagination(total, parsed.page, parsed.perPage); - const activities = await prisma.staffActivities - .findMany({ - where: whereClause, - select: { - id: true, - userId: true, - action: true, - description: true, - targetType: true, - targetId: true, - ipAddress: true, - createdAt: true, - }, - orderBy: { [sortField]: parsed.order }, - skip: pagination.offset, - take: pagination.perPage, + const activities = await db + .select({ + id: StaffActivities.id, + userId: StaffActivities.userId, + action: StaffActivities.action, + description: StaffActivities.description, + targetType: StaffActivities.targetType, + targetId: StaffActivities.targetId, + ipAddress: StaffActivities.ipAddress, + createdAt: StaffActivities.createdAt, }) + .from(StaffActivities) + .where(where) + .orderBy(orderBy) + .limit(pagination.perPage) + .offset(pagination.offset) .catch(() => []); const staffIds = Array.from( @@ -67,11 +89,10 @@ export default async function AdminStaffActivities({ ).filter((n) => n > 0); let nameById = new Map(); if (staffIds.length) { - const users = await prisma.user - .findMany({ - where: { id: { in: staffIds } }, - select: { id: true, username: true }, - }) + const users = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, staffIds)) .catch(() => []); nameById = new Map(users.map((x) => [x.id, x.username])); } diff --git a/src/app/admin/maintenance/page.tsx b/src/app/admin/maintenance/page.tsx index c0db5635..31afa0f4 100644 --- a/src/app/admin/maintenance/page.tsx +++ b/src/app/admin/maintenance/page.tsx @@ -1,10 +1,11 @@ +import { inArray } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { saveMaintenance } from "@/actions/admin-maintenance"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, WebsiteSetting } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; const KEYS = [ "maintenance_enabled", @@ -22,10 +23,10 @@ export default async function AdminMaintenance() { let rows: { key: string; value: string }[] = []; try { - rows = await prisma.websiteSetting.findMany({ - where: { key: { in: [...KEYS] } }, - select: { key: true, value: true }, - }); + rows = await db + .select({ key: WebsiteSetting.key, value: WebsiteSetting.value }) + .from(WebsiteSetting) + .where(inArray(WebsiteSetting.key, [...KEYS])); } catch { rows = []; } diff --git a/src/app/admin/marketplace/page.tsx b/src/app/admin/marketplace/page.tsx index a47abc71..2d3fef18 100644 --- a/src/app/admin/marketplace/page.tsx +++ b/src/app/admin/marketplace/page.tsx @@ -1,3 +1,4 @@ +import { count, desc, eq, inArray } from "drizzle-orm"; import { Store } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -8,9 +9,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db, MarketplaceItems, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function AdminMarketplacePage({ searchParams, @@ -28,27 +29,37 @@ export default async function AdminMarketplacePage({ const parsed = parseListParams(new URLSearchParams(raw)); const filter = raw.filter === "sold" ? "sold" : "active"; - const where = filter === "sold" ? { state: 2 } : { state: 1 }; + const where = eq(MarketplaceItems.state, filter === "sold" ? 2 : 1); let total = 0; - let rows: Awaited> = []; + let rows: (typeof MarketplaceItems.$inferSelect)[] = []; let activeCount = 0; let soldCount = 0; let loadError = false; try { - [total, activeCount, soldCount] = await Promise.all([ - prisma.marketplaceItems.count({ where }), - prisma.marketplaceItems.count({ where: { state: 1 } }), - prisma.marketplaceItems.count({ where: { state: 2 } }), + const [totalRows, activeRows, soldRows] = await Promise.all([ + db.select({ total: count() }).from(MarketplaceItems).where(where), + db + .select({ total: count() }) + .from(MarketplaceItems) + .where(eq(MarketplaceItems.state, 1)), + db + .select({ total: count() }) + .from(MarketplaceItems) + .where(eq(MarketplaceItems.state, 2)), ]); + total = Number(totalRows[0]?.total ?? 0); + activeCount = Number(activeRows[0]?.total ?? 0); + soldCount = Number(soldRows[0]?.total ?? 0); const pagination = calcPagination(total, parsed.page, parsed.perPage); - rows = await prisma.marketplaceItems.findMany({ - where, - orderBy: { timestamp: "desc" }, - skip: pagination.offset, - take: pagination.perPage, - }); + rows = await db + .select() + .from(MarketplaceItems) + .where(where) + .orderBy(desc(MarketplaceItems.timestamp)) + .limit(pagination.perPage) + .offset(pagination.offset); } catch { loadError = true; } @@ -59,11 +70,10 @@ export default async function AdminMarketplacePage({ ]; const users = userIds.length > 0 - ? await prisma.user - .findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) .catch(() => []) : []; const usernameById = new Map(users.map((u) => [u.id, u.username])); diff --git a/src/app/admin/moderation/cfh/[id]/page.tsx b/src/app/admin/moderation/cfh/[id]/page.tsx index 2f82490b..b1d8a12b 100644 --- a/src/app/admin/moderation/cfh/[id]/page.tsx +++ b/src/app/admin/moderation/cfh/[id]/page.tsx @@ -1,6 +1,7 @@ +import { count, desc, eq, inArray } from "drizzle-orm"; import { notFound, redirect } from "next/navigation"; +import { Ban, db, SupportTickets, User } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { CfhDetail } from "./cfh-detail"; export default async function CfhDetailPage({ @@ -16,9 +17,11 @@ export default async function CfhDetailPage({ const ticketId = Number(id); if (Number.isNaN(ticketId)) notFound(); - const ticket = await prisma.supportTickets.findUnique({ - where: { id: ticketId }, - }); + const [ticket] = await db + .select() + .from(SupportTickets) + .where(eq(SupportTickets.id, ticketId)) + .limit(1); if (!ticket) notFound(); const canEdit = canAccess( @@ -30,36 +33,42 @@ export default async function CfhDetailPage({ // Fetch sender, reported, mod info const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter( Boolean, - ); + ) as number[]; const users = userIds.length > 0 - ? await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { - id: true, - username: true, - look: true, - rank: true, - online: true, - ipRegister: true, - mail: true, - }, - }) + ? await db + .select({ + id: User.id, + username: User.username, + look: User.look, + rank: User.rank, + online: User.online, + ipRegister: User.ipRegister, + mail: User.mail, + }) + .from(User) + .where(inArray(User.id, userIds)) : []; const userMap = Object.fromEntries(users.map((u) => [u.id, u])); // Get ban/mute history for reported user const reportedId = ticket.reportedId; - const [banCount, recentBans] = reportedId + const [banCountRows, recentBans] = reportedId ? await Promise.all([ - prisma.ban.count({ where: { userId: reportedId } }), - prisma.ban.findMany({ - where: { userId: reportedId }, - orderBy: { id: "desc" }, - take: 5, - }), + db + .select({ total: count() }) + .from(Ban) + .where(eq(Ban.userId, reportedId)), + db + .select() + .from(Ban) + .where(eq(Ban.userId, reportedId)) + .orderBy(desc(Ban.id)) + .limit(5), ]) - : [0, []]; + : [[{ total: 0 }], []]; + + const banCount = Number(banCountRows[0]?.total ?? 0); return ( = 0) { - conditions.push({ state: stateFilter }); + conditions.push(eq(SupportTickets.state, stateFilter)); } if (parsed.search.trim()) { @@ -33,63 +44,63 @@ export default async function CfhListPage({ const asId = Number(q); let userIds: number[] = []; try { - const users = await prisma.user.findMany({ - where: { username: { contains: q } }, - select: { id: true }, - take: 50, - }); + const users = await db + .select({ id: User.id }) + .from(User) + .where(like(User.username, `%${q}%`)) + .limit(50); userIds = users.map((u) => u.id); } catch { userIds = []; } - conditions.push({ - OR: [ - { issue: { contains: q } }, - ...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []), - ...(userIds.length - ? [ - { senderId: { in: userIds } }, - { reportedId: { in: userIds } }, - { modId: { in: userIds } }, - ] - : []), - ], - }); + const searchOr = [ + like(SupportTickets.issue, `%${q}%`), + ...(Number.isFinite(asId) && asId > 0 + ? [eq(SupportTickets.id, asId)] + : []), + ...(userIds.length + ? [ + inArray(SupportTickets.senderId, userIds), + inArray(SupportTickets.reportedId, userIds), + inArray(SupportTickets.modId, userIds), + ] + : []), + ]; + conditions.push(or(...searchOr)!); } - const where: any = - conditions.length === 0 - ? {} - : conditions.length === 1 - ? conditions[0] - : { AND: conditions }; + const where = conditions.length > 0 ? and(...conditions) : undefined; - const SORT_MAP: Record = { - id: { id: "desc" }, - issue: { issue: "asc" }, - sender: { senderId: "asc" }, - reported: { reportedId: "asc" }, - mod: { modId: "asc" }, - state: { state: "asc" }, - }; + const orderCol = + parsed.sort === "issue" + ? SupportTickets.issue + : parsed.sort === "sender" + ? SupportTickets.senderId + : parsed.sort === "reported" + ? SupportTickets.reportedId + : parsed.sort === "mod" + ? SupportTickets.modId + : parsed.sort === "state" + ? SupportTickets.state + : SupportTickets.id; + const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol); - const baseOrder = SORT_MAP[parsed.sort ?? "id"] ?? { id: "desc" }; - const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; - const orderBy = { - [orderField]: parsed.order, - } as any; - - const total = await prisma.supportTickets.count({ where }).catch(() => 0); + const [totalRows] = await db + .select({ total: count() }) + .from(SupportTickets) + .where(where) + .catch(() => [{ total: 0 }]); + const total = Number(totalRows?.total ?? 0); const pagination = calcPagination(total, parsed.page, parsed.perPage); - const tickets = await prisma.supportTickets - .findMany({ - where, - orderBy, - skip: pagination.offset, - take: pagination.perPage, - }) + const tickets = await db + .select() + .from(SupportTickets) + .where(where) + .orderBy(orderBy) + .limit(pagination.perPage) + .offset(pagination.offset) .catch(() => []); const userIds = [ @@ -98,15 +109,15 @@ export default async function CfhListPage({ ...tickets.map((x) => x.reportedId), ...tickets.map((x) => x.modId), ]), - ].filter(Boolean); + ].filter(Boolean) as number[]; let userMap = new Map(); if (userIds.length > 0) { try { - const users = await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }); + const users = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)); userMap = new Map(users.map((u) => [u.id, u.username])); } catch { userMap = new Map(); diff --git a/src/app/admin/moderation/page.tsx b/src/app/admin/moderation/page.tsx index 1b12cf22..ffb249e9 100644 --- a/src/app/admin/moderation/page.tsx +++ b/src/app/admin/moderation/page.tsx @@ -1,11 +1,12 @@ +import { and, count, desc, eq, gte, inArray, like } from "drizzle-orm"; import { AlertTriangle, - Ban, + Ban as BanIcon, Clock, Gavel, Radio, Shield, - User, + User as UserIcon, } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -13,8 +14,8 @@ import { StatsCard } from "@/components/admin/stats-card"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; +import { AdminAuditLog, Ban, db, SupportTickets, User } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; const CFH_STATES: Record = { 0: { @@ -38,33 +39,53 @@ export default async function ModerationDashboard() { const minStaffRank = await getMinStaffRank(); const [ - openCfh, - pickedCfh, - todayBans, - modsOnline, + openCfhRows, + pickedCfhRows, + todayBansRows, + modsOnlineRows, recentCfh, recentModActions, ] = await Promise.all([ - prisma.supportTickets.count({ where: { state: 0 } }), - prisma.supportTickets.count({ where: { state: 1 } }), - prisma.ban.count({ where: { timestamp: { gte: todayStart } } }), - prisma.user.count({ - where: { online: "1", rank: { gte: minStaffRank } }, - }), - prisma.supportTickets.findMany({ - orderBy: { id: "desc" }, - take: 10, - }), - prisma.adminAuditLog.findMany({ - where: { - action: { startsWith: "mod_" }, - }, - orderBy: { id: "desc" }, - take: 10, - include: { user: { select: { username: true } } }, - }), + db + .select({ total: count() }) + .from(SupportTickets) + .where(eq(SupportTickets.state, 0)), + db + .select({ total: count() }) + .from(SupportTickets) + .where(eq(SupportTickets.state, 1)), + db + .select({ total: count() }) + .from(Ban) + .where(gte(Ban.timestamp, todayStart)), + db + .select({ total: count() }) + .from(User) + .where(and(eq(User.online, "1"), gte(User.rank, minStaffRank))), + db.select().from(SupportTickets).orderBy(desc(SupportTickets.id)).limit(10), + db + .select({ + id: AdminAuditLog.id, + action: AdminAuditLog.action, + details: AdminAuditLog.details, + createdAt: AdminAuditLog.createdAt, + userId: AdminAuditLog.userId, + target: AdminAuditLog.target, + targetId: AdminAuditLog.targetId, + username: User.username, + }) + .from(AdminAuditLog) + .leftJoin(User, eq(AdminAuditLog.userId, User.id)) + .where(like(AdminAuditLog.action, "mod_%")) + .orderBy(desc(AdminAuditLog.id)) + .limit(10), ]); + const openCfh = Number(openCfhRows[0]?.total ?? 0); + const pickedCfh = Number(pickedCfhRows[0]?.total ?? 0); + const todayBans = Number(todayBansRows[0]?.total ?? 0); + const modsOnline = Number(modsOnlineRows[0]?.total ?? 0); + // Fetch usernames for CFH tickets const userIds = [ ...new Set([ @@ -72,14 +93,14 @@ export default async function ModerationDashboard() { ...recentCfh.map((t) => t.reportedId), ...recentCfh.map((t) => t.modId), ]), - ].filter(Boolean); + ].filter(Boolean) as number[]; const users = userIds.length > 0 - ? await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) : []; const userMap = new Map(users.map((u) => [u.id, u.username])); @@ -97,7 +118,7 @@ export default async function ModerationDashboard() { } + icon={} href="/admin/bans" /> - +

{log.action.replace("mod_", "").replace(/_/g, " ")}

- by {log.user?.username ?? "?"} · {log.target}{" "} + by {log.username ?? "?"} · {log.target}{" "} {log.targetId ? `#${log.targetId}` : ""}

diff --git a/src/app/admin/moderation/team/page.tsx b/src/app/admin/moderation/team/page.tsx index 05778888..e98556c2 100644 --- a/src/app/admin/moderation/team/page.tsx +++ b/src/app/admin/moderation/team/page.tsx @@ -1,11 +1,18 @@ +import { and, count, desc, gte, inArray, like } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { AvatarImage } from "@/components/shared/avatar-image"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; +import { + AdminAuditLog, + db, + SupportTickets, + User, + WebsiteTicket, +} from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function ModTeamPage() { const { session, permissions } = await getAdminContext(); @@ -13,48 +20,67 @@ export default async function ModTeamPage() { redirect("/admin"); const minStaffRank = await getMinStaffRank(); - const staff = await prisma.user.findMany({ - where: { rank: { gte: minStaffRank } }, - select: { - id: true, - username: true, - look: true, - rank: true, - online: true, - lastLogin: true, - lastOnline: true, - }, - orderBy: [{ online: "desc" }, { rank: "desc" }], - }); + const staff = await db + .select({ + id: User.id, + username: User.username, + look: User.look, + rank: User.rank, + online: User.online, + lastLogin: User.lastLogin, + lastOnline: User.lastOnline, + }) + .from(User) + .where(gte(User.rank, minStaffRank)) + .orderBy(desc(User.online), desc(User.rank)); + + const staffIds = staff.map((s) => s.id); // Get CFH ticket counts per mod and web ticket counts const [cfhCounts, webTicketCounts, modActionCounts] = await Promise.all([ - prisma.supportTickets.groupBy({ - by: ["modId"], - where: { modId: { in: staff.map((s) => s.id) } }, - _count: { _all: true }, - }), - prisma.websiteTicket.groupBy({ - by: ["assigneeId"], - where: { assigneeId: { in: staff.map((s) => s.id) } }, - _count: { _all: true }, - }), - prisma.adminAuditLog.groupBy({ - by: ["userId"], - where: { - userId: { in: staff.map((s) => s.id) }, - action: { startsWith: "mod_" }, - }, - _count: { _all: true }, - }), + staffIds.length + ? db + .select({ + modId: SupportTickets.modId, + total: count(), + }) + .from(SupportTickets) + .where(inArray(SupportTickets.modId, staffIds)) + .groupBy(SupportTickets.modId) + : Promise.resolve([]), + staffIds.length + ? db + .select({ + assigneeId: WebsiteTicket.assigneeId, + total: count(), + }) + .from(WebsiteTicket) + .where(inArray(WebsiteTicket.assigneeId, staffIds)) + .groupBy(WebsiteTicket.assigneeId) + : Promise.resolve([]), + staffIds.length + ? db + .select({ + userId: AdminAuditLog.userId, + total: count(), + }) + .from(AdminAuditLog) + .where( + and( + inArray(AdminAuditLog.userId, staffIds), + like(AdminAuditLog.action, "mod_%"), + ), + ) + .groupBy(AdminAuditLog.userId) + : Promise.resolve([]), ]); - const cfhMap = new Map(cfhCounts.map((c) => [c.modId, c._count._all])); + const cfhMap = new Map(cfhCounts.map((c) => [c.modId, Number(c.total)])); const webMap = new Map( - webTicketCounts.map((c) => [c.assigneeId, c._count._all]), + webTicketCounts.map((c) => [c.assigneeId, Number(c.total)]), ); const actionMap = new Map( - modActionCounts.map((c) => [c.userId, c._count._all]), + modActionCounts.map((c) => [c.userId, Number(c.total)]), ); return ( diff --git a/src/app/admin/navigation/page.tsx b/src/app/admin/navigation/page.tsx index 8280d4a0..3bd651a9 100644 --- a/src/app/admin/navigation/page.tsx +++ b/src/app/admin/navigation/page.tsx @@ -1,11 +1,12 @@ +import { asc, inArray, sql } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; +import { db, NavigatorFlatcats, NavigatorPubliccats, Rooms } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; // navigator_flatcats — emulator-owned flat (private-room) categories shown in the -// navigator. The Prisma delegate exists because the table has a primary key. +// navigator. type Flatcat = { id: number; caption: string; @@ -18,16 +19,13 @@ type Flatcat = { orderNum: number; }; -// navigator_publics — emulator-owned public-room assignments. This table has no -// primary/unique key, so Prisma marks the model `@@ignore` and generates NO -// delegate (prisma.navigatorPublics does not exist). It must be read via raw SQL. +// navigator_publics — keyless table, read via raw SQL. type PublicRow = { public_cat_id: number; room_id: number; visible: string; }; -// Reusable yes/no pill so emulator boolean-ish "1"/"0" columns read consistently. function YesNo({ on }: { on: boolean }) { return ( ` + const [rows] = (await db.execute(sql` SELECT public_cat_id, room_id, visible FROM navigator_publics ORDER BY public_cat_id ASC, room_id ASC - `; + `)) as unknown as [PublicRow[], unknown]; + publics = rows ?? []; } catch { publics = []; } - // Resolve the public-category names and room names referenced by the rows above - // so the read-only view is legible. Each lookup degrades to an empty map on error. const catIds = Array.from(new Set(publics.map((p) => p.public_cat_id))); const roomIds = Array.from(new Set(publics.map((p) => p.room_id))); let catNameById = new Map(); if (catIds.length) { try { - const cats = await prisma.navigatorPubliccats.findMany({ - where: { id: { in: catIds } }, - select: { id: true, name: true }, - }); + const cats = await db + .select({ id: NavigatorPubliccats.id, name: NavigatorPubliccats.name }) + .from(NavigatorPubliccats) + .where(inArray(NavigatorPubliccats.id, catIds)); catNameById = new Map(cats.map((c) => [c.id, c.name])); } catch { catNameById = new Map(); @@ -98,10 +94,10 @@ export default async function AdminNavigation() { let roomNameById = new Map(); if (roomIds.length) { try { - const rooms = await prisma.rooms.findMany({ - where: { id: { in: roomIds } }, - select: { id: true, name: true }, - }); + const rooms = await db + .select({ id: Rooms.id, name: Rooms.name }) + .from(Rooms) + .where(inArray(Rooms.id, roomIds)); roomNameById = new Map(rooms.map((r) => [r.id, r.name])); } catch { roomNameById = new Map(); @@ -134,7 +130,6 @@ export default async function AdminNavigation() { /> - {/* ── navigator_flatcats ───────────────────────────────── */}

{t("flatCategories")}

@@ -186,7 +181,6 @@ export default async function AdminNavigation() { )}

- {/* ── navigator_publics ────────────────────────────────── */}

{t("publicRooms")}

diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index cc7e324c..4a8bc3bd 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,3 +1,4 @@ +import { count, desc, eq, gt, inArray, or } from "drizzle-orm"; import { Activity, ArrowRight, @@ -9,57 +10,68 @@ import { import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; -import { activeBanWhere } from "@/lib/bans"; +import { unixNow } from "@/lib/bans"; +import { + Ban as BanTable, + db, + StaffActivities, + User, + WebsiteArticles, +} from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; type RankCount = { rank: number; count: number }; export default async function AdminDashboard() { const t = await getTranslations("pages.admin.dashboard"); + const now = unixNow(); let users = -1; let online = 0; let articles = 0; let bans = 0; try { - [users, online, articles, bans] = await Promise.all([ - prisma.user.count(), - prisma.user.count({ where: { online: "1" } }), - prisma.websiteArticles.count(), - prisma.ban.count({ - where: activeBanWhere(), - }), + const [usersRow, onlineRow, articlesRow, bansRow] = await Promise.all([ + db.select({ total: count() }).from(User), + db.select({ total: count() }).from(User).where(eq(User.online, "1")), + db.select({ total: count() }).from(WebsiteArticles), + db + .select({ total: count() }) + .from(BanTable) + .where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now))), ]); + users = usersRow[0]?.total ?? 0; + online = onlineRow[0]?.total ?? 0; + articles = articlesRow[0]?.total ?? 0; + bans = bansRow[0]?.total ?? 0; } catch {} const dbOk = users >= 0; - const rankGroups = await prisma.user - .groupBy({ - by: ["rank"], - _count: { _all: true }, - orderBy: { rank: "desc" }, - }) - .catch(() => [] as Array<{ rank: number; _count: { _all: number } }>); + const rankGroups = await db + .select({ rank: User.rank, total: count() }) + .from(User) + .groupBy(User.rank) + .orderBy(desc(User.rank)) + .catch(() => [] as Array<{ rank: number; total: number }>); const rankCounts: RankCount[] = rankGroups - .map((g) => ({ rank: g.rank, count: g._count._all })) + .map((g) => ({ rank: g.rank, count: Number(g.total) })) .sort((a, b) => b.count - a.count); const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0); - const activity = await prisma.staffActivities - .findMany({ orderBy: { createdAt: "desc" }, take: 8 }) - .catch( - () => [] as Awaited>, - ); + const activity = await db + .select() + .from(StaffActivities) + .orderBy(desc(StaffActivities.createdAt)) + .limit(8) + .catch(() => [] as (typeof StaffActivities.$inferSelect)[]); const staffIds = Array.from( new Set(activity.map((a) => Number(a.userId))), ).filter((n) => n > 0); let nameById = new Map(); if (staffIds.length) { - const u = await prisma.user - .findMany({ - where: { id: { in: staffIds } }, - select: { id: true, username: true }, - }) + const u = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, staffIds)) .catch(() => []); nameById = new Map(u.map((x) => [x.id, x.username])); } diff --git a/src/app/admin/photos/page.tsx b/src/app/admin/photos/page.tsx index 50389bf8..899e8045 100644 --- a/src/app/admin/photos/page.tsx +++ b/src/app/admin/photos/page.tsx @@ -1,3 +1,4 @@ +import { count, desc, inArray } from "drizzle-orm"; import { Camera } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -7,8 +8,8 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { CameraWeb, db, User } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function AdminPhotos({ searchParams, @@ -23,16 +24,21 @@ export default async function AdminPhotos({ const t = await getTranslations("pages.admin.photos"); const parsed = parseListParams(new URLSearchParams(await searchParams)); - const total = await prisma.cameraWeb.count().catch(() => 0); + const [totalRow] = await db + .select({ total: count() }) + .from(CameraWeb) + .catch(() => [{ total: 0 }]); + const total = Number(totalRow?.total ?? 0); const pagination = calcPagination(total, parsed.page, parsed.perPage || 24); - let photos: Awaited> = []; + let photos: (typeof CameraWeb.$inferSelect)[] = []; try { - photos = await prisma.cameraWeb.findMany({ - orderBy: { timestamp: "desc" }, - skip: pagination.offset, - take: pagination.perPage, - }); + photos = await db + .select() + .from(CameraWeb) + .orderBy(desc(CameraWeb.timestamp)) + .limit(pagination.perPage) + .offset(pagination.offset); } catch { photos = []; } @@ -40,11 +46,10 @@ export default async function AdminPhotos({ const userIds = [...new Set(photos.map((p) => p.userId))]; const users = userIds.length > 0 - ? await prisma.user - .findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) .catch(() => []) : []; const usernameById = new Map(users.map((u) => [u.id, u.username])); diff --git a/src/app/admin/polls/[id]/page.tsx b/src/app/admin/polls/[id]/page.tsx index e1c45241..5953741d 100644 --- a/src/app/admin/polls/[id]/page.tsx +++ b/src/app/admin/polls/[id]/page.tsx @@ -1,10 +1,11 @@ +import { eq } from "drizzle-orm"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { db, WebsitePoll } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { PollForm } from "../poll-form"; import { PollQuestionsManager } from "./poll-questions"; import { PollResultsView } from "./poll-results"; @@ -26,12 +27,12 @@ export default async function PollDetailPage({ const t = await getTranslations("pages.admin.polls"); const tHubs = await getTranslations("pages.admin.hubs"); - const poll = await prisma.websitePoll.findUnique({ - where: { id: pollId }, - include: { + const poll = await db.query.WebsitePoll.findFirst({ + where: eq(WebsitePoll.id, pollId), + with: { questions: { - orderBy: { sortOrder: "asc" }, - include: { + orderBy: (q, { asc }) => [asc(q.sortOrder)], + with: { votes: true, }, }, diff --git a/src/app/admin/radio/api-keys/page.tsx b/src/app/admin/radio/api-keys/page.tsx index 876b8b9f..80333525 100644 --- a/src/app/admin/radio/api-keys/page.tsx +++ b/src/app/admin/radio/api-keys/page.tsx @@ -1,3 +1,4 @@ +import { desc } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { createApiKey, @@ -6,8 +7,8 @@ import { } from "@/actions/admin-radio-api-keys"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, RadioApiKeys } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const metadata = { title: "Radio API Keys" }; @@ -34,22 +35,21 @@ export default async function AdminRadioApiKeysPage() { let dbError = false; try { - keys = await prisma.radioApiKeys - .findMany({ - select: { - id: true, - name: true, - key: true, - allowedIps: true, - rateLimit: true, - lastUsedAt: true, - expiresAt: true, - isActive: true, - createdAt: true, - }, - orderBy: [{ isActive: "desc" }, { id: "desc" }], - take: 500, + keys = await db + .select({ + id: RadioApiKeys.id, + name: RadioApiKeys.name, + key: RadioApiKeys.key, + allowedIps: RadioApiKeys.allowedIps, + rateLimit: RadioApiKeys.rateLimit, + lastUsedAt: RadioApiKeys.lastUsedAt, + expiresAt: RadioApiKeys.expiresAt, + isActive: RadioApiKeys.isActive, + createdAt: RadioApiKeys.createdAt, }) + .from(RadioApiKeys) + .orderBy(desc(RadioApiKeys.isActive), desc(RadioApiKeys.id)) + .limit(500) .catch(() => { dbError = true; return []; diff --git a/src/app/admin/radio/autodj/page.tsx b/src/app/admin/radio/autodj/page.tsx index c870e7f9..84ef90c3 100644 --- a/src/app/admin/radio/autodj/page.tsx +++ b/src/app/admin/radio/autodj/page.tsx @@ -1,3 +1,4 @@ +import { asc } from "drizzle-orm"; import Image from "next/image"; import { getTranslations } from "next-intl/server"; import { @@ -7,8 +8,8 @@ import { } from "@/actions/admin-radio-autodj"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, RadioAutoDjPlaylist } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const metadata = { title: "Radio AutoDJ" }; @@ -39,22 +40,24 @@ export default async function AdminRadioAutoDjPage() { let dbError = false; try { - tracks = await prisma.radioAutoDjPlaylist - .findMany({ - select: { - id: true, - title: true, - artist: true, - album: true, - artworkUrl: true, - duration: true, - playCount: true, - lastPlayedAt: true, - isActive: true, - sortOrder: true, - }, - orderBy: [{ sortOrder: "asc" }, { title: "asc" }], + tracks = await db + .select({ + id: RadioAutoDjPlaylist.id, + title: RadioAutoDjPlaylist.title, + artist: RadioAutoDjPlaylist.artist, + album: RadioAutoDjPlaylist.album, + artworkUrl: RadioAutoDjPlaylist.artworkUrl, + duration: RadioAutoDjPlaylist.duration, + playCount: RadioAutoDjPlaylist.playCount, + lastPlayedAt: RadioAutoDjPlaylist.lastPlayedAt, + isActive: RadioAutoDjPlaylist.isActive, + sortOrder: RadioAutoDjPlaylist.sortOrder, }) + .from(RadioAutoDjPlaylist) + .orderBy( + asc(RadioAutoDjPlaylist.sortOrder), + asc(RadioAutoDjPlaylist.title), + ) .catch(() => { dbError = true; return []; diff --git a/src/app/admin/radio/banners/page.tsx b/src/app/admin/radio/banners/page.tsx index 94b03b29..1f9da898 100644 --- a/src/app/admin/radio/banners/page.tsx +++ b/src/app/admin/radio/banners/page.tsx @@ -1,3 +1,4 @@ +import { asc } from "drizzle-orm"; import NextImage from "next/image"; import { getTranslations } from "next-intl/server"; import { @@ -7,8 +8,8 @@ import { } from "@/actions/admin-radio-extra"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, RadioBanners } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; type Banner = { id: bigint; @@ -28,19 +29,19 @@ export default async function AdminRadioBannersPage() { let dbError = false; try { - banners = await prisma.radioBanners.findMany({ - select: { - id: true, - userId: true, - imagePath: true, - title: true, - description: true, - sortOrder: true, - isActive: true, - createdAt: true, - }, - orderBy: [{ sortOrder: "asc" }, { id: "asc" }], - }); + banners = await db + .select({ + id: RadioBanners.id, + userId: RadioBanners.userId, + imagePath: RadioBanners.imagePath, + title: RadioBanners.title, + description: RadioBanners.description, + sortOrder: RadioBanners.sortOrder, + isActive: RadioBanners.isActive, + createdAt: RadioBanners.createdAt, + }) + .from(RadioBanners) + .orderBy(asc(RadioBanners.sortOrder), asc(RadioBanners.id)); } catch { dbError = true; banners = []; diff --git a/src/app/admin/radio/history/page.tsx b/src/app/admin/radio/history/page.tsx index e3ef68a5..9c99d2f4 100644 --- a/src/app/admin/radio/history/page.tsx +++ b/src/app/admin/radio/history/page.tsx @@ -1,7 +1,8 @@ +import { desc, inArray } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; +import { db, RadioHistory, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; type History = { id: bigint; @@ -31,28 +32,28 @@ export default async function AdminRadioHistoryPage() { let dbError = false; try { - history = await prisma.radioHistory.findMany({ - select: { - id: true, - userId: true, - showName: true, - startedAt: true, - endedAt: true, - listenersCount: true, - notes: true, - }, - orderBy: { startedAt: "desc" }, - take: 100, - }); + history = await db + .select({ + id: RadioHistory.id, + userId: RadioHistory.userId, + showName: RadioHistory.showName, + startedAt: RadioHistory.startedAt, + endedAt: RadioHistory.endedAt, + listenersCount: RadioHistory.listenersCount, + notes: RadioHistory.notes, + }) + .from(RadioHistory) + .orderBy(desc(RadioHistory.startedAt)) + .limit(100); const ids = Array.from( new Set(history.map((h) => Number(h.userId))), ).filter((n) => Number.isFinite(n) && n > 0); if (ids.length > 0) { - const users = await prisma.user.findMany({ - where: { id: { in: ids } }, - select: { id: true, username: true }, - }); + const users = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, ids)); names = new Map(users.map((u) => [String(u.id), u.username])); } } catch { diff --git a/src/app/admin/radio/moderation/page.tsx b/src/app/admin/radio/moderation/page.tsx index 150a8d3e..96ce9e5c 100644 --- a/src/app/admin/radio/moderation/page.tsx +++ b/src/app/admin/radio/moderation/page.tsx @@ -1,9 +1,10 @@ +import { desc } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { deleteShout } from "@/actions/admin-radio-moderation"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, RadioShouts } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; type Shout = { id: bigint; @@ -18,17 +19,17 @@ export default async function AdminRadioModerationPage() { let shouts: Shout[] = []; try { - shouts = await prisma.radioShouts.findMany({ - select: { - id: true, - userId: true, - message: true, - createdAt: true, - updatedAt: true, - }, - orderBy: { id: "desc" }, - take: 50, - }); + shouts = await db + .select({ + id: RadioShouts.id, + userId: RadioShouts.userId, + message: RadioShouts.message, + createdAt: RadioShouts.createdAt, + updatedAt: RadioShouts.updatedAt, + }) + .from(RadioShouts) + .orderBy(desc(RadioShouts.id)) + .limit(50); } catch { shouts = []; } diff --git a/src/app/admin/radio/page.tsx b/src/app/admin/radio/page.tsx index 652c0819..5b301105 100644 --- a/src/app/admin/radio/page.tsx +++ b/src/app/admin/radio/page.tsx @@ -1,9 +1,10 @@ +import { desc } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { deleteShout } from "@/actions/admin-radio-moderation"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, RadioApplications, RadioSchedules, RadioShouts } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; type Shout = { id: bigint; @@ -52,48 +53,53 @@ export default async function AdminRadioPage() { let schedules: Schedule[] = []; try { - shouts = await prisma.radioShouts.findMany({ - select: { id: true, userId: true, message: true, createdAt: true }, - orderBy: { id: "desc" }, - take: 25, - }); + shouts = await db + .select({ + id: RadioShouts.id, + userId: RadioShouts.userId, + message: RadioShouts.message, + createdAt: RadioShouts.createdAt, + }) + .from(RadioShouts) + .orderBy(desc(RadioShouts.id)) + .limit(25); } catch { shouts = []; } try { - applications = await prisma.radioApplications.findMany({ - select: { - id: true, - userId: true, - realName: true, - age: true, - status: true, - musicStyle: true, - discordUsername: true, - createdAt: true, - }, - orderBy: { id: "desc" }, - take: 25, - }); + applications = await db + .select({ + id: RadioApplications.id, + userId: RadioApplications.userId, + realName: RadioApplications.realName, + age: RadioApplications.age, + status: RadioApplications.status, + musicStyle: RadioApplications.musicStyle, + discordUsername: RadioApplications.discordUsername, + createdAt: RadioApplications.createdAt, + }) + .from(RadioApplications) + .orderBy(desc(RadioApplications.id)) + .limit(25); } catch { applications = []; } try { - schedules = await prisma.radioSchedules.findMany({ - select: { - id: true, - userId: true, - day: true, - startTime: true, - endTime: true, - showName: true, - isActive: true, - }, - orderBy: { id: "desc" }, - take: 25, - }); + schedules = await db + .select({ + id: RadioSchedules.id, + userId: RadioSchedules.userId, + day: RadioSchedules.day, + startTime: RadioSchedules.startTime, + endTime: RadioSchedules.endTime, + showName: RadioSchedules.showName, + isActive: RadioSchedules.isActive, + }) + .from(RadioSchedules) + .orderBy(desc(RadioSchedules.id)) + .limit(25); } catch { schedules = []; } diff --git a/src/app/admin/radio/ranks/page.tsx b/src/app/admin/radio/ranks/page.tsx index 757a7f5b..bcba6376 100644 --- a/src/app/admin/radio/ranks/page.tsx +++ b/src/app/admin/radio/ranks/page.tsx @@ -1,3 +1,4 @@ +import { asc } from "drizzle-orm"; import { getTranslations } from "next-intl/server"; import { createRadioRank, @@ -6,7 +7,7 @@ import { } from "@/actions/admin-radio-extra"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; -import { prisma } from "@/lib/prisma"; +import { db, RadioRanks } from "@/lib/db"; type Rank = { id: bigint; @@ -23,16 +24,16 @@ export default async function AdminRadioRanksPage() { let dbError = false; try { - ranks = await prisma.radioRanks.findMany({ - select: { - id: true, - name: true, - description: true, - badgeCode: true, - isActive: true, - }, - orderBy: { id: "asc" }, - }); + ranks = await db + .select({ + id: RadioRanks.id, + name: RadioRanks.name, + description: RadioRanks.description, + badgeCode: RadioRanks.badgeCode, + isActive: RadioRanks.isActive, + }) + .from(RadioRanks) + .orderBy(asc(RadioRanks.id)); } catch { dbError = true; ranks = []; diff --git a/src/app/admin/radio/settings/page.tsx b/src/app/admin/radio/settings/page.tsx index dd2e7001..c8e80d76 100644 --- a/src/app/admin/radio/settings/page.tsx +++ b/src/app/admin/radio/settings/page.tsx @@ -5,7 +5,7 @@ import { } from "@/actions/admin-radio-extra"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; -import { prisma } from "@/lib/prisma"; +import { db, WebsiteSetting } from "@/lib/db"; type Field = { key: string; comment: string }; type Group = { title: string; fields: Field[] }; @@ -364,9 +364,13 @@ export default async function AdminRadioSettingsPage() { let extra: Field[] = []; try { - const rows = await prisma.websiteSetting.findMany({ - select: { key: true, value: true, comment: true }, - }); + const rows = await db + .select({ + key: WebsiteSetting.key, + value: WebsiteSetting.value, + comment: WebsiteSetting.comment, + }) + .from(WebsiteSetting); values = new Map(rows.map((r) => [r.key, r.value])); extra = rows .filter( diff --git a/src/app/admin/rare-values/page.tsx b/src/app/admin/rare-values/page.tsx index e0f13e81..c816cdc4 100644 --- a/src/app/admin/rare-values/page.tsx +++ b/src/app/admin/rare-values/page.tsx @@ -1,3 +1,4 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { @@ -8,8 +9,8 @@ import { } from "@/actions/admin-rare-values"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"]; @@ -21,23 +22,25 @@ export default async function AdminRareValues() { const t = await getTranslations("pages.admin.rareValues"); - let categories: Awaited< - ReturnType - > = []; + let categories: (typeof WebsiteRareValueCategories.$inferSelect)[] = []; try { - categories = await prisma.websiteRareValueCategories.findMany({ - orderBy: [{ priority: "asc" }, { name: "asc" }], - }); + categories = await db + .select() + .from(WebsiteRareValueCategories) + .orderBy( + asc(WebsiteRareValueCategories.priority), + asc(WebsiteRareValueCategories.name), + ); } catch { categories = []; } - let values: Awaited> = - []; + let values: (typeof WebsiteRareValues.$inferSelect)[] = []; try { - values = await prisma.websiteRareValues.findMany({ - orderBy: { name: "asc" }, - }); + values = await db + .select() + .from(WebsiteRareValues) + .orderBy(asc(WebsiteRareValues.name)); } catch { values = []; } diff --git a/src/app/admin/rooms/[id]/furni/page.tsx b/src/app/admin/rooms/[id]/furni/page.tsx index a9411e13..0fc5f099 100644 --- a/src/app/admin/rooms/[id]/furni/page.tsx +++ b/src/app/admin/rooms/[id]/furni/page.tsx @@ -1,7 +1,8 @@ +import { desc, eq, inArray } from "drizzle-orm"; import { redirect } from "next/navigation"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; +import { db, Items, ItemsBase, User } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { loadRoom } from "../../_lib/load-room"; import { FurniClient } from "./furni-client"; @@ -18,10 +19,11 @@ export default async function RoomFurniPage({ const room = await loadRoom(id); // Load items in this room. items.item_id → items_base.id - const items = await prisma.items.findMany({ - where: { roomId: room.id }, - orderBy: { id: "desc" }, - }); + const items = await db + .select() + .from(Items) + .where(eq(Items.roomId, room.id)) + .orderBy(desc(Items.id)); // Resolve item bases const baseIds = Array.from( @@ -30,10 +32,15 @@ export default async function RoomFurniPage({ ), ); const bases = baseIds.length - ? await prisma.itemsBase.findMany({ - where: { id: { in: baseIds } }, - select: { id: true, itemName: true, publicName: true, type: true }, - }) + ? await db + .select({ + id: ItemsBase.id, + itemName: ItemsBase.itemName, + publicName: ItemsBase.publicName, + type: ItemsBase.type, + }) + .from(ItemsBase) + .where(inArray(ItemsBase.id, baseIds)) : []; const baseMap = new Map(bases.map((b) => [b.id, b])); @@ -42,10 +49,10 @@ export default async function RoomFurniPage({ new Set(items.map((i) => i.userId).filter((u) => u > 0)), ); const users = userIds.length - ? await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) : []; const userMap = new Map(users.map((u) => [u.id, u.username])); diff --git a/src/app/admin/rooms/_lib/load-room.ts b/src/app/admin/rooms/_lib/load-room.ts index 7098777a..202d3a3a 100644 --- a/src/app/admin/rooms/_lib/load-room.ts +++ b/src/app/admin/rooms/_lib/load-room.ts @@ -1,21 +1,31 @@ +import { eq } from "drizzle-orm"; import { notFound } from "next/navigation"; -import { prisma } from "@/lib/prisma"; +import { db, Rooms, User } from "@/lib/db"; export async function loadRoom(id: string) { const roomId = parseInt(id, 10); if (Number.isNaN(roomId)) notFound(); - const room = await prisma.rooms.findUnique({ - where: { id: roomId }, - }); + const [room] = await db + .select() + .from(Rooms) + .where(eq(Rooms.id, roomId)) + .limit(1); if (!room) notFound(); const owner = room.ownerId > 0 - ? await prisma.user.findUnique({ - where: { id: room.ownerId }, - select: { id: true, username: true, look: true }, - }) + ? (( + await db + .select({ + id: User.id, + username: User.username, + look: User.look, + }) + .from(User) + .where(eq(User.id, room.ownerId)) + .limit(1) + )[0] ?? null) : null; return { ...room, owner }; } diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index fabb3561..16c6a795 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -1,9 +1,10 @@ +import { asc } from "drizzle-orm"; import { Settings } from "lucide-react"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AdminPageShell } from "@/components/admin/admin-page-shell"; +import { db, WebsiteSetting } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { AdvancedSettingsPanel } from "./advanced-settings-panel"; import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config"; import { CmsSettingsForm } from "./cms-settings-form"; @@ -22,11 +23,12 @@ export default async function AdminSettings() { ); const t = await getTranslations("pages.admin.settings"); - let settings: Awaited> = []; + let settings: (typeof WebsiteSetting.$inferSelect)[] = []; try { - settings = await prisma.websiteSetting.findMany({ - orderBy: { key: "asc" }, - }); + settings = await db + .select() + .from(WebsiteSetting) + .orderBy(asc(WebsiteSetting.key)); } catch { settings = []; } diff --git a/src/app/admin/shop/[id]/page.tsx b/src/app/admin/shop/[id]/page.tsx index c578216a..c9147ec0 100644 --- a/src/app/admin/shop/[id]/page.tsx +++ b/src/app/admin/shop/[id]/page.tsx @@ -1,11 +1,12 @@ +import { eq } from "drizzle-orm"; import { Package } from "lucide-react"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop"; import { Button } from "@/components/ui/button"; +import { db, WebsiteShopArticles } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function EditShopArticle({ params, @@ -20,12 +21,14 @@ export default async function EditShopArticle({ const t = await getTranslations("pages.admin.shop"); const { id } = await params; - let pkg: Awaited> = - null; + let pkg: typeof WebsiteShopArticles.$inferSelect | null = null; try { - pkg = await prisma.websiteShopArticles.findUnique({ - where: { id: BigInt(id) }, - }); + const [row] = await db + .select() + .from(WebsiteShopArticles) + .where(eq(WebsiteShopArticles.id, BigInt(id))) + .limit(1); + pkg = row ?? null; } catch { notFound(); } diff --git a/src/app/admin/shop/page.tsx b/src/app/admin/shop/page.tsx index 97521be8..d2cff9ea 100644 --- a/src/app/admin/shop/page.tsx +++ b/src/app/admin/shop/page.tsx @@ -1,3 +1,4 @@ +import { asc, count, desc } from "drizzle-orm"; import { ShoppingCart } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -8,9 +9,9 @@ import { StatusCard } from "@/components/admin/dashboard"; import { CurrencyIcon } from "@/components/shared/currency-icon"; import { Button } from "@/components/ui/button"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db, WebsitePaypalTransactions, WebsiteShopArticles } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]); const RECENT_ORDERS = 25; @@ -65,22 +66,24 @@ export default async function AdminShop({ const parsed = parseListParams(new URLSearchParams(raw)); let packageTotal = 0; - let packages: Awaited< - ReturnType - > = []; + let packages: (typeof WebsiteShopArticles.$inferSelect)[] = []; let loadError = false; try { - packageTotal = await prisma.websiteShopArticles.count(); + const [totalRow] = await db + .select({ total: count() }) + .from(WebsiteShopArticles); + packageTotal = Number(totalRow?.total ?? 0); const pagination = calcPagination( packageTotal, parsed.page, parsed.perPage, ); - packages = await prisma.websiteShopArticles.findMany({ - orderBy: [{ position: "asc" }, { name: "asc" }], - skip: pagination.offset, - take: pagination.perPage, - }); + packages = await db + .select() + .from(WebsiteShopArticles) + .orderBy(asc(WebsiteShopArticles.position), asc(WebsiteShopArticles.name)) + .limit(pagination.perPage) + .offset(pagination.offset); } catch { packages = []; packageTotal = 0; @@ -93,14 +96,13 @@ export default async function AdminShop({ parsed.perPage, ); - let orders: Awaited< - ReturnType - > = []; + let orders: (typeof WebsitePaypalTransactions.$inferSelect)[] = []; try { - orders = await prisma.websitePaypalTransactions.findMany({ - orderBy: { createdAt: "desc" }, - take: RECENT_ORDERS, - }); + orders = await db + .select() + .from(WebsitePaypalTransactions) + .orderBy(desc(WebsitePaypalTransactions.createdAt)) + .limit(RECENT_ORDERS); } catch { orders = []; loadError = true; diff --git a/src/app/admin/sounds/page.tsx b/src/app/admin/sounds/page.tsx index 1728316a..a29c2cc6 100644 --- a/src/app/admin/sounds/page.tsx +++ b/src/app/admin/sounds/page.tsx @@ -1,6 +1,7 @@ +import { asc, count, gt } from "drizzle-orm"; import { redirect } from "next/navigation"; +import { CatalogItems, db, Soundtracks } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; import { DEFAULT_SOUNDTRACK_BASE_URL, @@ -18,28 +19,31 @@ export default async function SoundsPage() { const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); const [rows, usage, baseUrlSetting] = await Promise.all([ - prisma.soundtracks.findMany({ - orderBy: { id: "asc" }, - select: { - id: true, - code: true, - name: true, - author: true, - length: true, - track: true, - }, - }), - prisma.catalogItems.groupBy({ - by: ["songId"], - where: { songId: { gt: 0 } }, - _count: { _all: true }, - }), + db + .select({ + id: Soundtracks.id, + code: Soundtracks.code, + name: Soundtracks.name, + author: Soundtracks.author, + length: Soundtracks.length, + track: Soundtracks.track, + }) + .from(Soundtracks) + .orderBy(asc(Soundtracks.id)), + db + .select({ + songId: CatalogItems.songId, + total: count(), + }) + .from(CatalogItems) + .where(gt(CatalogItems.songId, 0)) + .groupBy(CatalogItems.songId), siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL), ]); const usageMap: Record = {}; for (const row of usage) { - if (row.songId != null) usageMap[row.songId] = row._count._all; + if (row.songId != null) usageMap[row.songId] = Number(row.total); } const soundtracks = rows.map((r) => { diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx index 3631e2de..ac56bc47 100644 --- a/src/app/admin/subscriptions/page.tsx +++ b/src/app/admin/subscriptions/page.tsx @@ -1,11 +1,12 @@ +import { sql } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AdminSimplePager } from "@/components/admin/admin-simple-pager"; import { StatusCard } from "@/components/admin/dashboard"; import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type SubRow = { id: number; @@ -14,6 +15,19 @@ type SubRow = { expiresAt: number; }; +type CountRow = { c: number | bigint }; +type SubSqlRow = { + id: number; + user_id: number | null; + subscription_type: string | null; + expires_at: number; +}; + +async function rawRows(query: ReturnType): Promise { + const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; + return rows ?? []; +} + export default async function AdminSubscriptions({ searchParams, }: { @@ -36,39 +50,32 @@ export default async function AdminSubscriptions({ let loadError = false; try { const [countRows, typeRows, soonRows] = await Promise.all([ - prisma.$queryRaw<{ c: bigint }[]>` + rawRows(sql` SELECT COUNT(*) AS c FROM users_subscriptions WHERE active = 1 AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} - `, - prisma.$queryRaw<{ c: bigint }[]>` + `), + rawRows(sql` SELECT COUNT(DISTINCT subscription_type) AS c FROM users_subscriptions WHERE active = 1 AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} - `, - prisma.$queryRaw<{ c: bigint }[]>` + `), + rawRows(sql` SELECT COUNT(*) AS c FROM users_subscriptions WHERE active = 1 AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60} - `, + `), ]); total = Number(countRows[0]?.c ?? 0); types = Number(typeRows[0]?.c ?? 0); expiringSoon = Number(soonRows[0]?.c ?? 0); const pagination = calcPagination(total, parsed.page, parsed.perPage); - const rows = await prisma.$queryRaw< - { - id: number; - user_id: number | null; - subscription_type: string | null; - expires_at: number; - }[] - >` + const rows = await rawRows(sql` SELECT id, user_id, subscription_type, @@ -78,7 +85,7 @@ export default async function AdminSubscriptions({ AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} ORDER BY expires_at ASC LIMIT ${pagination.perPage} OFFSET ${pagination.offset} - `; + `); active = rows.map((r) => ({ id: Number(r.id), diff --git a/src/app/admin/tags/page.tsx b/src/app/admin/tags/page.tsx index 1eeb15d6..5c89baa2 100644 --- a/src/app/admin/tags/page.tsx +++ b/src/app/admin/tags/page.tsx @@ -1,11 +1,12 @@ +import { asc, count } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { createTag, deleteTag, updateTag } from "@/actions/admin-tags"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, Taggables, Tags } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type Tag = { id: bigint; @@ -26,18 +27,26 @@ export default async function AdminTagsPage() { let dbError = false; try { - tags = await prisma.tags.findMany({ - select: { id: true, name: true, backgroundColor: true, createdAt: true }, - orderBy: { name: "asc" }, - take: 500, - }); + tags = await db + .select({ + id: Tags.id, + name: Tags.name, + backgroundColor: Tags.backgroundColor, + createdAt: Tags.createdAt, + }) + .from(Tags) + .orderBy(asc(Tags.name)) + .limit(500); // How many things each tag is attached to (taggables join table). - const counts = await prisma.taggables.groupBy({ - by: ["tagId"], - _count: { tagId: true }, - }); - usage = new Map(counts.map((c) => [String(c.tagId), c._count.tagId])); + const counts = await db + .select({ + tagId: Taggables.tagId, + total: count(), + }) + .from(Taggables) + .groupBy(Taggables.tagId); + usage = new Map(counts.map((c) => [String(c.tagId), Number(c.total)])); } catch { dbError = true; tags = []; diff --git a/src/app/admin/teams/page.tsx b/src/app/admin/teams/page.tsx index 44f624fb..29c6c7a7 100644 --- a/src/app/admin/teams/page.tsx +++ b/src/app/admin/teams/page.tsx @@ -1,10 +1,11 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { createTeam, deleteTeam } from "@/actions/admin-teams"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, WebsiteTeams } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type TeamRow = { id: bigint; @@ -24,17 +25,17 @@ export default async function AdminTeams() { const t = await getTranslations("pages.admin.teams"); let teams: TeamRow[] = []; try { - teams = await prisma.websiteTeams.findMany({ - select: { - id: true, - rankName: true, - badge: true, - jobDescription: true, - staffColor: true, - hiddenRank: true, - }, - orderBy: { id: "asc" }, - }); + teams = await db + .select({ + id: WebsiteTeams.id, + rankName: WebsiteTeams.rankName, + badge: WebsiteTeams.badge, + jobDescription: WebsiteTeams.jobDescription, + staffColor: WebsiteTeams.staffColor, + hiddenRank: WebsiteTeams.hiddenRank, + }) + .from(WebsiteTeams) + .orderBy(asc(WebsiteTeams.id)); } catch { teams = []; }