refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (4)
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
580972c0a0
commit
9aa4f331bf
40 files changed
+1259
-927
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const VALID_REPORTS = new Set(["registrations", "online-by-hour", "economy"]);
|
||||
|
||||
@@ -19,13 +20,13 @@ function csvRow(values: unknown[]): string {
|
||||
}
|
||||
|
||||
async function streamRegistrations(): Promise<ReadableStream<Uint8Array>> {
|
||||
const rows = await prisma.$queryRaw<{ day: string; cnt: bigint }[]>`
|
||||
const [rows] = (await db.execute(sql`
|
||||
SELECT FROM_UNIXTIME(account_created, '%Y-%m-%d') AS day, COUNT(*) AS cnt
|
||||
FROM users
|
||||
WHERE account_created > UNIX_TIMESTAMP(NOW() - INTERVAL 90 DAY)
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`;
|
||||
`)) as unknown as [{ day: string; cnt: bigint }[], unknown];
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
@@ -39,13 +40,13 @@ async function streamRegistrations(): Promise<ReadableStream<Uint8Array>> {
|
||||
}
|
||||
|
||||
async function streamOnlineByHour(): Promise<ReadableStream<Uint8Array>> {
|
||||
const rows = await prisma.$queryRaw<{ hour: number; cnt: bigint }[]>`
|
||||
const [rows] = (await db.execute(sql`
|
||||
SELECT HOUR(FROM_UNIXTIME(last_online)) AS hour, COUNT(*) AS cnt
|
||||
FROM users
|
||||
WHERE last_online > UNIX_TIMESTAMP(NOW() - INTERVAL 30 DAY)
|
||||
GROUP BY hour
|
||||
ORDER BY hour ASC
|
||||
`;
|
||||
`)) as unknown as [{ hour: number; cnt: bigint }[], unknown];
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
@@ -61,11 +62,13 @@ async function streamOnlineByHour(): Promise<ReadableStream<Uint8Array>> {
|
||||
}
|
||||
|
||||
async function streamEconomy(): Promise<ReadableStream<Uint8Array>> {
|
||||
const [totals] = await prisma.$queryRaw<
|
||||
{ total_credits: bigint; total_pixels: bigint }[]
|
||||
>`
|
||||
const [rows] = (await db.execute(sql`
|
||||
SELECT SUM(credits) AS total_credits, SUM(pixels) AS total_pixels FROM users
|
||||
`;
|
||||
`)) as unknown as [
|
||||
{ total_credits: bigint; total_pixels: bigint }[],
|
||||
unknown,
|
||||
];
|
||||
const totals = rows[0];
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { asc, eq, like, or } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiOk } from "@/lib/api-response";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* Search items_base for Quick Add / catalog tooling.
|
||||
@@ -27,26 +28,26 @@ export const GET = withAdmin(
|
||||
}
|
||||
|
||||
const idExact = Number.parseInt(q, 10);
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ itemName: { contains: q } },
|
||||
{ publicName: { contains: q } },
|
||||
...(Number.isFinite(idExact) && String(idExact) === q
|
||||
? [{ id: idExact }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
itemName: true,
|
||||
publicName: true,
|
||||
type: true,
|
||||
spriteId: true,
|
||||
},
|
||||
take: limit,
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const conditions = [
|
||||
like(ItemsBase.itemName, `%${q}%`),
|
||||
like(ItemsBase.publicName, `%${q}%`),
|
||||
];
|
||||
if (Number.isFinite(idExact) && String(idExact) === q) {
|
||||
conditions.push(eq(ItemsBase.id, idExact));
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
itemName: ItemsBase.itemName,
|
||||
publicName: ItemsBase.publicName,
|
||||
type: ItemsBase.type,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(or(...conditions))
|
||||
.orderBy(asc(ItemsBase.id))
|
||||
.limit(limit);
|
||||
|
||||
return apiOk({
|
||||
results: rows.map((r) => ({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import {
|
||||
cloneSingleFurni,
|
||||
@@ -51,11 +52,10 @@ export const POST = withAdmin(
|
||||
const classnames = entries.map((e) => e.classname);
|
||||
if (classnames.length === 0) continue;
|
||||
|
||||
const placeholders = classnames.map(() => "?").join(",");
|
||||
const rows = await prisma.$queryRawUnsafe<Array<{ item_name: string }>>(
|
||||
`SELECT item_name FROM items_base WHERE item_name IN (${placeholders})`,
|
||||
...classnames,
|
||||
);
|
||||
const rows = await db
|
||||
.select({ item_name: ItemsBase.itemName })
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.itemName, classnames));
|
||||
const have = new Set(rows.map((r) => r.item_name));
|
||||
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { existsSync, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||
import {
|
||||
appendFurniEntriesBatch,
|
||||
@@ -120,15 +121,16 @@ export const POST = withAdmin(
|
||||
// Try spriteId as fallback revision
|
||||
const revsToTry: number[] = [];
|
||||
if (rev > 0) revsToTry.push(rev);
|
||||
const dbItem = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: item.classname },
|
||||
select: {
|
||||
id: true,
|
||||
spriteId: true,
|
||||
publicName: true,
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
const [dbItem] = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
publicName: ItemsBase.publicName,
|
||||
type: ItemsBase.type,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, item.classname))
|
||||
.limit(1);
|
||||
if (dbItem?.spriteId && !revsToTry.includes(dbItem.spriteId)) {
|
||||
revsToTry.push(dbItem.spriteId);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { existsSync, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||
import {
|
||||
@@ -47,16 +48,17 @@ export const GET = withAdmin(
|
||||
const { json } = parseNitroBundle(buffer);
|
||||
|
||||
// Fetch interaction flags from DB
|
||||
const items = await prisma.$queryRaw<
|
||||
const [items] = (await db.execute(sql`
|
||||
SELECT allow_sit, allow_lay, allow_walk
|
||||
FROM items_base WHERE item_name = ${rawClassname} LIMIT 1
|
||||
`)) as unknown as [
|
||||
Array<{
|
||||
allow_sit: string;
|
||||
allow_lay: string;
|
||||
allow_walk: string;
|
||||
}>
|
||||
>`
|
||||
SELECT allow_sit, allow_lay, allow_walk
|
||||
FROM items_base WHERE item_name = ${rawClassname} LIMIT 1
|
||||
`;
|
||||
}>,
|
||||
unknown,
|
||||
];
|
||||
|
||||
const dbRow = items[0];
|
||||
const flags = {
|
||||
@@ -153,14 +155,14 @@ export const PUT = withAdmin(
|
||||
|
||||
// 5. Update items_base (best-effort)
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
UPDATE items_base
|
||||
SET width = ${dims.x}, length = ${dims.y}, stack_height = ${dims.z},
|
||||
allow_sit = ${cansiton ? "1" : "0"},
|
||||
allow_lay = ${canlayon ? "1" : "0"},
|
||||
allow_walk = ${canstandon ? "1" : "0"}
|
||||
WHERE item_name = ${rawClassname}
|
||||
`;
|
||||
`);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[nitro-editor] items_base update failed:",
|
||||
@@ -180,10 +182,11 @@ export const PUT = withAdmin(
|
||||
|
||||
// 7. Audit log
|
||||
try {
|
||||
const dbItem = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: rawClassname },
|
||||
select: { id: true },
|
||||
});
|
||||
const [dbItem] = await db
|
||||
.select({ id: ItemsBase.id })
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, rawClassname))
|
||||
.limit(1);
|
||||
await logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
action: "furni_edit_nitro",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { asc, inArray, sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiOk } from "@/lib/api-response";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
appendFurniEntriesBatch,
|
||||
buildFurniEntry,
|
||||
@@ -64,19 +65,19 @@ export const POST = withAdmin(
|
||||
|
||||
for (let i = 0; i < targetIds.length; i += CHUNK) {
|
||||
const slice = targetIds.slice(i, i + CHUNK);
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
where: { id: { in: slice } },
|
||||
select: {
|
||||
id: true,
|
||||
spriteId: true,
|
||||
itemName: true,
|
||||
publicName: true,
|
||||
type: true,
|
||||
width: true,
|
||||
length: true,
|
||||
stackHeight: true,
|
||||
},
|
||||
});
|
||||
const rows = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
itemName: ItemsBase.itemName,
|
||||
publicName: ItemsBase.publicName,
|
||||
type: ItemsBase.type,
|
||||
width: ItemsBase.width,
|
||||
length: ItemsBase.length,
|
||||
stackHeight: ItemsBase.stackHeight,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, slice));
|
||||
|
||||
const entries: Array<{
|
||||
entry: Record<string, unknown>;
|
||||
@@ -158,20 +159,20 @@ async function resolveTargetIds(
|
||||
days: number,
|
||||
): Promise<number[]> {
|
||||
if (mode === "all") {
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: { id: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const rows = await db
|
||||
.select({ id: ItemsBase.id })
|
||||
.from(ItemsBase)
|
||||
.orderBy(asc(ItemsBase.id));
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
|
||||
if (mode === "missing" || mode === "broken") {
|
||||
const furniDataPath = await getFurnitureDataPath();
|
||||
if (!existsSync(furniDataPath)) {
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: { id: true },
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const rows = await db
|
||||
.select({ id: ItemsBase.id })
|
||||
.from(ItemsBase)
|
||||
.orderBy(asc(ItemsBase.id));
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
const furniData = (await readFurniData()) as {
|
||||
@@ -184,16 +185,16 @@ async function resolveTargetIds(
|
||||
if (typeof e?.classname === "string") byClassname.set(e.classname, e);
|
||||
}
|
||||
}
|
||||
const rows = await prisma.itemsBase.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
itemName: true,
|
||||
spriteId: true,
|
||||
width: true,
|
||||
length: true,
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
const rows = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
itemName: ItemsBase.itemName,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
width: ItemsBase.width,
|
||||
length: ItemsBase.length,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.orderBy(asc(ItemsBase.id));
|
||||
|
||||
if (mode === "missing") {
|
||||
return rows.filter((r) => !byClassname.has(r.itemName)).map((r) => r.id);
|
||||
@@ -220,7 +221,7 @@ async function resolveTargetIds(
|
||||
Date.now() - days * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const sinceMysql = sinceIso.slice(0, 19).replace("T", " ");
|
||||
const auditRows = await prisma.$queryRaw<Array<{ target_id: number }>>`
|
||||
const [auditRows] = (await db.execute(sql`
|
||||
SELECT DISTINCT target_id
|
||||
FROM admin_audit_log
|
||||
WHERE action = 'furni_import'
|
||||
@@ -228,6 +229,6 @@ async function resolveTargetIds(
|
||||
AND target_id IS NOT NULL
|
||||
AND created_at >= ${sinceMysql}
|
||||
ORDER BY target_id ASC
|
||||
`;
|
||||
`)) as unknown as [Array<{ target_id: number }>, unknown];
|
||||
return auditRows.map((r) => r.target_id).filter(Boolean);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { existsSync, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||
import {
|
||||
@@ -97,29 +98,32 @@ export const GET = withAdmin(
|
||||
if (action === "stats") {
|
||||
const [totalResult, catalogResult, nitroMissingResult] =
|
||||
await Promise.all([
|
||||
prisma.$queryRaw<
|
||||
[{ cnt: bigint }]
|
||||
>`SELECT COUNT(*) as cnt FROM items_base`,
|
||||
prisma.$queryRaw<
|
||||
[{ cnt: bigint }]
|
||||
>`SELECT COUNT(DISTINCT item_ids) as cnt FROM catalog_items`,
|
||||
prisma.$queryRaw<
|
||||
[{ cnt: bigint }]
|
||||
>`SELECT COUNT(*) as cnt FROM items_base WHERE item_name NOT IN (SELECT catalog_name FROM catalog_items)`,
|
||||
db.execute(sql`SELECT COUNT(*) as cnt FROM items_base`),
|
||||
db.execute(
|
||||
sql`SELECT COUNT(DISTINCT item_ids) as cnt FROM catalog_items`,
|
||||
),
|
||||
db.execute(
|
||||
sql`SELECT COUNT(*) as cnt FROM items_base WHERE item_name NOT IN (SELECT catalog_name FROM catalog_items)`,
|
||||
),
|
||||
]);
|
||||
|
||||
const totalInDb = Number(totalResult[0].cnt);
|
||||
const inCatalog = Number(catalogResult[0].cnt);
|
||||
const notInCatalog = Number(nitroMissingResult[0].cnt);
|
||||
const totalInDb = Number(
|
||||
(totalResult as unknown as [[{ cnt: bigint }], unknown])[0][0].cnt,
|
||||
);
|
||||
const inCatalog = Number(
|
||||
(catalogResult as unknown as [[{ cnt: bigint }], unknown])[0][0].cnt,
|
||||
);
|
||||
const notInCatalog = Number(
|
||||
(nitroMissingResult as unknown as [[{ cnt: bigint }], unknown])[0][0]
|
||||
.cnt,
|
||||
);
|
||||
|
||||
// Count missing .nitro files by checking filesystem
|
||||
const importedItems = await prisma.$queryRaw<
|
||||
Array<{ item_name: string }>
|
||||
>`
|
||||
const [importedItems] = (await db.execute(sql`
|
||||
SELECT ib.item_name FROM items_base ib
|
||||
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
||||
LIMIT 10000
|
||||
`;
|
||||
`)) as unknown as [Array<{ item_name: string }>, unknown];
|
||||
|
||||
let missingNitro = 0;
|
||||
for (const item of importedItems) {
|
||||
@@ -146,17 +150,18 @@ export const GET = withAdmin(
|
||||
);
|
||||
const perPage = 50;
|
||||
|
||||
const importedItems = await prisma.$queryRaw<
|
||||
const [importedItems] = (await db.execute(sql`
|
||||
SELECT ib.item_name, ib.public_name, ib.sprite_id, ib.type FROM items_base ib
|
||||
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
||||
`)) as unknown as [
|
||||
Array<{
|
||||
item_name: string;
|
||||
public_name: string;
|
||||
sprite_id: number;
|
||||
type: string;
|
||||
}>
|
||||
>`
|
||||
SELECT ib.item_name, ib.public_name, ib.sprite_id, ib.type FROM items_base ib
|
||||
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
||||
`;
|
||||
}>,
|
||||
unknown,
|
||||
];
|
||||
|
||||
// Filter to only those missing .nitro on filesystem
|
||||
const missing = importedItems.filter((item) => {
|
||||
@@ -209,9 +214,9 @@ export const GET = withAdmin(
|
||||
// Pull every imported classname once so we can both (a) sort non-imported
|
||||
// items to the top *before* pagination and (b) skip the per-page lookup
|
||||
// below. Single-column indexed query — fast even at 10k+ rows.
|
||||
const allImported = await prisma.itemsBase.findMany({
|
||||
select: { itemName: true },
|
||||
});
|
||||
const allImported = await db
|
||||
.select({ itemName: ItemsBase.itemName })
|
||||
.from(ItemsBase);
|
||||
const importedSet = new Set(
|
||||
allImported.map((e) => e.itemName).filter((n): n is string => !!n),
|
||||
);
|
||||
@@ -442,10 +447,11 @@ export const PATCH = withAdmin(
|
||||
// Download SWF if not present — try multiple revision candidates
|
||||
if (!existsSync(swfPath)) {
|
||||
// Build list of revisions to try: known rev, spriteId, common fallbacks
|
||||
const spriteId = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: item.classname },
|
||||
select: { spriteId: true },
|
||||
});
|
||||
const [spriteId] = await db
|
||||
.select({ spriteId: ItemsBase.spriteId })
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, item.classname))
|
||||
.limit(1);
|
||||
const revsToTry = [
|
||||
...new Set([rev, spriteId?.spriteId || 0].filter((r) => r > 0)),
|
||||
];
|
||||
@@ -500,15 +506,16 @@ export const PATCH = withAdmin(
|
||||
);
|
||||
|
||||
if (!alreadyInFD) {
|
||||
const dbItem = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: item.classname },
|
||||
select: {
|
||||
id: true,
|
||||
spriteId: true,
|
||||
publicName: true,
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
const [dbItem] = await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
publicName: ItemsBase.publicName,
|
||||
type: ItemsBase.type,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, item.classname))
|
||||
.limit(1);
|
||||
const furnidata = await getHabboItFurnidata();
|
||||
const fd =
|
||||
furnidata.get(item.classname) || furnidata.get(baseClassname);
|
||||
@@ -560,10 +567,11 @@ export const PUT = withAdmin(
|
||||
const isDryRun = request.nextUrl.searchParams.get("dryrun") === "1";
|
||||
|
||||
// 1. Find the root "imported_furni" page
|
||||
const parentPage = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE },
|
||||
select: { id: true },
|
||||
});
|
||||
const [parentPage] = await db
|
||||
.select({ id: CatalogPages.id })
|
||||
.from(CatalogPages)
|
||||
.where(eq(CatalogPages.captionSave, IMPORTED_PAGE_CAPTION_SAVE))
|
||||
.limit(1);
|
||||
if (!parentPage) {
|
||||
return apiOk({
|
||||
dryRun: isDryRun,
|
||||
@@ -574,10 +582,13 @@ export const PUT = withAdmin(
|
||||
}
|
||||
|
||||
// 2. Collect ALL imported page IDs (parent + every imp_* sub-page)
|
||||
const subPages = await prisma.catalogPages.findMany({
|
||||
where: { parentId: parentPage.id },
|
||||
select: { id: true, captionSave: true },
|
||||
});
|
||||
const subPages = await db
|
||||
.select({
|
||||
id: CatalogPages.id,
|
||||
captionSave: CatalogPages.captionSave,
|
||||
})
|
||||
.from(CatalogPages)
|
||||
.where(eq(CatalogPages.parentId, parentPage.id));
|
||||
const allImportedPageIds = [
|
||||
String(parentPage.id),
|
||||
...subPages.map((p) => String(p.id)),
|
||||
@@ -585,19 +596,23 @@ export const PUT = withAdmin(
|
||||
|
||||
// 3. Find ALL catalog_items across all imported pages
|
||||
// page_id is VARCHAR in DB so we use string placeholders
|
||||
const inPlaceholders = allImportedPageIds.map(() => "?").join(",");
|
||||
const allItems = await prisma.$queryRawUnsafe<
|
||||
const [allItems] = (await db.execute(sql`
|
||||
SELECT id, item_ids, catalog_name, page_id, cost_credits
|
||||
FROM catalog_items
|
||||
WHERE CAST(page_id AS CHAR) IN (${sql.join(
|
||||
allImportedPageIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})
|
||||
`)) as unknown as [
|
||||
Array<{
|
||||
id: number;
|
||||
item_ids: string;
|
||||
catalog_name: string;
|
||||
page_id: string;
|
||||
cost_credits: number;
|
||||
}>
|
||||
>(
|
||||
`SELECT id, item_ids, catalog_name, page_id, cost_credits FROM catalog_items WHERE page_id IN (${inPlaceholders})`,
|
||||
...allImportedPageIds,
|
||||
);
|
||||
}>,
|
||||
unknown,
|
||||
];
|
||||
|
||||
if (allItems.length === 0) {
|
||||
return apiOk({
|
||||
@@ -614,10 +629,14 @@ export const PUT = withAdmin(
|
||||
].filter((n) => !Number.isNaN(n));
|
||||
const dbItems =
|
||||
uniqueItemIds.length > 0
|
||||
? await prisma.itemsBase.findMany({
|
||||
where: { id: { in: uniqueItemIds } },
|
||||
select: { id: true, itemName: true, type: true },
|
||||
})
|
||||
? await db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
itemName: ItemsBase.itemName,
|
||||
type: ItemsBase.type,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(inArray(ItemsBase.id, uniqueItemIds))
|
||||
: [];
|
||||
const typeMap = new Map<string, string>();
|
||||
for (const di of dbItems) {
|
||||
@@ -641,14 +660,20 @@ export const PUT = withAdmin(
|
||||
// Create the page (only happens once per new category)
|
||||
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
|
||||
const parentId = parentPage?.id;
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
||||
VALUES (${captionSave}, ${catInfo.label}, 'default_3x3', ${parentId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', '')
|
||||
`;
|
||||
const created = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave, parentId },
|
||||
select: { id: true },
|
||||
});
|
||||
`);
|
||||
const [created] = await db
|
||||
.select({ id: CatalogPages.id })
|
||||
.from(CatalogPages)
|
||||
.where(
|
||||
and(
|
||||
eq(CatalogPages.captionSave, captionSave),
|
||||
eq(CatalogPages.parentId, parentId!),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
categoryPageMap.set(captionSave, created?.id);
|
||||
return String(created?.id);
|
||||
}
|
||||
@@ -719,15 +744,17 @@ export const PUT = withAdmin(
|
||||
for (const [batchKey, ids] of updateBatches) {
|
||||
const [pageId, credits, points, pointsType] = batchKey.split("|");
|
||||
try {
|
||||
const idPlaceholders = ids.map(() => "?").join(",");
|
||||
await prisma.$queryRawUnsafe(
|
||||
`UPDATE catalog_items SET page_id = ?, cost_credits = ?, cost_points = ?, points_type = ? WHERE id IN (${idPlaceholders})`,
|
||||
pageId,
|
||||
Number(credits),
|
||||
Number(points),
|
||||
Number(pointsType),
|
||||
...ids,
|
||||
);
|
||||
await db.execute(sql`
|
||||
UPDATE catalog_items
|
||||
SET page_id = ${pageId},
|
||||
cost_credits = ${Number(credits)},
|
||||
cost_points = ${Number(points)},
|
||||
points_type = ${Number(pointsType)}
|
||||
WHERE id IN (${sql.join(
|
||||
ids.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})
|
||||
`);
|
||||
} catch (err) {
|
||||
errors.push(`batch update: ${(err as Error).message}`);
|
||||
}
|
||||
@@ -741,15 +768,14 @@ export const PUT = withAdmin(
|
||||
const subPageIdStr = String(subPage.id);
|
||||
if (!pagesWithItems.has(subPageIdStr)) {
|
||||
try {
|
||||
const remaining = await prisma.$queryRawUnsafe<
|
||||
Array<{ cnt: bigint }>
|
||||
>(
|
||||
`SELECT COUNT(*) as cnt FROM catalog_items WHERE page_id = ?`,
|
||||
subPageIdStr,
|
||||
);
|
||||
const [remaining] = (await db.execute(
|
||||
sql`SELECT COUNT(*) as cnt FROM catalog_items WHERE CAST(page_id AS CHAR) = ${subPageIdStr}`,
|
||||
)) as unknown as [Array<{ cnt: bigint }>, unknown];
|
||||
const count = Number(remaining[0]?.cnt ?? 0);
|
||||
if (count === 0) {
|
||||
await prisma.$executeRaw`DELETE FROM catalog_pages WHERE id = ${subPage.id}`;
|
||||
await db.execute(
|
||||
sql`DELETE FROM catalog_pages WHERE id = ${subPage.id}`,
|
||||
);
|
||||
pagesDeleted++;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { AclModelRole, db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const POST = withAdmin(
|
||||
{ permission: PERMS.PERMISSIONS_MANAGE },
|
||||
@@ -20,16 +21,32 @@ export const POST = withAdmin(
|
||||
return apiError("Valid userId and roleId required", 400);
|
||||
}
|
||||
if (body.action === "remove") {
|
||||
await prisma.aclModelRole.deleteMany({
|
||||
where: { modelType: "User", modelId: userId, roleId },
|
||||
});
|
||||
await db
|
||||
.delete(AclModelRole)
|
||||
.where(
|
||||
and(
|
||||
eq(AclModelRole.modelType, "User"),
|
||||
eq(AclModelRole.modelId, userId),
|
||||
eq(AclModelRole.roleId, roleId),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const existing = await prisma.aclModelRole.findFirst({
|
||||
where: { modelType: "User", modelId: userId, roleId },
|
||||
});
|
||||
const [existing] = await db
|
||||
.select({ modelId: AclModelRole.modelId })
|
||||
.from(AclModelRole)
|
||||
.where(
|
||||
and(
|
||||
eq(AclModelRole.modelType, "User"),
|
||||
eq(AclModelRole.modelId, userId),
|
||||
eq(AclModelRole.roleId, roleId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing)
|
||||
await prisma.aclModelRole.create({
|
||||
data: { modelType: "User", modelId: userId, roleId },
|
||||
await db.insert(AclModelRole).values({
|
||||
modelType: "User",
|
||||
modelId: userId,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
revalidateTag("permissions", { expire: 0 });
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { ResultSetHeader } from "mysql2";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError } from "@/lib/api-response";
|
||||
import { AclModelPermission, AclModelRole, AclRole, db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const POST = withAdmin(
|
||||
{ permission: PERMS.PERMISSIONS_MANAGE },
|
||||
@@ -15,7 +17,10 @@ export const POST = withAdmin(
|
||||
const title = String(body.title ?? "").trim();
|
||||
if (!/^[a-z0-9._-]{2,64}$/.test(slug) || !title)
|
||||
return apiError("Valid slug and title required", 400);
|
||||
const role = await prisma.aclRole.create({ data: { slug, title } });
|
||||
const [result] = (await db
|
||||
.insert(AclRole)
|
||||
.values({ slug, title })) as unknown as [ResultSetHeader];
|
||||
const role = { id: Number(result.insertId), slug, title };
|
||||
revalidateTag("permissions", { expire: 0 });
|
||||
return NextResponse.json({ role });
|
||||
},
|
||||
@@ -27,20 +32,26 @@ export const DELETE = withAdmin(
|
||||
const id = Number(request.nextUrl.searchParams.get("id"));
|
||||
if (!Number.isInteger(id) || id <= 0)
|
||||
return apiError("Valid ID required", 400);
|
||||
const role = await prisma.aclRole.findUnique({
|
||||
where: { id },
|
||||
select: { slug: true },
|
||||
});
|
||||
const [role] = await db
|
||||
.select({ slug: AclRole.slug })
|
||||
.from(AclRole)
|
||||
.where(eq(AclRole.id, id))
|
||||
.limit(1);
|
||||
if (!role) return apiError("Role not found", 404);
|
||||
if (role.slug.startsWith("rank_"))
|
||||
return apiError("Rank roles must be managed through emulator ranks", 409);
|
||||
await prisma.$transaction([
|
||||
prisma.aclModelPermission.deleteMany({
|
||||
where: { modelType: "Role", modelId: id },
|
||||
}),
|
||||
prisma.aclModelRole.deleteMany({ where: { roleId: id } }),
|
||||
prisma.aclRole.delete({ where: { id } }),
|
||||
]);
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.delete(AclModelPermission)
|
||||
.where(
|
||||
and(
|
||||
eq(AclModelPermission.modelType, "Role"),
|
||||
eq(AclModelPermission.modelId, id),
|
||||
),
|
||||
);
|
||||
await tx.delete(AclModelRole).where(eq(AclModelRole.roleId, id));
|
||||
await tx.delete(AclRole).where(eq(AclRole.id, id));
|
||||
});
|
||||
revalidateTag("permissions", { expire: 0 });
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface BlacklistWord {
|
||||
id: number;
|
||||
@@ -9,9 +10,9 @@ interface BlacklistWord {
|
||||
}
|
||||
|
||||
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
||||
const words = await prisma.$queryRaw<BlacklistWord[]>`
|
||||
SELECT id, word FROM custom_prefix_blacklist ORDER BY word ASC
|
||||
`;
|
||||
const [words] = (await db.execute(
|
||||
sql`SELECT id, word FROM custom_prefix_blacklist ORDER BY word ASC`,
|
||||
)) as unknown as [BlacklistWord[], unknown];
|
||||
|
||||
return apiOk({
|
||||
words: words.map((w) => ({ ...w, created_at: "" })),
|
||||
@@ -26,7 +27,9 @@ export const POST = withAdmin(
|
||||
|
||||
if (!word) return apiError("Word is required");
|
||||
|
||||
await prisma.$executeRaw`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`;
|
||||
await db.execute(
|
||||
sql`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`,
|
||||
);
|
||||
|
||||
return apiOk();
|
||||
},
|
||||
@@ -41,7 +44,7 @@ export const DELETE = withAdmin(
|
||||
if (!Number.isInteger(id) || id <= 0)
|
||||
return apiError("Missing or invalid word id");
|
||||
|
||||
await prisma.$executeRaw`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`;
|
||||
await db.execute(sql`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`);
|
||||
|
||||
return apiOk({ deleted: id });
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { Prisma, prisma } from "@/lib/prisma";
|
||||
|
||||
interface UserPrefix {
|
||||
id: number;
|
||||
@@ -24,23 +25,24 @@ export const GET = withAdmin(
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const whereFragment = q
|
||||
? Prisma.sql`WHERE up.text LIKE ${`%${q}%`} OR u.username LIKE ${`%${q}%`}`
|
||||
: Prisma.empty;
|
||||
? sql`WHERE up.text LIKE ${`%${q}%`} OR u.username LIKE ${`%${q}%`}`
|
||||
: sql``;
|
||||
|
||||
const prefixes = await prisma.$queryRaw<UserPrefix[]>(
|
||||
Prisma.sql`SELECT up.id, up.user_id, up.text, up.color, up.icon, up.effect, up.active, u.username
|
||||
const [prefixes] =
|
||||
(await db.execute(sql`SELECT up.id, up.user_id, up.text, up.color, up.icon, up.effect, up.active, u.username
|
||||
FROM custom_prefixes up
|
||||
LEFT JOIN users u ON u.id = up.user_id
|
||||
${whereFragment}
|
||||
ORDER BY up.id DESC
|
||||
LIMIT ${limit} OFFSET ${offset}`,
|
||||
);
|
||||
LIMIT ${limit} OFFSET ${offset}`)) as unknown as [
|
||||
UserPrefix[],
|
||||
unknown,
|
||||
];
|
||||
|
||||
const countResult = await prisma.$queryRaw<[{ total: bigint }]>(
|
||||
Prisma.sql`SELECT COUNT(*) as total FROM custom_prefixes up
|
||||
const [countResult] =
|
||||
(await db.execute(sql`SELECT COUNT(*) as total FROM custom_prefixes up
|
||||
LEFT JOIN users u ON u.id = up.user_id
|
||||
${whereFragment}`,
|
||||
);
|
||||
${whereFragment}`)) as unknown as [[{ total: bigint }], unknown];
|
||||
|
||||
const total = Number(countResult[0]?.total || 0);
|
||||
|
||||
@@ -69,9 +71,9 @@ export const POST = withAdmin(
|
||||
return apiError("Missing required fields: username, text, color");
|
||||
}
|
||||
|
||||
const users = await prisma.$queryRaw<{ id: number }[]>(
|
||||
Prisma.sql`SELECT id FROM users WHERE username = ${username} LIMIT 1`,
|
||||
);
|
||||
const [users] = (await db.execute(
|
||||
sql`SELECT id FROM users WHERE username = ${username} LIMIT 1`,
|
||||
)) as unknown as [{ id: number }[], unknown];
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
return apiError("User not found");
|
||||
@@ -79,8 +81,8 @@ export const POST = withAdmin(
|
||||
|
||||
const userId = users[0].id;
|
||||
|
||||
await prisma.$executeRaw(
|
||||
Prisma.sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
|
||||
await db.execute(
|
||||
sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
|
||||
VALUES (${userId}, ${text}, ${color}, ${icon || ""}, ${effect || ""}, ${active ? 1 : 0})`,
|
||||
);
|
||||
|
||||
@@ -96,8 +98,8 @@ export const PUT = withAdmin(
|
||||
|
||||
if (!id) return apiError("Missing prefix id");
|
||||
|
||||
await prisma.$executeRaw(
|
||||
Prisma.sql`UPDATE custom_prefixes
|
||||
await db.execute(
|
||||
sql`UPDATE custom_prefixes
|
||||
SET text = ${text}, color = ${color}, icon = ${icon || ""}, effect = ${effect || ""}, active = ${active ? 1 : 0}
|
||||
WHERE id = ${id}`,
|
||||
);
|
||||
@@ -114,9 +116,7 @@ export const DELETE = withAdmin(
|
||||
|
||||
if (!id) return apiError("Missing prefix id");
|
||||
|
||||
await prisma.$executeRaw(
|
||||
Prisma.sql`DELETE FROM custom_prefixes WHERE id = ${id}`,
|
||||
);
|
||||
await db.execute(sql`DELETE FROM custom_prefixes WHERE id = ${id}`);
|
||||
|
||||
return apiOk({ deleted: id });
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { db } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
interface PrefixSetting {
|
||||
key: string;
|
||||
@@ -31,9 +32,9 @@ const DEFAULT_SETTINGS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
||||
const settings = await prisma.$queryRaw<PrefixSetting[]>`
|
||||
SELECT \`key\`, \`value\` FROM custom_prefix_settings
|
||||
`;
|
||||
const [settings] = (await db.execute(
|
||||
sql`SELECT \`key\`, \`value\` FROM custom_prefix_settings`,
|
||||
)) as unknown as [PrefixSetting[], unknown];
|
||||
|
||||
const result: Record<string, string> = { ...DEFAULT_SETTINGS };
|
||||
for (const s of settings) {
|
||||
@@ -58,11 +59,11 @@ export const PUT = withAdmin(
|
||||
return apiError(`Invalid setting key: ${key}`);
|
||||
}
|
||||
const strValue = String(value);
|
||||
await prisma.$executeRaw`
|
||||
await db.execute(sql`
|
||||
INSERT INTO custom_prefix_settings (\`key\`, \`value\`)
|
||||
VALUES (${key}, ${strValue})
|
||||
ON DUPLICATE KEY UPDATE \`value\` = ${strValue}
|
||||
`;
|
||||
`);
|
||||
}
|
||||
|
||||
return apiOk();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
|
||||
@@ -31,9 +32,14 @@ export const POST = withAdmin(
|
||||
);
|
||||
}
|
||||
|
||||
const rankExists = await prisma.$queryRaw<{ id: number }[]>`
|
||||
SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1
|
||||
`.catch(() => [] as { id: number }[]);
|
||||
const [rankExists] = (await db
|
||||
.execute(
|
||||
sql`SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1`,
|
||||
)
|
||||
.catch(() => [[]] as unknown as [unknown[], unknown])) as unknown as [
|
||||
{ id: number }[],
|
||||
unknown,
|
||||
];
|
||||
if (rankExists.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Rank does not exist" },
|
||||
@@ -41,10 +47,11 @@ export const POST = withAdmin(
|
||||
);
|
||||
}
|
||||
|
||||
const target = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { rank: true },
|
||||
});
|
||||
const [target] = await db
|
||||
.select({ rank: User.rank })
|
||||
.from(User)
|
||||
.where(eq(User.id, userId))
|
||||
.limit(1);
|
||||
if (!target) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "User not found" },
|
||||
@@ -74,7 +81,7 @@ export const POST = withAdmin(
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.user.update({ where: { id: userId }, data: { rank } });
|
||||
await db.update(User).set({ rank }).where(eq(User.id, userId));
|
||||
await rcon.setRank(userId, rank);
|
||||
await logStaffActivity({
|
||||
staffId,
|
||||
|
||||
Reference in new issue
Block a user