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

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
2026-08-01 14:15:39 +02:00
co-authored by Cursor
parent 9aa4f331bf
commit 30b54e99e7
40 changed files with 1183 additions and 784 deletions
+39 -32
View File
@@ -1,10 +1,16 @@
import { asc, eq, inArray } from "drizzle-orm";
import { notFound } from "next/navigation";
import { AdminHelpTicketDetail } from "@/app/admin/help-tickets/[id]/admin-help-ticket-detail";
import { requireModPermission } from "@/lib/admin/guard";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { positiveBigInt } from "@/lib/api";
import {
db,
User,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function ModHelpTicketDetailPage({
params,
@@ -18,31 +24,32 @@ export default async function ModHelpTicketDetailPage({
const ticketId = positiveBigInt(id);
if (!ticketId) notFound();
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
where: { id: ticketId },
select: {
id: true,
userId: true,
title: true,
content: true,
open: true,
createdAt: true,
updatedAt: true,
},
});
const [ticket] = await db
.select({
id: WebsiteHelpCenterTickets.id,
userId: WebsiteHelpCenterTickets.userId,
title: WebsiteHelpCenterTickets.title,
content: WebsiteHelpCenterTickets.content,
open: WebsiteHelpCenterTickets.open,
createdAt: WebsiteHelpCenterTickets.createdAt,
updatedAt: WebsiteHelpCenterTickets.updatedAt,
})
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
.limit(1);
if (!ticket) notFound();
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({
where: { ticketId: ticket.id },
orderBy: { id: "asc" },
select: {
id: true,
userId: true,
content: true,
createdAt: true,
},
});
const replies = await db
.select({
id: WebsiteHelpCenterTicketReplies.id,
userId: WebsiteHelpCenterTicketReplies.userId,
content: WebsiteHelpCenterTicketReplies.content,
createdAt: WebsiteHelpCenterTicketReplies.createdAt,
})
.from(WebsiteHelpCenterTicketReplies)
.where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticket.id))
.orderBy(asc(WebsiteHelpCenterTicketReplies.id));
const authorIds = [
...new Set([
@@ -53,15 +60,15 @@ export default async function ModHelpTicketDetailPage({
const users =
authorIds.length > 0
? await prisma.user.findMany({
where: { id: { in: authorIds } },
select: {
id: true,
username: true,
rank: true,
mail: true,
},
})
? await db
.select({
id: User.id,
username: User.username,
rank: User.rank,
mail: User.mail,
})
.from(User)
.where(inArray(User.id, authorIds))
: [];
const userById = new Map(users.map((u) => [u.id, u]));
+103 -70
View File
@@ -1,3 +1,14 @@
import {
and,
asc,
count,
desc,
eq,
inArray,
like,
or,
type SQL,
} from "drizzle-orm";
import { getTranslations } from "next-intl/server";
import {
type HelpTicketRow,
@@ -8,9 +19,23 @@ import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import { requireModPermission } from "@/lib/admin/guard";
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import {
db,
User,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const SORT_COLS = {
title: WebsiteHelpCenterTickets.title,
open: WebsiteHelpCenterTickets.open,
user: WebsiteHelpCenterTickets.userId,
date: WebsiteHelpCenterTickets.createdAt,
updated: WebsiteHelpCenterTickets.updatedAt,
id: WebsiteHelpCenterTickets.id,
} as const;
export default async function ModHelpTicketsPage({
searchParams,
@@ -24,87 +49,94 @@ export default async function ModHelpTicketsPage({
const parsed = parseListParams(new URLSearchParams(raw));
const statusFilter = raw.filter_status || "open";
const conditions: any[] = [];
const conditions: SQL[] = [];
if (statusFilter === "open") {
conditions.push({ open: true });
conditions.push(eq(WebsiteHelpCenterTickets.open, true));
} else if (statusFilter === "closed") {
conditions.push({ open: false });
conditions.push(eq(WebsiteHelpCenterTickets.open, false));
}
if (parsed.search.trim()) {
const q = parsed.search.trim();
const asId = /^\d+$/.test(q) ? BigInt(q) : null;
const matchingUsers = await prisma.user
.findMany({
where: { username: { contains: q } },
select: { id: true },
take: 50,
})
const matchingUsers = await db
.select({ id: User.id })
.from(User)
.where(like(User.username, `%${q}%`))
.limit(50)
.catch(() => []);
conditions.push({
OR: [
{ title: { contains: q } },
{ content: { contains: q } },
...(matchingUsers.length > 0
? [{ userId: { in: matchingUsers.map((u) => u.id) } }]
: []),
...(asId !== null ? [{ id: asId }] : []),
],
});
const ors: SQL[] = [
like(WebsiteHelpCenterTickets.title, `%${q}%`),
like(WebsiteHelpCenterTickets.content, `%${q}%`),
];
if (matchingUsers.length > 0) {
ors.push(
inArray(
WebsiteHelpCenterTickets.userId,
matchingUsers.map((u) => u.id),
),
);
}
if (asId !== null) {
ors.push(eq(WebsiteHelpCenterTickets.id, asId));
}
conditions.push(or(...ors)!);
}
const where: any =
conditions.length === 0
? {}
: conditions.length === 1
? conditions[0]
: { AND: conditions };
const where = conditions.length > 0 ? and(...conditions) : undefined;
const SORT_MAP: Record<string, any> = {
title: { title: "asc" },
open: { open: "desc" },
user: { userId: "asc" },
date: { createdAt: "desc" },
updated: { updatedAt: "desc" },
id: { id: "desc" },
};
const baseOrder = SORT_MAP[parsed.sort ?? "updated"] ?? { updatedAt: "desc" };
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
const orderBy = {
[orderField]: parsed.order,
} as any;
const sortKey =
parsed.sort && parsed.sort in SORT_COLS
? (parsed.sort as keyof typeof SORT_COLS)
: "updated";
const sortCol = SORT_COLS[sortKey];
const finalOrder =
parsed.sort && parsed.sort in SORT_COLS
? parsed.order === "asc"
? asc(sortCol)
: desc(sortCol)
: desc(WebsiteHelpCenterTickets.updatedAt);
const [total, openCount, closedCount, queues] = await Promise.all([
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
prisma.websiteHelpCenterTickets
.count({ where: { open: true } })
db
.select({ total: count() })
.from(WebsiteHelpCenterTickets)
.where(where)
.then((rows) => rows[0]?.total ?? 0)
.catch(() => 0),
prisma.websiteHelpCenterTickets
.count({ where: { open: false } })
db
.select({ total: count() })
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.open, true))
.then((rows) => rows[0]?.total ?? 0)
.catch(() => 0),
db
.select({ total: count() })
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.open, false))
.then((rows) => rows[0]?.total ?? 0)
.catch(() => 0),
fetchTicketQueueOpenCounts(),
]);
const pagination = calcPagination(total, parsed.page, parsed.perPage);
const tickets = await prisma.websiteHelpCenterTickets
.findMany({
where,
orderBy,
skip: pagination.offset,
take: pagination.perPage,
select: {
id: true,
userId: true,
title: true,
open: true,
createdAt: true,
updatedAt: true,
},
const tickets = await db
.select({
id: WebsiteHelpCenterTickets.id,
userId: WebsiteHelpCenterTickets.userId,
title: WebsiteHelpCenterTickets.title,
open: WebsiteHelpCenterTickets.open,
createdAt: WebsiteHelpCenterTickets.createdAt,
updatedAt: WebsiteHelpCenterTickets.updatedAt,
})
.from(WebsiteHelpCenterTickets)
.where(where)
.orderBy(finalOrder)
.offset(pagination.offset)
.limit(pagination.perPage)
.catch(() => []);
const ticketIds = tickets.map((ticket) => ticket.id);
@@ -118,26 +150,27 @@ export default async function ModHelpTicketsPage({
const [replyGroups, users] = await Promise.all([
ticketIds.length > 0
? prisma.websiteHelpCenterTicketReplies
.groupBy({
by: ["ticketId"],
where: { ticketId: { in: ticketIds } },
_count: true,
? db
.select({
ticketId: WebsiteHelpCenterTicketReplies.ticketId,
total: count(),
})
.from(WebsiteHelpCenterTicketReplies)
.where(inArray(WebsiteHelpCenterTicketReplies.ticketId, ticketIds))
.groupBy(WebsiteHelpCenterTicketReplies.ticketId)
.catch(() => [])
: Promise.resolve([]),
userIds.length > 0
? prisma.user
.findMany({
where: { id: { in: userIds } },
select: { id: true, username: true },
})
? db
.select({ id: User.id, username: User.username })
.from(User)
.where(inArray(User.id, userIds))
.catch(() => [])
: Promise.resolve([]),
]);
const replyCountByTicket = new Map(
replyGroups.map((g) => [String(g.ticketId), g._count]),
replyGroups.map((g) => [String(g.ticketId), g.total]),
);
const usernameById = new Map(users.map((u) => [u.id, u.username]));