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

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 14:15:39 +02:00
1 parent 9aa4f331bf
commit 30b54e99e7
40 files changed
+1183 -784

No files matched your search

+34 -23
View File
@@ -4,9 +4,14 @@
// into website_help_center_ticket_replies. The target ticket must exist and
// belong to the authed user. Fail-soft: never a 500.
import { eq } from "drizzle-orm";
import { apiError, apiJson, positiveBigInt } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { prisma } from "@/lib/prisma";
import {
db,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
import { rateLimit } from "@/lib/rate-limit";
import { createOwnedTicketReply } from "@/lib/services/ticket-replies";
@@ -36,30 +41,36 @@ export async function POST(
try {
// Ownership check — only the ticket owner may reply.
const reply = await prisma.$transaction((tx) =>
const reply = await db.transaction(async (tx) =>
createOwnedTicketReply(
{
findTicket: (ticketId) =>
tx.websiteHelpCenterTickets.findUnique({
where: { id: ticketId },
select: { id: true, userId: true },
}),
createReply: (data) =>
tx.websiteHelpCenterTicketReplies.create({
data,
select: {
id: true,
userId: true,
content: true,
createdAt: true,
},
}),
touchTicket: (ticketId, updatedAt) =>
tx.websiteHelpCenterTickets.update({
where: { id: ticketId },
data: { updatedAt },
select: { id: true },
}),
findTicket: async (id) => {
const [row] = await tx
.select({
id: WebsiteHelpCenterTickets.id,
userId: WebsiteHelpCenterTickets.userId,
})
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.id, id))
.limit(1);
return row ?? null;
},
createReply: async (data) => {
const [result] = await tx
.insert(WebsiteHelpCenterTicketReplies)
.values(data);
return {
id: BigInt(result.insertId),
userId: data.userId,
content: data.content,
createdAt: data.createdAt,
};
},
touchTicket: (id, updatedAt) =>
tx
.update(WebsiteHelpCenterTickets)
.set({ updatedAt })
.where(eq(WebsiteHelpCenterTickets.id, id)),
},
{ ticketId, userId: uid, content },
),
+34 -22
View File
@@ -4,9 +4,15 @@
// together with its replies; reply author usernames are resolved in a single
// users lookup. Fail-soft: never a 500.
import { asc, eq, inArray } from "drizzle-orm";
import { apiError, apiJson, positiveBigInt } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { prisma } from "@/lib/prisma";
import {
db,
User,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -23,37 +29,43 @@ export async function GET(
if (!ticketId) return apiError("Invalid ticket id", 422);
try {
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
where: { id: ticketId },
select: {
id: true,
userId: true,
categoryId: true,
title: true,
content: true,
open: true,
createdAt: true,
},
});
const [ticket] = await db
.select({
id: WebsiteHelpCenterTickets.id,
userId: WebsiteHelpCenterTickets.userId,
categoryId: WebsiteHelpCenterTickets.categoryId,
title: WebsiteHelpCenterTickets.title,
content: WebsiteHelpCenterTickets.content,
open: WebsiteHelpCenterTickets.open,
createdAt: WebsiteHelpCenterTickets.createdAt,
})
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
.limit(1);
// Ownership check — return 404 (not 403) so a foreign id is indistinguishable
// from a missing one.
if (!ticket || ticket.userId !== uid)
return apiError("Ticket not found", 404);
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({
where: { ticketId },
select: { id: true, userId: true, content: true, createdAt: true },
orderBy: { id: "asc" },
});
const replies = await db
.select({
id: WebsiteHelpCenterTicketReplies.id,
userId: WebsiteHelpCenterTicketReplies.userId,
content: WebsiteHelpCenterTicketReplies.content,
createdAt: WebsiteHelpCenterTicketReplies.createdAt,
})
.from(WebsiteHelpCenterTicketReplies)
.where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticketId))
.orderBy(asc(WebsiteHelpCenterTicketReplies.id));
// Resolve author usernames in one query.
const authorIds = [...new Set(replies.map((r) => r.userId))];
const authors = authorIds.length
? await prisma.user.findMany({
where: { id: { in: authorIds } },
select: { id: true, username: true },
})
? await db
.select({ id: User.id, username: User.username })
.from(User)
.where(inArray(User.id, authorIds))
: [];
const nameById = new Map(authors.map((a) => [a.id, a.username]));
+26 -22
View File
@@ -4,9 +4,11 @@
// Backed by website_help_center_tickets (WebsiteHelpCenterTickets). Fail-soft:
// DB errors return an apiError envelope, never a 500.
import { desc, eq } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import { apiError, apiJson, positiveBigInt } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { prisma } from "@/lib/prisma";
import { db, WebsiteHelpCenterTickets } from "@/lib/db";
import { rateLimit } from "@/lib/rate-limit";
export const dynamic = "force-dynamic";
@@ -17,11 +19,16 @@ export async function GET(req: Request) {
if (!uid) return apiError("Unauthorized", 401);
try {
const tickets = await prisma.websiteHelpCenterTickets.findMany({
where: { userId: uid },
select: { id: true, title: true, open: true, createdAt: true },
orderBy: { id: "desc" },
});
const tickets = await db
.select({
id: WebsiteHelpCenterTickets.id,
title: WebsiteHelpCenterTickets.title,
open: WebsiteHelpCenterTickets.open,
createdAt: WebsiteHelpCenterTickets.createdAt,
})
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.userId, uid))
.orderBy(desc(WebsiteHelpCenterTickets.id));
return apiJson({
tickets: tickets.map((t) => ({
@@ -74,26 +81,23 @@ export async function POST(req: Request) {
try {
const now = new Date();
const ticket = await prisma.websiteHelpCenterTickets.create({
data: {
userId: uid,
categoryId,
title,
content,
open: true,
createdAt: now,
updatedAt: now,
},
select: { id: true, title: true, open: true, createdAt: true },
});
const [result] = (await db.insert(WebsiteHelpCenterTickets).values({
userId: uid,
categoryId,
title,
content,
open: true,
createdAt: now,
updatedAt: now,
})) as unknown as [ResultSetHeader];
return apiJson(
{
ticket: {
id: ticket.id,
title: ticket.title,
open: ticket.open,
createdAt: ticket.createdAt,
id: BigInt(result.insertId),
title,
open: true,
createdAt: now,
},
},
{ status: 201 },