refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (3)

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 14:15:32 +02:00
1 parent 2cd0863cb8
commit 580972c0a0
40 files changed
+1089 -808

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+9 -7
View File
@@ -1,10 +1,11 @@
import { eq } from "drizzle-orm";
import { HelpCircle } from "lucide-react"; import { HelpCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help"; import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function EditHelpQuestion({ export default async function EditHelpQuestion({
params, params,
@@ -20,13 +21,14 @@ export default async function EditHelpQuestion({
const { id } = await params; const { id } = await params;
const { error } = await searchParams; const { error } = await searchParams;
let entry: Awaited< let entry: typeof WebsiteHelpCenterCategories.$inferSelect | null = null;
ReturnType<typeof prisma.websiteHelpCenterCategories.findUnique>
> = null;
try { try {
entry = await prisma.websiteHelpCenterCategories.findUnique({ const [row] = await db
where: { id: BigInt(id) }, .select()
}); .from(WebsiteHelpCenterCategories)
.where(eq(WebsiteHelpCenterCategories.id, BigInt(id)))
.limit(1);
entry = row ?? null;
} catch { } catch {
notFound(); notFound();
} }
+11 -8
View File
@@ -1,10 +1,11 @@
import { asc } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function AdminHelpQuestions() { export default async function AdminHelpQuestions() {
const { session, permissions } = await getAdminContext(); const { session, permissions } = await getAdminContext();
@@ -13,14 +14,16 @@ export default async function AdminHelpQuestions() {
} }
const t = await getTranslations("pages.admin.helpCenter"); const t = await getTranslations("pages.admin.helpCenter");
let entries: Awaited< let entries: (typeof WebsiteHelpCenterCategories.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteHelpCenterCategories.findMany>
> = [];
try { try {
entries = await prisma.websiteHelpCenterCategories.findMany({ entries = await db
orderBy: [{ position: "asc" }, { id: "asc" }], .select()
take: 500, .from(WebsiteHelpCenterCategories)
}); .orderBy(
asc(WebsiteHelpCenterCategories.position),
asc(WebsiteHelpCenterCategories.id),
)
.limit(500);
} catch { } catch {
entries = []; entries = [];
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+40 -22
View File
@@ -1,8 +1,15 @@
import { asc, eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import {
AclModelPermission,
AclPermission,
AclRole,
db,
WebsiteHousekeepingPermissions,
} from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { getAuditLogs } from "@/lib/services/audit"; import { getAuditLogs } from "@/lib/services/audit";
import { AuditSection, ExportSection } from "./client"; import { AuditSection, ExportSection } from "./client";
@@ -31,12 +38,14 @@ export default async function AdminHousekeeping({
const sp = await searchParams; const sp = await searchParams;
const tab = sp.tab ?? "overview"; const tab = sp.tab ?? "overview";
type HKPerm = Awaited< const allPermissions = await db
ReturnType<typeof prisma.websiteHousekeepingPermissions.findMany> .select()
>[0]; .from(WebsiteHousekeepingPermissions)
const allPermissions: HKPerm[] = await prisma.websiteHousekeepingPermissions .orderBy(
.findMany({ orderBy: [{ groupName: "asc" }, { permission: "asc" }] }) asc(WebsiteHousekeepingPermissions.groupName),
.catch(() => []); asc(WebsiteHousekeepingPermissions.permission),
)
.catch((): (typeof WebsiteHousekeepingPermissions.$inferSelect)[] => []);
const ranks = allPermissions.map((p) => p.minRank); const ranks = allPermissions.map((p) => p.minRank);
const maxRank = ranks.length ? Math.max(...ranks) : 0; const maxRank = ranks.length ? Math.max(...ranks) : 0;
@@ -48,34 +57,43 @@ export default async function AdminHousekeeping({
groupCounts[g] = (groupCounts[g] || 0) + 1; groupCounts[g] = (groupCounts[g] || 0) + 1;
} }
const rankGroups: Record<string, typeof allPermissions> = {}; const rankGroups: Record<
string,
(typeof WebsiteHousekeepingPermissions.$inferSelect)[]
> = {};
for (const p of allPermissions) { for (const p of allPermissions) {
if (!rankGroups[p.minRank]) rankGroups[p.minRank] = []; const rk = String(p.minRank);
rankGroups[p.minRank].push(p); if (!rankGroups[rk]) rankGroups[rk] = [];
rankGroups[rk].push(p);
} }
const aclSlugs = await prisma.aclPermission const aclSlugs = await db
.findMany({ select: { slug: true } }) .select({ slug: AclPermission.slug })
.from(AclPermission)
.catch(() => [] as { slug: string }[]); .catch(() => [] as { slug: string }[]);
const aclSlugSet = new Set(aclSlugs.map((p) => p.slug)); const aclSlugSet = new Set(aclSlugs.map((p) => p.slug));
const aclRolePerms = await prisma.aclModelPermission const aclRolePerms = await db
.findMany({ .select({
where: { modelType: "Role" }, roleSlug: AclRole.slug,
include: { permissionSlug: AclPermission.slug,
permission: { select: { slug: true } },
role: { select: { slug: true } },
},
}) })
.catch(() => []); .from(AclModelPermission)
.innerJoin(
AclPermission,
eq(AclModelPermission.permissionId, AclPermission.id),
)
.innerJoin(AclRole, eq(AclModelPermission.modelId, AclRole.id))
.where(eq(AclModelPermission.modelType, "Role"))
.catch((): { roleSlug: string; permissionSlug: string }[] => []);
const aclMinRankBySlug = new Map<string, number>(); const aclMinRankBySlug = new Map<string, number>();
for (const row of aclRolePerms) { for (const row of aclRolePerms) {
const roleSlug = row.role.slug; const roleSlug = row.roleSlug;
if (!roleSlug.startsWith("rank_")) continue; if (!roleSlug.startsWith("rank_")) continue;
const rank = Number(roleSlug.slice(5)); const rank = Number(roleSlug.slice(5));
if (!Number.isFinite(rank)) continue; if (!Number.isFinite(rank)) continue;
const slug = row.permission.slug; const slug = row.permissionSlug;
const prev = aclMinRankBySlug.get(slug); const prev = aclMinRankBySlug.get(slug);
if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank); if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank);
} }
+12 -9
View File
@@ -1,3 +1,4 @@
import { desc } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -8,9 +9,9 @@ import {
} from "@/actions/admin-ip"; } from "@/actions/admin-ip";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteIpBlacklist, WebsiteIpWhitelist } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
type IpRow = { type IpRow = {
id: bigint; id: bigint;
@@ -30,19 +31,21 @@ export default async function AdminIp() {
let blacklist: IpRow[] = []; let blacklist: IpRow[] = [];
try { try {
whitelist = await prisma.websiteIpWhitelist.findMany({ whitelist = await db
orderBy: { id: "desc" }, .select()
take: 200, .from(WebsiteIpWhitelist)
}); .orderBy(desc(WebsiteIpWhitelist.id))
.limit(200);
} catch { } catch {
whitelist = []; whitelist = [];
} }
try { try {
blacklist = await prisma.websiteIpBlacklist.findMany({ blacklist = await db
orderBy: { id: "desc" }, .select()
take: 200, .from(WebsiteIpBlacklist)
}); .orderBy(desc(WebsiteIpBlacklist.id))
.limit(200);
} catch { } catch {
blacklist = []; blacklist = [];
} }
+7 -2
View File
@@ -1,9 +1,10 @@
import { eq } from "drizzle-orm";
import { Package } from "lucide-react"; import { Package } from "lucide-react";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { db, ItemsBase } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { ItemsBaseEditForm } from "./items-base-edit-form"; import { ItemsBaseEditForm } from "./items-base-edit-form";
export default async function ItemsBaseDetailPage({ export default async function ItemsBaseDetailPage({
@@ -21,7 +22,11 @@ export default async function ItemsBaseDetailPage({
const id = Number(rawId); const id = Number(rawId);
if (!Number.isFinite(id) || id <= 0) notFound(); if (!Number.isFinite(id) || id <= 0) notFound();
const item = await prisma.itemsBase.findUnique({ where: { id } }); const [item] = await db
.select()
.from(ItemsBase)
.where(eq(ItemsBase.id, id))
.limit(1);
if (!item) notFound(); if (!item) notFound();
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
+8 -6
View File
@@ -1,3 +1,4 @@
import { eq } from "drizzle-orm";
import { LogOut } from "lucide-react"; import { LogOut } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -15,9 +16,9 @@ import {
ADMIN_NAV_CONFIG_KEY, ADMIN_NAV_CONFIG_KEY,
parseAdminNavConfig, parseAdminNavConfig,
} from "@/lib/admin-nav-config"; } from "@/lib/admin-nav-config";
import { db, User } from "@/lib/db";
import { setCsrfCookie } from "@/lib/foundation/security"; import { setCsrfCookie } from "@/lib/foundation/security";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
// Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be // Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be
@@ -37,11 +38,12 @@ export default async function AdminLayout({
csrfToken = ""; csrfToken = "";
} }
if (await siteSettings.getBool("force_staff_2fa", false)) { if (await siteSettings.getBool("force_staff_2fa", false)) {
const u = await prisma.user const u = await db
.findUnique({ .select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt })
where: { id: staff.id }, .from(User)
select: { twoFactorConfirmedAt: true }, .where(eq(User.id, staff.id))
}) .limit(1)
.then((rows) => rows[0] ?? null)
.catch(() => null); .catch(() => null);
if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired"); if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired");
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -1,10 +1,11 @@
import { inArray } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { saveMaintenance } from "@/actions/admin-maintenance"; import { saveMaintenance } from "@/actions/admin-maintenance";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteSetting } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const KEYS = [ const KEYS = [
"maintenance_enabled", "maintenance_enabled",
@@ -22,10 +23,10 @@ export default async function AdminMaintenance() {
let rows: { key: string; value: string }[] = []; let rows: { key: string; value: string }[] = [];
try { try {
rows = await prisma.websiteSetting.findMany({ rows = await db
where: { key: { in: [...KEYS] } }, .select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
select: { key: true, value: true }, .from(WebsiteSetting)
}); .where(inArray(WebsiteSetting.key, [...KEYS]));
} catch { } catch {
rows = []; rows = [];
} }
+28 -18
View File
@@ -1,3 +1,4 @@
import { count, desc, eq, inArray } from "drizzle-orm";
import { Store } from "lucide-react"; import { Store } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -8,9 +9,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, MarketplaceItems, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function AdminMarketplacePage({ export default async function AdminMarketplacePage({
searchParams, searchParams,
@@ -28,27 +29,37 @@ export default async function AdminMarketplacePage({
const parsed = parseListParams(new URLSearchParams(raw)); const parsed = parseListParams(new URLSearchParams(raw));
const filter = raw.filter === "sold" ? "sold" : "active"; const filter = raw.filter === "sold" ? "sold" : "active";
const where = filter === "sold" ? { state: 2 } : { state: 1 }; const where = eq(MarketplaceItems.state, filter === "sold" ? 2 : 1);
let total = 0; let total = 0;
let rows: Awaited<ReturnType<typeof prisma.marketplaceItems.findMany>> = []; let rows: (typeof MarketplaceItems.$inferSelect)[] = [];
let activeCount = 0; let activeCount = 0;
let soldCount = 0; let soldCount = 0;
let loadError = false; let loadError = false;
try { try {
[total, activeCount, soldCount] = await Promise.all([ const [totalRows, activeRows, soldRows] = await Promise.all([
prisma.marketplaceItems.count({ where }), db.select({ total: count() }).from(MarketplaceItems).where(where),
prisma.marketplaceItems.count({ where: { state: 1 } }), db
prisma.marketplaceItems.count({ where: { state: 2 } }), .select({ total: count() })
.from(MarketplaceItems)
.where(eq(MarketplaceItems.state, 1)),
db
.select({ total: count() })
.from(MarketplaceItems)
.where(eq(MarketplaceItems.state, 2)),
]); ]);
total = Number(totalRows[0]?.total ?? 0);
activeCount = Number(activeRows[0]?.total ?? 0);
soldCount = Number(soldRows[0]?.total ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
rows = await prisma.marketplaceItems.findMany({ rows = await db
where, .select()
orderBy: { timestamp: "desc" }, .from(MarketplaceItems)
skip: pagination.offset, .where(where)
take: pagination.perPage, .orderBy(desc(MarketplaceItems.timestamp))
}); .limit(pagination.perPage)
.offset(pagination.offset);
} catch { } catch {
loadError = true; loadError = true;
} }
@@ -59,11 +70,10 @@ export default async function AdminMarketplacePage({
]; ];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: userIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, userIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const usernameById = new Map(users.map((u) => [u.id, u.username])); const usernameById = new Map(users.map((u) => [u.id, u.username]));
+34 -25
View File
@@ -1,6 +1,7 @@
import { count, desc, eq, inArray } from "drizzle-orm";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { Ban, db, SupportTickets, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { CfhDetail } from "./cfh-detail"; import { CfhDetail } from "./cfh-detail";
export default async function CfhDetailPage({ export default async function CfhDetailPage({
@@ -16,9 +17,11 @@ export default async function CfhDetailPage({
const ticketId = Number(id); const ticketId = Number(id);
if (Number.isNaN(ticketId)) notFound(); if (Number.isNaN(ticketId)) notFound();
const ticket = await prisma.supportTickets.findUnique({ const [ticket] = await db
where: { id: ticketId }, .select()
}); .from(SupportTickets)
.where(eq(SupportTickets.id, ticketId))
.limit(1);
if (!ticket) notFound(); if (!ticket) notFound();
const canEdit = canAccess( const canEdit = canAccess(
@@ -30,36 +33,42 @@ export default async function CfhDetailPage({
// Fetch sender, reported, mod info // Fetch sender, reported, mod info
const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter( const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter(
Boolean, Boolean,
); ) as number[];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user.findMany({ ? await db
where: { id: { in: userIds } }, .select({
select: { id: User.id,
id: true, username: User.username,
username: true, look: User.look,
look: true, rank: User.rank,
rank: true, online: User.online,
online: true, ipRegister: User.ipRegister,
ipRegister: true, mail: User.mail,
mail: true, })
}, .from(User)
}) .where(inArray(User.id, userIds))
: []; : [];
const userMap = Object.fromEntries(users.map((u) => [u.id, u])); const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
// Get ban/mute history for reported user // Get ban/mute history for reported user
const reportedId = ticket.reportedId; const reportedId = ticket.reportedId;
const [banCount, recentBans] = reportedId const [banCountRows, recentBans] = reportedId
? await Promise.all([ ? await Promise.all([
prisma.ban.count({ where: { userId: reportedId } }), db
prisma.ban.findMany({ .select({ total: count() })
where: { userId: reportedId }, .from(Ban)
orderBy: { id: "desc" }, .where(eq(Ban.userId, reportedId)),
take: 5, db
}), .select()
.from(Ban)
.where(eq(Ban.userId, reportedId))
.orderBy(desc(Ban.id))
.limit(5),
]) ])
: [0, []]; : [[{ total: 0 }], []];
const banCount = Number(banCountRows[0]?.total ?? 0);
return ( return (
<CfhDetail <CfhDetail
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+39 -27
View File
@@ -1,3 +1,4 @@
import { count, desc, eq, gt, inArray, or } from "drizzle-orm";
import { import {
Activity, Activity,
ArrowRight, ArrowRight,
@@ -9,57 +10,68 @@ import {
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { activeBanWhere } from "@/lib/bans"; import { unixNow } from "@/lib/bans";
import {
Ban as BanTable,
db,
StaffActivities,
User,
WebsiteArticles,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type RankCount = { rank: number; count: number }; type RankCount = { rank: number; count: number };
export default async function AdminDashboard() { export default async function AdminDashboard() {
const t = await getTranslations("pages.admin.dashboard"); const t = await getTranslations("pages.admin.dashboard");
const now = unixNow();
let users = -1; let users = -1;
let online = 0; let online = 0;
let articles = 0; let articles = 0;
let bans = 0; let bans = 0;
try { try {
[users, online, articles, bans] = await Promise.all([ const [usersRow, onlineRow, articlesRow, bansRow] = await Promise.all([
prisma.user.count(), db.select({ total: count() }).from(User),
prisma.user.count({ where: { online: "1" } }), db.select({ total: count() }).from(User).where(eq(User.online, "1")),
prisma.websiteArticles.count(), db.select({ total: count() }).from(WebsiteArticles),
prisma.ban.count({ db
where: activeBanWhere(), .select({ total: count() })
}), .from(BanTable)
.where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now))),
]); ]);
users = usersRow[0]?.total ?? 0;
online = onlineRow[0]?.total ?? 0;
articles = articlesRow[0]?.total ?? 0;
bans = bansRow[0]?.total ?? 0;
} catch {} } catch {}
const dbOk = users >= 0; const dbOk = users >= 0;
const rankGroups = await prisma.user const rankGroups = await db
.groupBy({ .select({ rank: User.rank, total: count() })
by: ["rank"], .from(User)
_count: { _all: true }, .groupBy(User.rank)
orderBy: { rank: "desc" }, .orderBy(desc(User.rank))
}) .catch(() => [] as Array<{ rank: number; total: number }>);
.catch(() => [] as Array<{ rank: number; _count: { _all: number } }>);
const rankCounts: RankCount[] = rankGroups const rankCounts: RankCount[] = rankGroups
.map((g) => ({ rank: g.rank, count: g._count._all })) .map((g) => ({ rank: g.rank, count: Number(g.total) }))
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count);
const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0); const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0);
const activity = await prisma.staffActivities const activity = await db
.findMany({ orderBy: { createdAt: "desc" }, take: 8 }) .select()
.catch( .from(StaffActivities)
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>, .orderBy(desc(StaffActivities.createdAt))
); .limit(8)
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]);
const staffIds = Array.from( const staffIds = Array.from(
new Set(activity.map((a) => Number(a.userId))), new Set(activity.map((a) => Number(a.userId))),
).filter((n) => n > 0); ).filter((n) => n > 0);
let nameById = new Map<number, string>(); let nameById = new Map<number, string>();
if (staffIds.length) { if (staffIds.length) {
const u = await prisma.user const u = await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: staffIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, staffIds))
})
.catch(() => []); .catch(() => []);
nameById = new Map(u.map((x) => [x.id, x.username])); nameById = new Map(u.map((x) => [x.id, x.username]));
} }
+18 -13
View File
@@ -1,3 +1,4 @@
import { count, desc, inArray } from "drizzle-orm";
import { Camera } from "lucide-react"; import { Camera } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -7,8 +8,8 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { CameraWeb, db, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function AdminPhotos({ export default async function AdminPhotos({
searchParams, searchParams,
@@ -23,16 +24,21 @@ export default async function AdminPhotos({
const t = await getTranslations("pages.admin.photos"); const t = await getTranslations("pages.admin.photos");
const parsed = parseListParams(new URLSearchParams(await searchParams)); const parsed = parseListParams(new URLSearchParams(await searchParams));
const total = await prisma.cameraWeb.count().catch(() => 0); const [totalRow] = await db
.select({ total: count() })
.from(CameraWeb)
.catch(() => [{ total: 0 }]);
const total = Number(totalRow?.total ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage || 24); const pagination = calcPagination(total, parsed.page, parsed.perPage || 24);
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = []; let photos: (typeof CameraWeb.$inferSelect)[] = [];
try { try {
photos = await prisma.cameraWeb.findMany({ photos = await db
orderBy: { timestamp: "desc" }, .select()
skip: pagination.offset, .from(CameraWeb)
take: pagination.perPage, .orderBy(desc(CameraWeb.timestamp))
}); .limit(pagination.perPage)
.offset(pagination.offset);
} catch { } catch {
photos = []; photos = [];
} }
@@ -40,11 +46,10 @@ export default async function AdminPhotos({
const userIds = [...new Set(photos.map((p) => p.userId))]; const userIds = [...new Set(photos.map((p) => p.userId))];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: userIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, userIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const usernameById = new Map(users.map((u) => [u.id, u.username])); const usernameById = new Map(users.map((u) => [u.id, u.username]));
+7 -6
View File
@@ -1,10 +1,11 @@
import { eq } from "drizzle-orm";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { db, WebsitePoll } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { PollForm } from "../poll-form"; import { PollForm } from "../poll-form";
import { PollQuestionsManager } from "./poll-questions"; import { PollQuestionsManager } from "./poll-questions";
import { PollResultsView } from "./poll-results"; import { PollResultsView } from "./poll-results";
@@ -26,12 +27,12 @@ export default async function PollDetailPage({
const t = await getTranslations("pages.admin.polls"); const t = await getTranslations("pages.admin.polls");
const tHubs = await getTranslations("pages.admin.hubs"); const tHubs = await getTranslations("pages.admin.hubs");
const poll = await prisma.websitePoll.findUnique({ const poll = await db.query.WebsitePoll.findFirst({
where: { id: pollId }, where: eq(WebsitePoll.id, pollId),
include: { with: {
questions: { questions: {
orderBy: { sortOrder: "asc" }, orderBy: (q, { asc }) => [asc(q.sortOrder)],
include: { with: {
votes: true, votes: true,
}, },
}, },
+16 -16
View File
@@ -1,3 +1,4 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
createApiKey, createApiKey,
@@ -6,8 +7,8 @@ import {
} from "@/actions/admin-radio-api-keys"; } from "@/actions/admin-radio-api-keys";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, RadioApiKeys } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const metadata = { title: "Radio API Keys" }; export const metadata = { title: "Radio API Keys" };
@@ -34,22 +35,21 @@ export default async function AdminRadioApiKeysPage() {
let dbError = false; let dbError = false;
try { try {
keys = await prisma.radioApiKeys keys = await db
.findMany({ .select({
select: { id: RadioApiKeys.id,
id: true, name: RadioApiKeys.name,
name: true, key: RadioApiKeys.key,
key: true, allowedIps: RadioApiKeys.allowedIps,
allowedIps: true, rateLimit: RadioApiKeys.rateLimit,
rateLimit: true, lastUsedAt: RadioApiKeys.lastUsedAt,
lastUsedAt: true, expiresAt: RadioApiKeys.expiresAt,
expiresAt: true, isActive: RadioApiKeys.isActive,
isActive: true, createdAt: RadioApiKeys.createdAt,
createdAt: true,
},
orderBy: [{ isActive: "desc" }, { id: "desc" }],
take: 500,
}) })
.from(RadioApiKeys)
.orderBy(desc(RadioApiKeys.isActive), desc(RadioApiKeys.id))
.limit(500)
.catch(() => { .catch(() => {
dbError = true; dbError = true;
return []; return [];
+19 -16
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -7,8 +8,8 @@ import {
} from "@/actions/admin-radio-autodj"; } from "@/actions/admin-radio-autodj";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, RadioAutoDjPlaylist } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const metadata = { title: "Radio AutoDJ" }; export const metadata = { title: "Radio AutoDJ" };
@@ -39,22 +40,24 @@ export default async function AdminRadioAutoDjPage() {
let dbError = false; let dbError = false;
try { try {
tracks = await prisma.radioAutoDjPlaylist tracks = await db
.findMany({ .select({
select: { id: RadioAutoDjPlaylist.id,
id: true, title: RadioAutoDjPlaylist.title,
title: true, artist: RadioAutoDjPlaylist.artist,
artist: true, album: RadioAutoDjPlaylist.album,
album: true, artworkUrl: RadioAutoDjPlaylist.artworkUrl,
artworkUrl: true, duration: RadioAutoDjPlaylist.duration,
duration: true, playCount: RadioAutoDjPlaylist.playCount,
playCount: true, lastPlayedAt: RadioAutoDjPlaylist.lastPlayedAt,
lastPlayedAt: true, isActive: RadioAutoDjPlaylist.isActive,
isActive: true, sortOrder: RadioAutoDjPlaylist.sortOrder,
sortOrder: true,
},
orderBy: [{ sortOrder: "asc" }, { title: "asc" }],
}) })
.from(RadioAutoDjPlaylist)
.orderBy(
asc(RadioAutoDjPlaylist.sortOrder),
asc(RadioAutoDjPlaylist.title),
)
.catch(() => { .catch(() => {
dbError = true; dbError = true;
return []; return [];
+15 -14
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import NextImage from "next/image"; import NextImage from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -7,8 +8,8 @@ import {
} from "@/actions/admin-radio-extra"; } from "@/actions/admin-radio-extra";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, RadioBanners } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type Banner = { type Banner = {
id: bigint; id: bigint;
@@ -28,19 +29,19 @@ export default async function AdminRadioBannersPage() {
let dbError = false; let dbError = false;
try { try {
banners = await prisma.radioBanners.findMany({ banners = await db
select: { .select({
id: true, id: RadioBanners.id,
userId: true, userId: RadioBanners.userId,
imagePath: true, imagePath: RadioBanners.imagePath,
title: true, title: RadioBanners.title,
description: true, description: RadioBanners.description,
sortOrder: true, sortOrder: RadioBanners.sortOrder,
isActive: true, isActive: RadioBanners.isActive,
createdAt: true, createdAt: RadioBanners.createdAt,
}, })
orderBy: [{ sortOrder: "asc" }, { id: "asc" }], .from(RadioBanners)
}); .orderBy(asc(RadioBanners.sortOrder), asc(RadioBanners.id));
} catch { } catch {
dbError = true; dbError = true;
banners = []; banners = [];
+19 -18
View File
@@ -1,7 +1,8 @@
import { desc, inArray } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { db, RadioHistory, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type History = { type History = {
id: bigint; id: bigint;
@@ -31,28 +32,28 @@ export default async function AdminRadioHistoryPage() {
let dbError = false; let dbError = false;
try { try {
history = await prisma.radioHistory.findMany({ history = await db
select: { .select({
id: true, id: RadioHistory.id,
userId: true, userId: RadioHistory.userId,
showName: true, showName: RadioHistory.showName,
startedAt: true, startedAt: RadioHistory.startedAt,
endedAt: true, endedAt: RadioHistory.endedAt,
listenersCount: true, listenersCount: RadioHistory.listenersCount,
notes: true, notes: RadioHistory.notes,
}, })
orderBy: { startedAt: "desc" }, .from(RadioHistory)
take: 100, .orderBy(desc(RadioHistory.startedAt))
}); .limit(100);
const ids = Array.from( const ids = Array.from(
new Set(history.map((h) => Number(h.userId))), new Set(history.map((h) => Number(h.userId))),
).filter((n) => Number.isFinite(n) && n > 0); ).filter((n) => Number.isFinite(n) && n > 0);
if (ids.length > 0) { if (ids.length > 0) {
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: ids } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}); .where(inArray(User.id, ids));
names = new Map(users.map((u) => [String(u.id), u.username])); names = new Map(users.map((u) => [String(u.id), u.username]));
} }
} catch { } catch {
+13 -12
View File
@@ -1,9 +1,10 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { deleteShout } from "@/actions/admin-radio-moderation"; import { deleteShout } from "@/actions/admin-radio-moderation";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, RadioShouts } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type Shout = { type Shout = {
id: bigint; id: bigint;
@@ -18,17 +19,17 @@ export default async function AdminRadioModerationPage() {
let shouts: Shout[] = []; let shouts: Shout[] = [];
try { try {
shouts = await prisma.radioShouts.findMany({ shouts = await db
select: { .select({
id: true, id: RadioShouts.id,
userId: true, userId: RadioShouts.userId,
message: true, message: RadioShouts.message,
createdAt: true, createdAt: RadioShouts.createdAt,
updatedAt: true, updatedAt: RadioShouts.updatedAt,
}, })
orderBy: { id: "desc" }, .from(RadioShouts)
take: 50, .orderBy(desc(RadioShouts.id))
}); .limit(50);
} catch { } catch {
shouts = []; shouts = [];
} }
+39 -33
View File
@@ -1,9 +1,10 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { deleteShout } from "@/actions/admin-radio-moderation"; import { deleteShout } from "@/actions/admin-radio-moderation";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, RadioApplications, RadioSchedules, RadioShouts } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type Shout = { type Shout = {
id: bigint; id: bigint;
@@ -52,48 +53,53 @@ export default async function AdminRadioPage() {
let schedules: Schedule[] = []; let schedules: Schedule[] = [];
try { try {
shouts = await prisma.radioShouts.findMany({ shouts = await db
select: { id: true, userId: true, message: true, createdAt: true }, .select({
orderBy: { id: "desc" }, id: RadioShouts.id,
take: 25, userId: RadioShouts.userId,
}); message: RadioShouts.message,
createdAt: RadioShouts.createdAt,
})
.from(RadioShouts)
.orderBy(desc(RadioShouts.id))
.limit(25);
} catch { } catch {
shouts = []; shouts = [];
} }
try { try {
applications = await prisma.radioApplications.findMany({ applications = await db
select: { .select({
id: true, id: RadioApplications.id,
userId: true, userId: RadioApplications.userId,
realName: true, realName: RadioApplications.realName,
age: true, age: RadioApplications.age,
status: true, status: RadioApplications.status,
musicStyle: true, musicStyle: RadioApplications.musicStyle,
discordUsername: true, discordUsername: RadioApplications.discordUsername,
createdAt: true, createdAt: RadioApplications.createdAt,
}, })
orderBy: { id: "desc" }, .from(RadioApplications)
take: 25, .orderBy(desc(RadioApplications.id))
}); .limit(25);
} catch { } catch {
applications = []; applications = [];
} }
try { try {
schedules = await prisma.radioSchedules.findMany({ schedules = await db
select: { .select({
id: true, id: RadioSchedules.id,
userId: true, userId: RadioSchedules.userId,
day: true, day: RadioSchedules.day,
startTime: true, startTime: RadioSchedules.startTime,
endTime: true, endTime: RadioSchedules.endTime,
showName: true, showName: RadioSchedules.showName,
isActive: true, isActive: RadioSchedules.isActive,
}, })
orderBy: { id: "desc" }, .from(RadioSchedules)
take: 25, .orderBy(desc(RadioSchedules.id))
}); .limit(25);
} catch { } catch {
schedules = []; schedules = [];
} }
+12 -11
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
createRadioRank, createRadioRank,
@@ -6,7 +7,7 @@ import {
} from "@/actions/admin-radio-extra"; } from "@/actions/admin-radio-extra";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { prisma } from "@/lib/prisma"; import { db, RadioRanks } from "@/lib/db";
type Rank = { type Rank = {
id: bigint; id: bigint;
@@ -23,16 +24,16 @@ export default async function AdminRadioRanksPage() {
let dbError = false; let dbError = false;
try { try {
ranks = await prisma.radioRanks.findMany({ ranks = await db
select: { .select({
id: true, id: RadioRanks.id,
name: true, name: RadioRanks.name,
description: true, description: RadioRanks.description,
badgeCode: true, badgeCode: RadioRanks.badgeCode,
isActive: true, isActive: RadioRanks.isActive,
}, })
orderBy: { id: "asc" }, .from(RadioRanks)
}); .orderBy(asc(RadioRanks.id));
} catch { } catch {
dbError = true; dbError = true;
ranks = []; ranks = [];
+8 -4
View File
@@ -5,7 +5,7 @@ import {
} from "@/actions/admin-radio-extra"; } from "@/actions/admin-radio-extra";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { prisma } from "@/lib/prisma"; import { db, WebsiteSetting } from "@/lib/db";
type Field = { key: string; comment: string }; type Field = { key: string; comment: string };
type Group = { title: string; fields: Field[] }; type Group = { title: string; fields: Field[] };
@@ -364,9 +364,13 @@ export default async function AdminRadioSettingsPage() {
let extra: Field[] = []; let extra: Field[] = [];
try { try {
const rows = await prisma.websiteSetting.findMany({ const rows = await db
select: { key: true, value: true, comment: true }, .select({
}); key: WebsiteSetting.key,
value: WebsiteSetting.value,
comment: WebsiteSetting.comment,
})
.from(WebsiteSetting);
values = new Map(rows.map((r) => [r.key, r.value])); values = new Map(rows.map((r) => [r.key, r.value]));
extra = rows extra = rows
.filter( .filter(
+15 -12
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -8,8 +9,8 @@ import {
} from "@/actions/admin-rare-values"; } from "@/actions/admin-rare-values";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"]; const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"];
@@ -21,23 +22,25 @@ export default async function AdminRareValues() {
const t = await getTranslations("pages.admin.rareValues"); const t = await getTranslations("pages.admin.rareValues");
let categories: Awaited< let categories: (typeof WebsiteRareValueCategories.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteRareValueCategories.findMany>
> = [];
try { try {
categories = await prisma.websiteRareValueCategories.findMany({ categories = await db
orderBy: [{ priority: "asc" }, { name: "asc" }], .select()
}); .from(WebsiteRareValueCategories)
.orderBy(
asc(WebsiteRareValueCategories.priority),
asc(WebsiteRareValueCategories.name),
);
} catch { } catch {
categories = []; categories = [];
} }
let values: Awaited<ReturnType<typeof prisma.websiteRareValues.findMany>> = let values: (typeof WebsiteRareValues.$inferSelect)[] = [];
[];
try { try {
values = await prisma.websiteRareValues.findMany({ values = await db
orderBy: { name: "asc" }, .select()
}); .from(WebsiteRareValues)
.orderBy(asc(WebsiteRareValues.name));
} catch { } catch {
values = []; values = [];
} }
+20 -13
View File
@@ -1,7 +1,8 @@
import { desc, eq, inArray } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
import { db, Items, ItemsBase, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { loadRoom } from "../../_lib/load-room"; import { loadRoom } from "../../_lib/load-room";
import { FurniClient } from "./furni-client"; import { FurniClient } from "./furni-client";
@@ -18,10 +19,11 @@ export default async function RoomFurniPage({
const room = await loadRoom(id); const room = await loadRoom(id);
// Load items in this room. items.item_id → items_base.id // Load items in this room. items.item_id → items_base.id
const items = await prisma.items.findMany({ const items = await db
where: { roomId: room.id }, .select()
orderBy: { id: "desc" }, .from(Items)
}); .where(eq(Items.roomId, room.id))
.orderBy(desc(Items.id));
// Resolve item bases // Resolve item bases
const baseIds = Array.from( const baseIds = Array.from(
@@ -30,10 +32,15 @@ export default async function RoomFurniPage({
), ),
); );
const bases = baseIds.length const bases = baseIds.length
? await prisma.itemsBase.findMany({ ? await db
where: { id: { in: baseIds } }, .select({
select: { id: true, itemName: true, publicName: true, type: true }, id: ItemsBase.id,
}) itemName: ItemsBase.itemName,
publicName: ItemsBase.publicName,
type: ItemsBase.type,
})
.from(ItemsBase)
.where(inArray(ItemsBase.id, baseIds))
: []; : [];
const baseMap = new Map(bases.map((b) => [b.id, b])); const baseMap = new Map(bases.map((b) => [b.id, b]));
@@ -42,10 +49,10 @@ export default async function RoomFurniPage({
new Set(items.map((i) => i.userId).filter((u) => u > 0)), new Set(items.map((i) => i.userId).filter((u) => u > 0)),
); );
const users = userIds.length const users = userIds.length
? await prisma.user.findMany({ ? await db
where: { id: { in: userIds } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}) .where(inArray(User.id, userIds))
: []; : [];
const userMap = new Map(users.map((u) => [u.id, u.username])); const userMap = new Map(users.map((u) => [u.id, u.username]));
+18 -8
View File
@@ -1,21 +1,31 @@
import { eq } from "drizzle-orm";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { db, Rooms, User } from "@/lib/db";
export async function loadRoom(id: string) { export async function loadRoom(id: string) {
const roomId = parseInt(id, 10); const roomId = parseInt(id, 10);
if (Number.isNaN(roomId)) notFound(); if (Number.isNaN(roomId)) notFound();
const room = await prisma.rooms.findUnique({ const [room] = await db
where: { id: roomId }, .select()
}); .from(Rooms)
.where(eq(Rooms.id, roomId))
.limit(1);
if (!room) notFound(); if (!room) notFound();
const owner = const owner =
room.ownerId > 0 room.ownerId > 0
? await prisma.user.findUnique({ ? ((
where: { id: room.ownerId }, await db
select: { id: true, username: true, look: true }, .select({
}) id: User.id,
username: User.username,
look: User.look,
})
.from(User)
.where(eq(User.id, room.ownerId))
.limit(1)
)[0] ?? null)
: null; : null;
return { ...room, owner }; return { ...room, owner };
} }
+7 -5
View File
@@ -1,9 +1,10 @@
import { asc } from "drizzle-orm";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell"; import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { db, WebsiteSetting } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { AdvancedSettingsPanel } from "./advanced-settings-panel"; import { AdvancedSettingsPanel } from "./advanced-settings-panel";
import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config"; import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config";
import { CmsSettingsForm } from "./cms-settings-form"; import { CmsSettingsForm } from "./cms-settings-form";
@@ -22,11 +23,12 @@ export default async function AdminSettings() {
); );
const t = await getTranslations("pages.admin.settings"); const t = await getTranslations("pages.admin.settings");
let settings: Awaited<ReturnType<typeof prisma.websiteSetting.findMany>> = []; let settings: (typeof WebsiteSetting.$inferSelect)[] = [];
try { try {
settings = await prisma.websiteSetting.findMany({ settings = await db
orderBy: { key: "asc" }, .select()
}); .from(WebsiteSetting)
.orderBy(asc(WebsiteSetting.key));
} catch { } catch {
settings = []; settings = [];
} }
+9 -6
View File
@@ -1,11 +1,12 @@
import { eq } from "drizzle-orm";
import { Package } from "lucide-react"; import { Package } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop"; import { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteShopArticles } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function EditShopArticle({ export default async function EditShopArticle({
params, params,
@@ -20,12 +21,14 @@ export default async function EditShopArticle({
const t = await getTranslations("pages.admin.shop"); const t = await getTranslations("pages.admin.shop");
const { id } = await params; const { id } = await params;
let pkg: Awaited<ReturnType<typeof prisma.websiteShopArticles.findUnique>> = let pkg: typeof WebsiteShopArticles.$inferSelect | null = null;
null;
try { try {
pkg = await prisma.websiteShopArticles.findUnique({ const [row] = await db
where: { id: BigInt(id) }, .select()
}); .from(WebsiteShopArticles)
.where(eq(WebsiteShopArticles.id, BigInt(id)))
.limit(1);
pkg = row ?? null;
} catch { } catch {
notFound(); notFound();
} }
+19 -17
View File
@@ -1,3 +1,4 @@
import { asc, count, desc } from "drizzle-orm";
import { ShoppingCart } from "lucide-react"; import { ShoppingCart } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -8,9 +9,9 @@ import { StatusCard } from "@/components/admin/dashboard";
import { CurrencyIcon } from "@/components/shared/currency-icon"; import { CurrencyIcon } from "@/components/shared/currency-icon";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, WebsitePaypalTransactions, WebsiteShopArticles } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]); const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]);
const RECENT_ORDERS = 25; const RECENT_ORDERS = 25;
@@ -65,22 +66,24 @@ export default async function AdminShop({
const parsed = parseListParams(new URLSearchParams(raw)); const parsed = parseListParams(new URLSearchParams(raw));
let packageTotal = 0; let packageTotal = 0;
let packages: Awaited< let packages: (typeof WebsiteShopArticles.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteShopArticles.findMany>
> = [];
let loadError = false; let loadError = false;
try { try {
packageTotal = await prisma.websiteShopArticles.count(); const [totalRow] = await db
.select({ total: count() })
.from(WebsiteShopArticles);
packageTotal = Number(totalRow?.total ?? 0);
const pagination = calcPagination( const pagination = calcPagination(
packageTotal, packageTotal,
parsed.page, parsed.page,
parsed.perPage, parsed.perPage,
); );
packages = await prisma.websiteShopArticles.findMany({ packages = await db
orderBy: [{ position: "asc" }, { name: "asc" }], .select()
skip: pagination.offset, .from(WebsiteShopArticles)
take: pagination.perPage, .orderBy(asc(WebsiteShopArticles.position), asc(WebsiteShopArticles.name))
}); .limit(pagination.perPage)
.offset(pagination.offset);
} catch { } catch {
packages = []; packages = [];
packageTotal = 0; packageTotal = 0;
@@ -93,14 +96,13 @@ export default async function AdminShop({
parsed.perPage, parsed.perPage,
); );
let orders: Awaited< let orders: (typeof WebsitePaypalTransactions.$inferSelect)[] = [];
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
> = [];
try { try {
orders = await prisma.websitePaypalTransactions.findMany({ orders = await db
orderBy: { createdAt: "desc" }, .select()
take: RECENT_ORDERS, .from(WebsitePaypalTransactions)
}); .orderBy(desc(WebsitePaypalTransactions.createdAt))
.limit(RECENT_ORDERS);
} catch { } catch {
orders = []; orders = [];
loadError = true; loadError = true;
+22 -18
View File
@@ -1,6 +1,7 @@
import { asc, count, gt } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { CatalogItems, db, Soundtracks } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { import {
DEFAULT_SOUNDTRACK_BASE_URL, DEFAULT_SOUNDTRACK_BASE_URL,
@@ -18,28 +19,31 @@ export default async function SoundsPage() {
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
const [rows, usage, baseUrlSetting] = await Promise.all([ const [rows, usage, baseUrlSetting] = await Promise.all([
prisma.soundtracks.findMany({ db
orderBy: { id: "asc" }, .select({
select: { id: Soundtracks.id,
id: true, code: Soundtracks.code,
code: true, name: Soundtracks.name,
name: true, author: Soundtracks.author,
author: true, length: Soundtracks.length,
length: true, track: Soundtracks.track,
track: true, })
}, .from(Soundtracks)
}), .orderBy(asc(Soundtracks.id)),
prisma.catalogItems.groupBy({ db
by: ["songId"], .select({
where: { songId: { gt: 0 } }, songId: CatalogItems.songId,
_count: { _all: true }, total: count(),
}), })
.from(CatalogItems)
.where(gt(CatalogItems.songId, 0))
.groupBy(CatalogItems.songId),
siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL), siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL),
]); ]);
const usageMap: Record<number, number> = {}; const usageMap: Record<number, number> = {};
for (const row of usage) { for (const row of usage) {
if (row.songId != null) usageMap[row.songId] = row._count._all; if (row.songId != null) usageMap[row.songId] = Number(row.total);
} }
const soundtracks = rows.map((r) => { const soundtracks = rows.map((r) => {
+23 -16
View File
@@ -1,11 +1,12 @@
import { sql } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { AdminSimplePager } from "@/components/admin/admin-simple-pager"; import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
type SubRow = { type SubRow = {
id: number; id: number;
@@ -14,6 +15,19 @@ type SubRow = {
expiresAt: number; expiresAt: number;
}; };
type CountRow = { c: number | bigint };
type SubSqlRow = {
id: number;
user_id: number | null;
subscription_type: string | null;
expires_at: number;
};
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
return rows ?? [];
}
export default async function AdminSubscriptions({ export default async function AdminSubscriptions({
searchParams, searchParams,
}: { }: {
@@ -36,39 +50,32 @@ export default async function AdminSubscriptions({
let loadError = false; let loadError = false;
try { try {
const [countRows, typeRows, soonRows] = await Promise.all([ const [countRows, typeRows, soonRows] = await Promise.all([
prisma.$queryRaw<{ c: bigint }[]>` rawRows<CountRow>(sql`
SELECT COUNT(*) AS c SELECT COUNT(*) AS c
FROM users_subscriptions FROM users_subscriptions
WHERE active = 1 WHERE active = 1
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
`, `),
prisma.$queryRaw<{ c: bigint }[]>` rawRows<CountRow>(sql`
SELECT COUNT(DISTINCT subscription_type) AS c SELECT COUNT(DISTINCT subscription_type) AS c
FROM users_subscriptions FROM users_subscriptions
WHERE active = 1 WHERE active = 1
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
`, `),
prisma.$queryRaw<{ c: bigint }[]>` rawRows<CountRow>(sql`
SELECT COUNT(*) AS c SELECT COUNT(*) AS c
FROM users_subscriptions FROM users_subscriptions
WHERE active = 1 WHERE active = 1
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60}
`, `),
]); ]);
total = Number(countRows[0]?.c ?? 0); total = Number(countRows[0]?.c ?? 0);
types = Number(typeRows[0]?.c ?? 0); types = Number(typeRows[0]?.c ?? 0);
expiringSoon = Number(soonRows[0]?.c ?? 0); expiringSoon = Number(soonRows[0]?.c ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
const rows = await prisma.$queryRaw< const rows = await rawRows<SubSqlRow>(sql`
{
id: number;
user_id: number | null;
subscription_type: string | null;
expires_at: number;
}[]
>`
SELECT id, SELECT id,
user_id, user_id,
subscription_type, subscription_type,
@@ -78,7 +85,7 @@ export default async function AdminSubscriptions({
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
ORDER BY expires_at ASC ORDER BY expires_at ASC
LIMIT ${pagination.perPage} OFFSET ${pagination.offset} LIMIT ${pagination.perPage} OFFSET ${pagination.offset}
`; `);
active = rows.map((r) => ({ active = rows.map((r) => ({
id: Number(r.id), id: Number(r.id),
+20 -11
View File
@@ -1,11 +1,12 @@
import { asc, count } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { createTag, deleteTag, updateTag } from "@/actions/admin-tags"; import { createTag, deleteTag, updateTag } from "@/actions/admin-tags";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, Taggables, Tags } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
type Tag = { type Tag = {
id: bigint; id: bigint;
@@ -26,18 +27,26 @@ export default async function AdminTagsPage() {
let dbError = false; let dbError = false;
try { try {
tags = await prisma.tags.findMany({ tags = await db
select: { id: true, name: true, backgroundColor: true, createdAt: true }, .select({
orderBy: { name: "asc" }, id: Tags.id,
take: 500, name: Tags.name,
}); backgroundColor: Tags.backgroundColor,
createdAt: Tags.createdAt,
})
.from(Tags)
.orderBy(asc(Tags.name))
.limit(500);
// How many things each tag is attached to (taggables join table). // How many things each tag is attached to (taggables join table).
const counts = await prisma.taggables.groupBy({ const counts = await db
by: ["tagId"], .select({
_count: { tagId: true }, tagId: Taggables.tagId,
}); total: count(),
usage = new Map(counts.map((c) => [String(c.tagId), c._count.tagId])); })
.from(Taggables)
.groupBy(Taggables.tagId);
usage = new Map(counts.map((c) => [String(c.tagId), Number(c.total)]));
} catch { } catch {
dbError = true; dbError = true;
tags = []; tags = [];
+13 -12
View File
@@ -1,10 +1,11 @@
import { asc } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { createTeam, deleteTeam } from "@/actions/admin-teams"; import { createTeam, deleteTeam } from "@/actions/admin-teams";
import { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteTeams } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
type TeamRow = { type TeamRow = {
id: bigint; id: bigint;
@@ -24,17 +25,17 @@ export default async function AdminTeams() {
const t = await getTranslations("pages.admin.teams"); const t = await getTranslations("pages.admin.teams");
let teams: TeamRow[] = []; let teams: TeamRow[] = [];
try { try {
teams = await prisma.websiteTeams.findMany({ teams = await db
select: { .select({
id: true, id: WebsiteTeams.id,
rankName: true, rankName: WebsiteTeams.rankName,
badge: true, badge: WebsiteTeams.badge,
jobDescription: true, jobDescription: WebsiteTeams.jobDescription,
staffColor: true, staffColor: WebsiteTeams.staffColor,
hiddenRank: true, hiddenRank: WebsiteTeams.hiddenRank,
}, })
orderBy: { id: "asc" }, .from(WebsiteTeams)
}); .orderBy(asc(WebsiteTeams.id));
} catch { } catch {
teams = []; teams = [];
} }