diff --git a/src/actions/admin-housekeeping.ts b/src/actions/admin-housekeeping.ts index 6b31955c..9887448b 100644 --- a/src/actions/admin-housekeeping.ts +++ b/src/actions/admin-housekeeping.ts @@ -1,9 +1,10 @@ "use server"; +import { asc } from "drizzle-orm"; import { requirePermission } from "@/lib/admin/guard"; +import { db, WebsiteHousekeepingPermissions } from "@/lib/db"; import { redirectSafe } from "@/lib/foundation/security"; import { PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; /** * Housekeeping table writes are retired. Runtime access uses ACL only. @@ -34,16 +35,19 @@ export async function bulkImportPermissions( export async function exportPermissions(): Promise { await requirePermission(PERMS.SETTINGS_VIEW); - const perms = await prisma.websiteHousekeepingPermissions.findMany({ - orderBy: [{ groupName: "asc" }, { permission: "asc" }], - select: { - permission: true, - minRank: true, - description: true, - groupName: true, - dependsOn: true, - }, - }); + const perms = await db + .select({ + permission: WebsiteHousekeepingPermissions.permission, + minRank: WebsiteHousekeepingPermissions.minRank, + description: WebsiteHousekeepingPermissions.description, + groupName: WebsiteHousekeepingPermissions.groupName, + dependsOn: WebsiteHousekeepingPermissions.dependsOn, + }) + .from(WebsiteHousekeepingPermissions) + .orderBy( + asc(WebsiteHousekeepingPermissions.groupName), + asc(WebsiteHousekeepingPermissions.permission), + ); return JSON.stringify(perms, null, 2); } diff --git a/src/actions/import-badges.ts b/src/actions/import-badges.ts index dac0ed66..ebb07c0b 100644 --- a/src/actions/import-badges.ts +++ b/src/actions/import-badges.ts @@ -1,8 +1,8 @@ "use server"; import { requirePermission } from "@/lib/admin/guard"; +import { db, WebsiteBadges } from "@/lib/db"; import { PERMS } from "@/lib/permissions"; -import { prisma } from "@/lib/prisma"; export async function importBadgeFromRemote({ code, @@ -15,21 +15,23 @@ export async function importBadgeFromRemote({ }) { await requirePermission(PERMS.ASSETS_IMPORT); try { - await prisma.websiteBadges.upsert({ - where: { badgeKey: code }, - update: { - badgeName: name, - badgeDescription: description, - updatedAt: new Date(), - }, - create: { + const now = new Date(); + await db + .insert(WebsiteBadges) + .values({ badgeKey: code, badgeName: name, badgeDescription: description, - createdAt: new Date(), - updatedAt: new Date(), - }, - }); + createdAt: now, + updatedAt: now, + }) + .onDuplicateKeyUpdate({ + set: { + badgeName: name, + badgeDescription: description, + updatedAt: now, + }, + }); return { ok: true as const }; } catch { return { ok: false as const, error: "Failed to import badge" }; diff --git a/src/actions/radio-apply.ts b/src/actions/radio-apply.ts index 068db3ad..1df9f274 100644 --- a/src/actions/radio-apply.ts +++ b/src/actions/radio-apply.ts @@ -3,10 +3,10 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, RadioApplications } from "@/lib/db"; import { clientIp, rateLimit } from "@/lib/rate-limit"; -// Column bounds from prisma/schema.prisma (radio_applications): +// Column bounds from schema (radio_applications): // real_name VARCHAR(255); the rest are TEXT. age is an INT. const NAME_MAX = 255; const TEXT_MAX = 5000; @@ -66,19 +66,17 @@ export async function applyDj(formData: FormData): Promise { outcome = "invalid"; } else { const now = new Date(); - await prisma.radioApplications.create({ - data: { - userId: BigInt(userId), - realName, - age, - availability, - motivation, - experience: experience || null, - musicStyle: musicStyle || null, - status: "pending", - createdAt: now, - updatedAt: now, - }, + await db.insert(RadioApplications).values({ + userId: BigInt(userId), + realName, + age, + availability, + motivation, + experience: experience || null, + musicStyle: musicStyle || null, + status: "pending", + createdAt: now, + updatedAt: now, }); outcome = "submitted"; } diff --git a/src/actions/radio-requests.ts b/src/actions/radio-requests.ts index 2d22a69d..aa18f2ba 100644 --- a/src/actions/radio-requests.ts +++ b/src/actions/radio-requests.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, RadioSongRequests } from "@/lib/db"; import { clientIp, rateLimit } from "@/lib/rate-limit"; const SONG_MAX = 255; @@ -52,15 +52,13 @@ export async function submitRequest(formData: FormData): Promise { outcome = "empty"; } else { const now = new Date(); - await prisma.radioSongRequests.create({ - data: { - userId: BigInt(userId), - songTitle: songTitle || null, - artist: artist || null, - submittedAt: now, - createdAt: now, - updatedAt: now, - }, + await db.insert(RadioSongRequests).values({ + userId: BigInt(userId), + songTitle: songTitle || null, + artist: artist || null, + submittedAt: now, + createdAt: now, + updatedAt: now, }); outcome = "posted"; } diff --git a/src/actions/radio-shouts.ts b/src/actions/radio-shouts.ts index 986401c8..7ed56f2f 100644 --- a/src/actions/radio-shouts.ts +++ b/src/actions/radio-shouts.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { z } from "zod"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { db, RadioShouts } from "@/lib/db"; import { clientIp, rateLimit } from "@/lib/rate-limit"; import { moderateOrThrow } from "@/lib/services/moderation"; @@ -77,13 +77,11 @@ export async function postShout(formData: FormData): Promise { if (!moderated) { const now = new Date(); - await prisma.radioShouts.create({ - data: { - userId: BigInt(userId), - message, - createdAt: now, - updatedAt: now, - }, + await db.insert(RadioShouts).values({ + userId: BigInt(userId), + message, + createdAt: now, + updatedAt: now, }); outcome = "posted"; } diff --git a/src/actions/referral.ts b/src/actions/referral.ts index bccb7a0f..1eac3035 100644 --- a/src/actions/referral.ts +++ b/src/actions/referral.ts @@ -1,12 +1,22 @@ "use server"; +import { desc, eq, inArray, sql } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { + ClaimedReferralLogs, + db, + UserReferrals, + WebsiteSetting, +} from "@/lib/db"; import { clientIp, rateLimit } from "@/lib/rate-limit"; import { rcon } from "@/lib/services/rcon"; -import { type CurrencyName, sendCurrency } from "@/lib/services/send-currency"; +import { + type CurrencyName, + currencyDb, + sendCurrency, +} from "@/lib/services/send-currency"; /** * Claim the referral reward for the SIGNED-IN user. Faithful to AtomCMS's @@ -60,51 +70,54 @@ export async function claimReferral(_formData: FormData): Promise { } else { // Reward configuration (CMS-owned website_settings). AtomCMS defaults: // 5 referrals needed, 30 diamonds reward. - const [neededRaw, amountRaw, currencyRaw] = await Promise.all([ - prisma.websiteSetting - .findUnique({ - where: { key: "referrals_needed" }, - select: { value: true }, - }) - .catch(() => null), - prisma.websiteSetting - .findUnique({ - where: { key: "referral_reward_amount" }, - select: { value: true }, - }) - .catch(() => null), - // The seeded key is referral_reward_currency_type; fall back to the - // shorter referral_reward_currency name if that is what is configured. - prisma.websiteSetting - .findFirst({ - where: { - key: { - in: [ - "referral_reward_currency_type", - "referral_reward_currency", - ], - }, - }, - select: { value: true }, - }) - .catch(() => null), - ]); + const settingKeys = [ + "referrals_needed", + "referral_reward_amount", + "referral_reward_currency_type", + "referral_reward_currency", + ] as const; - const needed = Number.parseInt(neededRaw?.value ?? "5", 10) || 5; - const amount = Number.parseInt(amountRaw?.value ?? "30", 10); - const currency = (currencyRaw?.value ?? "diamonds") + let settings: Array<{ key: string; value: string }> = []; + try { + settings = await db + .select({ key: WebsiteSetting.key, value: WebsiteSetting.value }) + .from(WebsiteSetting) + .where(inArray(WebsiteSetting.key, [...settingKeys])); + } catch { + settings = []; + } + + const byKey = new Map(settings.map((s) => [s.key, s.value])); + const neededRaw = byKey.get("referrals_needed"); + const amountRaw = byKey.get("referral_reward_amount"); + // The seeded key is referral_reward_currency_type; fall back to the + // shorter referral_reward_currency name if that is what is configured. + const currencyRaw = + byKey.get("referral_reward_currency_type") ?? + byKey.get("referral_reward_currency"); + + const needed = Number.parseInt(neededRaw ?? "5", 10) || 5; + const amount = Number.parseInt(amountRaw ?? "30", 10); + const currency = (currencyRaw ?? "diamonds") .trim() .toLowerCase() as CurrencyName; // The user's referral tally lives in user_referrals (one row per user). - const referrals = await prisma.userReferrals - .findFirst({ - where: { userId }, - select: { id: true, referralsTotal: true }, - orderBy: { id: "desc" }, - }) - .catch(() => null); - + let referrals: { id: bigint; referralsTotal: bigint } | undefined; + try { + const rows = await db + .select({ + id: UserReferrals.id, + referralsTotal: UserReferrals.referralsTotal, + }) + .from(UserReferrals) + .where(eq(UserReferrals.userId, userId)) + .orderBy(desc(UserReferrals.id)) + .limit(1); + referrals = rows[0]; + } catch { + referrals = undefined; + } const total = referrals ? Number(referrals.referralsTotal) : 0; if (!referrals || total <= 0) { @@ -117,40 +130,48 @@ export async function claimReferral(_formData: FormData): Promise { } else { // Spend the threshold first so a concurrent double-submit can't claim // twice off the same balance, then deliver the reward and log it. - await prisma.userReferrals.update({ - where: { id: referrals.id }, - data: { referralsTotal: { decrement: needed } }, - }); + await db + .update(UserReferrals) + .set({ + referralsTotal: sql`${UserReferrals.referralsTotal} - ${needed}`, + }) + .where(eq(UserReferrals.id, referrals.id)); try { - await sendCurrency({ rcon, db: prisma }, userId, currency, amount); + await sendCurrency( + { rcon, db: currencyDb }, + userId, + currency, + amount, + ); } catch { // sendCurrency already falls back to a direct DB write; if it still // throws the spend stands. Roll the threshold back so the user isn't // charged for an undelivered reward. - await prisma.userReferrals - .update({ - where: { id: referrals.id }, - data: { referralsTotal: { increment: needed } }, - }) - .catch(() => {}); + try { + await db + .update(UserReferrals) + .set({ + referralsTotal: sql`${UserReferrals.referralsTotal} + ${needed}`, + }) + .where(eq(UserReferrals.id, referrals.id)); + } catch { + /* best-effort rollback */ + } outcome = "error"; throw new Error("currency-delivery-failed"); } - await prisma.claimedReferralLogs - .create({ - data: { - userId, - ipAddress: await clientIp(), - createdAt: new Date(), - updatedAt: new Date(), - }, - }) - .catch(() => { - // Best-effort audit log; the reward already landed. + try { + await db.insert(ClaimedReferralLogs).values({ + userId, + ipAddress: await clientIp(), + createdAt: new Date(), + updatedAt: new Date(), }); - + } catch { + // Best-effort audit log; the reward already landed. + } outcome = "claimed"; } } diff --git a/src/lib/admin/ops-health.ts b/src/lib/admin/ops-health.ts index 3381ba68..c7b6f7ef 100644 --- a/src/lib/admin/ops-health.ts +++ b/src/lib/admin/ops-health.ts @@ -1,7 +1,8 @@ import "server-only"; +import { count, eq, sql } from "drizzle-orm"; import { env } from "@/env"; -import { prisma } from "@/lib/prisma"; +import { db, User } from "@/lib/db"; import { redis } from "@/lib/redis"; import { rcon } from "@/lib/services/rcon"; @@ -19,11 +20,11 @@ export type OpsHealth = { * Semantics match `/api/health` for Redis (null when not configured). */ export async function fetchOpsHealth(): Promise { - const [db, redisOk, emulatorOk, onlineUsers] = await Promise.all([ + const [dbProbe, redisOk, emulatorOk, onlineUsers] = await Promise.all([ (async () => { try { const start = Date.now(); - await prisma.$queryRaw`SELECT 1`; + await db.execute(sql`SELECT 1`); return { dbOk: true, dbLatencyMs: Date.now() - start }; } catch { return { dbOk: false, dbLatencyMs: 0 }; @@ -40,12 +41,16 @@ export async function fetchOpsHealth(): Promise { } })(), rcon.send("ping", null).catch(() => false), - prisma.user.count({ where: { online: "1" } }).catch(() => 0), + db + .select({ total: count() }) + .from(User) + .where(eq(User.online, "1")) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), ]); - return { - dbOk: db.dbOk, - dbLatencyMs: db.dbLatencyMs, + dbOk: dbProbe.dbOk, + dbLatencyMs: dbProbe.dbLatencyMs, redisOk, emulatorOk: Boolean(emulatorOk), onlineUsers, diff --git a/src/lib/admin/ops-online-users.ts b/src/lib/admin/ops-online-users.ts index 00dd793e..3b60597d 100644 --- a/src/lib/admin/ops-online-users.ts +++ b/src/lib/admin/ops-online-users.ts @@ -1,25 +1,39 @@ +import { count, desc, eq } from "drizzle-orm"; import type { OnlineUser } from "@/components/admin/dashboard"; -import { prisma } from "@/lib/prisma"; +import { db, User } from "@/lib/db"; /** Shared online roster used by CommandoCentrum (and linked from other ops hubs). */ export async function fetchOpsOnlineUsers( limit = 40, ): Promise<{ count: number; users: OnlineUser[] }> { - const [count, onlineUsersRaw] = await Promise.all([ - prisma.user.count({ where: { online: "1" } }).catch(() => 0), - prisma.user - .findMany({ - where: { online: "1" }, - select: { - id: true, - username: true, - look: true, - lastOnline: true, - }, - orderBy: { lastOnline: "desc" }, - take: limit, + const [countRow, onlineUsersRaw] = await Promise.all([ + db + .select({ total: count() }) + .from(User) + .where(eq(User.online, "1")) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), + db + .select({ + id: User.id, + username: User.username, + look: User.look, + lastOnline: User.lastOnline, }) - .catch(() => []), + .from(User) + .where(eq(User.online, "1")) + .orderBy(desc(User.lastOnline)) + .limit(limit) + .then((rows) => rows) + .catch( + () => + [] as Array<{ + id: number; + username: string; + look: string; + lastOnline: number; + }>, + ), ]); const users: OnlineUser[] = onlineUsersRaw.map((u) => ({ @@ -31,5 +45,5 @@ export async function fetchOpsOnlineUsers( lastActivity: u.lastOnline > 0 ? new Date(u.lastOnline * 1000) : new Date(), })); - return { count, users }; + return { count: countRow, users }; } diff --git a/src/lib/admin/ticket-queue-counts.ts b/src/lib/admin/ticket-queue-counts.ts index c831344c..dc2a37cd 100644 --- a/src/lib/admin/ticket-queue-counts.ts +++ b/src/lib/admin/ticket-queue-counts.ts @@ -1,6 +1,7 @@ import "server-only"; -import { prisma } from "@/lib/prisma"; +import { count, eq, ne } from "drizzle-orm"; +import { db, WebsiteHelpCenterTickets, WebsiteTicket } from "@/lib/db"; /** Open-queue sizes for dual ticket products (CMS desk vs help-center). */ export async function fetchTicketQueueOpenCounts(): Promise<{ @@ -8,11 +9,17 @@ export async function fetchTicketQueueOpenCounts(): Promise<{ helpOpen: number; }> { const [cmsOpen, helpOpen] = await Promise.all([ - prisma.websiteTicket - .count({ where: { status: { not: "closed" } } }) + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(ne(WebsiteTicket.status, "closed")) + .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), - prisma.websiteHelpCenterTickets - .count({ where: { open: true } }) + db + .select({ total: count() }) + .from(WebsiteHelpCenterTickets) + .where(eq(WebsiteHelpCenterTickets.open, true)) + .then((rows) => rows[0]?.total ?? 0) .catch(() => 0), ]); return { cmsOpen, helpOpen }; diff --git a/src/lib/services/alert.test.ts b/src/lib/services/alert.test.ts index 373be4a0..63e94055 100644 --- a/src/lib/services/alert.test.ts +++ b/src/lib/services/alert.test.ts @@ -1,14 +1,16 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const createFn = vi.hoisted(() => vi.fn()); -const sendMailFn = vi.hoisted(() => vi.fn().mockResolvedValue(true)); +const insertValues = vi.hoisted(() => vi.fn()); -vi.mock("@/lib/prisma", () => ({ - prisma: { alertLogs: { create: createFn } }, +vi.mock("@/lib/db", () => ({ + db: { + insert: vi.fn(() => ({ values: insertValues })), + }, + AlertLogs: {}, })); vi.mock("@/lib/services/email", () => ({ - sendMail: sendMailFn, + sendMail: vi.fn().mockResolvedValue(true), })); vi.mock("@/env", () => ({ @@ -21,12 +23,14 @@ vi.mock("@/env", () => ({ }, })); +import { sendMail } from "@/lib/services/email"; import { sendAlert } from "./alert"; beforeEach(() => { - createFn.mockReset(); - sendMailFn.mockReset(); - createFn.mockResolvedValue({ id: 1 }); + insertValues.mockReset(); + vi.mocked(sendMail).mockReset(); + vi.mocked(sendMail).mockResolvedValue(true); + insertValues.mockResolvedValue([{ insertId: 1 }]); }); describe("sendAlert", () => { @@ -36,14 +40,14 @@ describe("sendAlert", () => { severity: "warning", message: "test alert", }); - expect(createFn).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(insertValues).toHaveBeenCalledWith( + expect.objectContaining({ type: "test", severity: "warning", message: "test alert", isRead: false, }), - }); + ); }); it("returns logged=true when DB write succeeds", async () => { @@ -57,6 +61,6 @@ describe("sendAlert", () => { it("does not call sendMail when ALERT_EMAIL is unset", async () => { await sendAlert({ type: "test", severity: "error", message: "m" }); - expect(sendMailFn).not.toHaveBeenCalled(); + expect(sendMail).not.toHaveBeenCalled(); }); }); diff --git a/src/lib/services/alert.ts b/src/lib/services/alert.ts index d6172b11..405e2f2a 100644 --- a/src/lib/services/alert.ts +++ b/src/lib/services/alert.ts @@ -1,14 +1,14 @@ import { env } from "@/env"; +import { AlertLogs, db } from "@/lib/db"; import { logger } from "@/lib/logger"; -import { prisma } from "@/lib/prisma"; import { sendMail } from "@/lib/services/email"; // === Alert service (AtomCMS → Next.js) =========================================== // // A pure, dependency-free server module for raising operational alerts. A single -// sendAlert() call (a) persists a row in alert_logs (prisma.alertLogs delegate), -// (b) pushes a Discord embed to DISCORD_WEBHOOK_URL when set, and (c) optionally -// emails staff via sendMail() when ALERT_EMAIL is set. +// sendAlert() call (a) persists a row in alert_logs, (b) pushes a Discord embed +// to DISCORD_WEBHOOK_URL when set, and (c) optionally emails staff via sendMail() +// when ALERT_EMAIL is set. // // Every external side-effect is wrapped in try/catch so a failing webhook, dead // SMTP host, or unreachable DB never throws into the caller (which is usually a @@ -186,18 +186,17 @@ export async function sendAlert( let logged = false; try { - await prisma.alertLogs.create({ - data: { - type: input.type.slice(0, 255), - severity: String(input.severity).slice(0, 255), - message: input.message, - context: input.context ? (input.context as object) : undefined, - sentViaDiscord, - sentViaEmail, - isRead: false, - createdAt: new Date(), - updatedAt: new Date(), - }, + const now = new Date(); + await db.insert(AlertLogs).values({ + type: input.type.slice(0, 255), + severity: String(input.severity).slice(0, 255), + message: input.message, + context: input.context ? JSON.stringify(input.context) : null, + sentViaDiscord, + sentViaEmail, + isRead: false, + createdAt: now, + updatedAt: now, }); logged = true; } catch (e) { diff --git a/src/lib/services/catalog-audit.ts b/src/lib/services/catalog-audit.ts index a6a50f86..85b26c28 100644 --- a/src/lib/services/catalog-audit.ts +++ b/src/lib/services/catalog-audit.ts @@ -1,6 +1,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; -import { prisma } from "@/lib/prisma"; +import { sql } from "drizzle-orm"; +import { db } from "@/lib/db"; import { fetchSourceFurnidata } from "@/lib/services/clone-import"; import { listSources } from "@/lib/services/clone-sources"; import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs"; @@ -90,14 +91,18 @@ export async function runCatalogAudit( type: string; }>; try { - items = await prisma.$queryRaw< + const [rows] = (await db.execute(sql` + SELECT id, item_name, public_name, type FROM items_base ORDER BY id + `)) as unknown as [ Array<{ id: number; item_name: string; public_name: string; type: string; - }> - >`SELECT id, item_name, public_name, type FROM items_base ORDER BY id`; + }>, + unknown, + ]; + items = rows; } catch (err) { onEvent?.({ type: "error", @@ -116,9 +121,13 @@ export async function runCatalogAudit( catalog_name: string; }>; try { - catalogItems = await prisma.$queryRaw< - Array<{ id: number; item_ids: string; catalog_name: string }> - >`SELECT id, item_ids, catalog_name FROM catalog_items ORDER BY id`; + const [rows] = (await db.execute(sql` + SELECT id, item_ids, catalog_name FROM catalog_items ORDER BY id + `)) as unknown as [ + Array<{ id: number; item_ids: string; catalog_name: string }>, + unknown, + ]; + catalogItems = rows; } catch (err) { onEvent?.({ type: "error", diff --git a/src/lib/services/clone-import.test.ts b/src/lib/services/clone-import.test.ts index d7ec44de..0953ccf5 100644 --- a/src/lib/services/clone-import.test.ts +++ b/src/lib/services/clone-import.test.ts @@ -8,16 +8,16 @@ const { downloadFile, appendFurniEntry, parseNitroBundle, - execRaw, - queryRaw, + executeFn, + selectLimit, fsUnlink, fsReadFile, } = vi.hoisted(() => ({ downloadFile: vi.fn(async () => ({ ok: true, size: 200 })), appendFurniEntry: vi.fn(async () => {}), parseNitroBundle: vi.fn(() => ({ json: {}, png: Buffer.alloc(0) })), - execRaw: vi.fn(async () => 1), - queryRaw: vi.fn(async () => [] as unknown[]), + executeFn: vi.fn(async () => [[{ next: 1 }], []]), + selectLimit: vi.fn(async () => []), fsUnlink: vi.fn(async () => {}), fsReadFile: vi.fn(async () => Buffer.from("NITRO")), })); @@ -55,13 +55,18 @@ vi.mock("node:fs", async (orig) => { }, }; }); -vi.mock("@/lib/prisma", () => ({ - prisma: { - $executeRaw: execRaw as AnyFn, - $queryRaw: queryRaw as AnyFn, - $executeRawUnsafe: execRaw as AnyFn, - $queryRawUnsafe: queryRaw as AnyFn, +vi.mock("@/lib/db", () => ({ + db: { + execute: executeFn, + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: selectLimit, + })), + })), + })), }, + ItemsBase: { id: "id", itemName: "itemName" }, })); import { cloneSingleFurni, parseFurnidata } from "./clone-import"; @@ -100,11 +105,14 @@ describe("clone-import", () => { // Reset mocks to clear state from other tests. downloadFile.mockReset(); downloadFile.mockResolvedValue({ ok: true, size: 200 }); - execRaw.mockReset(); - execRaw.mockResolvedValue(1); - queryRaw.mockReset(); - // First call: dedup check (no existing row); second call: allocateItemsBaseId MAX(id)+1. - queryRaw.mockResolvedValueOnce([]).mockResolvedValueOnce([{ next: 42 }]); + executeFn.mockReset(); + // allocateItemsBaseId MAX(id)+1, then items_base INSERT, then catalog INSERT. + executeFn + .mockResolvedValueOnce([[{ next: 42 }], []]) + .mockResolvedValueOnce([[], []]) + .mockResolvedValueOnce([[], []]); + selectLimit.mockReset(); + selectLimit.mockResolvedValue([]); appendFurniEntry.mockReset(); appendFurniEntry.mockResolvedValue(undefined); @@ -124,12 +132,12 @@ describe("clone-import", () => { expect.objectContaining({ validate: "png" }), ); expect(appendFurniEntry).toHaveBeenCalled(); - expect(execRaw).toHaveBeenCalled(); // items_base insert + catalog + expect(executeFn).toHaveBeenCalled(); // items_base insert + catalog }); it("skips when classname already exists (dedup)", async () => { - queryRaw.mockReset(); - queryRaw.mockResolvedValueOnce([{ id: 5 }]); // existing items_base row + selectLimit.mockReset(); + selectLimit.mockResolvedValueOnce([{ id: 5 }]); // existing items_base row const r = await cloneSingleFurni({ source: SOURCE, entry: { ...ENTRY, itemType: "s" }, @@ -145,14 +153,14 @@ describe("clone-import", () => { fsUnlink.mockReset(); fsUnlink.mockResolvedValue(undefined); appendFurniEntry.mockReset(); - queryRaw.mockReset(); - execRaw.mockReset(); + selectLimit.mockReset(); + executeFn.mockReset(); // Dedup query: no existing row. - queryRaw.mockResolvedValueOnce([]); + selectLimit.mockResolvedValueOnce([]); // allocateItemsBaseId: MAX(id)+1 query returns next id. - queryRaw.mockResolvedValueOnce([{ next: 7 }]); + executeFn.mockResolvedValueOnce([[{ next: 7 }], []]); // items_base INSERT rejects. - execRaw.mockRejectedValueOnce(new Error("Duplicate entry")); + executeFn.mockRejectedValueOnce(new Error("Duplicate entry")); try { const r = await cloneSingleFurni({ diff --git a/src/lib/services/clone-import.ts b/src/lib/services/clone-import.ts index 1e21e762..b5aa73eb 100644 --- a/src/lib/services/clone-import.ts +++ b/src/lib/services/clone-import.ts @@ -1,7 +1,8 @@ import { promises as fs } from "node:fs"; import path from "node:path"; +import { eq, inArray, sql } from "drizzle-orm"; +import { db, ItemsBase } from "@/lib/db"; import { logger } from "@/lib/logger"; -import { prisma } from "@/lib/prisma"; import { extractFurniIconPng } from "@/lib/services/clone-icon"; import type { CloneSource } from "@/lib/services/clone-sources"; import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs"; @@ -118,9 +119,10 @@ async function allocateItemsBaseId( }); await prev.catch(() => {}); try { - const idRow = await prisma.$queryRaw>` - SELECT COALESCE(MAX(id), 0) + 1 AS next FROM items_base`; - const nextId = Number(idRow[0]?.next ?? 1); + const [idRows] = (await db.execute(sql` + SELECT COALESCE(MAX(id), 0) + 1 AS next FROM items_base + `)) as unknown as [Array<{ next: number }>, unknown]; + const nextId = Number(idRows[0]?.next ?? 1); return await insertFn(nextId); } finally { settle(); @@ -143,8 +145,11 @@ export async function cloneSingleFurni(params: { } // Dedup by classname. - const existing = await prisma.$queryRaw>` - SELECT id FROM items_base WHERE item_name = ${classname} LIMIT 1`; + const existing = await db + .select({ id: ItemsBase.id }) + .from(ItemsBase) + .where(eq(ItemsBase.itemName, classname)) + .limit(1); if (existing.length > 0) { return { ok: false, @@ -221,13 +226,13 @@ export async function cloneSingleFurni(params: { let newId: number; try { newId = await allocateItemsBaseId(async (nextId) => { - await prisma.$executeRaw` + await db.execute(sql` INSERT INTO items_base (id, sprite_id, public_name, item_name, type, width, length, stack_height, allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams) VALUES (${nextId}, ${nextId}, ${entry.name}, ${classname}, ${itemType}, ${entry.xdim}, ${entry.ydim}, ${stackHeight}, - ${allowStack}, ${entry.cansiton ? "1" : "0"}, ${entry.canlayon ? "1" : "0"}, ${entry.canstandon ? "1" : "0"}, 'default', ${entry.customparams})`; + ${allowStack}, ${entry.cansiton ? "1" : "0"}, ${entry.canlayon ? "1" : "0"}, ${entry.canstandon ? "1" : "0"}, 'default', ${entry.customparams})`); return nextId; }); } catch (err) { @@ -266,9 +271,9 @@ export async function cloneSingleFurni(params: { const pageId = await getOrCreateCategoryPage(classname, itemType); const price = autoPriceFurni(classname); await allocateCatalogItemId(async (nextCatalogId) => { - await prisma.$executeRaw` + await db.execute(sql` INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) - VALUES (${nextCatalogId}, ${String(pageId)}, ${String(newId)}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '')`; + VALUES (${nextCatalogId}, ${String(pageId)}, ${String(newId)}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '')`); return nextCatalogId; }); } catch (err) { @@ -308,12 +313,11 @@ export async function getCloneList(params: { const allNames = searched.map((e) => e.classname); const present = new Set(); if (allNames.length) { - const placeholders = allNames.map(() => "?").join(","); - const rows = await prisma.$queryRawUnsafe>( - `SELECT item_name FROM items_base WHERE item_name IN (${placeholders})`, - ...allNames, - ); - for (const r of rows) present.add(r.item_name); + const rows = await db + .select({ itemName: ItemsBase.itemName }) + .from(ItemsBase) + .where(inArray(ItemsBase.itemName, allNames)); + for (const r of rows) present.add(r.itemName); } const filtered = @@ -339,12 +343,11 @@ export async function getCloneStats( const classnames = all.map((e) => e.classname); const have = new Set(); if (classnames.length) { - const placeholders = classnames.map(() => "?").join(","); - const rows = await prisma.$queryRawUnsafe>( - `SELECT item_name FROM items_base WHERE item_name IN (${placeholders})`, - ...classnames, - ); - for (const r of rows) have.add(r.item_name); + const rows = await db + .select({ itemName: ItemsBase.itemName }) + .from(ItemsBase) + .where(inArray(ItemsBase.itemName, classnames)); + for (const r of rows) have.add(r.itemName); } const present = all.filter((e) => have.has(e.classname)).length; return { total: all.length, present, clonable: all.length - present }; @@ -361,12 +364,11 @@ export async function getClonableClassnames( const classnames = all.map((e) => e.classname); const have = new Set(); if (classnames.length) { - const placeholders = classnames.map(() => "?").join(","); - const rows = await prisma.$queryRawUnsafe>( - `SELECT item_name FROM items_base WHERE item_name IN (${placeholders})`, - ...classnames, - ); - for (const r of rows) have.add(r.item_name); + const rows = await db + .select({ itemName: ItemsBase.itemName }) + .from(ItemsBase) + .where(inArray(ItemsBase.itemName, classnames)); + for (const r of rows) have.add(r.itemName); } return all.filter((e) => !have.has(e.classname)).map((e) => e.classname); } diff --git a/src/lib/services/furni-import.test.ts b/src/lib/services/furni-import.test.ts index 43f8c95d..5958b12b 100644 --- a/src/lib/services/furni-import.test.ts +++ b/src/lib/services/furni-import.test.ts @@ -1,8 +1,10 @@ // @vitest-environment node import { describe, expect, it, vi } from "vitest"; -vi.mock("@/lib/prisma", () => ({ - prisma: {}, +vi.mock("@/lib/db", () => ({ + db: {}, + CatalogPages: {}, + ItemsBase: {}, })); vi.mock("@/lib/services/furni-data", () => ({ @@ -40,17 +42,11 @@ describe("buildItemsBaseEnrichmentUpdate", () => { interactionType: "default", }); - expect(update.sql).not.toContain("specialtype"); - expect(update.values).toEqual([ - 2, - 3, - 1.5, - "1", - "0", - "1", - "1", - "default", - 123, - ]); + const serialized = JSON.stringify(update); + expect(serialized).not.toContain("specialtype"); + expect(serialized).toContain("UPDATE items_base"); + expect(serialized).toContain("123"); + expect(serialized).toContain("1.5"); + expect(serialized).toContain("default"); }); }); diff --git a/src/lib/services/furni-import.ts b/src/lib/services/furni-import.ts index 10fbaf09..3216bdc3 100644 --- a/src/lib/services/furni-import.ts +++ b/src/lib/services/furni-import.ts @@ -1,7 +1,8 @@ import { existsSync, promises as fs } from "node:fs"; import path from "node:path"; +import { and, eq, type SQL, sql } from "drizzle-orm"; +import { CatalogPages, db, ItemsBase } from "@/lib/db"; import { logger } from "@/lib/logger"; -import { prisma } from "@/lib/prisma"; import { appendFurniEntry, buildFurniEntry, @@ -166,50 +167,39 @@ export function buildItemsBaseEnrichmentUpdate(params: { canstandon: boolean; allowStack: string; interactionType: string; -}): { sql: string; values: Array } { - return { - sql: ` +}): SQL { + return sql` UPDATE items_base - SET width = ?, length = ?, stack_height = ?, - allow_sit = ?, - allow_lay = ?, - allow_walk = ?, - allow_stack = ?, - interaction_type = ? - WHERE id = ? - `, - values: [ - params.dims.x, - params.dims.y, - params.dims.z, - params.cansiton ? "1" : "0", - params.canlayon ? "1" : "0", - params.canstandon ? "1" : "0", - params.allowStack, - params.interactionType, - params.id, - ], - }; + SET width = ${params.dims.x}, length = ${params.dims.y}, stack_height = ${params.dims.z}, + allow_sit = ${params.cansiton ? "1" : "0"}, + allow_lay = ${params.canlayon ? "1" : "0"}, + allow_walk = ${params.canstandon ? "1" : "0"}, + allow_stack = ${params.allowStack}, + interaction_type = ${params.interactionType} + WHERE id = ${params.id} + `; } /** Get or create the root "Imported Furniture" parent page (no items directly on this page). */ export async function getOrCreateImportedParentPage(): Promise { - const existing = await prisma.catalogPages.findFirst({ - where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE }, - select: { id: true }, - }); + const [existing] = await db + .select({ id: CatalogPages.id }) + .from(CatalogPages) + .where(eq(CatalogPages.captionSave, IMPORTED_PAGE_CAPTION_SAVE)) + .limit(1); if (existing) return existing.id; // Use raw SQL to avoid ENUM mismatch (page_layout, visible, enabled) // All string columns must be '' (not NULL) or the Java emulator may skip the page - 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 (${IMPORTED_PAGE_CAPTION_SAVE}, ${IMPORTED_PAGE_CAPTION}, 'default_3x3', -1, '1', 9999, 263, '1', '1', '', '', '', '', '', '', '', '') - `; - const created = await prisma.catalogPages.findFirst({ - where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE }, - select: { id: true }, - }); + `); + const [created] = await db + .select({ id: CatalogPages.id }) + .from(CatalogPages) + .where(eq(CatalogPages.captionSave, IMPORTED_PAGE_CAPTION_SAVE)) + .limit(1); if (!created) { throw new Error( 'Failed to create "Imported Furniture" parent catalog page', @@ -281,7 +271,7 @@ export function autoPriceFurni(classname: string): { * Allocate the next `catalog_items.id` and run the INSERT through a * process-level async mutex so concurrent imports in the same Node process * cannot race on MAX(id). A DB-level lock would require pinning a single - * Prisma connection for the whole operation; the in-process mutex is enough + * connection for the whole operation; the in-process mutex is enough * for the single-server deployment and avoids pool-connection gymnastics. */ let catalogIdAllocChain: Promise = Promise.resolve(); @@ -296,11 +286,9 @@ export async function allocateCatalogItemId( }); await prev.catch(() => {}); try { - const maxIdResult = await prisma.$queryRaw< - Array<{ maxId: number | bigint | null }> - >` + const [maxIdResult] = (await db.execute(sql` SELECT MAX(id) as maxId FROM catalog_items - `; + `)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown]; const nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1; return await insertFn(nextId); } finally { @@ -318,21 +306,33 @@ export async function getOrCreateCategoryPage( const captionSave = `imp_${catKey}`; const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other; - const existing = await prisma.catalogPages.findFirst({ - where: { captionSave, parentId }, - select: { id: true }, - }); + const [existing] = await db + .select({ id: CatalogPages.id }) + .from(CatalogPages) + .where( + and( + eq(CatalogPages.captionSave, captionSave), + eq(CatalogPages.parentId, parentId), + ), + ) + .limit(1); if (existing) return existing.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); if (!created) { throw new Error( `Failed to create catalog sub-page "${captionSave}" under parent ${parentId}`, @@ -368,10 +368,11 @@ export async function importSingleFurni(params: { const warnings: string[] = []; // Check if already exists by classname OR by spriteId (primary key collision). - const existsByName = await prisma.itemsBase.findFirst({ - where: { itemName: classname }, - select: { id: true }, - }); + const [existsByName] = await db + .select({ id: ItemsBase.id }) + .from(ItemsBase) + .where(eq(ItemsBase.itemName, classname)) + .limit(1); if (existsByName) { return { ok: false, @@ -379,10 +380,11 @@ export async function importSingleFurni(params: { error: `Item "${classname}" already exists (id: ${existsByName.id})`, }; } - const existsById = await prisma.itemsBase.findUnique({ - where: { id: originalId }, - select: { itemName: true }, - }); + const [existsById] = await db + .select({ itemName: ItemsBase.itemName }) + .from(ItemsBase) + .where(eq(ItemsBase.id, originalId)) + .limit(1); if (existsById) { return { ok: false, @@ -417,10 +419,10 @@ export async function importSingleFurni(params: { // NOTE: live Arcturus `items_base` has no description/revision/rare columns — // those metadata fields are persisted in FurnitureData.json (see entry below), // not in the DB. Inserting them here throws "Unknown column 'description'". - await prisma.$executeRaw` + await db.execute(sql` INSERT INTO items_base (id, sprite_id, public_name, item_name, type) VALUES (${spriteId}, ${spriteId}, ${bestName}, ${classname}, ${itemType}) - `; + `); } catch (err) { return { ok: false, @@ -547,7 +549,9 @@ export async function importSingleFurni(params: { // ── Rollback: if conversion failed AND no .nitro exists, remove DB record ── if (!existsSync(/*turbopackIgnore: true*/ nitroPath) && !conversionResult) { try { - await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`; + await db.execute( + sql`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`, + ); } catch { /* best effort cleanup */ } @@ -635,7 +639,7 @@ export async function importSingleFurni(params: { allowStack, interactionType, }); - await prisma.$executeRawUnsafe(update.sql, ...update.values); + await db.execute(update); } catch (err) { warnings.push( `items_base dimension update failed: ${(err as Error).message}`, @@ -676,11 +680,9 @@ export async function importSingleFurni(params: { const itemIdStr = String(spriteId); // Check if this item already exists anywhere in the catalog - const existingCatalog = await prisma.$queryRaw< - Array<{ id: number; page_id: string }> - >` + const [existingCatalog] = (await db.execute(sql` SELECT id, page_id FROM catalog_items WHERE item_ids = ${itemIdStr} LIMIT 1 - `; + `)) as unknown as [Array<{ id: number; page_id: string }>, unknown]; if (existingCatalog.length > 0) { catalogItemId = existingCatalog[0].id; @@ -696,10 +698,10 @@ export async function importSingleFurni(params: { // across concurrent imports via a MySQL named lock to avoid MAX(id) // TOCTOU that would cause duplicate-key failures. catalogItemId = await allocateCatalogItemId(async (nextCatalogId) => { - await prisma.$executeRaw` + await db.execute(sql` INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) VALUES (${nextCatalogId}, ${pageIdStr}, ${itemIdStr}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '') - `; + `); return nextCatalogId; }); } @@ -734,10 +736,11 @@ export async function deleteImportedItem(classname: string): Promise<{ const errors: string[] = []; // Look up item in DB - const item = await prisma.itemsBase.findFirst({ - where: { itemName: classname }, - select: { id: true, type: true }, - }); + const [item] = await db + .select({ id: ItemsBase.id, type: ItemsBase.type }) + .from(ItemsBase) + .where(eq(ItemsBase.itemName, classname)) + .limit(1); if (!item) { throw new Error(`Item "${classname}" not found in database`); @@ -754,14 +757,18 @@ export async function deleteImportedItem(classname: string): Promise<{ // 1. Delete catalog_items try { const itemIdStr = String(spriteId); - await prisma.$executeRaw`DELETE FROM catalog_items WHERE item_ids = ${itemIdStr} AND catalog_name = ${classname}`; + await db.execute( + sql`DELETE FROM catalog_items WHERE item_ids = ${itemIdStr} AND catalog_name = ${classname}`, + ); } catch (err) { errors.push(`catalog_items: ${(err as Error).message}`); } // 2. Delete items_base try { - await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`; + await db.execute( + sql`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`, + ); } catch (err) { errors.push(`items_base: ${(err as Error).message}`); } diff --git a/src/lib/services/repair-icons.ts b/src/lib/services/repair-icons.ts index fc11f103..3b79cd67 100644 --- a/src/lib/services/repair-icons.ts +++ b/src/lib/services/repair-icons.ts @@ -1,6 +1,7 @@ import { existsSync, promises as fs } from "node:fs"; import path from "node:path"; -import { prisma } from "@/lib/prisma"; +import { sql } from "drizzle-orm"; +import { db } from "@/lib/db"; import { extractFurniIconPng } from "@/lib/services/clone-icon"; import { listSources } from "@/lib/services/clone-sources"; import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs"; @@ -54,9 +55,10 @@ export async function repairMissingIcons( let items: Array<{ id: number; item_name: string }>; try { - items = await prisma.$queryRaw< - Array<{ id: number; item_name: string }> - >`SELECT id, item_name FROM items_base ORDER BY id`; + const [rows] = (await db.execute(sql` + SELECT id, item_name FROM items_base ORDER BY id + `)) as unknown as [Array<{ id: number; item_name: string }>, unknown]; + items = rows; } catch (err) { onEvent?.({ type: "error", diff --git a/src/lib/services/upload-import.ts b/src/lib/services/upload-import.ts index 84d75618..4fad02f0 100644 --- a/src/lib/services/upload-import.ts +++ b/src/lib/services/upload-import.ts @@ -1,6 +1,7 @@ import { existsSync, promises as fs } from "node:fs"; import path from "node:path"; -import { prisma } from "@/lib/prisma"; +import { eq, sql } from "drizzle-orm"; +import { db, ItemsBase } from "@/lib/db"; import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs"; import { appendFurniEntry, buildFurniEntry } from "@/lib/services/furni-data"; import { @@ -52,9 +53,10 @@ async function allocateItemsBaseId( }); await prev.catch(() => {}); try { - const idRow = await prisma.$queryRaw>` - SELECT COALESCE(MAX(id), 0) + 1 AS next FROM items_base`; - const nextId = Number(idRow[0]?.next ?? 1); + const [idRows] = (await db.execute(sql` + SELECT COALESCE(MAX(id), 0) + 1 AS next FROM items_base + `)) as unknown as [Array<{ next: number }>, unknown]; + const nextId = Number(idRows[0]?.next ?? 1); return await insertFn(nextId); } finally { settle(); @@ -175,8 +177,11 @@ export async function uploadSingleFurni(params: { return { ok: false, classname, warnings, error: "invalid classname" }; } - const existing = await prisma.$queryRaw>` - SELECT id FROM items_base WHERE item_name = ${classname} LIMIT 1`; + const existing = await db + .select({ id: ItemsBase.id }) + .from(ItemsBase) + .where(eq(ItemsBase.itemName, classname)) + .limit(1); if (existing.length > 0) { return { ok: false, @@ -231,13 +236,13 @@ export async function uploadSingleFurni(params: { let newId: number; try { newId = await allocateItemsBaseId(async (nextId) => { - await prisma.$executeRaw` + await db.execute(sql` INSERT INTO items_base (id, sprite_id, public_name, item_name, type, width, length, stack_height, allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams) VALUES (${nextId}, ${nextId}, ${name}, ${classname}, ${itemType}, ${xdim}, ${ydim}, ${stackHeight}, - ${allowStack}, ${cansiton ? "1" : "0"}, ${canlayon ? "1" : "0"}, ${canstandon ? "1" : "0"}, ${userInteractionType}, ${customparams})`; + ${allowStack}, ${cansiton ? "1" : "0"}, ${canlayon ? "1" : "0"}, ${canstandon ? "1" : "0"}, ${userInteractionType}, ${customparams})`); return nextId; }); } catch (err) { @@ -272,9 +277,9 @@ export async function uploadSingleFurni(params: { try { const pageId = await getOrCreateCategoryPage(classname, itemType); catalogItemId = await allocateCatalogItemId(async (nextCatalogId) => { - await prisma.$executeRaw` + await db.execute(sql` INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) - VALUES (${nextCatalogId}, ${String(pageId)}, ${String(newId)}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '')`; + VALUES (${nextCatalogId}, ${String(pageId)}, ${String(newId)}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '')`); return nextCatalogId; }); } catch (err) { diff --git a/src/lib/services/watch.ts b/src/lib/services/watch.ts index 9fb4ef6f..51cc1656 100644 --- a/src/lib/services/watch.ts +++ b/src/lib/services/watch.ts @@ -1,5 +1,6 @@ +import { eq } from "drizzle-orm"; import { unstable_cache } from "next/cache"; -import { prisma } from "@/lib/prisma"; +import { db, UserWatch } from "@/lib/db"; /** * Returns the set of user IDs that the given staff member has marked as @@ -12,10 +13,10 @@ export async function getWatchedUserIds(staffId: number): Promise> { const getCachedWatchedIds = unstable_cache( async (staffId: number): Promise => { - const rows = await prisma.userWatch.findMany({ - where: { staffId }, - select: { targetUserId: true }, - }); + const rows = await db + .select({ targetUserId: UserWatch.targetUserId }) + .from(UserWatch) + .where(eq(UserWatch.staffId, staffId)); return rows.map((r) => r.targetUserId); }, ["user-watch-list"],