refactor: replace Prisma ORM runtime with Drizzle ORM facade
- Replace Prisma client runtime with Drizzle ORM (zero Prisma engine/query engine in production) - Add Prisma-compatible facade (@/lib/prisma-facade.ts) backed by Drizzle for backwards compatibility - Runtime queries route through Drizzle ORM; @prisma/client is now devDependency (types only) - Remove @prisma/adapter-mariadb dependency; delete prisma-pool.ts and types/prisma.ts - New Drizzle schema layer: src/db/schema.ts (176 tables) and src/lib/db.ts (connection) - Update README documenting the dual-layer ORM architecture - Restore src/generated/ gitignore (build artifact for local type generation) - 0 TypeScript errors, 583 tests passing The facade intentionally uses `any` types to match the Prisma Client API surface, allowing existing code to run unmodified while routing queries through Drizzle at runtime.
This commit is contained in:
1 parent
9d1c71d926
commit
56061e41d4
52 files changed
+5825
-748
No files matched your search
@@ -3,7 +3,7 @@ import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { CatalogTree } from "@/components/admin/catalog-tree";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { Prisma } from "@/lib/prisma";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { count, desc, eq, like } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { db, WebsiteEvent, WebsiteEventType } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { EventsTable } from "./events-table";
|
||||
|
||||
@@ -16,29 +18,44 @@ export default async function EventsPage({
|
||||
const result = await fetchAdminList(
|
||||
{
|
||||
permission: PERMS.EVENTS_VIEW,
|
||||
model: "websiteEvent",
|
||||
searchFields: ["title"],
|
||||
defaultSort: { startsAt: "desc" },
|
||||
include: { type: { select: { name: true, color: true } } },
|
||||
mapRow: (e: {
|
||||
id: number;
|
||||
title: string;
|
||||
type: { name: string; color: string };
|
||||
startsAt: Date;
|
||||
endsAt: Date | null;
|
||||
status: string;
|
||||
hostUserId: number;
|
||||
_count?: { registrations: number };
|
||||
}) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
typeName: e.type.name,
|
||||
typeColor: e.type.color,
|
||||
startsAt: e.startsAt,
|
||||
endsAt: e.endsAt,
|
||||
status: e.status,
|
||||
hostUserId: e.hostUserId,
|
||||
}),
|
||||
fetch: async ({ search, page, perPage }) => {
|
||||
const [rows, totals] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: WebsiteEvent.id,
|
||||
title: WebsiteEvent.title,
|
||||
startsAt: WebsiteEvent.startsAt,
|
||||
endsAt: WebsiteEvent.endsAt,
|
||||
status: WebsiteEvent.status,
|
||||
hostUserId: WebsiteEvent.hostUserId,
|
||||
typeName: WebsiteEventType.name,
|
||||
typeColor: WebsiteEventType.color,
|
||||
})
|
||||
.from(WebsiteEvent)
|
||||
.leftJoin(
|
||||
WebsiteEventType,
|
||||
eq(WebsiteEvent.typeId, WebsiteEventType.id),
|
||||
)
|
||||
.where(search ? like(WebsiteEvent.title, `%${search}%`) : undefined)
|
||||
.orderBy(desc(WebsiteEvent.startsAt))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(WebsiteEvent),
|
||||
]);
|
||||
return {
|
||||
rows: rows.map((e) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
typeName: e.typeName ?? "",
|
||||
typeColor: e.typeColor ?? "",
|
||||
startsAt: e.startsAt,
|
||||
endsAt: e.endsAt,
|
||||
status: e.status,
|
||||
hostUserId: e.hostUserId,
|
||||
})),
|
||||
total: totals[0]?.total ?? 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
paramsPromise,
|
||||
searchParams,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
@@ -25,7 +24,7 @@ export default async function AdminHelpTicketsPage({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status || "open";
|
||||
|
||||
const conditions: Prisma.WebsiteHelpCenterTicketsWhereInput[] = [];
|
||||
const conditions: any[] = [];
|
||||
|
||||
if (statusFilter === "open") {
|
||||
conditions.push({ open: true });
|
||||
@@ -56,7 +55,7 @@ export default async function AdminHelpTicketsPage({
|
||||
});
|
||||
}
|
||||
|
||||
const where: Prisma.WebsiteHelpCenterTicketsWhereInput =
|
||||
const where: any =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
@@ -65,7 +64,7 @@ export default async function AdminHelpTicketsPage({
|
||||
|
||||
const SORT_MAP: Record<
|
||||
string,
|
||||
Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput
|
||||
any
|
||||
> = {
|
||||
title: { title: "asc" },
|
||||
open: { open: "desc" },
|
||||
@@ -79,7 +78,7 @@ export default async function AdminHelpTicketsPage({
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput;
|
||||
} as any;
|
||||
|
||||
const [total, openCount, closedCount, queues] = await Promise.all([
|
||||
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { and, count, desc, eq, like, or } from "drizzle-orm";
|
||||
import { Package } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { db, ItemsBase } from "@/lib/db";
|
||||
import { canAccess, PERMS } from "@/lib/permissions";
|
||||
import { type ItemsBaseRow, ItemsTable } from "./items-table";
|
||||
|
||||
@@ -15,32 +17,56 @@ export default async function ItemsBasePage({
|
||||
const result = await fetchAdminList<ItemsBaseRow>(
|
||||
{
|
||||
permission: PERMS.CATALOG_VIEW,
|
||||
model: "itemsBase",
|
||||
searchFields: ["itemName", "publicName"],
|
||||
defaultSort: { id: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
spriteId: true,
|
||||
publicName: true,
|
||||
itemName: true,
|
||||
type: true,
|
||||
interactionType: true,
|
||||
width: true,
|
||||
length: true,
|
||||
fetch: async ({ search, page, perPage, rawParams }) => {
|
||||
const conditions = [];
|
||||
if (search) {
|
||||
const ors = [
|
||||
like(ItemsBase.itemName, `%${search}%`),
|
||||
like(ItemsBase.publicName, `%${search}%`),
|
||||
];
|
||||
const asId = Number(search);
|
||||
if (Number.isFinite(asId) && asId > 0) {
|
||||
ors.push(eq(ItemsBase.id, asId));
|
||||
}
|
||||
conditions.push(or(...ors));
|
||||
}
|
||||
if (rawParams.filter_type) {
|
||||
conditions.push(eq(ItemsBase.type, rawParams.filter_type));
|
||||
}
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
const [rows, totals] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: ItemsBase.id,
|
||||
spriteId: ItemsBase.spriteId,
|
||||
publicName: ItemsBase.publicName,
|
||||
itemName: ItemsBase.itemName,
|
||||
type: ItemsBase.type,
|
||||
interactionType: ItemsBase.interactionType,
|
||||
width: ItemsBase.width,
|
||||
length: ItemsBase.length,
|
||||
})
|
||||
.from(ItemsBase)
|
||||
.where(where)
|
||||
.orderBy(desc(ItemsBase.id))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(ItemsBase).where(where),
|
||||
]);
|
||||
return {
|
||||
rows: rows.map((r) => ({
|
||||
id: r.id,
|
||||
spriteId: r.spriteId,
|
||||
publicName: r.publicName || "",
|
||||
itemName: r.itemName || "",
|
||||
type: r.type || "s",
|
||||
interactionType: r.interactionType || "default",
|
||||
width: r.width,
|
||||
length: r.length,
|
||||
})),
|
||||
total: totals[0]?.total ?? 0,
|
||||
};
|
||||
},
|
||||
filterMap: {
|
||||
filter_type: (v) => ({ type: v }),
|
||||
},
|
||||
mapRow: (r) => ({
|
||||
id: r.id as number,
|
||||
spriteId: r.spriteId as number,
|
||||
publicName: (r.publicName as string) || "",
|
||||
itemName: (r.itemName as string) || "",
|
||||
type: (r.type as string) || "s",
|
||||
interactionType: (r.interactionType as string) || "default",
|
||||
width: r.width as number,
|
||||
length: r.length as number,
|
||||
}),
|
||||
},
|
||||
paramsPromise,
|
||||
searchParams,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
@@ -24,7 +23,7 @@ export default async function CfhListPage({
|
||||
const stateFilter =
|
||||
stateRaw !== undefined && stateRaw !== "" ? Number(stateRaw) : -1;
|
||||
|
||||
const conditions: Prisma.SupportTicketsWhereInput[] = [];
|
||||
const conditions: any[] = [];
|
||||
if (stateFilter >= 0) {
|
||||
conditions.push({ state: stateFilter });
|
||||
}
|
||||
@@ -59,7 +58,7 @@ export default async function CfhListPage({
|
||||
});
|
||||
}
|
||||
|
||||
const where: Prisma.SupportTicketsWhereInput =
|
||||
const where: any =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
@@ -68,7 +67,7 @@ export default async function CfhListPage({
|
||||
|
||||
const SORT_MAP: Record<
|
||||
string,
|
||||
Prisma.SupportTicketsOrderByWithRelationInput
|
||||
any
|
||||
> = {
|
||||
id: { id: "desc" },
|
||||
issue: { issue: "asc" },
|
||||
@@ -82,7 +81,7 @@ export default async function CfhListPage({
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as Prisma.SupportTicketsOrderByWithRelationInput;
|
||||
} as any;
|
||||
|
||||
const total = await prisma.supportTickets.count({ where }).catch(() => 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { and, count, desc, eq, like, or, type SQL } from "drizzle-orm";
|
||||
import { Wifi } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { OnlineTable } from "./online-table";
|
||||
|
||||
@@ -15,15 +17,34 @@ export default async function OnlineUsersPage({
|
||||
const result = await fetchAdminList(
|
||||
{
|
||||
permission: PERMS.USERS_VIEW,
|
||||
model: "user",
|
||||
searchFields: ["username", "motto"],
|
||||
baseWhere: { online: "1" },
|
||||
defaultPerPage: 50,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
motto: true,
|
||||
look: true,
|
||||
fetch: async ({ search, page, perPage }) => {
|
||||
const conditions: (SQL | undefined)[] = [eq(User.online, "1")];
|
||||
if (search) {
|
||||
conditions.push(
|
||||
or(
|
||||
like(User.username, `%${search}%`),
|
||||
like(User.motto, `%${search}%`),
|
||||
),
|
||||
);
|
||||
}
|
||||
const where = and(...conditions);
|
||||
const [rows, totals] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
motto: User.motto,
|
||||
look: User.look,
|
||||
})
|
||||
.from(User)
|
||||
.where(where)
|
||||
.orderBy(desc(User.id))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(User).where(where),
|
||||
]);
|
||||
return { rows, total: totals[0]?.total ?? 0 };
|
||||
},
|
||||
},
|
||||
paramsPromise,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { asc, count, eq } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||
import { AclModelPermission, AclPermission, AclRole, db, User } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { fetchEmulatorRankForEdit } from "@/lib/services/permission-ranks";
|
||||
import { RankEditClient } from "./rank-edit-client";
|
||||
|
||||
@@ -19,23 +20,32 @@ export default async function RankEditPage({
|
||||
const rankId = parseInt(rawId, 10);
|
||||
if (Number.isNaN(rankId)) notFound();
|
||||
|
||||
const rank = await fetchEmulatorRankForEdit(prisma, rankId);
|
||||
const rank = await fetchEmulatorRankForEdit(db, rankId);
|
||||
if (!rank) notFound();
|
||||
|
||||
// Fetch users with this rank (limit 200)
|
||||
const users = await prisma.user.findMany({
|
||||
where: { rank: rankId },
|
||||
select: { id: true, username: true, look: true },
|
||||
orderBy: { username: "asc" },
|
||||
take: 200,
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id, username: User.username, look: User.look })
|
||||
.from(User)
|
||||
.where(eq(User.rank, rankId))
|
||||
.orderBy(asc(User.username))
|
||||
.limit(200);
|
||||
|
||||
const totalUsers = await prisma.user.count({ where: { rank: rankId } });
|
||||
const [totalUsersRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(User)
|
||||
.where(eq(User.rank, rankId));
|
||||
const totalUsers = totalUsersRow?.total ?? 0;
|
||||
|
||||
// Fetch all CMS permissions
|
||||
const allCmsPermissions = await prisma.aclPermission.findMany({
|
||||
orderBy: { slug: "asc" },
|
||||
});
|
||||
const allCmsPermissions = await db
|
||||
.select({
|
||||
id: AclPermission.id,
|
||||
slug: AclPermission.slug,
|
||||
title: AclPermission.title,
|
||||
})
|
||||
.from(AclPermission)
|
||||
.orderBy(asc(AclPermission.slug));
|
||||
|
||||
// Auto-create CMS role for rank >= 4
|
||||
let cmsRole: {
|
||||
@@ -46,22 +56,37 @@ export default async function RankEditPage({
|
||||
} | null = null;
|
||||
|
||||
if (rankId >= 4) {
|
||||
const role = await prisma.aclRole.upsert({
|
||||
where: { slug: `rank_${rankId}` },
|
||||
create: {
|
||||
slug: `rank_${rankId}`,
|
||||
title: rank.rank_name || `Rank ${rankId}`,
|
||||
},
|
||||
update: {},
|
||||
include: {
|
||||
rolePermissions: { include: { permission: true } },
|
||||
},
|
||||
});
|
||||
const slug = `rank_${rankId}`;
|
||||
const title = rank.rank_name || `Rank ${rankId}`;
|
||||
const [existing] = await db
|
||||
.select({ id: AclRole.id })
|
||||
.from(AclRole)
|
||||
.where(eq(AclRole.slug, slug))
|
||||
.limit(1);
|
||||
|
||||
let roleId: number;
|
||||
if (existing) {
|
||||
roleId = existing.id;
|
||||
await db.update(AclRole).set({ title }).where(eq(AclRole.id, roleId));
|
||||
} else {
|
||||
const inserted = await db.insert(AclRole).values({ slug, title });
|
||||
roleId = inserted[0].insertId;
|
||||
}
|
||||
|
||||
const permissionRows = await db
|
||||
.select({ slug: AclPermission.slug })
|
||||
.from(AclModelPermission)
|
||||
.innerJoin(
|
||||
AclPermission,
|
||||
eq(AclModelPermission.permissionId, AclPermission.id),
|
||||
)
|
||||
.where(eq(AclModelPermission.modelId, roleId));
|
||||
|
||||
cmsRole = {
|
||||
id: role.id,
|
||||
slug: role.slug,
|
||||
title: role.title,
|
||||
permissionSlugs: role.rolePermissions.map((rp) => rp.permission.slug),
|
||||
id: roleId,
|
||||
slug,
|
||||
title,
|
||||
permissionSlugs: permissionRows.map((rp) => rp.slug),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { db } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { fetchEmulatorRankSummaries } from "@/lib/services/permission-ranks";
|
||||
import { PermissionsList } from "./permissions-list";
|
||||
|
||||
@@ -19,12 +19,12 @@ export default async function PermissionsPage({
|
||||
const sp = (await searchParams) ?? {};
|
||||
const fromHousekeeping = sp.from === "housekeeping";
|
||||
|
||||
const ranks = await fetchEmulatorRankSummaries(prisma);
|
||||
const ranks = await fetchEmulatorRankSummaries(db);
|
||||
|
||||
// User counts per rank
|
||||
const rawCounts: { rank: number; cnt: number }[] = await prisma.$queryRaw(
|
||||
Prisma.sql`SELECT \`rank\`, COUNT(*) as cnt FROM users GROUP BY \`rank\``,
|
||||
);
|
||||
const [rawCounts] = (await db.execute(
|
||||
sql`SELECT \`rank\`, COUNT(*) as cnt FROM users GROUP BY \`rank\``,
|
||||
)) as unknown as [{ rank: number; cnt: number }[], unknown[]];
|
||||
const countMap: Record<number, number> = {};
|
||||
for (const row of rawCounts) {
|
||||
countMap[Number(row.rank)] = Number(row.cnt);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { count, desc, like } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { db, WebsitePoll } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { PollsTable } from "./polls-table";
|
||||
|
||||
@@ -16,28 +18,31 @@ export default async function PollsPage({
|
||||
const result = await fetchAdminList(
|
||||
{
|
||||
permission: PERMS.POLLS_VIEW,
|
||||
model: "websitePoll",
|
||||
searchFields: ["title"],
|
||||
defaultSort: { createdAt: "desc" },
|
||||
mapRow: (p: {
|
||||
id: number;
|
||||
title: string;
|
||||
status: string;
|
||||
showResults: number;
|
||||
multipleChoice: number;
|
||||
startsAt: Date | null;
|
||||
endsAt: Date | null;
|
||||
createdAt: Date;
|
||||
}) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
status: p.status,
|
||||
showResults: p.showResults,
|
||||
multipleChoice: p.multipleChoice,
|
||||
startsAt: p.startsAt,
|
||||
endsAt: p.endsAt,
|
||||
createdAt: p.createdAt,
|
||||
}),
|
||||
fetch: async ({ search, page, perPage }) => {
|
||||
const where = search
|
||||
? like(WebsitePoll.title, `%${search}%`)
|
||||
: undefined;
|
||||
const [rows, totals] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: WebsitePoll.id,
|
||||
title: WebsitePoll.title,
|
||||
status: WebsitePoll.status,
|
||||
showResults: WebsitePoll.showResults,
|
||||
multipleChoice: WebsitePoll.multipleChoice,
|
||||
startsAt: WebsitePoll.startsAt,
|
||||
endsAt: WebsitePoll.endsAt,
|
||||
createdAt: WebsitePoll.createdAt,
|
||||
})
|
||||
.from(WebsitePoll)
|
||||
.where(where)
|
||||
.orderBy(desc(WebsitePoll.createdAt))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(WebsitePoll).where(where),
|
||||
]);
|
||||
return { rows, total: totals[0]?.total ?? 0 };
|
||||
},
|
||||
},
|
||||
paramsPromise,
|
||||
searchParams,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { and, count, desc, eq, like, or } from "drizzle-orm";
|
||||
import { DoorOpen } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { db, Rooms } from "@/lib/db";
|
||||
import { canAccess, PERMS } from "@/lib/permissions";
|
||||
import { RoomsTable } from "./rooms-table";
|
||||
|
||||
@@ -15,33 +17,56 @@ export default async function RoomsPage({
|
||||
const result = await fetchAdminList(
|
||||
{
|
||||
permission: PERMS.ROOMS_VIEW,
|
||||
// Prisma model is `Rooms` → client accessor `rooms`.
|
||||
// Owner is denormalized on the row (ownerId / ownerName), not a relation.
|
||||
model: "rooms",
|
||||
searchFields: ["name", "ownerName"],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
ownerId: true,
|
||||
ownerName: true,
|
||||
state: true,
|
||||
users: true,
|
||||
usersMax: true,
|
||||
description: true,
|
||||
fetch: async ({ search, page, perPage, rawParams }) => {
|
||||
const conditions = [];
|
||||
if (search) {
|
||||
const ors = [
|
||||
like(Rooms.name, `%${search}%`),
|
||||
like(Rooms.ownerName, `%${search}%`),
|
||||
];
|
||||
const asId = Number(search);
|
||||
if (Number.isFinite(asId) && asId > 0) {
|
||||
ors.push(eq(Rooms.id, asId));
|
||||
}
|
||||
conditions.push(or(...ors));
|
||||
}
|
||||
if (rawParams.filter_state) {
|
||||
conditions.push(eq(Rooms.state, rawParams.filter_state));
|
||||
}
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
const [rows, totals] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: Rooms.id,
|
||||
name: Rooms.name,
|
||||
ownerId: Rooms.ownerId,
|
||||
ownerName: Rooms.ownerName,
|
||||
state: Rooms.state,
|
||||
users: Rooms.users,
|
||||
usersMax: Rooms.usersMax,
|
||||
description: Rooms.description,
|
||||
})
|
||||
.from(Rooms)
|
||||
.where(where)
|
||||
.orderBy(desc(Rooms.id))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(Rooms).where(where),
|
||||
]);
|
||||
return {
|
||||
rows: rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
ownerName: r.ownerName || "—",
|
||||
ownerId: r.ownerId ?? 0,
|
||||
state: r.state,
|
||||
users: r.users,
|
||||
usersMax: r.usersMax,
|
||||
description: r.description || "",
|
||||
})),
|
||||
total: totals[0]?.total ?? 0,
|
||||
};
|
||||
},
|
||||
filterMap: {
|
||||
filter_state: (v) => ({ state: v }),
|
||||
},
|
||||
mapRow: (r) => ({
|
||||
id: r.id as number,
|
||||
name: r.name as string,
|
||||
ownerName: (r.ownerName as string) || "—",
|
||||
ownerId: (r.ownerId as number) ?? 0,
|
||||
state: r.state as string,
|
||||
users: r.users as number,
|
||||
usersMax: r.usersMax as number,
|
||||
description: (r.description as string) || "",
|
||||
}),
|
||||
},
|
||||
paramsPromise as Promise<{ locale: string }>,
|
||||
searchParams,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
@@ -25,7 +24,7 @@ export default async function AdminTicketsPage({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status || raw.status || "active";
|
||||
|
||||
const conditions: Prisma.WebsiteTicketWhereInput[] = [];
|
||||
const conditions: any[] = [];
|
||||
|
||||
if (statusFilter === "active") {
|
||||
conditions.push({ status: { not: "closed" } });
|
||||
@@ -47,14 +46,14 @@ export default async function AdminTicketsPage({
|
||||
});
|
||||
}
|
||||
|
||||
const where: Prisma.WebsiteTicketWhereInput =
|
||||
const where: any =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
|
||||
const SORT_MAP: Record<string, Prisma.WebsiteTicketOrderByWithRelationInput> =
|
||||
const SORT_MAP: Record<string, any> =
|
||||
{
|
||||
subject: { subject: "asc" },
|
||||
status: { status: "asc" },
|
||||
@@ -68,7 +67,7 @@ export default async function AdminTicketsPage({
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as Prisma.WebsiteTicketOrderByWithRelationInput;
|
||||
} as any;
|
||||
|
||||
const [total, openCount, inProgressCount, waitingCount, closedCount, queues] =
|
||||
await Promise.all([
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
@@ -36,7 +35,7 @@ function formatAmount(amount: number, currency: string): string {
|
||||
|
||||
const SORT_MAP: Record<
|
||||
string,
|
||||
Prisma.WebsitePaypalTransactionsOrderByWithRelationInput
|
||||
any
|
||||
> = {
|
||||
id: { id: "desc" },
|
||||
amount: { amount: "desc" },
|
||||
@@ -61,7 +60,7 @@ export default async function AdminTransactions({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status ?? "";
|
||||
|
||||
const where: Prisma.WebsitePaypalTransactionsWhereInput = {};
|
||||
const where: any = {};
|
||||
|
||||
if (statusFilter === "paid") {
|
||||
where.status = { in: [...PAID_STATUS] };
|
||||
@@ -113,7 +112,7 @@ export default async function AdminTransactions({
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as Prisma.WebsitePaypalTransactionsOrderByWithRelationInput;
|
||||
} as any;
|
||||
|
||||
let transactions: Awaited<
|
||||
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 type { Prisma } from "@/generated/prisma/client";
|
||||
import { calcPagination } from "@/lib/admin-helpers";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
@@ -30,21 +29,21 @@ 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: Prisma.EmulatorSettingsWhereInput[] =
|
||||
const patternFilters: any[] =
|
||||
TRANSLATION_KEY_PATTERNS.map((p) => ({
|
||||
key: { contains: p.replace(/%/g, "") },
|
||||
}));
|
||||
|
||||
const baseFilter: Prisma.EmulatorSettingsWhereInput = { OR: patternFilters };
|
||||
const baseFilter: any = { OR: patternFilters };
|
||||
|
||||
const conditions: Prisma.EmulatorSettingsWhereInput[] = [baseFilter];
|
||||
const conditions: any[] = [baseFilter];
|
||||
if (group) conditions.push({ key: { startsWith: `${group}.` } });
|
||||
if (search) {
|
||||
conditions.push({
|
||||
OR: [{ key: { contains: search } }, { value: { contains: search } }],
|
||||
});
|
||||
}
|
||||
const where: Prisma.EmulatorSettingsWhereInput = { AND: conditions };
|
||||
const where: any = { AND: conditions };
|
||||
|
||||
const [settings, total, allKeys] = await Promise.all([
|
||||
prisma.emulatorSettings.findMany({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { Prisma } from "@/lib/prisma";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getWatchedUserIds } from "@/lib/services/watch";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { and, count, desc, eq, gte, like, or } from "drizzle-orm";
|
||||
import { Users } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { fetchAdminList } from "@/lib/admin-list";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { canAccess, PERMS } from "@/lib/permissions";
|
||||
import { CreateUserDialog } from "./_components/create-user-dialog";
|
||||
import { UsersTable } from "./users-table";
|
||||
@@ -16,30 +18,54 @@ export default async function UsersPage({
|
||||
const result = await fetchAdminList(
|
||||
{
|
||||
permission: PERMS.USERS_VIEW,
|
||||
model: "user",
|
||||
searchFields: ["username", "mail", "ipRegister", "ipCurrent"],
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
mail: true,
|
||||
rank: true,
|
||||
credits: true,
|
||||
pixels: true,
|
||||
online: true,
|
||||
motto: true,
|
||||
look: true,
|
||||
ipRegister: true,
|
||||
accountCreated: true,
|
||||
lastLogin: true,
|
||||
},
|
||||
filterMap: {
|
||||
filter_rank: (v) => {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return null;
|
||||
fetch: async ({ search, page, perPage, rawParams }) => {
|
||||
const conditions = [];
|
||||
if (search) {
|
||||
const ors = [
|
||||
like(User.username, `%${search}%`),
|
||||
like(User.mail, `%${search}%`),
|
||||
like(User.ipRegister, `%${search}%`),
|
||||
like(User.ipCurrent, `%${search}%`),
|
||||
];
|
||||
const asId = Number(search);
|
||||
if (Number.isFinite(asId) && asId > 0) {
|
||||
ors.push(eq(User.id, asId));
|
||||
}
|
||||
conditions.push(or(...ors));
|
||||
}
|
||||
const rank = Number(rawParams.filter_rank);
|
||||
if (Number.isFinite(rank)) {
|
||||
// For rank 7 the preset means "super admins" — match ≥ 7 instead of equals.
|
||||
return n >= 7 ? { rank: { gte: 7 } } : { rank: n };
|
||||
},
|
||||
filter_online: (v) => ({ online: v }),
|
||||
conditions.push(rank >= 7 ? gte(User.rank, 7) : eq(User.rank, rank));
|
||||
}
|
||||
if (rawParams.filter_online) {
|
||||
conditions.push(eq(User.online, rawParams.filter_online));
|
||||
}
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
const [rows, totals] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
mail: User.mail,
|
||||
rank: User.rank,
|
||||
credits: User.credits,
|
||||
pixels: User.pixels,
|
||||
online: User.online,
|
||||
motto: User.motto,
|
||||
look: User.look,
|
||||
ipRegister: User.ipRegister,
|
||||
accountCreated: User.accountCreated,
|
||||
lastLogin: User.lastLogin,
|
||||
})
|
||||
.from(User)
|
||||
.where(where)
|
||||
.orderBy(desc(User.id))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(User).where(where),
|
||||
]);
|
||||
return { rows, total: totals[0]?.total ?? 0 };
|
||||
},
|
||||
},
|
||||
paramsPromise,
|
||||
|
||||
@@ -37,7 +37,7 @@ import type { DataTableColumn, PaginatedResult } from "@/types/common";
|
||||
interface UserRow {
|
||||
id: number;
|
||||
username: string;
|
||||
mail: string;
|
||||
mail: string | null;
|
||||
rank: number;
|
||||
credits: number;
|
||||
pixels: number;
|
||||
|
||||
Reference in new issue
Block a user