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
+1239 -907

No files matched your search

+15 -14
View File
@@ -1,9 +1,10 @@
import { asc, eq, gte } from "drizzle-orm";
import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { db, User, WebsiteTicket } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { AdminTicketDetail } from "./admin-ticket-detail";
export default async function AdminTicketDetailPage({
@@ -21,11 +22,11 @@ export default async function AdminTicketDetailPage({
const ticketId = Number(id);
if (Number.isNaN(ticketId)) notFound();
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ticketId },
include: {
const ticket = await db.query.WebsiteTicket.findFirst({
where: eq(WebsiteTicket.id, ticketId),
with: {
creator: {
select: {
columns: {
id: true,
username: true,
look: true,
@@ -35,12 +36,12 @@ export default async function AdminTicketDetailPage({
accountCreated: true,
},
},
assignee: { select: { id: true, username: true } },
assignee: { columns: { id: true, username: true } },
messages: {
orderBy: { createdAt: "asc" },
include: {
orderBy: (m, { asc: a }) => [a(m.createdAt)],
with: {
user: {
select: { id: true, username: true, look: true, rank: true },
columns: { id: true, username: true, look: true, rank: true },
},
},
},
@@ -51,11 +52,11 @@ export default async function AdminTicketDetailPage({
// Staff assignees: min_staff_rank and above
const minStaffRank = await getMinStaffRank();
const staffMembers = await prisma.user.findMany({
where: { rank: { gte: minStaffRank } },
select: { id: true, username: true },
orderBy: { username: "asc" },
});
const staffMembers = await db
.select({ id: User.id, username: User.username })
.from(User)
.where(gte(User.rank, minStaffRank))
.orderBy(asc(User.username));
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
File diff suppressed because it is too large. Load diff
+6 -4
View File
@@ -1,6 +1,7 @@
import { asc } from "drizzle-orm";
import { redirect } from "next/navigation";
import { db, WebsiteTicketTemplate } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { TicketTemplatesManager } from "./templates-manager";
export default async function TicketTemplatesPage() {
@@ -9,9 +10,10 @@ export default async function TicketTemplatesPage() {
redirect("/admin/tickets");
}
const templates = await prisma.websiteTicketTemplate.findMany({
orderBy: { sortOrder: "asc" },
});
const templates = await db
.select()
.from(WebsiteTicketTemplate)
.orderBy(asc(WebsiteTicketTemplate.sortOrder));
return (
<div className="space-y-6">
File diff suppressed because it is too large. Load diff
+37 -32
View File
@@ -1,20 +1,21 @@
import { and, asc, count, like, or, type SQL } from "drizzle-orm";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { EmulatorSettingsForm } from "@/components/admin/emulator-settings-form";
import { Button } from "@/components/ui/button";
import { calcPagination } from "@/lib/admin-helpers";
import { prisma } from "@/lib/prisma";
import { db, EmulatorSettings } from "@/lib/db";
const TRANSLATION_KEY_PATTERNS = [
"commands.description.%",
"commands.keys.%",
"commands.cmd_%",
"wordfilter.%",
"pet.%message%",
"matchmaking.%message%",
"message.%",
"notification.%",
"commands.description.",
"commands.keys.",
"commands.cmd_",
"wordfilter.",
"pet.",
"matchmaking.",
"message.",
"notification.",
] as const;
export default async function EmulatorTranslationsPage({
@@ -29,35 +30,39 @@ export default async function EmulatorTranslationsPage({
const perPage = Math.min(Math.max(parseInt(sp.perPage || "50", 10), 10), 200);
const page = Math.max(parseInt(sp.page || "1", 10), 1);
const patternFilters: any[] = TRANSLATION_KEY_PATTERNS.map((p) => ({
key: { contains: p.replace(/%/g, "") },
}));
const patternFilters = or(
...TRANSLATION_KEY_PATTERNS.map((p) => like(EmulatorSettings.key, `${p}%`)),
)!;
const baseFilter: any = { OR: patternFilters };
const conditions: any[] = [baseFilter];
if (group) conditions.push({ key: { startsWith: `${group}.` } });
const conditions: SQL[] = [patternFilters];
if (group) conditions.push(like(EmulatorSettings.key, `${group}.%`));
if (search) {
conditions.push({
OR: [{ key: { contains: search } }, { value: { contains: search } }],
});
conditions.push(
or(
like(EmulatorSettings.key, `%${search}%`),
like(EmulatorSettings.value, `%${search}%`),
)!,
);
}
const where: any = { AND: conditions };
const where = and(...conditions);
const [settings, total, allKeys] = await Promise.all([
prisma.emulatorSettings.findMany({
where,
orderBy: { key: "asc" },
skip: (page - 1) * perPage,
take: perPage,
}),
prisma.emulatorSettings.count({ where }),
prisma.emulatorSettings.findMany({
where: baseFilter,
select: { key: true },
}),
const [settings, totalRows, allKeys] = await Promise.all([
db
.select()
.from(EmulatorSettings)
.where(where)
.orderBy(asc(EmulatorSettings.key))
.limit(perPage)
.offset((page - 1) * perPage),
db.select({ total: count() }).from(EmulatorSettings).where(where),
db
.select({ key: EmulatorSettings.key })
.from(EmulatorSettings)
.where(patternFilters),
]);
const total = Number(totalRows[0]?.total ?? 0);
const groupCounts = new Map<string, number>();
for (const { key } of allKeys) {
const g = key.includes(".") ? key.split(".")[0] : "general";
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,7 +1,8 @@
import { sql } from "drizzle-orm";
import { redirect } from "next/navigation";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { db } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { Prisma, prisma } from "@/lib/prisma";
import { getWatchedUserIds } from "@/lib/services/watch";
import { UserAccountInfo } from "../../_components/user-account-info";
import { UserHero } from "../../_components/user-hero";
@@ -28,10 +29,10 @@ export default async function UserEditPage({
const watched = await getWatchedUserIds(session.user.id);
// Fetch rank list from permissions table for the rank dropdown
const rawRanks: { id: number; rank_name: string }[] = await prisma.$queryRaw(
Prisma.sql`SELECT id, rank_name FROM permissions ORDER BY id ASC`,
);
const ranks = rawRanks.map((r) => ({
const [rawRanks] = (await db.execute(
sql`SELECT id, rank_name FROM permissions ORDER BY id ASC`,
)) as unknown as [{ id: number; rank_name: string }[], unknown];
const ranks = (rawRanks ?? []).map((r) => ({
id: Number(r.id),
name: String(r.rank_name || `Rank ${r.id}`),
}));
+15 -14
View File
@@ -1,6 +1,7 @@
import { desc } from "drizzle-orm";
import { redirect } from "next/navigation";
import { db, WebsiteShopVouchers } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { type VoucherRow, VouchersClient } from "./vouchers-client";
export default async function AdminVouchers() {
@@ -14,19 +15,19 @@ export default async function AdminVouchers() {
let rows: VoucherRow[] = [];
try {
const vouchers = await prisma.websiteShopVouchers.findMany({
select: {
id: true,
code: true,
amount: true,
maxUses: true,
useCount: true,
expiresAt: true,
createdAt: true,
},
orderBy: { id: "desc" },
take: 200,
});
const vouchers = await db
.select({
id: WebsiteShopVouchers.id,
code: WebsiteShopVouchers.code,
amount: WebsiteShopVouchers.amount,
maxUses: WebsiteShopVouchers.maxUses,
useCount: WebsiteShopVouchers.useCount,
expiresAt: WebsiteShopVouchers.expiresAt,
createdAt: WebsiteShopVouchers.createdAt,
})
.from(WebsiteShopVouchers)
.orderBy(desc(WebsiteShopVouchers.id))
.limit(200);
rows = vouchers.map((v) => {
const expired = v.expiresAt ? v.expiresAt.getTime() <= now : false;
const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses;
+7 -6
View File
@@ -1,9 +1,10 @@
import { asc } from "drizzle-orm";
import { Filter } from "lucide-react";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { db, WebsiteWordfilter } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { WordFilterClient } from "./wordfilter-client";
export default async function AdminWordFilter() {
@@ -21,11 +22,11 @@ export default async function AdminWordFilter() {
let words: { id: string; word: string }[] = [];
try {
const rows = await prisma.websiteWordfilter.findMany({
select: { id: true, word: true },
orderBy: { word: "asc" },
take: 500,
});
const rows = await db
.select({ id: WebsiteWordfilter.id, word: WebsiteWordfilter.word })
.from(WebsiteWordfilter)
.orderBy(asc(WebsiteWordfilter.word))
.limit(500);
words = rows.map((w) => ({ id: String(w.id), word: w.word }));
} catch {
words = [];
+7 -6
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import {
@@ -8,8 +9,8 @@ import {
} from "@/actions/admin-writeable-boxes";
import { StatusCard } from "@/components/admin/dashboard";
import { Button } from "@/components/ui/button";
import { db, WebsiteWriteableBoxes } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export const metadata = { title: "Writeable boxes" };
@@ -20,11 +21,11 @@ export default async function AdminWriteableBoxes() {
}
const t = await getTranslations("pages.admin.writeableBoxes");
const boxes = await prisma.websiteWriteableBoxes
.findMany({
orderBy: [{ position: "asc" }, { id: "asc" }],
take: 500,
})
const boxes = await db
.select()
.from(WebsiteWriteableBoxes)
.orderBy(asc(WebsiteWriteableBoxes.position), asc(WebsiteWriteableBoxes.id))
.limit(500)
.catch(() => []);
const activeCount = boxes.filter((b) => b.isActive).length;
+12 -9
View File
@@ -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) {
File diff suppressed because it is too large. Load diff
+22 -21
View File
@@ -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",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -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 });
+24 -13
View File
@@ -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 });
},
+21 -21
View File
@@ -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 });
},
+7 -6
View File
@@ -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();
+16 -9
View File
@@ -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,
+8 -9
View File
@@ -6,9 +6,10 @@
// chars (matches the VARCHAR(255) column). Fails soft — never returns a 500 for
// DB issues, just a generic error envelope.
import { eq } from "drizzle-orm";
import { apiError, apiJson } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { prisma } from "@/lib/prisma";
import { db, WebsiteArticleComments, WebsiteArticles } from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -31,24 +32,22 @@ export async function POST(
}
try {
const article = await prisma.websiteArticles.findUnique({
where: { slug },
select: { id: true },
});
const [article] = await db
.select({ id: WebsiteArticles.id })
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1);
if (!article) {
return apiError("Article not found", 404);
}
const now = new Date();
await prisma.websiteArticleComments.create({
data: {
await db.insert(WebsiteArticleComments).values({
articleId: article.id,
userId: uid,
comment,
createdAt: now,
updatedAt: now,
},
select: { id: true },
});
return apiJson({ ok: true });
+16 -14
View File
@@ -1,5 +1,6 @@
import { eq } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { db, WebsiteArticles } from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -14,19 +15,20 @@ export async function GET(
const { slug } = await params;
try {
const article = await prisma.websiteArticles.findUnique({
where: { slug },
select: {
id: true,
title: true,
slug: true,
shortStory: true,
fullStory: true,
image: true,
createdAt: true,
updatedAt: true,
},
});
const [article] = await db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
fullStory: WebsiteArticles.fullStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
updatedAt: WebsiteArticles.updatedAt,
})
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1);
if (!article) {
return apiJson({ error: "Article not found" }, { status: 404 });
+20 -18
View File
@@ -1,34 +1,36 @@
import { count, desc } from "drizzle-orm";
import { apiJson, pagination } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { db, WebsiteArticles } from "@/lib/db";
export const dynamic = "force-dynamic";
/**
* GET /api/articles — paginated list of website_articles, newest first.
* Mirrors the /news page query (prisma.websiteArticles). Returns list cards
* (shortStory only, never fullStory) plus pagination metadata.
* Mirrors the /news page query. Returns list cards (shortStory only, never
* fullStory) plus pagination metadata.
*/
export async function GET(req: Request) {
const sp = new URL(req.url).searchParams;
const { page, perPage, skip, take } = pagination(sp);
try {
const [total, articles] = await Promise.all([
prisma.websiteArticles.count(),
prisma.websiteArticles.findMany({
select: {
id: true,
title: true,
slug: true,
shortStory: true,
image: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
skip,
take,
}),
const [totalRows, articles] = await Promise.all([
db.select({ total: count() }).from(WebsiteArticles),
db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
})
.from(WebsiteArticles)
.orderBy(desc(WebsiteArticles.createdAt))
.limit(take)
.offset(skip),
]);
const total = totalRows[0]?.total ?? 0;
return apiJson({
data: articles,
File diff suppressed because it is too large. Load diff
+20 -16
View File
@@ -2,8 +2,9 @@
// (guilds_members). AtomCMS JSON API parity. Returns { error } (404) when the
// id is unknown. The owner is the user_id column (accessor userId), surfaced
// here as ownerId.
import { count, eq } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { db, Guilds, GuildsMembers } from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -19,18 +20,19 @@ export async function GET(
const guildId = Number(id);
try {
const guild = await prisma.guilds.findUnique({
where: { id: guildId },
select: {
id: true,
name: true,
description: true,
userId: true,
roomId: true,
badge: true,
dateCreated: true,
},
});
const [guild] = await db
.select({
id: Guilds.id,
name: Guilds.name,
description: Guilds.description,
userId: Guilds.userId,
roomId: Guilds.roomId,
badge: Guilds.badge,
dateCreated: Guilds.dateCreated,
})
.from(Guilds)
.where(eq(Guilds.id, guildId))
.limit(1);
if (!guild) {
return apiJson({ error: "Guild not found" }, { status: 404 });
@@ -39,9 +41,11 @@ export async function GET(
// Member count is a separate guarded query (no relation is modelled).
let memberCount = 0;
try {
memberCount = await prisma.guildsMembers.count({
where: { guildId: guild.id },
});
const [row] = await db
.select({ total: count() })
.from(GuildsMembers)
.where(eq(GuildsMembers.guildId, guild.id));
memberCount = row?.total ?? 0;
} catch {
memberCount = 0;
}
+16 -14
View File
@@ -2,8 +2,9 @@
// paginated list ordered newest first (by id), exposing the basic public
// fields. The owner is stored as the user_id column (accessor userId), surfaced
// here as ownerId for API clarity.
import { count, desc } from "drizzle-orm";
import { apiJson, pagination } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { db, Guilds } from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -12,20 +13,21 @@ export async function GET(req: Request) {
const { page, perPage, skip, take } = pagination(sp);
try {
const [total, rows] = await Promise.all([
prisma.guilds.count(),
prisma.guilds.findMany({
orderBy: { id: "desc" },
skip,
take,
select: {
id: true,
name: true,
description: true,
userId: true,
},
}),
const [totalRows, rows] = await Promise.all([
db.select({ total: count() }).from(Guilds),
db
.select({
id: Guilds.id,
name: Guilds.name,
description: Guilds.description,
userId: Guilds.userId,
})
.from(Guilds)
.orderBy(desc(Guilds.id))
.limit(take)
.offset(skip),
]);
const total = totalRows[0]?.total ?? 0;
// Rename userId → ownerId for the public payload.
const data = rows.map(({ userId, ...rest }) => ({
+4 -2
View File
@@ -1,6 +1,7 @@
import { sql } from "drizzle-orm";
import { env } from "@/env";
import { apiJson } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { db } from "@/lib/db";
import { redis } from "@/lib/redis";
import { rcon } from "@/lib/services/rcon";
@@ -13,7 +14,8 @@ export const dynamic = "force-dynamic";
* only care about reachability.
*/
export async function GET() {
const database = await prisma.$queryRaw`SELECT 1`
const database = await db
.execute(sql`SELECT 1`)
.then(() => true)
.catch(() => false);
+17 -16
View File
@@ -1,7 +1,8 @@
import { count, desc, eq } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
import { db, User, WebsiteArticles } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
import { prisma } from "@/lib/prisma";
import { apiCacheKey, redisCache } from "@/lib/redis-cache";
export const dynamic = "force-dynamic";
@@ -14,23 +15,23 @@ export const dynamic = "force-dynamic";
export async function GET(_req: Request) {
try {
const data = await redisCache(apiCacheKey("home"), 15, async () => {
const [articles, online, hotelName] = await Promise.all([
prisma.websiteArticles.findMany({
select: {
id: true,
title: true,
slug: true,
shortStory: true,
image: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
take: 4,
}),
prisma.user.count({ where: { online: "1" } }),
const [articles, onlineRows, hotelName] = await Promise.all([
db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
})
.from(WebsiteArticles)
.orderBy(desc(WebsiteArticles.createdAt))
.limit(4),
db.select({ total: count() }).from(User).where(eq(User.online, "1")),
resolveHotelName(),
]);
return { articles, online, hotelName };
return { articles, online: onlineRows[0]?.total ?? 0, hotelName };
});
return apiJson(data);
} catch {
+30 -17
View File
@@ -1,8 +1,9 @@
import { desc, eq, inArray } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { db, User, UsersCurrency } from "@/lib/db";
// Public REST API — leaderboard. Mirrors src/app/leaderboard/page.tsx.
// AtomCMS-faithful currency type ids (see prisma/schema.prisma UsersCurrency):
// AtomCMS-faithful currency type ids (see UsersCurrency):
// Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5.
export const dynamic = "force-dynamic";
@@ -16,11 +17,15 @@ const CURRENCY_TYPE: Record<Exclude<LeaderboardType, "credits">, number> = {
type Row = { rank: number; username: string; look: string; value: number };
async function loadCreditsRows(): Promise<Row[]> {
const users = await prisma.user.findMany({
orderBy: { credits: "desc" },
take: 20,
select: { username: true, look: true, credits: true },
});
const users = await db
.select({
username: User.username,
look: User.look,
credits: User.credits,
})
.from(User)
.orderBy(desc(User.credits))
.limit(20);
return users.map((u, i) => ({
rank: i + 1,
username: u.username,
@@ -30,18 +35,26 @@ async function loadCreditsRows(): Promise<Row[]> {
}
async function loadCurrencyRows(type: number): Promise<Row[]> {
const top = await prisma.usersCurrency.findMany({
where: { type },
orderBy: { amount: "desc" },
take: 20,
select: { userId: true, amount: true },
});
const top = await db
.select({
userId: UsersCurrency.userId,
amount: UsersCurrency.amount,
})
.from(UsersCurrency)
.where(eq(UsersCurrency.type, type))
.orderBy(desc(UsersCurrency.amount))
.limit(20);
if (top.length === 0) return [];
const users = await prisma.user.findMany({
where: { id: { in: top.map((t) => t.userId) } },
select: { id: true, username: true, look: true },
});
const users = await db
.select({ id: User.id, username: User.username, look: User.look })
.from(User)
.where(
inArray(
User.id,
top.map((t) => t.userId),
),
);
const byId = new Map(users.map((u) => [u.id, u]));
return top
+20 -18
View File
@@ -1,13 +1,14 @@
// Public REST API — the currently signed-in user.
//
// Reads the NextAuth session, then re-queries prisma.user by the session id to
// Reads the NextAuth session, then re-queries users by the session id to
// return a safe field set (never password / auth_ticket / 2FA secrets / pincode
// / mail). Returns { user: null } only when unauthenticated or missing.
import { eq } from "drizzle-orm";
import { apiJson, apiUnavailable } from "@/lib/api";
import { auth } from "@/lib/auth";
import { sessionUserId } from "@/lib/auth/session-user";
import { prisma } from "@/lib/prisma";
import { db, User } from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -19,22 +20,23 @@ export async function GET(_req: Request) {
}
try {
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
username: true,
look: true,
motto: true,
rank: true,
credits: true,
pixels: true,
points: true,
gender: true,
online: true,
accountCreated: true,
},
});
const [user] = await db
.select({
id: User.id,
username: User.username,
look: User.look,
motto: User.motto,
rank: User.rank,
credits: User.credits,
pixels: User.pixels,
points: User.points,
gender: User.gender,
online: User.online,
accountCreated: User.accountCreated,
})
.from(User)
.where(eq(User.id, id))
.limit(1);
if (!user) {
return apiJson({ user: null });
+29 -10
View File
@@ -8,11 +8,13 @@
// to the user via personal_access_tokens.tokenable_id (a BigInt). NOTE: the live
// table has no expires_at column, so it is never read or written here.
import { and, desc, eq } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import { apiError, apiJson, apiUnavailable, positiveBigInt } from "@/lib/api";
import { auth } from "@/lib/auth";
import { personalTokenScope } from "@/lib/auth/personal-token-scope";
import { sessionUserId } from "@/lib/auth/session-user";
import { prisma } from "@/lib/prisma";
import { db, PersonalAccessTokens } from "@/lib/db";
export const dynamic = "force-dynamic";
@@ -26,11 +28,21 @@ export async function GET(_req: Request) {
if (!id) return apiError("Unauthorized", 401);
try {
const tokens = await prisma.personalAccessTokens.findMany({
where: personalTokenScope(id),
select: { id: true, name: true, lastUsedAt: true },
orderBy: { id: "desc" },
});
const scope = personalTokenScope(id);
const tokens = await db
.select({
id: PersonalAccessTokens.id,
name: PersonalAccessTokens.name,
lastUsedAt: PersonalAccessTokens.lastUsedAt,
})
.from(PersonalAccessTokens)
.where(
and(
eq(PersonalAccessTokens.tokenableId, scope.tokenableId),
eq(PersonalAccessTokens.tokenableType, scope.tokenableType),
),
)
.orderBy(desc(PersonalAccessTokens.id));
// Never expose the token hash.
return apiJson({ data: tokens });
} catch {
@@ -50,10 +62,17 @@ export async function DELETE(req: Request) {
try {
// Scope the delete to the owner so users cannot revoke others' tokens.
const result = await prisma.personalAccessTokens.deleteMany({
where: { id: parsedTokenId, ...personalTokenScope(id) },
});
if (result.count === 0) {
const scope = personalTokenScope(id);
const [result] = (await db
.delete(PersonalAccessTokens)
.where(
and(
eq(PersonalAccessTokens.id, parsedTokenId),
eq(PersonalAccessTokens.tokenableId, scope.tokenableId),
eq(PersonalAccessTokens.tokenableType, scope.tokenableType),
),
)) as unknown as [ResultSetHeader];
if (!result.affectedRows) {
return apiError("Token not found", 404);
}
return apiJson({ ok: true });
+10 -5
View File
@@ -3,18 +3,23 @@
// `online` is the emulator's string flag "1" / "0" (see User model). Returns
// { count: 0 } (never 500) on DB failure.
import { count, eq } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { cached } from "@/lib/cache";
import { prisma } from "@/lib/prisma";
import { db, User } from "@/lib/db";
export const dynamic = "force-dynamic";
export async function GET(_req: Request) {
try {
const count = await cached("online_count", 10_000, () =>
prisma.user.count({ where: { online: "1" } }),
);
return apiJson({ count });
const result = await cached("online_count", 10_000, async () => {
const [row] = await db
.select({ total: count() })
.from(User)
.where(eq(User.online, "1"));
return row?.total ?? 0;
});
return apiJson({ count: result });
} catch {
return apiJson({ count: 0 });
}
+9 -9
View File
@@ -1,23 +1,23 @@
// Public REST API — list of currently-online users (username + look only).
//
// `online` is stored by the emulator as the string "1" / "0" (see User model in
// prisma/schema.prisma). Capped at 100 rows. Returns empty data (never 500) on
// DB failure.
// `online` is stored by the emulator as the string "1" / "0" (see User model).
// Capped at 100 rows. Returns empty data (never 500) on DB failure.
import { eq } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { cached } from "@/lib/cache";
import { prisma } from "@/lib/prisma";
import { db, User } from "@/lib/db";
export const dynamic = "force-dynamic";
export async function GET(_req: Request) {
try {
const users = await cached("online_users", 10_000, async () =>
prisma.user.findMany({
where: { online: "1" },
select: { username: true, look: true },
take: 100,
}),
db
.select({ username: User.username, look: User.look })
.from(User)
.where(eq(User.online, "1"))
.limit(100),
);
return apiJson({ users });
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -2,9 +2,9 @@ import { NextResponse } from "next/server";
import { env } from "@/env";
import { auth } from "@/lib/auth";
import { sessionUserId } from "@/lib/auth/session-user";
import { db, WebsitePaypalTransactions } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
import { logger } from "@/lib/logger";
import { prisma } from "@/lib/prisma";
import {
createOrder,
creditsPerUnit,
@@ -87,7 +87,7 @@ export async function POST(req: Request): Promise<Response> {
await recordCreatedTopup(
{ userId, orderId: order.id, amount, currency: PAYPAL_CURRENCY, credits },
(data) => prisma.websitePaypalTransactions.create({ data }),
(data) => db.insert(WebsitePaypalTransactions).values(data),
);
return NextResponse.json({