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

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 14:15:36 +02:00
1 parent 580972c0a0
commit 9aa4f331bf
40 files changed
+1259 -927

No files matched your search

+150 -101
View File
@@ -1,7 +1,8 @@
import { asc, count, eq, inArray, like, or } from "drizzle-orm";
import { withAdmin } from "@/lib/api-handler";
import { apiError, apiOk } from "@/lib/api-response";
import { CatalogItemsBc, CatalogPages, CatalogPagesBc, db } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import {
deletePage,
getAncestors,
@@ -26,10 +27,16 @@ type PageRow = {
async function getBcTreeFlat(): Promise<TreeNode[]> {
const [allPages, itemCounts] = await Promise.all([
prisma.catalogPagesBc.findMany({ orderBy: { orderNum: "asc" } }),
prisma.catalogItemsBc.groupBy({ by: ["pageId"], _count: true }),
db.select().from(CatalogPagesBc).orderBy(asc(CatalogPagesBc.orderNum)),
db
.select({
pageId: CatalogItemsBc.pageId,
total: count(),
})
.from(CatalogItemsBc)
.groupBy(CatalogItemsBc.pageId),
]);
const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c._count]));
const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c.total]));
const childCountMap = new Map<number, number>();
for (const p of allPages) {
childCountMap.set(p.parentId, (childCountMap.get(p.parentId) ?? 0) + 1);
@@ -91,37 +98,49 @@ async function getChildren(
): Promise<TreeNode[]> {
// Roots in Habbo DBs are usually parent_id = -1, sometimes 0.
const parentFilter =
parentId <= 0 ? { OR: [{ parentId: -1 }, { parentId: 0 }] } : { parentId };
parentId <= 0
? or(eq(CatalogPages.parentId, -1), eq(CatalogPages.parentId, 0))
: eq(CatalogPages.parentId, parentId);
const parentFilterBc =
parentId <= 0
? or(eq(CatalogPagesBc.parentId, -1), eq(CatalogPagesBc.parentId, 0))
: eq(CatalogPagesBc.parentId, parentId);
if (isBc) {
const pages = await prisma.catalogPagesBc.findMany({
where: parentFilter,
orderBy: { orderNum: "asc" },
});
const pages = await db
.select()
.from(CatalogPagesBc)
.where(parentFilterBc)
.orderBy(asc(CatalogPagesBc.orderNum));
const ids = pages.map((p) => toInt(p.id));
const [childCounts, itemCounts] = await Promise.all([
ids.length
? prisma.catalogPagesBc.groupBy({
by: ["parentId"],
where: { parentId: { in: ids } },
_count: true,
})
? db
.select({
parentId: CatalogPagesBc.parentId,
total: count(),
})
.from(CatalogPagesBc)
.where(inArray(CatalogPagesBc.parentId, ids))
.groupBy(CatalogPagesBc.parentId)
: Promise.resolve([]),
ids.length
? prisma.catalogItemsBc
.groupBy({
by: ["pageId"],
where: { pageId: { in: ids } },
_count: true,
? db
.select({
pageId: CatalogItemsBc.pageId,
total: count(),
})
.from(CatalogItemsBc)
.where(inArray(CatalogItemsBc.pageId, ids))
.groupBy(CatalogItemsBc.pageId)
.catch(() => [])
: Promise.resolve([]),
]);
const childMap = Object.fromEntries(
childCounts.map((c) => [toInt(c.parentId), c._count]),
childCounts.map((c) => [toInt(c.parentId), c.total]),
);
const itemMap = Object.fromEntries(
itemCounts.map((c) => [toInt(c.pageId), c._count]),
itemCounts.map((c) => [toInt(c.pageId), c.total]),
);
return pages.map((p) => {
const id = toInt(p.id);
@@ -143,23 +162,27 @@ async function getChildren(
});
}
const pages = await prisma.catalogPages.findMany({
where: parentFilter,
orderBy: { orderNum: "asc" },
});
const pages = await db
.select()
.from(CatalogPages)
.where(parentFilter)
.orderBy(asc(CatalogPages.orderNum));
const ids = pages.map((p) => toInt(p.id));
const [childCounts, itemMap] = await Promise.all([
ids.length
? prisma.catalogPages.groupBy({
by: ["parentId"],
where: { parentId: { in: ids } },
_count: true,
})
? db
.select({
parentId: CatalogPages.parentId,
total: count(),
})
.from(CatalogPages)
.where(inArray(CatalogPages.parentId, ids))
.groupBy(CatalogPages.parentId)
: Promise.resolve([]),
getCatalogItemCounts(ids),
]);
const childMap = Object.fromEntries(
childCounts.map((c) => [toInt(c.parentId), c._count]),
childCounts.map((c) => [toInt(c.parentId), c.total]),
);
return pages.map((p) => {
const id = toInt(p.id);
@@ -184,19 +207,17 @@ async function getChildren(
async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
const needle = q.trim();
const idExact = Number.parseInt(needle, 10);
const idFilter =
Number.isFinite(idExact) && String(idExact) === needle
? [{ id: idExact }]
: [];
const idOk = Number.isFinite(idExact) && String(idExact) === needle;
if (isBc) {
const pages = await prisma.catalogPagesBc.findMany({
where: {
OR: [{ caption: { contains: needle } }, ...idFilter],
},
orderBy: { orderNum: "asc" },
take: 50,
});
const conditions = [like(CatalogPagesBc.caption, `%${needle}%`)];
if (idOk) conditions.push(eq(CatalogPagesBc.id, idExact));
const pages = await db
.select()
.from(CatalogPagesBc)
.where(or(...conditions))
.orderBy(asc(CatalogPagesBc.orderNum))
.limit(50);
return pages.map((p) =>
toTreeNode(
{
@@ -216,26 +237,30 @@ async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
);
}
const pages = await prisma.catalogPages.findMany({
where: {
OR: [{ caption: { contains: needle } }, ...idFilter],
},
orderBy: { orderNum: "asc" },
take: 50,
});
const conditions = [like(CatalogPages.caption, `%${needle}%`)];
if (idOk) conditions.push(eq(CatalogPages.id, idExact));
const pages = await db
.select()
.from(CatalogPages)
.where(or(...conditions))
.orderBy(asc(CatalogPages.orderNum))
.limit(50);
const ids = pages.map((p) => toInt(p.id));
const [childCounts, itemMap] = await Promise.all([
ids.length
? prisma.catalogPages.groupBy({
by: ["parentId"],
where: { parentId: { in: ids } },
_count: true,
})
? db
.select({
parentId: CatalogPages.parentId,
total: count(),
})
.from(CatalogPages)
.where(inArray(CatalogPages.parentId, ids))
.groupBy(CatalogPages.parentId)
: Promise.resolve([]),
getCatalogItemCounts(ids),
]);
const childMap = Object.fromEntries(
childCounts.map((c) => [toInt(c.parentId), c._count]),
childCounts.map((c) => [toInt(c.parentId), c.total]),
);
return pages.map((p) => {
const id = toInt(p.id);
@@ -273,11 +298,19 @@ export const GET = withAdmin(
const id = Number(pageIdRaw);
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid pageId");
if (isBc) {
const page = await prisma.catalogPagesBc.findUnique({ where: { id } });
const [page] = await db
.select()
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, id))
.limit(1);
if (!page) return apiError("Page not found", 404);
return apiOk({ page });
}
const page = await prisma.catalogPages.findUnique({ where: { id } });
const [page] = await db
.select()
.from(CatalogPages)
.where(eq(CatalogPages.id, id))
.limit(1);
if (!page) return apiError("Page not found", 404);
return apiOk({ page });
}
@@ -290,10 +323,15 @@ export const GET = withAdmin(
const chain: Array<{ id: number; caption: string }> = [];
let currentId = id;
for (let i = 0; i < 20; i++) {
const page = await prisma.catalogPagesBc.findUnique({
where: { id: currentId },
select: { id: true, caption: true, parentId: true },
});
const [page] = await db
.select({
id: CatalogPagesBc.id,
caption: CatalogPagesBc.caption,
parentId: CatalogPagesBc.parentId,
})
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, currentId))
.limit(1);
if (!page) break;
chain.unshift({ id: page.id, caption: page.caption });
if (page.parentId <= 0) break;
@@ -340,29 +378,37 @@ export const PATCH = withAdmin(
}
const isBc = body.catalog === "bc";
if (isBc) {
const page = await prisma.catalogPagesBc.findUnique({
where: { id: pageId },
select: { enabled: true, visible: true },
});
const [page] = await db
.select({
enabled: CatalogPagesBc.enabled,
visible: CatalogPagesBc.visible,
})
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, pageId))
.limit(1);
if (!page) return apiError("Page not found", 404);
const current = field === "enabled" ? page.enabled : page.visible;
await prisma.catalogPagesBc.update({
where: { id: pageId },
data: { [field]: current === "1" ? "0" : "1" },
});
await db
.update(CatalogPagesBc)
.set({ [field]: current === "1" ? "0" : "1" })
.where(eq(CatalogPagesBc.id, pageId));
await rcon.updateCatalog();
return apiOk({});
}
const page = await prisma.catalogPages.findUnique({
where: { id: pageId },
select: { enabled: true, visible: true },
});
const [page] = await db
.select({
enabled: CatalogPages.enabled,
visible: CatalogPages.visible,
})
.from(CatalogPages)
.where(eq(CatalogPages.id, pageId))
.limit(1);
if (!page) return apiError("Page not found", 404);
const current = field === "enabled" ? page.enabled : page.visible;
await prisma.catalogPages.update({
where: { id: pageId },
data: { [field]: current === "1" ? "0" : "1" },
});
await db
.update(CatalogPages)
.set({ [field]: current === "1" ? "0" : "1" })
.where(eq(CatalogPages.id, pageId));
await rcon.updateCatalog();
return apiOk({});
},
@@ -379,43 +425,46 @@ export const DELETE = withAdmin(
return apiError("Invalid pageId");
}
if (isBc) {
const page = await prisma.catalogPagesBc.findUnique({
where: { id: pageId },
select: { parentId: true },
});
const [page] = await db
.select({ parentId: CatalogPagesBc.parentId })
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, pageId))
.limit(1);
if (!page) return apiError("Page not found", 404);
if (mode === "reparent") {
await prisma.$transaction([
prisma.catalogPagesBc.updateMany({
where: { parentId: pageId },
data: { parentId: page.parentId },
}),
prisma.catalogItemsBc.deleteMany({ where: { pageId } }),
prisma.catalogPagesBc.delete({ where: { id: pageId } }),
]);
await db.transaction(async (tx) => {
await tx
.update(CatalogPagesBc)
.set({ parentId: page.parentId })
.where(eq(CatalogPagesBc.parentId, pageId));
await tx
.delete(CatalogItemsBc)
.where(eq(CatalogItemsBc.pageId, pageId));
await tx.delete(CatalogPagesBc).where(eq(CatalogPagesBc.id, pageId));
});
} else {
// Deep cascade: collect all descendants
const toDelete: number[] = [pageId];
const queue: number[] = [pageId];
while (queue.length > 0) {
const children = await prisma.catalogPagesBc.findMany({
where: { parentId: { in: queue } },
select: { id: true },
});
const children = await db
.select({ id: CatalogPagesBc.id })
.from(CatalogPagesBc)
.where(inArray(CatalogPagesBc.parentId, queue));
queue.length = 0;
for (const child of children) {
toDelete.push(child.id);
queue.push(child.id);
}
}
await prisma.$transaction([
prisma.catalogItemsBc.deleteMany({
where: { pageId: { in: toDelete } },
}),
prisma.catalogPagesBc.deleteMany({
where: { id: { in: toDelete } },
}),
]);
await db.transaction(async (tx) => {
await tx
.delete(CatalogItemsBc)
.where(inArray(CatalogItemsBc.pageId, toDelete));
await tx
.delete(CatalogPagesBc)
.where(inArray(CatalogPagesBc.id, toDelete));
});
}
await rcon.updateCatalog();
return apiOk({});