From 2cd0863cb81407030a3ba61932fbdd3651671a7b Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 1 Aug 2026 14:15:28 +0200 Subject: [PATCH] refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (2) Co-authored-by: Cursor --- src/app/(site)/rares/page.tsx | 36 ++- src/app/(site)/redeem/page.tsx | 13 +- src/app/(site)/register/page.tsx | 31 +- src/app/(site)/room/[id]/page.tsx | 52 ++-- src/app/(site)/search/page.tsx | 52 ++-- src/app/(site)/settings/2fa/page.tsx | 21 +- src/app/(site)/settings/page.tsx | 18 +- src/app/(site)/settings/sessions/page.tsx | 33 +- src/app/(site)/shop/page.tsx | 73 ++--- src/app/(site)/shop/topup/page.tsx | 13 +- src/app/(site)/staff/page.tsx | 56 ++-- src/app/(site)/u/[username]/page.tsx | 235 ++++++++------ src/app/(site)/verify/page.tsx | 20 +- src/app/admin/achievements/page.tsx | 31 +- src/app/admin/ads/[id]/page.tsx | 12 +- src/app/admin/ads/page.tsx | 14 +- src/app/admin/alerts/page.tsx | 14 +- src/app/admin/analytics/activity/page.tsx | 74 +++-- src/app/admin/analytics/economy/page.tsx | 127 +++++--- src/app/admin/analytics/page.tsx | 106 +++++-- src/app/admin/applications/page.tsx | 20 +- src/app/admin/articles/[id]/page.tsx | 27 +- src/app/admin/articles/page.tsx | 35 +-- src/app/admin/badges/page.tsx | 14 +- src/app/admin/banners/page.tsx | 10 +- src/app/admin/bans/page.tsx | 18 +- src/app/admin/calendar/[id]/page.tsx | 67 ++-- src/app/admin/calendar/page.tsx | 44 +-- src/app/admin/catalog/[id]/page.tsx | 25 +- .../admin/catalog/builder-club/[id]/page.tsx | 18 +- src/app/admin/catalog/page.tsx | 290 +++++++++++------- src/app/admin/commandocentrum/page.tsx | 39 ++- src/app/admin/devops/errors/page.tsx | 19 +- src/app/admin/devops/page.tsx | 38 ++- src/app/admin/email-templates/page.tsx | 13 +- src/app/admin/emulator/page.tsx | 22 +- src/app/admin/events/[id]/page.tsx | 32 +- src/app/admin/events/create/page.tsx | 12 +- src/app/admin/events/types/page.tsx | 10 +- src/app/admin/favicon/page.tsx | 11 +- 40 files changed, 1069 insertions(+), 726 deletions(-) diff --git a/src/app/(site)/rares/page.tsx b/src/app/(site)/rares/page.tsx index d5e3098a..28268b80 100644 --- a/src/app/(site)/rares/page.tsx +++ b/src/app/(site)/rares/page.tsx @@ -1,7 +1,8 @@ +import { asc, count } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; -import { prisma } from "@/lib/prisma"; +import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db"; import { siteSettings } from "@/lib/services/site-settings"; export const revalidate = 300; @@ -19,10 +20,18 @@ export default async function RareValuesPage() { const t = await getTranslations("pages.rares"); let categories: CategoryRow[] = []; try { - categories = await prisma.websiteRareValueCategories.findMany({ - orderBy: [{ priority: "asc" }, { name: "asc" }], - select: { id: true, name: true, badge: true, priority: true }, - }); + categories = await db + .select({ + id: WebsiteRareValueCategories.id, + name: WebsiteRareValueCategories.name, + badge: WebsiteRareValueCategories.badge, + priority: WebsiteRareValueCategories.priority, + }) + .from(WebsiteRareValueCategories) + .orderBy( + asc(WebsiteRareValueCategories.priority), + asc(WebsiteRareValueCategories.name), + ); } catch { categories = []; } @@ -30,12 +39,15 @@ export default async function RareValuesPage() { // Per-category rare counts (grouped so we avoid N queries). const counts = new Map(); try { - const grouped = await prisma.websiteRareValues.groupBy({ - by: ["categoryId"], - _count: { _all: true }, - }); + const grouped = await db + .select({ + categoryId: WebsiteRareValues.categoryId, + value: count(), + }) + .from(WebsiteRareValues) + .groupBy(WebsiteRareValues.categoryId); for (const g of grouped) { - counts.set(String(g.categoryId), g._count._all); + counts.set(String(g.categoryId), Number(g.value)); } } catch { // leave counts empty -> shown as 0 @@ -55,7 +67,7 @@ export default async function RareValuesPage() {
{categories.map((c) => { const id = String(c.id); - const count = counts.get(id) ?? 0; + const rareCount = counts.get(id) ?? 0; const badgeSrc = badgesBase && c.badge ? `${badgesBase}/${c.badge}.gif` : ""; return ( @@ -83,7 +95,7 @@ export default async function RareValuesPage() {

{c.name}

- {t("rareCount", { count })} + {t("rareCount", { count: rareCount })}

diff --git a/src/app/(site)/redeem/page.tsx b/src/app/(site)/redeem/page.tsx index 9a427fa7..4e793eb8 100644 --- a/src/app/(site)/redeem/page.tsx +++ b/src/app/(site)/redeem/page.tsx @@ -1,8 +1,9 @@ +import { eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard, StatBlock } from "@/components/public/ui"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, User } from "@/lib/db"; import RedeemForm from "./RedeemForm"; export const dynamic = "force-dynamic"; @@ -17,10 +18,12 @@ export default async function RedeemPage() { // Show the signed-in user's current credit balance for context (fail soft). let user: { username: string; credits: number } | null = null; try { - user = await prisma.user.findUnique({ - where: { id }, - select: { username: true, credits: true }, - }); + const [row] = await db + .select({ username: User.username, credits: User.credits }) + .from(User) + .where(eq(User.id, id)) + .limit(1); + user = row ?? null; } catch { user = null; } diff --git a/src/app/(site)/register/page.tsx b/src/app/(site)/register/page.tsx index d3d2fd3c..54e8dccb 100644 --- a/src/app/(site)/register/page.tsx +++ b/src/app/(site)/register/page.tsx @@ -1,3 +1,4 @@ +import { count, desc, eq } from "drizzle-orm"; import { headers } from "next/headers"; import Image from "next/image"; import Link from "next/link"; @@ -8,9 +9,9 @@ import { LanguageSwitcher } from "@/components/language-switcher"; import { Reveal } from "@/components/motion-reveal"; import { ThemeSwitcher } from "@/components/theme-switcher"; import { cached } from "@/lib/cache"; +import { db, User } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; import { captchaConfig } from "@/lib/services/captcha"; import { siteSettings } from "@/lib/services/site-settings"; @@ -32,21 +33,23 @@ export default async function RegisterPage() { const [online, recentUsers, latestUsers] = await Promise.all([ cached("online_count", 10_000, () => - prisma.user.count({ where: { online: "1" } }), + db + .select({ total: count() }) + .from(User) + .where(eq(User.online, "1")) + .then((rows) => rows[0]?.total ?? 0), ).catch(() => 0), - prisma.user - .findMany({ - where: { online: "1" }, - select: { username: true, look: true }, - take: 8, - }) + db + .select({ username: User.username, look: User.look }) + .from(User) + .where(eq(User.online, "1")) + .limit(8) .catch(() => []), - prisma.user - .findMany({ - orderBy: { accountCreated: "desc" }, - select: { username: true, look: true }, - take: 8, - }) + db + .select({ username: User.username, look: User.look }) + .from(User) + .orderBy(desc(User.accountCreated)) + .limit(8) .catch(() => []), ]); diff --git a/src/app/(site)/room/[id]/page.tsx b/src/app/(site)/room/[id]/page.tsx index e4694cf3..fefa1419 100644 --- a/src/app/(site)/room/[id]/page.tsx +++ b/src/app/(site)/room/[id]/page.tsx @@ -1,8 +1,9 @@ +import { eq } from "drizzle-orm"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; -import { prisma } from "@/lib/prisma"; +import { db, Rooms, User } from "@/lib/db"; export const revalidate = 300; @@ -31,24 +32,25 @@ function parseRoomId(raw: string): number | null { async function getRoom(raw: string) { const id = parseRoomId(raw); if (id === null) return null; - return prisma.rooms - .findUnique({ - where: { id }, - select: { - id: true, - ownerId: true, - ownerName: true, - name: true, - description: true, - state: true, - users: true, - usersMax: true, - category: true, - score: true, - tags: true, - }, + const [room] = await db + .select({ + id: Rooms.id, + ownerId: Rooms.ownerId, + ownerName: Rooms.ownerName, + name: Rooms.name, + description: Rooms.description, + state: Rooms.state, + users: Rooms.users, + usersMax: Rooms.usersMax, + category: Rooms.category, + score: Rooms.score, + tags: Rooms.tags, }) - .catch(() => null); + .from(Rooms) + .where(eq(Rooms.id, id)) + .limit(1) + .catch(() => []); + return room ?? null; } export async function generateMetadata({ @@ -83,12 +85,14 @@ export default async function RoomPage({ // owner_name column if the account row is missing or the query degrades. const owner = room.ownerId > 0 - ? await prisma.user - .findUnique({ - where: { id: room.ownerId }, - select: { username: true }, - }) - .catch(() => null) + ? ( + await db + .select({ username: User.username }) + .from(User) + .where(eq(User.id, room.ownerId)) + .limit(1) + .catch(() => []) + )[0] : null; const ownerName = owner?.username || room.ownerName || "Unknown"; diff --git a/src/app/(site)/search/page.tsx b/src/app/(site)/search/page.tsx index ecfff827..79cb9623 100644 --- a/src/app/(site)/search/page.tsx +++ b/src/app/(site)/search/page.tsx @@ -1,8 +1,9 @@ +import { and, eq, like } from "drizzle-orm"; import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; +import { db, Rooms, User } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; export const metadata: Metadata = { title: "Search" }; @@ -43,38 +44,31 @@ export default async function SearchPage({ ); } - const users = await prisma.user - .findMany({ - where: { - username: { contains: query }, - }, - select: { - id: true, - username: true, - look: true, - motto: true, - online: true, - rank: true, - }, - take: 30, + const users = await db + .select({ + id: User.id, + username: User.username, + look: User.look, + motto: User.motto, + online: User.online, + rank: User.rank, }) + .from(User) + .where(like(User.username, `%${query}%`)) + .limit(30) .catch(() => []); - const rooms = await prisma.rooms - .findMany({ - where: { - name: { contains: query }, - state: "open", - }, - select: { - id: true, - name: true, - ownerName: true, - users: true, - usersMax: true, - }, - take: 12, + const rooms = await db + .select({ + id: Rooms.id, + name: Rooms.name, + ownerName: Rooms.ownerName, + users: Rooms.users, + usersMax: Rooms.usersMax, }) + .from(Rooms) + .where(and(like(Rooms.name, `%${query}%`), eq(Rooms.state, "open"))) + .limit(12) .catch(() => []); return ( diff --git a/src/app/(site)/settings/2fa/page.tsx b/src/app/(site)/settings/2fa/page.tsx index f3f78c9a..194ac32c 100644 --- a/src/app/(site)/settings/2fa/page.tsx +++ b/src/app/(site)/settings/2fa/page.tsx @@ -1,3 +1,4 @@ +import { eq } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -11,8 +12,8 @@ import { env } from "@/env"; import { auth } from "@/lib/auth"; import { LaravelEncrypter } from "@/lib/auth/laravel-encrypter"; import { totpKeyUri } from "@/lib/auth/totp"; +import { db, User } from "@/lib/db"; import { resolveHotelName } from "@/lib/hotel-name"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -37,14 +38,16 @@ export default async function TwoFactorPage({ twoFactorRecoveryCodes: string | null; } | null = null; try { - user = await prisma.user.findUnique({ - where: { id }, - select: { - twoFactorSecret: true, - twoFactorConfirmedAt: true, - twoFactorRecoveryCodes: true, - }, - }); + const [row] = await db + .select({ + twoFactorSecret: User.twoFactorSecret, + twoFactorConfirmedAt: User.twoFactorConfirmedAt, + twoFactorRecoveryCodes: User.twoFactorRecoveryCodes, + }) + .from(User) + .where(eq(User.id, id)) + .limit(1); + user = row ?? null; } catch { user = null; } diff --git a/src/app/(site)/settings/page.tsx b/src/app/(site)/settings/page.tsx index bdc0f527..c2699d3f 100644 --- a/src/app/(site)/settings/page.tsx +++ b/src/app/(site)/settings/page.tsx @@ -1,3 +1,4 @@ +import { eq } from "drizzle-orm"; import Image from "next/image"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -5,8 +6,8 @@ import { getTranslations } from "next-intl/server"; import { updateMotto } from "@/actions/user-settings"; import { Reveal } from "@/components/motion-reveal"; import { auth } from "@/lib/auth"; +import { db, User } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; export const dynamic = "force-dynamic"; @@ -25,10 +26,17 @@ export default async function SettingsPage() { mail: string | null; } | null = null; try { - user = await prisma.user.findUnique({ - where: { id }, - select: { username: true, motto: true, look: true, mail: true }, - }); + const [row] = await db + .select({ + username: User.username, + motto: User.motto, + look: User.look, + mail: User.mail, + }) + .from(User) + .where(eq(User.id, id)) + .limit(1); + user = row ?? null; } catch { user = null; } diff --git a/src/app/(site)/settings/sessions/page.tsx b/src/app/(site)/settings/sessions/page.tsx index d6fb0775..d04647d4 100644 --- a/src/app/(site)/settings/sessions/page.tsx +++ b/src/app/(site)/settings/sessions/page.tsx @@ -1,10 +1,11 @@ +import { desc, eq, sql } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { signOutEverywhere } from "@/actions/sessions"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, WebsiteLoginLogs } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -54,20 +55,30 @@ export default async function SessionsPage({ const userId = Number(session.user.id); const { signedOutAll } = await searchParams; - const logins = await prisma.websiteLoginLogs - .findMany({ - where: { userId }, - orderBy: { id: "desc" }, - take: 30, - select: { id: true, ip: true, userAgent: true, createdAt: true }, + const logins = await db + .select({ + id: WebsiteLoginLogs.id, + ip: WebsiteLoginLogs.ip, + userAgent: WebsiteLoginLogs.userAgent, + createdAt: WebsiteLoginLogs.createdAt, }) + .from(WebsiteLoginLogs) + .where(eq(WebsiteLoginLogs.userId, userId)) + .orderBy(desc(WebsiteLoginLogs.id)) + .limit(30) .catch(() => []); type Failed = { id: number; ip_address: string; attempted_at: Date | string }; - const failed = await prisma.$queryRaw` - SELECT id, ip_address, attempted_at FROM failed_logins - WHERE username = ${session.user.name} ORDER BY id DESC LIMIT 15 - `.catch(() => [] as Failed[]); + let failed: Failed[] = []; + try { + const [rows] = (await db.execute(sql` + SELECT id, ip_address, attempted_at FROM failed_logins + WHERE username = ${session.user.name} ORDER BY id DESC LIMIT 15 + `)) as unknown as [Failed[], unknown]; + failed = rows ?? []; + } catch { + failed = []; + } return (
diff --git a/src/app/(site)/shop/page.tsx b/src/app/(site)/shop/page.tsx index 061cc76d..2672b492 100644 --- a/src/app/(site)/shop/page.tsx +++ b/src/app/(site)/shop/page.tsx @@ -1,11 +1,12 @@ +import { asc, eq } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { buyShopArticle } from "@/actions/shop"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db"; import { excerpt } from "@/lib/format"; -import { prisma } from "@/lib/prisma"; import { creditsPerUnit } from "@/lib/services/paypal"; export const revalidate = 300; @@ -62,18 +63,18 @@ type ShopCategory = { icon: string | null; }; -const ARTICLE_SELECT = { - id: true, - name: true, - info: true, - iconUrl: true, - color: true, - costs: true, - credits: true, - duckets: true, - diamonds: true, - badges: true, - position: true, +const articleSelect = { + id: WebsiteShopArticles.id, + name: WebsiteShopArticles.name, + info: WebsiteShopArticles.info, + iconUrl: WebsiteShopArticles.iconUrl, + color: WebsiteShopArticles.color, + costs: WebsiteShopArticles.costs, + credits: WebsiteShopArticles.credits, + duckets: WebsiteShopArticles.duckets, + diamonds: WebsiteShopArticles.diamonds, + badges: WebsiteShopArticles.badges, + position: WebsiteShopArticles.position, } as const; export default async function ShopPage({ @@ -115,43 +116,31 @@ export default async function ShopPage({ // Categories for the filter bar. let categories: ShopCategory[] = []; try { - categories = await prisma.websiteShopCategories.findMany({ - select: { id: true, name: true, icon: true }, - orderBy: { name: "asc" }, - }); + categories = await db + .select({ + id: WebsiteShopCategories.id, + name: WebsiteShopCategories.name, + icon: WebsiteShopCategories.icon, + }) + .from(WebsiteShopCategories) + .orderBy(asc(WebsiteShopCategories.name)); } catch { categories = []; } - // Articles. The category foreign key (website_shop_category_id) is not modelled - // in the typed Prisma client, so when filtering we resolve the matching ids via - // a guarded raw query, then load the rows through the typed client. let articles: ShopArticle[] = []; try { if (categoryId) { - let ids: bigint[] = []; - try { - const rows = await prisma.$queryRawUnsafe<{ id: bigint }[]>( - "SELECT id FROM website_shop_articles WHERE website_shop_category_id = ?", - BigInt(categoryId), - ); - ids = rows.map((r) => r.id); - } catch { - ids = []; - } - - articles = ids.length - ? await prisma.websiteShopArticles.findMany({ - where: { id: { in: ids } }, - select: ARTICLE_SELECT, - orderBy: { position: "asc" }, - }) - : []; + articles = await db + .select(articleSelect) + .from(WebsiteShopArticles) + .where(eq(WebsiteShopArticles.categoryId, BigInt(categoryId))) + .orderBy(asc(WebsiteShopArticles.position)); } else { - articles = await prisma.websiteShopArticles.findMany({ - select: ARTICLE_SELECT, - orderBy: { position: "asc" }, - }); + articles = await db + .select(articleSelect) + .from(WebsiteShopArticles) + .orderBy(asc(WebsiteShopArticles.position)); } } catch { articles = []; diff --git a/src/app/(site)/shop/topup/page.tsx b/src/app/(site)/shop/topup/page.tsx index d630315e..dd6996d1 100644 --- a/src/app/(site)/shop/topup/page.tsx +++ b/src/app/(site)/shop/topup/page.tsx @@ -1,8 +1,9 @@ +import { eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard, StatBlock } from "@/components/public/ui"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, User } from "@/lib/db"; import { creditsPerUnit, isPayPalConfigured, @@ -27,10 +28,12 @@ export default async function TopUpPage({ // Current credits balance for context (fail soft). let user: { username: string; credits: number } | null = null; try { - user = await prisma.user.findUnique({ - where: { id }, - select: { username: true, credits: true }, - }); + const [row] = await db + .select({ username: User.username, credits: User.credits }) + .from(User) + .where(eq(User.id, id)) + .limit(1); + user = row ?? null; } catch { user = null; } diff --git a/src/app/(site)/staff/page.tsx b/src/app/(site)/staff/page.tsx index 8656e0d3..371379a0 100644 --- a/src/app/(site)/staff/page.tsx +++ b/src/app/(site)/staff/page.tsx @@ -1,7 +1,8 @@ +import { asc, desc, eq, gte } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; -import { prisma } from "@/lib/prisma"; +import { db, User, WebsiteTeams } from "@/lib/db"; import { siteSettings } from "@/lib/services/site-settings"; export const revalidate = 300; @@ -30,30 +31,35 @@ export default async function StaffPage() { let staff: StaffMember[] = []; try { - staff = await prisma.user.findMany({ - where: { rank: { gte: minStaffRank } }, - select: { username: true, look: true, rank: true, motto: true }, - orderBy: [{ rank: "desc" }, { username: "asc" }], - take: 100, - }); + staff = await db + .select({ + username: User.username, + look: User.look, + rank: User.rank, + motto: User.motto, + }) + .from(User) + .where(gte(User.rank, minStaffRank)) + .orderBy(desc(User.rank), asc(User.username)) + .limit(100); } catch { staff = []; } let teams: Team[] = []; try { - const rows = await prisma.websiteTeams.findMany({ - where: { hiddenRank: false }, - select: { - id: true, - rankName: true, - badge: true, - jobDescription: true, - staffColor: true, - }, - orderBy: { id: "asc" }, - }); - teams = rows.map((t) => ({ ...t, id: String(t.id) })); + const rows = await db + .select({ + id: WebsiteTeams.id, + rankName: WebsiteTeams.rankName, + badge: WebsiteTeams.badge, + jobDescription: WebsiteTeams.jobDescription, + staffColor: WebsiteTeams.staffColor, + }) + .from(WebsiteTeams) + .where(eq(WebsiteTeams.hiddenRank, false)) + .orderBy(asc(WebsiteTeams.id)); + teams = rows.map((row) => ({ ...row, id: String(row.id) })); } catch { teams = []; } @@ -69,14 +75,14 @@ export default async function StaffPage() { subtitle={t("ranksSubtitle")} >
- {teams.map((t) => ( -
-

- {t.rankName} + {teams.map((team) => ( +
+

+ {team.rankName}

- {t.jobDescription ? ( + {team.jobDescription ? (

- {t.jobDescription} + {team.jobDescription}

) : null}
diff --git a/src/app/(site)/u/[username]/page.tsx b/src/app/(site)/u/[username]/page.tsx index 9723e60d..dd12ca6b 100644 --- a/src/app/(site)/u/[username]/page.tsx +++ b/src/app/(site)/u/[username]/page.tsx @@ -1,3 +1,4 @@ +import { and, asc, desc, eq, inArray, or } from "drizzle-orm"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -12,9 +13,19 @@ import { StatBlock, } from "@/components/public/ui"; import { auth } from "@/lib/auth"; +import { + CameraWeb, + db, + MessengerFriendrequests, + MessengerFriendships, + Rooms, + User, + UsersBadges, + UsersCurrency, + WebsiteUserGuestbooks, +} from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; import { formatDate } from "@/lib/format-date"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; export const dynamic = "force-dynamic"; @@ -47,12 +58,12 @@ export async function generateMetadata({ params: Promise<{ username: string }>; }): Promise { const { username } = await params; - const user = await prisma.user - .findUnique({ - where: { username }, - select: { username: true, motto: true }, - }) - .catch(() => null); + const [user] = await db + .select({ username: User.username, motto: User.motto }) + .from(User) + .where(eq(User.username, username)) + .limit(1) + .catch(() => []); if (!user) return { title: "Profile" }; return { title: user.username, @@ -78,26 +89,28 @@ export default async function ProfilePage({ const { username } = await params; const { friend, guestbook: guestbookStatus, error } = await searchParams; - const [user, imagerBase, session] = await Promise.all([ - prisma.user.findUnique({ - where: { username }, - select: { - id: true, - username: true, - motto: true, - look: true, - rank: true, - credits: true, - online: true, - accountCreated: true, - }, - }), + const [userRows, imagerBase, session] = await Promise.all([ + db + .select({ + id: User.id, + username: User.username, + motto: User.motto, + look: User.look, + rank: User.rank, + credits: User.credits, + online: User.online, + accountCreated: User.accountCreated, + }) + .from(User) + .where(eq(User.username, username)) + .limit(1), siteSettings.get( "habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage", ), auth(), ]); + const user = userRows[0] ?? null; if (!user) notFound(); @@ -112,34 +125,50 @@ export default async function ProfilePage({ let outgoingPending = false; let incomingPending = false; if (isLoggedIn && !isSelf) { - const [friendship, outgoing, incoming] = await Promise.all([ - prisma.messengerFriendships - .findFirst({ - where: { - OR: [ - { userOneId: meId, userTwoId: user.id }, - { userOneId: user.id, userTwoId: meId }, - ], - }, - select: { id: true }, - }) - .catch(() => null), - prisma.messengerFriendrequests - .findFirst({ - where: { userFromId: meId, userToId: user.id }, - select: { id: true }, - }) - .catch(() => null), - prisma.messengerFriendrequests - .findFirst({ - where: { userFromId: user.id, userToId: meId }, - select: { id: true }, - }) - .catch(() => null), + const [friendshipRows, outgoingRows, incomingRows] = await Promise.all([ + db + .select({ id: MessengerFriendships.id }) + .from(MessengerFriendships) + .where( + or( + and( + eq(MessengerFriendships.userOneId, meId), + eq(MessengerFriendships.userTwoId, user.id), + ), + and( + eq(MessengerFriendships.userOneId, user.id), + eq(MessengerFriendships.userTwoId, meId), + ), + ), + ) + .limit(1) + .catch(() => []), + db + .select({ id: MessengerFriendrequests.id }) + .from(MessengerFriendrequests) + .where( + and( + eq(MessengerFriendrequests.userFromId, meId), + eq(MessengerFriendrequests.userToId, user.id), + ), + ) + .limit(1) + .catch(() => []), + db + .select({ id: MessengerFriendrequests.id }) + .from(MessengerFriendrequests) + .where( + and( + eq(MessengerFriendrequests.userFromId, user.id), + eq(MessengerFriendrequests.userToId, meId), + ), + ) + .limit(1) + .catch(() => []), ]); - isFriend = Boolean(friendship); - outgoingPending = Boolean(outgoing); - incomingPending = Boolean(incoming); + isFriend = Boolean(friendshipRows[0]); + outgoingPending = Boolean(outgoingRows[0]); + incomingPending = Boolean(incomingRows[0]); } const feedbackErrorMessage = @@ -168,14 +197,18 @@ export default async function ProfilePage({ // Currency ledger rows (duckets/diamonds). Credits live on the users row // itself; the alt-currencies sit in the users_currency ledger keyed by type. - const currencyRows = await prisma.usersCurrency - .findMany({ - where: { - userId: user.id, - type: { in: [CURRENCY_DUCKETS, CURRENCY_DIAMONDS] }, - }, - select: { type: true, amount: true }, + const currencyRows = await db + .select({ + type: UsersCurrency.type, + amount: UsersCurrency.amount, }) + .from(UsersCurrency) + .where( + and( + eq(UsersCurrency.userId, user.id), + inArray(UsersCurrency.type, [CURRENCY_DUCKETS, CURRENCY_DIAMONDS]), + ), + ) .catch(() => []); const ducketsAmount = currencyRows.find((c) => c.type === CURRENCY_DUCKETS)?.amount ?? 0; @@ -207,21 +240,23 @@ export default async function ProfilePage({ // Friends. messenger_friendships stores one directed row per pair keyed on // user_one_id; resolve the user_two_id side to usernames/looks for the grid. - const friendships = await prisma.messengerFriendships - .findMany({ - where: { userOneId: user.id }, - orderBy: { friendsSince: "desc" }, - take: 18, - select: { userTwoId: true }, - }) + const friendships = await db + .select({ userTwoId: MessengerFriendships.userTwoId }) + .from(MessengerFriendships) + .where(eq(MessengerFriendships.userOneId, user.id)) + .orderBy(desc(MessengerFriendships.friendsSince)) + .limit(18) .catch(() => []); const friendIds = Array.from(new Set(friendships.map((f) => f.userTwoId))); const friendUsers = friendIds.length - ? await prisma.user - .findMany({ - where: { id: { in: friendIds } }, - select: { id: true, username: true, look: true }, + ? await db + .select({ + id: User.id, + username: User.username, + look: User.look, }) + .from(User) + .where(inArray(User.id, friendIds)) .catch(() => []) : []; const friendById = new Map(friendUsers.map((f) => [f.id, f])); @@ -230,52 +265,54 @@ export default async function ProfilePage({ .filter((f): f is NonNullable => Boolean(f)); // Rooms owned by this user. - const rooms = await prisma.rooms - .findMany({ - where: { ownerId: user.id }, - orderBy: { users: "desc" }, - take: 8, - select: { - id: true, - name: true, - users: true, - usersMax: true, - state: true, - }, + const rooms = await db + .select({ + id: Rooms.id, + name: Rooms.name, + users: Rooms.users, + usersMax: Rooms.usersMax, + state: Rooms.state, }) + .from(Rooms) + .where(eq(Rooms.ownerId, user.id)) + .orderBy(desc(Rooms.users)) + .limit(8) .catch(() => []); - const badges = await prisma.usersBadges - .findMany({ - where: { userId: user.id }, - orderBy: { slotId: "asc" }, - }) + const badges = await db + .select() + .from(UsersBadges) + .where(eq(UsersBadges.userId, user.id)) + .orderBy(asc(UsersBadges.slotId)) .catch(() => []); - const photos = await prisma.cameraWeb - .findMany({ - where: { userId: user.id }, - orderBy: { timestamp: "desc" }, - take: 6, - }) + const photos = await db + .select() + .from(CameraWeb) + .where(eq(CameraWeb.userId, user.id)) + .orderBy(desc(CameraWeb.timestamp)) + .limit(6) .catch(() => []); - const guestbook = await prisma.websiteUserGuestbooks - .findMany({ - where: { profileId: user.id }, - orderBy: { createdAt: "desc" }, - take: 30, - }) + const guestbook = await db + .select() + .from(WebsiteUserGuestbooks) + .where(eq(WebsiteUserGuestbooks.profileId, user.id)) + .orderBy(desc(WebsiteUserGuestbooks.createdAt)) + .limit(30) .catch(() => []); // Resolve author usernames/looks for the guestbook entries in one query. const authorIds = Array.from(new Set(guestbook.map((g) => g.userId))); const authors = authorIds.length - ? await prisma.user - .findMany({ - where: { id: { in: authorIds } }, - select: { id: true, username: true, look: true }, + ? await db + .select({ + id: User.id, + username: User.username, + look: User.look, }) + .from(User) + .where(inArray(User.id, authorIds)) .catch(() => []) : []; const authorById = new Map(authors.map((a) => [a.id, a])); diff --git a/src/app/(site)/verify/page.tsx b/src/app/(site)/verify/page.tsx index ce737ef5..4b7cd1c4 100644 --- a/src/app/(site)/verify/page.tsx +++ b/src/app/(site)/verify/page.tsx @@ -1,8 +1,9 @@ +import { eq } from "drizzle-orm"; import { CheckCircle2, Clock, MailX } from "lucide-react"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { isValidVerificationToken } from "@/actions/email-verify"; -import { prisma } from "@/lib/prisma"; +import { db, User } from "@/lib/db"; export const dynamic = "force-dynamic"; @@ -88,19 +89,20 @@ export default async function VerifyPage({ const ok = await isValidVerificationToken(normalisedEmail, token); if (ok) { try { - const user = await prisma.user.findFirst({ - where: { mail: normalisedEmail }, - select: { id: true, mailVerified: true }, - }); + const [user] = await db + .select({ id: User.id, mailVerified: User.mailVerified }) + .from(User) + .where(eq(User.mail, normalisedEmail)) + .limit(1); if (!user) { status = "invalid"; } else if (user.mailVerified === "1") { status = "already"; } else { - await prisma.user.update({ - where: { id: user.id }, - data: { mailVerified: "1" }, - }); + await db + .update(User) + .set({ mailVerified: "1" }) + .where(eq(User.id, user.id)); status = "verified"; } } catch { diff --git a/src/app/admin/achievements/page.tsx b/src/app/admin/achievements/page.tsx index c523995b..9c7db09f 100644 --- a/src/app/admin/achievements/page.tsx +++ b/src/app/admin/achievements/page.tsx @@ -1,8 +1,9 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; +import { Achievements, db } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type Achievement = { name: string; @@ -24,18 +25,22 @@ export default async function AdminAchievements() { let achievements: Achievement[]; try { - achievements = await prisma.achievements.findMany({ - select: { - name: true, - category: true, - level: true, - rewardAmount: true, - rewardType: true, - points: true, - progressNeeded: true, - }, - orderBy: [{ category: "asc" }, { name: "asc" }, { level: "asc" }], - }); + achievements = await db + .select({ + name: Achievements.name, + category: Achievements.category, + level: Achievements.level, + rewardAmount: Achievements.rewardAmount, + rewardType: Achievements.rewardType, + points: Achievements.points, + progressNeeded: Achievements.progressNeeded, + }) + .from(Achievements) + .orderBy( + asc(Achievements.category), + asc(Achievements.name), + asc(Achievements.level), + ); } catch { achievements = []; } diff --git a/src/app/admin/ads/[id]/page.tsx b/src/app/admin/ads/[id]/page.tsx index 70c0f4dc..3a434f9b 100644 --- a/src/app/admin/ads/[id]/page.tsx +++ b/src/app/admin/ads/[id]/page.tsx @@ -1,11 +1,12 @@ +import { eq } from "drizzle-orm"; import { Megaphone } from "lucide-react"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { updateAd } from "@/actions/admin-ads"; import { Button } from "@/components/ui/button"; +import { db, WebsiteAds } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { EditAdDeleteButton } from "../ads-table"; export default async function EditAd({ @@ -19,9 +20,14 @@ export default async function EditAd({ } const { id } = await params; - let ad: Awaited> = null; + let ad: typeof WebsiteAds.$inferSelect | null = null; try { - ad = await prisma.websiteAds.findUnique({ where: { id: BigInt(id) } }); + const [row] = await db + .select() + .from(WebsiteAds) + .where(eq(WebsiteAds.id, BigInt(id))) + .limit(1); + ad = row ?? null; } catch { notFound(); } diff --git a/src/app/admin/ads/page.tsx b/src/app/admin/ads/page.tsx index aa6b29dd..bbcc6973 100644 --- a/src/app/admin/ads/page.tsx +++ b/src/app/admin/ads/page.tsx @@ -1,11 +1,12 @@ +import { desc } 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, WebsiteAds } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { AdsTable } from "./ads-table"; export default async function AdminAds() { @@ -15,12 +16,13 @@ export default async function AdminAds() { } const t = await getTranslations("pages.admin.ads"); - let ads: Awaited>; + let ads: (typeof WebsiteAds.$inferSelect)[] = []; try { - ads = await prisma.websiteAds.findMany({ - orderBy: { id: "desc" }, - take: 500, - }); + ads = await db + .select() + .from(WebsiteAds) + .orderBy(desc(WebsiteAds.id)) + .limit(500); } catch { ads = []; } diff --git a/src/app/admin/alerts/page.tsx b/src/app/admin/alerts/page.tsx index 872708d4..2fe80aa2 100644 --- a/src/app/admin/alerts/page.tsx +++ b/src/app/admin/alerts/page.tsx @@ -1,11 +1,12 @@ +import { desc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { sendHotelAlert } from "@/actions/admin-alerts"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { AlertLogs, db } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; const CRITICAL = new Set(["critical", "error", "danger"]); const WARNING = new Set(["warning", "warn"]); @@ -58,12 +59,13 @@ export default async function AdminAlerts() { const t = await getTranslations("pages.admin.alerts"); - let alerts: Awaited>; + let alerts: (typeof AlertLogs.$inferSelect)[] = []; try { - alerts = await prisma.alertLogs.findMany({ - orderBy: { id: "desc" }, - take: 100, - }); + alerts = await db + .select() + .from(AlertLogs) + .orderBy(desc(AlertLogs.id)) + .limit(100); } catch { alerts = []; } diff --git a/src/app/admin/analytics/activity/page.tsx b/src/app/admin/analytics/activity/page.tsx index c1935630..40d740ef 100644 --- a/src/app/admin/analytics/activity/page.tsx +++ b/src/app/admin/analytics/activity/page.tsx @@ -1,10 +1,11 @@ +import { count, gte, inArray, sql } from "drizzle-orm"; import { MessageSquare, Shield, Terminal, Users } from "lucide-react"; import { redirect } from "next/navigation"; import { getLocale, getTranslations } from "next-intl/server"; import { StatsCard } from "@/components/admin/stats-card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Ban, db, User } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { redisCache } from "@/lib/redis-cache"; type DayRow = { @@ -34,11 +35,16 @@ async function safeCount( } } +async function rawRows(query: ReturnType): Promise { + const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; + return rows ?? []; +} + async function safeRawCount( - sql: Promise<{ count: bigint }[]>, + query: ReturnType, ): Promise<{ value: number; ok: boolean }> { try { - const rows = await sql; + const rows = await rawRows<{ count: bigint }>(query); return { value: Number(rows[0]?.count ?? 0), ok: true }; } catch { return { value: 0, ok: false }; @@ -81,24 +87,24 @@ async function getActivityData( try { const [chatRows, cmdRows, banRows, regRows] = await Promise.all([ - prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>` + rawRows<{ day_bucket: bigint; count: bigint }>(sql` SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count - FROM chatlogs_room WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`.catch( + FROM chatlogs_room WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`).catch( () => [], ), - prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>` + rawRows<{ day_bucket: bigint; count: bigint }>(sql` SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count - FROM commandlogs WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`.catch( + FROM commandlogs WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`).catch( () => [], ), - prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>` + rawRows<{ day_bucket: bigint; count: bigint }>(sql` SELECT FLOOR(timestamp / 86400) AS day_bucket, COUNT(*) AS count - FROM bans WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`.catch( + FROM bans WHERE timestamp >= ${rangeStart} GROUP BY day_bucket`).catch( () => [], ), - prisma.$queryRaw<{ day_bucket: bigint; count: bigint }[]>` + rawRows<{ day_bucket: bigint; count: bigint }>(sql` SELECT FLOOR(account_created / 86400) AS day_bucket, COUNT(*) AS count - FROM users WHERE account_created >= ${rangeStart} GROUP BY day_bucket`.catch( + FROM users WHERE account_created >= ${rangeStart} GROUP BY day_bucket`).catch( () => [], ), ]); @@ -124,19 +130,29 @@ async function getActivityData( const [weekChats, weekCommands, weekBans, weekRegistrations] = await Promise.all([ - safeRawCount( - prisma.$queryRaw<{ count: bigint }[]>` - SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`, + safeRawCount(sql` + SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`), + safeRawCount(sql` + SELECT COUNT(*) AS count FROM commandlogs WHERE timestamp >= ${weekAgo}`), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(Ban) + .where(gte(Ban.timestamp, weekAgo)) + )[0]?.total ?? 0, + ), ), - safeRawCount( - prisma.$queryRaw<{ count: bigint }[]>` - SELECT COUNT(*) AS count FROM commandlogs WHERE timestamp >= ${weekAgo}`, - ), - safeCount(() => - prisma.ban.count({ where: { timestamp: { gte: weekAgo } } }), - ), - safeCount(() => - prisma.user.count({ where: { accountCreated: { gte: weekAgo } } }), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(User) + .where(gte(User.accountCreated, weekAgo)) + )[0]?.total ?? 0, + ), ), ]); @@ -146,18 +162,18 @@ async function getActivityData( let topChatters: ActivityData["topChatters"] = []; try { - const rows = await prisma.$queryRaw<{ userId: number; messages: bigint }[]>` + const rows = await rawRows<{ userId: number; messages: bigint }>(sql` SELECT user_from_id AS userId, COUNT(*) AS messages FROM chatlogs_room WHERE timestamp >= ${weekAgo} GROUP BY user_from_id ORDER BY messages DESC LIMIT 10 - `; + `); const userIds = rows.map((c) => Number(c.userId)).filter((n) => n > 0); 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])); topChatters = rows.map((c) => { diff --git a/src/app/admin/analytics/economy/page.tsx b/src/app/admin/analytics/economy/page.tsx index 1bf5ce69..901dafe2 100644 --- a/src/app/admin/analytics/economy/page.tsx +++ b/src/app/admin/analytics/economy/page.tsx @@ -1,3 +1,4 @@ +import { and, count, eq, gte, sql, sum } from "drizzle-orm"; import { ArrowLeftRight } from "lucide-react"; import { redirect } from "next/navigation"; import { getLocale, getTranslations } from "next-intl/server"; @@ -5,8 +6,14 @@ import { RevenueChart } from "@/components/admin/revenue-chart"; import { StatsCard } from "@/components/admin/stats-card"; import { CurrencyIcon } from "@/components/shared/currency-icon"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + db, + LogsShopPurchases, + MarketplaceItems, + RoomTradeLog, + User, +} from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { redisCache } from "@/lib/redis-cache"; type EconomyData = { @@ -43,6 +50,11 @@ async function safeCount(fn: () => Promise): Promise { } } +async function rawRows(query: ReturnType): Promise { + const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; + return rows ?? []; +} + async function getEconomyData( locale: string, ): Promise<{ data: EconomyData; degraded: boolean }> { @@ -54,49 +66,86 @@ async function getEconomyData( try { const [creditsAgg, pixelsAgg, pointsAgg] = await Promise.all([ - prisma.user.aggregate({ _sum: { credits: true } }).catch(() => { - degraded = true; - return null; - }), - prisma.user.aggregate({ _sum: { pixels: true } }).catch(() => { - degraded = true; - return null; - }), - prisma.user.aggregate({ _sum: { points: true } }).catch(() => { - degraded = true; - return null; - }), + db + .select({ total: sum(User.credits) }) + .from(User) + .catch(() => { + degraded = true; + return [{ total: null }]; + }), + db + .select({ total: sum(User.pixels) }) + .from(User) + .catch(() => { + degraded = true; + return [{ total: null }]; + }), + db + .select({ total: sum(User.points) }) + .from(User) + .catch(() => { + degraded = true; + return [{ total: null }]; + }), ]); - const totalCredits = creditsAgg?._sum.credits ?? 0; - const totalPixels = pixelsAgg?._sum.pixels ?? 0; - const totalPoints = pointsAgg?._sum.points ?? 0; + const totalCredits = Number(creditsAgg[0]?.total ?? 0); + const totalPixels = Number(pixelsAgg[0]?.total ?? 0); + const totalPoints = Number(pointsAgg[0]?.total ?? 0); let shopPurchases = 0; let shopCreditsSpent = 0; let shopPointsSpent = 0; try { - const shopRevenue = await prisma.logsShopPurchases.aggregate({ - where: { timestamp: { gte: weekAgo } }, - _sum: { costCredits: true, costPoints: true }, - _count: true, - }); - shopPurchases = - typeof shopRevenue._count === "number" ? shopRevenue._count : 0; - shopCreditsSpent = shopRevenue._sum?.costCredits ?? 0; - shopPointsSpent = shopRevenue._sum?.costPoints ?? 0; + const [shopRevenue] = await db + .select({ + purchases: count(), + credits: sum(LogsShopPurchases.costCredits), + points: sum(LogsShopPurchases.costPoints), + }) + .from(LogsShopPurchases) + .where(gte(LogsShopPurchases.timestamp, weekAgo)); + shopPurchases = Number(shopRevenue?.purchases ?? 0); + shopCreditsSpent = Number(shopRevenue?.credits ?? 0); + shopPointsSpent = Number(shopRevenue?.points ?? 0); } catch { degraded = true; } const [marketplaceActive, marketplaceSold, weekTrades] = await Promise.all([ - safeCount(() => prisma.marketplaceItems.count({ where: { state: 1 } })), - safeCount(() => - prisma.marketplaceItems.count({ - where: { state: 2, soldTimestamp: { gte: weekAgo } }, - }), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(MarketplaceItems) + .where(eq(MarketplaceItems.state, 1)) + )[0]?.total ?? 0, + ), ), - safeCount(() => - prisma.roomTradeLog.count({ where: { timestamp: { gte: weekAgo } } }), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(MarketplaceItems) + .where( + and( + eq(MarketplaceItems.state, 2), + gte(MarketplaceItems.soldTimestamp, weekAgo), + ), + ) + )[0]?.total ?? 0, + ), + ), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(RoomTradeLog) + .where(gte(RoomTradeLog.timestamp, weekAgo)) + )[0]?.total ?? 0, + ), ), ]); @@ -112,20 +161,18 @@ async function getEconomyData( } try { - const revRows = await prisma.$queryRaw< - { - day_bucket: bigint; - credits: number | bigint; - points: number | bigint; - }[] - >` + const revRows = await rawRows<{ + day_bucket: bigint; + credits: number | bigint; + points: number | bigint; + }>(sql` SELECT FLOOR(timestamp / 86400) AS day_bucket, COALESCE(SUM(cost_credits), 0) AS credits, COALESCE(SUM(cost_points), 0) AS points FROM logs_shop_purchases WHERE timestamp >= ${rangeStart} GROUP BY day_bucket ORDER BY day_bucket - `; + `); const revMap = new Map( revRows.map((r) => [ Number(r.day_bucket), diff --git a/src/app/admin/analytics/page.tsx b/src/app/admin/analytics/page.tsx index eba1dd5a..e38a7236 100644 --- a/src/app/admin/analytics/page.tsx +++ b/src/app/admin/analytics/page.tsx @@ -1,3 +1,4 @@ +import { count, eq, gt, gte, sql } from "drizzle-orm"; import { ArrowLeftRight, Clock, @@ -12,8 +13,8 @@ import { DashboardChart } from "@/components/admin/dashboard-chart"; import { PeakHoursHeatmap } from "@/components/admin/peak-hours-heatmap"; import { StatsCard } from "@/components/admin/stats-card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Ban, db, Rooms, RoomTradeLog, User } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { redisCache } from "@/lib/redis-cache"; type AnalyticsData = { @@ -57,6 +58,11 @@ async function safeCount( } } +async function rawRows(query: ReturnType): Promise { + const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; + return rows ?? []; +} + async function getAnalyticsData( locale: string, ): Promise<{ data: AnalyticsData; degraded: boolean }> { @@ -78,30 +84,82 @@ async function getAnalyticsData( weekTrades, weekBans, ] = await Promise.all([ - safeCount(() => prisma.user.count()), - safeCount(() => prisma.user.count({ where: { online: "1" } })), - safeCount(() => - prisma.user.count({ where: { accountCreated: { gte: weekAgo } } }), + safeCount(async () => + Number((await db.select({ total: count() }).from(User))[0]?.total ?? 0), ), - safeCount(() => - prisma.user.count({ where: { accountCreated: { gte: monthAgo } } }), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(User) + .where(eq(User.online, "1")) + )[0]?.total ?? 0, + ), + ), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(User) + .where(gte(User.accountCreated, weekAgo)) + )[0]?.total ?? 0, + ), + ), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(User) + .where(gte(User.accountCreated, monthAgo)) + )[0]?.total ?? 0, + ), + ), + safeCount(async () => + Number( + (await db.select({ total: count() }).from(Rooms))[0]?.total ?? 0, + ), + ), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(Rooms) + .where(gt(Rooms.users, 0)) + )[0]?.total ?? 0, + ), ), - safeCount(() => prisma.rooms.count()), - safeCount(() => prisma.rooms.count({ where: { users: { gt: 0 } } })), (async () => { try { - const rows = await prisma.$queryRaw<{ count: bigint }[]>` - SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`; + const rows = await rawRows<{ count: bigint }>(sql` + SELECT COUNT(*) AS count FROM chatlogs_room WHERE timestamp >= ${weekAgo}`); return { value: Number(rows[0]?.count ?? 0), ok: true }; } catch { return { value: 0, ok: false }; } })(), - safeCount(() => - prisma.roomTradeLog.count({ where: { timestamp: { gte: weekAgo } } }), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(RoomTradeLog) + .where(gte(RoomTradeLog.timestamp, weekAgo)) + )[0]?.total ?? 0, + ), ), - safeCount(() => - prisma.ban.count({ where: { timestamp: { gte: weekAgo } } }), + safeCount(async () => + Number( + ( + await db + .select({ total: count() }) + .from(Ban) + .where(gte(Ban.timestamp, weekAgo)) + )[0]?.total ?? 0, + ), ), ]); @@ -121,14 +179,12 @@ async function getAnalyticsData( let registrationChart: { label: string; value: number }[] = []; try { - const regRows = await prisma.$queryRaw< - { day_bucket: bigint; count: bigint }[] - >` + const regRows = await rawRows<{ day_bucket: bigint; count: bigint }>(sql` SELECT FLOOR(account_created / 86400) AS day_bucket, COUNT(*) AS count FROM users WHERE account_created >= ${monthAgo} GROUP BY day_bucket ORDER BY day_bucket - `; + `); const regCountMap = new Map( regRows.map((r) => [Number(r.day_bucket), Number(r.count)]), ); @@ -152,23 +208,21 @@ async function getAnalyticsData( const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0)); let max = 0; try { - const peakRows = await prisma.$queryRaw< - { d: number; h: number; c: bigint }[] - >` + const peakRows = await rawRows<{ d: number; h: number; c: bigint }>(sql` SELECT ((DAYOFWEEK(FROM_UNIXTIME(last_online)) + 5) % 7) AS d, HOUR(FROM_UNIXTIME(last_online)) AS h, COUNT(*) AS c FROM users WHERE last_online >= ${weekAgo} GROUP BY d, h - `; + `); for (const r of peakRows) { const day = Number(r.d); const hour = Number(r.h); if (day < 0 || day > 6 || hour < 0 || hour > 23) continue; - const count = Number(r.c); - grid[day][hour] = count; - if (count > max) max = count; + const cnt = Number(r.c); + grid[day][hour] = cnt; + if (cnt > max) max = cnt; } } catch { degraded = true; diff --git a/src/app/admin/applications/page.tsx b/src/app/admin/applications/page.tsx index e58f6cc1..9c9a3ab8 100644 --- a/src/app/admin/applications/page.tsx +++ b/src/app/admin/applications/page.tsx @@ -1,12 +1,13 @@ +import { desc, inArray } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { dismissApplication } from "@/actions/admin-applications"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, User, WebsiteStaffApplications } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type ApplicationRow = { id: bigint; @@ -25,10 +26,11 @@ export default async function AdminApplications() { const t = await getTranslations("pages.admin.applications"); let applications: ApplicationRow[]; try { - applications = await prisma.websiteStaffApplications.findMany({ - orderBy: { createdAt: "desc" }, - take: 100, - }); + applications = await db + .select() + .from(WebsiteStaffApplications) + .orderBy(desc(WebsiteStaffApplications.createdAt)) + .limit(100); } catch { applications = []; } @@ -38,10 +40,10 @@ export default async function AdminApplications() { if (applications.length > 0) { try { const ids = Array.from(new Set(applications.map((a) => a.userId))); - 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)); for (const u of users) userMap.set(u.id, u.username); } catch { // no DB — fall back to raw ids diff --git a/src/app/admin/articles/[id]/page.tsx b/src/app/admin/articles/[id]/page.tsx index b3c9b248..b27e2c43 100644 --- a/src/app/admin/articles/[id]/page.tsx +++ b/src/app/admin/articles/[id]/page.tsx @@ -1,3 +1,4 @@ +import { eq } from "drizzle-orm"; import { Newspaper } from "lucide-react"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; @@ -5,8 +6,8 @@ import { getTranslations } from "next-intl/server"; import { deleteArticle, updateArticle } from "@/actions/admin-articles"; import { ArticleForm } from "@/components/admin/article-form"; import { Button } from "@/components/ui/button"; +import { db, WebsiteArticles } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function EditArticle({ params, @@ -23,19 +24,19 @@ export default async function EditArticle({ const { id } = await params; const { error } = await searchParams; const t = await getTranslations("pages.admin.articles"); - const article = await prisma.websiteArticles - .findUnique({ - where: { id: BigInt(id) }, - select: { - id: true, - slug: true, - title: true, - shortStory: true, - fullStory: true, - image: true, - }, + const [article] = await db + .select({ + id: WebsiteArticles.id, + slug: WebsiteArticles.slug, + title: WebsiteArticles.title, + shortStory: WebsiteArticles.shortStory, + fullStory: WebsiteArticles.fullStory, + image: WebsiteArticles.image, }) - .catch(() => null); + .from(WebsiteArticles) + .where(eq(WebsiteArticles.id, BigInt(id))) + .limit(1) + .catch(() => []); if (!article) notFound(); return ( diff --git a/src/app/admin/articles/page.tsx b/src/app/admin/articles/page.tsx index 142da4b4..de79b214 100644 --- a/src/app/admin/articles/page.tsx +++ b/src/app/admin/articles/page.tsx @@ -1,3 +1,4 @@ +import { desc, inArray } from "drizzle-orm"; import { Newspaper } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -6,9 +7,9 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { ArticleCard } from "@/components/admin/article-card"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, User, WebsiteArticles } 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 AdminArticles({ searchParams, @@ -22,19 +23,18 @@ export default async function AdminArticles({ const { error } = await searchParams; const t = await getTranslations("pages.admin.articles"); - const articles = await prisma.websiteArticles - .findMany({ - orderBy: { createdAt: "desc" }, - select: { - id: true, - title: true, - slug: true, - image: true, - createdAt: true, - userId: true, - }, - take: 50, + const articles = await db + .select({ + id: WebsiteArticles.id, + title: WebsiteArticles.title, + slug: WebsiteArticles.slug, + image: WebsiteArticles.image, + createdAt: WebsiteArticles.createdAt, + userId: WebsiteArticles.userId, }) + .from(WebsiteArticles) + .orderBy(desc(WebsiteArticles.createdAt)) + .limit(50) .catch(() => []); const authorMap = new Map(); @@ -42,11 +42,10 @@ export default async function AdminArticles({ .map((a) => a.userId) .filter((id): id is number => Boolean(id)); if (authorIds.length > 0) { - const authors = await prisma.user - .findMany({ - where: { id: { in: authorIds } }, - select: { id: true, username: true }, - }) + const authors = await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, authorIds)) .catch(() => []); for (const u of authors) authorMap.set(u.id, u.username); } diff --git a/src/app/admin/badges/page.tsx b/src/app/admin/badges/page.tsx index d46eb338..b615c6e1 100644 --- a/src/app/admin/badges/page.tsx +++ b/src/app/admin/badges/page.tsx @@ -1,10 +1,11 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { uploadBadge } from "@/actions/admin-badge-upload"; import { giveBadge } from "@/actions/admin-badges"; import { Button } from "@/components/ui/button"; +import { db, WebsiteBadges } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export const metadata = { title: "Badges" }; @@ -21,12 +22,13 @@ export default async function AdminBadges({ const t = await getTranslations("pages.admin.badges"); const { uploaded, error } = await searchParams; - let badges: Awaited>; + let badges: (typeof WebsiteBadges.$inferSelect)[] = []; try { - badges = await prisma.websiteBadges.findMany({ - orderBy: { badgeName: "asc" }, - take: 500, - }); + badges = await db + .select() + .from(WebsiteBadges) + .orderBy(asc(WebsiteBadges.badgeName)) + .limit(500); } catch { badges = []; } diff --git a/src/app/admin/banners/page.tsx b/src/app/admin/banners/page.tsx index 5f66bb09..b2ecaa26 100644 --- a/src/app/admin/banners/page.tsx +++ b/src/app/admin/banners/page.tsx @@ -1,6 +1,7 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; +import { db, WebsiteBanner } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { BannersManager } from "./banners-manager"; export default async function BannersPage() { @@ -9,9 +10,10 @@ export default async function BannersPage() { redirect("/admin"); } - const banners = await prisma.websiteBanner.findMany({ - orderBy: { sortOrder: "asc" }, - }); + const banners = await db + .select() + .from(WebsiteBanner) + .orderBy(asc(WebsiteBanner.sortOrder)); return (
diff --git a/src/app/admin/bans/page.tsx b/src/app/admin/bans/page.tsx index ade09696..3f2ac45c 100644 --- a/src/app/admin/bans/page.tsx +++ b/src/app/admin/bans/page.tsx @@ -1,3 +1,4 @@ +import { desc, eq, gt, or } from "drizzle-orm"; import { Ban } from "lucide-react"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -5,10 +6,10 @@ import { createBan, liftBan } from "@/actions/admin-bans"; import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; -import { activeBanWhere, unixNow } from "@/lib/bans"; +import { unixNow } from "@/lib/bans"; +import { Ban as BanTable, db } 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 AdminBans() { const { session, permissions } = await getAdminContext(); @@ -18,13 +19,14 @@ export default async function AdminBans() { const t = await getTranslations("pages.admin.bans"); const now = unixNow(); - let bans: Awaited> = []; + let bans: (typeof BanTable.$inferSelect)[] = []; try { - bans = await prisma.ban.findMany({ - where: activeBanWhere(now), - orderBy: { timestamp: "desc" }, - take: 100, - }); + bans = await db + .select() + .from(BanTable) + .where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now))) + .orderBy(desc(BanTable.timestamp)) + .limit(100); } catch { bans = []; } diff --git a/src/app/admin/calendar/[id]/page.tsx b/src/app/admin/calendar/[id]/page.tsx index ffc65a9c..6ab0e6e8 100644 --- a/src/app/admin/calendar/[id]/page.tsx +++ b/src/app/admin/calendar/[id]/page.tsx @@ -1,9 +1,10 @@ +import { asc, eq } from "drizzle-orm"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; +import { CalendarCampaigns, CalendarRewards, db } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type Campaign = { id: number; @@ -48,18 +49,20 @@ export default async function AdminCalendarCampaign({ let campaign: Campaign | null = null; try { - campaign = await prisma.calendarCampaigns.findUnique({ - where: { id: campaignId }, - select: { - id: true, - name: true, - image: true, - startTimestamp: true, - totalDays: true, - lockExpired: true, - enabled: true, - }, - }); + const [row] = await db + .select({ + id: CalendarCampaigns.id, + name: CalendarCampaigns.name, + image: CalendarCampaigns.image, + startTimestamp: CalendarCampaigns.startTimestamp, + totalDays: CalendarCampaigns.totalDays, + lockExpired: CalendarCampaigns.lockExpired, + enabled: CalendarCampaigns.enabled, + }) + .from(CalendarCampaigns) + .where(eq(CalendarCampaigns.id, campaignId)) + .limit(1); + campaign = row ?? null; } catch { campaign = null; } @@ -67,25 +70,25 @@ export default async function AdminCalendarCampaign({ let rewards: Reward[] = []; try { - rewards = await prisma.calendarRewards.findMany({ - where: { campaignId }, - select: { - id: true, - campaignId: true, - productName: true, - customImage: true, - credits: true, - pixels: true, - points: true, - pointsType: true, - badge: true, - itemId: true, - subscriptionType: true, - subscriptionDays: true, - }, - orderBy: { id: "asc" }, - take: 500, - }); + rewards = await db + .select({ + id: CalendarRewards.id, + campaignId: CalendarRewards.campaignId, + productName: CalendarRewards.productName, + customImage: CalendarRewards.customImage, + credits: CalendarRewards.credits, + pixels: CalendarRewards.pixels, + points: CalendarRewards.points, + pointsType: CalendarRewards.pointsType, + badge: CalendarRewards.badge, + itemId: CalendarRewards.itemId, + subscriptionType: CalendarRewards.subscriptionType, + subscriptionDays: CalendarRewards.subscriptionDays, + }) + .from(CalendarRewards) + .where(eq(CalendarRewards.campaignId, campaignId)) + .orderBy(asc(CalendarRewards.id)) + .limit(500); } catch { rewards = []; } diff --git a/src/app/admin/calendar/page.tsx b/src/app/admin/calendar/page.tsx index 41cdc87f..c8505a35 100644 --- a/src/app/admin/calendar/page.tsx +++ b/src/app/admin/calendar/page.tsx @@ -1,10 +1,11 @@ +import { count, desc } 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 { CalendarCampaigns, CalendarRewards, db } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; type Campaign = { id: number; @@ -27,19 +28,19 @@ export default async function AdminCalendar() { let campaigns: Campaign[] = []; let loadError = false; try { - campaigns = await prisma.calendarCampaigns.findMany({ - select: { - id: true, - name: true, - image: true, - startTimestamp: true, - totalDays: true, - lockExpired: true, - enabled: true, - }, - orderBy: { id: "desc" }, - take: 200, - }); + campaigns = await db + .select({ + id: CalendarCampaigns.id, + name: CalendarCampaigns.name, + image: CalendarCampaigns.image, + startTimestamp: CalendarCampaigns.startTimestamp, + totalDays: CalendarCampaigns.totalDays, + lockExpired: CalendarCampaigns.lockExpired, + enabled: CalendarCampaigns.enabled, + }) + .from(CalendarCampaigns) + .orderBy(desc(CalendarCampaigns.id)) + .limit(200); } catch { campaigns = []; loadError = true; @@ -48,13 +49,16 @@ export default async function AdminCalendar() { const rewardCounts = new Map(); let totalRewards = 0; try { - const grouped = await prisma.calendarRewards.groupBy({ - by: ["campaignId"], - _count: { _all: true }, - }); + const grouped = await db + .select({ + campaignId: CalendarRewards.campaignId, + total: count(), + }) + .from(CalendarRewards) + .groupBy(CalendarRewards.campaignId); for (const g of grouped) { - rewardCounts.set(g.campaignId, g._count._all); - totalRewards += g._count._all; + rewardCounts.set(g.campaignId, Number(g.total)); + totalRewards += Number(g.total); } } catch { loadError = true; diff --git a/src/app/admin/catalog/[id]/page.tsx b/src/app/admin/catalog/[id]/page.tsx index fb8dc805..432b16ba 100644 --- a/src/app/admin/catalog/[id]/page.tsx +++ b/src/app/admin/catalog/[id]/page.tsx @@ -1,9 +1,10 @@ +import { asc, eq } from "drizzle-orm"; import { ChevronRight } from "lucide-react"; import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { Fragment } from "react"; +import { CatalogPages, db } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { loadCatalogItemsData } from "@/lib/services/catalog-items-loader"; import { getAncestors } from "@/lib/services/catalog-tree"; import { CatalogDetailTabs } from "./catalog-detail-tabs"; @@ -22,18 +23,24 @@ export default async function CatalogEditPage({ const pageId = parseInt(id, 10); if (Number.isNaN(pageId)) notFound(); - const catalogPage = await prisma.catalogPages.findUnique({ - where: { id: pageId }, - }); + const [catalogPage] = await db + .select() + .from(CatalogPages) + .where(eq(CatalogPages.id, pageId)) + .limit(1); if (!catalogPage) notFound(); const [children, itemsData, ancestors] = await Promise.all([ - prisma.catalogPages.findMany({ - where: { parentId: pageId }, - orderBy: { orderNum: "asc" }, - select: { id: true, caption: true, enabled: true }, - }), + db + .select({ + id: CatalogPages.id, + caption: CatalogPages.caption, + enabled: CatalogPages.enabled, + }) + .from(CatalogPages) + .where(eq(CatalogPages.parentId, pageId)) + .orderBy(asc(CatalogPages.orderNum)), loadCatalogItemsData(pageId), getAncestors(catalogPage.parentId), ]); diff --git a/src/app/admin/catalog/builder-club/[id]/page.tsx b/src/app/admin/catalog/builder-club/[id]/page.tsx index a868833f..4a7ee9bd 100644 --- a/src/app/admin/catalog/builder-club/[id]/page.tsx +++ b/src/app/admin/catalog/builder-club/[id]/page.tsx @@ -1,6 +1,7 @@ +import { asc, eq } from "drizzle-orm"; import { notFound, redirect } from "next/navigation"; +import { CatalogItemsBc, CatalogPagesBc, db } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { BcPageDetail } from "../bc-manager"; export default async function BcPageDetailPage({ @@ -19,13 +20,18 @@ export default async function BcPageDetailPage({ const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); - const bcPage = await prisma.catalogPagesBc.findUnique({ where: { id } }); + const [bcPage] = await db + .select() + .from(CatalogPagesBc) + .where(eq(CatalogPagesBc.id, id)) + .limit(1); if (!bcPage) notFound(); - const bcItems = await prisma.catalogItemsBc.findMany({ - where: { pageId: id }, - orderBy: { orderNumber: "asc" }, - }); + const bcItems = await db + .select() + .from(CatalogItemsBc) + .where(eq(CatalogItemsBc.pageId, id)) + .orderBy(asc(CatalogItemsBc.orderNumber)); return ( (query: ReturnType): Promise { + const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; + return rows ?? []; +} + export default async function CatalogPagesPage({ params: paramsPromise, searchParams, @@ -33,7 +58,6 @@ export default async function CatalogPagesPage({ const viewMode = sp.get("view") || "tree"; const catalogType = sp.get("catalog") || "normal"; - // ── Builder Club mode ────────────────────────────────────────── type TreeNodeShape = { id: number; caption: string; @@ -48,41 +72,56 @@ export default async function CatalogPagesPage({ }; if (catalogType === "bc") { - const [totalBcPages, totalBcItems, enabledBcPages] = await Promise.all([ - prisma.catalogPagesBc.count(), - prisma.catalogItemsBc.count(), - prisma.catalogPagesBc.count({ where: { enabled: "1" } }), - ]); + const [totalBcPagesRows, totalBcItemsRows, enabledBcPagesRows] = + await Promise.all([ + db.select({ total: count() }).from(CatalogPagesBc), + db.select({ total: count() }).from(CatalogItemsBc), + db + .select({ total: count() }) + .from(CatalogPagesBc) + .where(eq(CatalogPagesBc.enabled, "1")), + ]); + const totalBcPages = Number(totalBcPagesRows[0]?.total ?? 0); + const totalBcItems = Number(totalBcItemsRows[0]?.total ?? 0); + const enabledBcPages = Number(enabledBcPagesRows[0]?.total ?? 0); - // ── BC tree view ────────────────────────────────────────────── let bcRootPages: TreeNodeShape[] = []; if (viewMode === "tree") { - const bcRootRaw = await prisma.catalogPagesBc.findMany({ - where: { OR: [{ parentId: -1 }, { parentId: 0 }] }, - orderBy: { orderNum: "asc" }, - }); + const bcRootRaw = await db + .select() + .from(CatalogPagesBc) + .where( + or(eq(CatalogPagesBc.parentId, -1), eq(CatalogPagesBc.parentId, 0)), + ) + .orderBy(asc(CatalogPagesBc.orderNum)); const bcRootIds = bcRootRaw.map((p) => p.id); const [bcChildCounts, bcItemCounts] = await Promise.all([ bcRootIds.length > 0 - ? prisma.catalogPagesBc.groupBy({ - by: ["parentId"], - where: { parentId: { in: bcRootIds } }, - _count: true, - }) + ? db + .select({ + parentId: CatalogPagesBc.parentId, + total: count(), + }) + .from(CatalogPagesBc) + .where(inArray(CatalogPagesBc.parentId, bcRootIds)) + .groupBy(CatalogPagesBc.parentId) : [], bcRootIds.length > 0 - ? prisma.catalogItemsBc.groupBy({ - by: ["pageId"], - where: { pageId: { in: bcRootIds } }, - _count: true, - }) + ? db + .select({ + pageId: CatalogItemsBc.pageId, + total: count(), + }) + .from(CatalogItemsBc) + .where(inArray(CatalogItemsBc.pageId, bcRootIds)) + .groupBy(CatalogItemsBc.pageId) : [], ]); const bcChildMap = Object.fromEntries( - bcChildCounts.map((c) => [c.parentId, c._count]), + bcChildCounts.map((c) => [c.parentId, Number(c.total)]), ); const bcItemMap = Object.fromEntries( - bcItemCounts.map((c) => [c.pageId, c._count]), + bcItemCounts.map((c) => [c.pageId, Number(c.total)]), ); bcRootPages = bcRootRaw.map((p) => ({ id: p.id, @@ -98,7 +137,6 @@ export default async function CatalogPagesPage({ })); } - // ── BC table view ───────────────────────────────────────────── let bcRows: { id: number; caption: string; @@ -119,32 +157,38 @@ export default async function CatalogPagesPage({ const parentId = parentFilter !== null ? parseInt(parentFilter, 10) : undefined; - const where: Record = {}; + const conditions: SQL[] = []; const hasParent = parentId !== undefined && !Number.isNaN(parentId); if (search) { - const pattern = { contains: search }; - where.OR = [{ caption: pattern }]; + conditions.push(like(CatalogPagesBc.caption, `%${search}%`)); } - if (hasParent) where.parentId = parentId; + if (hasParent) conditions.push(eq(CatalogPagesBc.parentId, parentId)); + const where = conditions.length > 0 ? and(...conditions) : undefined; - // Prisma sort key is snake_case for BC model. Translate known keys. - const sortKeyMap: Record = { - parentId: "parentId", - pageLayout: "pageLayout", - orderNum: "orderNum", + const sortKeyMap: Record = { + parentId: CatalogPagesBc.parentId, + pageLayout: CatalogPagesBc.pageLayout, + orderNum: CatalogPagesBc.orderNum, + caption: CatalogPagesBc.caption, + id: CatalogPagesBc.id, }; - const sortKey = sort ? (sortKeyMap[sort] ?? sort) : "orderNum"; - const orderBy = { [sortKey]: sort ? order : "asc" }; + const sortCol = sort + ? (sortKeyMap[sort] ?? CatalogPagesBc.orderNum) + : CatalogPagesBc.orderNum; + const orderBy = + (sort ? order : "asc") === "desc" ? desc(sortCol) : asc(sortCol); - const [pages, total] = await Promise.all([ - prisma.catalogPagesBc.findMany({ - where, - orderBy, - skip: (page - 1) * perPage, - take: perPage, - }), - prisma.catalogPagesBc.count({ where }), + const [pages, totalRows] = await Promise.all([ + db + .select() + .from(CatalogPagesBc) + .where(where) + .orderBy(orderBy) + .limit(perPage) + .offset((page - 1) * perPage), + db.select({ total: count() }).from(CatalogPagesBc).where(where), ]); + const total = Number(totalRows[0]?.total ?? 0); bcPagination = calcPagination(total, page, perPage); @@ -154,31 +198,40 @@ export default async function CatalogPagesPage({ ]; const [childCounts, itemCounts, parentPages] = await Promise.all([ pageIds.length > 0 - ? prisma.catalogPagesBc.groupBy({ - by: ["parentId"], - where: { parentId: { in: pageIds } }, - _count: true, - }) + ? db + .select({ + parentId: CatalogPagesBc.parentId, + total: count(), + }) + .from(CatalogPagesBc) + .where(inArray(CatalogPagesBc.parentId, pageIds)) + .groupBy(CatalogPagesBc.parentId) : [], pageIds.length > 0 - ? prisma.catalogItemsBc.groupBy({ - by: ["pageId"], - where: { pageId: { in: pageIds } }, - _count: true, - }) + ? db + .select({ + pageId: CatalogItemsBc.pageId, + total: count(), + }) + .from(CatalogItemsBc) + .where(inArray(CatalogItemsBc.pageId, pageIds)) + .groupBy(CatalogItemsBc.pageId) : [], parentIds.length > 0 - ? prisma.catalogPagesBc.findMany({ - where: { id: { in: parentIds } }, - select: { id: true, caption: true }, - }) + ? db + .select({ + id: CatalogPagesBc.id, + caption: CatalogPagesBc.caption, + }) + .from(CatalogPagesBc) + .where(inArray(CatalogPagesBc.id, parentIds)) : [], ]); const childCountMap = Object.fromEntries( - childCounts.map((c) => [c.parentId, c._count]), + childCounts.map((c) => [c.parentId, Number(c.total)]), ); const itemCountMap = Object.fromEntries( - itemCounts.map((c) => [c.pageId, c._count]), + itemCounts.map((c) => [c.pageId, Number(c.total)]), ); const parentNameMap = Object.fromEntries( parentPages.map((p) => [p.id, p.caption]), @@ -195,7 +248,7 @@ export default async function CatalogPagesPage({ pageLayout: String(p.pageLayout), enabled: String(p.enabled), visible: String(p.visible), - minRank: 0, // BC pages have no min rank column + minRank: 0, orderNum: p.orderNum, itemCount: itemCountMap[p.id] ?? 0, childCount: childCountMap[p.id] ?? 0, @@ -252,33 +305,39 @@ export default async function CatalogPagesPage({ ); } - // ── Normal catalog mode ──────────────────────────────────────── - - // Global stats (lightweight counts) - const [totalPages, totalItems, enabledPages] = await Promise.all([ - prisma.catalogPages.count(), - prisma.catalogItems.count(), - prisma.catalogPages.count({ where: { enabled: "1" } }), + const [totalPagesRows, totalItemsRows, enabledPagesRows] = await Promise.all([ + db.select({ total: count() }).from(CatalogPages), + db.select({ total: count() }).from(CatalogItems), + db + .select({ total: count() }) + .from(CatalogPages) + .where(eq(CatalogPages.enabled, "1")), ]); + const totalPages = Number(totalPagesRows[0]?.total ?? 0); + const totalItems = Number(totalItemsRows[0]?.total ?? 0); + const enabledPages = Number(enabledPagesRows[0]?.total ?? 0); - // ── Root pages (tree view + Visual Manager seed) ──────────────── - const rootPagesRaw = await prisma.catalogPages.findMany({ - where: { OR: [{ parentId: -1 }, { parentId: 0 }] }, - orderBy: { orderNum: "asc" }, - }); + const rootPagesRaw = await db + .select() + .from(CatalogPages) + .where(or(eq(CatalogPages.parentId, -1), eq(CatalogPages.parentId, 0))) + .orderBy(asc(CatalogPages.orderNum)); const rootIds = rootPagesRaw.map((p) => p.id); const [rootChildCounts, rootItemCountMap] = await Promise.all([ rootIds.length > 0 - ? prisma.catalogPages.groupBy({ - by: ["parentId"], - where: { parentId: { in: rootIds } }, - _count: true, - }) + ? db + .select({ + parentId: CatalogPages.parentId, + total: count(), + }) + .from(CatalogPages) + .where(inArray(CatalogPages.parentId, rootIds)) + .groupBy(CatalogPages.parentId) : [], getCatalogItemCounts(rootIds), ]); const rootChildMap = Object.fromEntries( - rootChildCounts.map((c) => [c.parentId, c._count]), + rootChildCounts.map((c) => [c.parentId, Number(c.total)]), ); const rootItemMap = Object.fromEntries(rootItemCountMap); const rootPages: TreeNodeShape[] = rootPagesRaw.map((p) => ({ @@ -294,7 +353,6 @@ export default async function CatalogPagesPage({ itemCount: rootItemMap[p.id] ?? 0, })); - // ── Table view data (skip if tree) ──────────────────────────── let rows: { id: number; caption: string; @@ -315,15 +373,15 @@ export default async function CatalogPagesPage({ const parentId = parentFilter !== null ? parseInt(parentFilter, 10) : undefined; - const where: Record = {}; + const conditions: SQL[] = []; const hasParent = parentId !== undefined && !Number.isNaN(parentId); if (search) { const searchPattern = `%${search}%`; const parentClause = hasParent - ? Prisma.sql`AND cp.parent_id = ${parentId}` - : Prisma.empty; + ? sql`AND cp.parent_id = ${parentId}` + : sql``; - const matchedIds = await prisma.$queryRaw<{ id: number }[]>(Prisma.sql` + const matchedIds = await rawRows<{ id: number }>(sql` SELECT DISTINCT cp.id FROM catalog_pages cp LEFT JOIN catalog_items ci ON CAST(ci.page_id AS CHAR) = CAST(cp.id AS CHAR) LEFT JOIN items_base ib ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ',')) @@ -334,48 +392,68 @@ export default async function CatalogPagesPage({ OR ib.item_name LIKE ${searchPattern}) ${parentClause} `); - where.id = { in: matchedIds.map((r) => r.id) }; + const ids = matchedIds.map((r) => r.id); + if (ids.length > 0) conditions.push(inArray(CatalogPages.id, ids)); + else conditions.push(sql`1 = 0`); } else if (hasParent) { - where.parentId = parentId; + conditions.push(eq(CatalogPages.parentId, parentId)); } - const orderBy = sort ? { [sort]: order } : { orderNum: "asc" as const }; + const where = conditions.length > 0 ? and(...conditions) : undefined; + const sortColMap: Record = { + caption: CatalogPages.caption, + parentId: CatalogPages.parentId, + pageLayout: CatalogPages.pageLayout, + enabled: CatalogPages.enabled, + orderNum: CatalogPages.orderNum, + id: CatalogPages.id, + minRank: CatalogPages.minRank, + }; + const sortCol = sort + ? (sortColMap[sort] ?? CatalogPages.orderNum) + : CatalogPages.orderNum; + const orderBy = + (sort ? order : "asc") === "desc" ? desc(sortCol) : asc(sortCol); - const [pages, total] = await Promise.all([ - prisma.catalogPages.findMany({ - where, - orderBy, - skip: (page - 1) * perPage, - take: perPage, - }), - prisma.catalogPages.count({ where }), + const [pages, totalRows] = await Promise.all([ + db + .select() + .from(CatalogPages) + .where(where) + .orderBy(orderBy) + .limit(perPage) + .offset((page - 1) * perPage), + db.select({ total: count() }).from(CatalogPages).where(where), ]); + const total = Number(totalRows[0]?.total ?? 0); pagination = calcPagination(total, page, perPage); - // Count children and items per page, and resolve parent names in parallel const pageIds = pages.map((p) => p.id); const parentIds = [ ...new Set(pages.map((p) => p.parentId).filter((id) => id > 0)), ]; const [childCounts, itemCountMapRaw, parentPages] = await Promise.all([ pageIds.length > 0 - ? prisma.catalogPages.groupBy({ - by: ["parentId"], - where: { parentId: { in: pageIds } }, - _count: true, - }) + ? db + .select({ + parentId: CatalogPages.parentId, + total: count(), + }) + .from(CatalogPages) + .where(inArray(CatalogPages.parentId, pageIds)) + .groupBy(CatalogPages.parentId) : [], getCatalogItemCounts(pageIds), parentIds.length > 0 - ? prisma.catalogPages.findMany({ - where: { id: { in: parentIds } }, - select: { id: true, caption: true }, - }) + ? db + .select({ id: CatalogPages.id, caption: CatalogPages.caption }) + .from(CatalogPages) + .where(inArray(CatalogPages.id, parentIds)) : [], ]); const childCountMap = Object.fromEntries( - childCounts.map((c) => [c.parentId, c._count]), + childCounts.map((c) => [c.parentId, Number(c.total)]), ); const itemCountMap = Object.fromEntries(itemCountMapRaw); const parentNameMap = Object.fromEntries( diff --git a/src/app/admin/commandocentrum/page.tsx b/src/app/admin/commandocentrum/page.tsx index d6b3442e..4463c861 100644 --- a/src/app/admin/commandocentrum/page.tsx +++ b/src/app/admin/commandocentrum/page.tsx @@ -1,4 +1,5 @@ import os from "node:os"; +import { count, desc, inArray } from "drizzle-orm"; import { Terminal } from "lucide-react"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -11,9 +12,9 @@ import { } from "@/components/admin/dashboard"; import { fetchOpsHealth } from "@/lib/admin/ops-health"; import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users"; +import { db, EmulatorErrors, StaffActivities, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; import { CommandocentrumControls } from "./commandocentrum-controls"; @@ -46,22 +47,29 @@ export default async function CommandoCentrum() { const t = await getTranslations("pages.admin.commandocentrum"); - const [totalUsers, errors, health, maintenance, activity, onlineRoster] = + const [totalUsersRows, errors, health, maintenance, activity, onlineRoster] = await Promise.all([ - prisma.user.count().catch(() => -1), - prisma.emulatorErrors - .findMany({ orderBy: { timestamp: "desc" }, take: 50 }) + db + .select({ total: count() }) + .from(User) + .catch(() => [{ total: -1 }]), + db + .select() + .from(EmulatorErrors) + .orderBy(desc(EmulatorErrors.timestamp)) + .limit(50) .catch(() => []), fetchOpsHealth(), siteSettings.getBool("maintenance_enabled", false), - prisma.staffActivities - .findMany({ orderBy: { createdAt: "desc" }, take: 12 }) - .catch( - () => - [] as Awaited>, - ), + db + .select() + .from(StaffActivities) + .orderBy(desc(StaffActivities.createdAt)) + .limit(12) + .catch(() => [] as (typeof StaffActivities.$inferSelect)[]), fetchOpsOnlineUsers(40), ]); + const totalUsers = Number(totalUsersRows[0]?.total ?? -1); const dbOk = health.dbOk; const redisOk = health.redisOk; @@ -74,11 +82,10 @@ export default async function CommandoCentrum() { ).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((u) => [u.id, u.username])); } diff --git a/src/app/admin/devops/errors/page.tsx b/src/app/admin/devops/errors/page.tsx index 348f208c..75a5dfcd 100644 --- a/src/app/admin/devops/errors/page.tsx +++ b/src/app/admin/devops/errors/page.tsx @@ -1,8 +1,9 @@ +import { count, desc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { Card, CardContent } from "@/components/ui/card"; +import { db, EmulatorErrors } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function EmulatorErrorsPage({ searchParams, @@ -19,15 +20,17 @@ export default async function EmulatorErrorsPage({ const page = Math.max(1, Number(params.page) || 1); const perPage = 50; - const [errors, total] = await Promise.all([ - prisma.emulatorErrors.findMany({ - orderBy: { id: "desc" }, - skip: (page - 1) * perPage, - take: perPage, - }), - prisma.emulatorErrors.count(), + const [errors, totalRows] = await Promise.all([ + db + .select() + .from(EmulatorErrors) + .orderBy(desc(EmulatorErrors.id)) + .limit(perPage) + .offset((page - 1) * perPage), + db.select({ total: count() }).from(EmulatorErrors), ]); + const total = Number(totalRows[0]?.total ?? 0); const lastPage = Math.ceil(total / perPage); return ( diff --git a/src/app/admin/devops/page.tsx b/src/app/admin/devops/page.tsx index 27eeba05..148325ce 100644 --- a/src/app/admin/devops/page.tsx +++ b/src/app/admin/devops/page.tsx @@ -1,3 +1,4 @@ +import { count, desc, eq, gte } from "drizzle-orm"; import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -5,35 +6,40 @@ import { getTranslations } from "next-intl/server"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { fetchOpsHealth } from "@/lib/admin/ops-health"; +import { db, EmulatorErrors, EmulatorSettings } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { HealthCheckClient } from "./health-check-client"; async function getDevOpsData() { const now = Math.floor(Date.now() / 1000); const dayAgo = now - 86400; - const [health, recentErrors, errorsToday, emuVersion] = await Promise.all([ - fetchOpsHealth(), - prisma.emulatorErrors.findMany({ - orderBy: { id: "desc" }, - take: 20, - }), - prisma.emulatorErrors.count({ - where: { timestamp: { gte: dayAgo } }, - }), - prisma.emulatorSettings.findFirst({ - where: { key: "version" }, - }), - ]); + const [health, recentErrors, errorsTodayRows, emuVersionRows] = + await Promise.all([ + fetchOpsHealth(), + db + .select() + .from(EmulatorErrors) + .orderBy(desc(EmulatorErrors.id)) + .limit(20), + db + .select({ total: count() }) + .from(EmulatorErrors) + .where(gte(EmulatorErrors.timestamp, dayAgo)), + db + .select({ value: EmulatorSettings.value }) + .from(EmulatorSettings) + .where(eq(EmulatorSettings.key, "version")) + .limit(1), + ]); return { dbStatus: health.dbOk ? ("healthy" as const) : ("error" as const), dbLatency: health.dbLatencyMs, redisOk: health.redisOk, recentErrors, - errorsToday, - emuVersion: emuVersion?.value ?? "Unknown", + errorsToday: Number(errorsTodayRows[0]?.total ?? 0), + emuVersion: emuVersionRows[0]?.value ?? "Unknown", onlineUsers: health.onlineUsers, }; } diff --git a/src/app/admin/email-templates/page.tsx b/src/app/admin/email-templates/page.tsx index 201b4303..ae8d8c34 100644 --- a/src/app/admin/email-templates/page.tsx +++ b/src/app/admin/email-templates/page.tsx @@ -1,3 +1,4 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { @@ -6,8 +7,8 @@ import { updateEmailTemplate, } from "@/actions/admin-email-templates"; import { Button } from "@/components/ui/button"; +import { db, EmailTemplates } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export default async function AdminEmailTemplates() { const { session, permissions } = await getAdminContext(); @@ -17,12 +18,12 @@ export default async function AdminEmailTemplates() { const t = await getTranslations("pages.admin.emailTemplates"); - let templates: Awaited> = - []; + let templates: (typeof EmailTemplates.$inferSelect)[] = []; try { - templates = await prisma.emailTemplates.findMany({ - orderBy: { name: "asc" }, - }); + templates = await db + .select() + .from(EmailTemplates) + .orderBy(asc(EmailTemplates.name)); } catch { templates = []; } diff --git a/src/app/admin/emulator/page.tsx b/src/app/admin/emulator/page.tsx index b1e8146b..06c183e4 100644 --- a/src/app/admin/emulator/page.tsx +++ b/src/app/admin/emulator/page.tsx @@ -1,3 +1,4 @@ +import { asc, count } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { @@ -6,8 +7,8 @@ import { } from "@/actions/admin-emulator"; import { StatusCard } from "@/components/admin/dashboard"; import { Button } from "@/components/ui/button"; +import { db, EmulatorSettings, EmulatorTexts } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; const TEXTS_LIMIT = 300; @@ -21,9 +22,10 @@ export default async function AdminEmulator() { let settings: { key: string; value: string }[] = []; try { - settings = await prisma.emulatorSettings.findMany({ - orderBy: { key: "asc" }, - }); + settings = await db + .select({ key: EmulatorSettings.key, value: EmulatorSettings.value }) + .from(EmulatorSettings) + .orderBy(asc(EmulatorSettings.key)); } catch { settings = []; } @@ -31,15 +33,17 @@ export default async function AdminEmulator() { let texts: { key: string; value: string }[] = []; let textsCount = 0; try { - textsCount = await prisma.emulatorTexts.count(); + const [row] = await db.select({ total: count() }).from(EmulatorTexts); + textsCount = Number(row?.total ?? 0); } catch { textsCount = 0; } try { - texts = await prisma.emulatorTexts.findMany({ - orderBy: { key: "asc" }, - take: TEXTS_LIMIT, - }); + texts = await db + .select({ key: EmulatorTexts.key, value: EmulatorTexts.value }) + .from(EmulatorTexts) + .orderBy(asc(EmulatorTexts.key)) + .limit(TEXTS_LIMIT); } catch { texts = []; } diff --git a/src/app/admin/events/[id]/page.tsx b/src/app/admin/events/[id]/page.tsx index 216e2336..a59fca3d 100644 --- a/src/app/admin/events/[id]/page.tsx +++ b/src/app/admin/events/[id]/page.tsx @@ -1,11 +1,12 @@ +import { asc, eq, inArray } from "drizzle-orm"; import { CalendarDays, Gift, Trophy, Users } from "lucide-react"; 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, User, WebsiteEvent, WebsiteEventType } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { EventForm } from "../event-form"; import { EventPrizesManager } from "./event-prizes"; import { EventRegistrations } from "./event-registrations"; @@ -28,22 +29,23 @@ export default async function EventDetailPage({ const t = await getTranslations("pages.admin.events"); const tHubs = await getTranslations("pages.admin.hubs"); - const event = await prisma.websiteEvent.findUnique({ - where: { id: eventId }, - include: { + const event = await db.query.WebsiteEvent.findFirst({ + where: eq(WebsiteEvent.id, eventId), + with: { type: true, - prizes: { orderBy: { position: "asc" } }, - winners: { orderBy: { position: "asc" } }, - registrations: { orderBy: { createdAt: "desc" } }, + prizes: { orderBy: (p, { asc: a }) => [a(p.position)] }, + winners: { orderBy: (w, { asc: a }) => [a(w.position)] }, + registrations: { orderBy: (r, { desc: d }) => [d(r.createdAt)] }, }, }); if (!event) notFound(); - const eventTypes = await prisma.websiteEventType.findMany({ - where: { isActive: 1 }, - orderBy: { name: "asc" }, - }); + const eventTypes = await db + .select() + .from(WebsiteEventType) + .where(eq(WebsiteEventType.isActive, 1)) + .orderBy(asc(WebsiteEventType.name)); // Fetch usernames for registrations and winners const userIds = [ @@ -52,10 +54,10 @@ export default async function EventDetailPage({ ]; const users = userIds.length > 0 - ? await prisma.user.findMany({ - where: { id: { in: [...new Set(userIds)] } }, - select: { id: true, username: true }, - }) + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, [...new Set(userIds)])) : []; const userMap = new Map(users.map((u) => [u.id, u.username])); diff --git a/src/app/admin/events/create/page.tsx b/src/app/admin/events/create/page.tsx index 9706cd97..65a14b23 100644 --- a/src/app/admin/events/create/page.tsx +++ b/src/app/admin/events/create/page.tsx @@ -1,7 +1,8 @@ +import { asc, eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; +import { db, WebsiteEventType } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { EventForm } from "../event-form"; export default async function CreateEventPage() { @@ -11,10 +12,11 @@ export default async function CreateEventPage() { } const t = await getTranslations("pages.admin.events"); - const eventTypes = await prisma.websiteEventType.findMany({ - where: { isActive: 1 }, - orderBy: { name: "asc" }, - }); + const eventTypes = await db + .select() + .from(WebsiteEventType) + .where(eq(WebsiteEventType.isActive, 1)) + .orderBy(asc(WebsiteEventType.name)); return (
diff --git a/src/app/admin/events/types/page.tsx b/src/app/admin/events/types/page.tsx index c6ded9c8..e74d9317 100644 --- a/src/app/admin/events/types/page.tsx +++ b/src/app/admin/events/types/page.tsx @@ -1,6 +1,7 @@ +import { asc } from "drizzle-orm"; import { redirect } from "next/navigation"; +import { db, WebsiteEventType } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { EventTypesManager } from "./event-types-manager"; export default async function EventTypesPage() { @@ -9,9 +10,10 @@ export default async function EventTypesPage() { redirect("/admin/events"); } - const eventTypes = await prisma.websiteEventType.findMany({ - orderBy: { name: "asc" }, - }); + const eventTypes = await db + .select() + .from(WebsiteEventType) + .orderBy(asc(WebsiteEventType.name)); return (
diff --git a/src/app/admin/favicon/page.tsx b/src/app/admin/favicon/page.tsx index 638b11d1..43c17e36 100644 --- a/src/app/admin/favicon/page.tsx +++ b/src/app/admin/favicon/page.tsx @@ -1,6 +1,7 @@ +import { eq } from "drizzle-orm"; import { redirect } from "next/navigation"; +import { db, WebsiteSetting } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; import { FaviconForm } from "./favicon-form"; export default async function AdminFaviconPage() { @@ -11,9 +12,11 @@ export default async function AdminFaviconPage() { let faviconUrl: string | null = null; try { - const setting = await prisma.websiteSetting.findUnique({ - where: { key: "cms_favicon" }, - }); + const [setting] = await db + .select({ value: WebsiteSetting.value }) + .from(WebsiteSetting) + .where(eq(WebsiteSetting.key, "cms_favicon")) + .limit(1); faviconUrl = setting?.value ?? null; } catch { faviconUrl = null;