- 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.
302 lines
7.6 KiB
TypeScript
302 lines
7.6 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { requirePermission } from "@/lib/admin/guard";
|
|
import { PERMS } from "@/lib/permissions";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { rcon } from "@/lib/services/rcon";
|
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
|
|
|
const BC_PAGE_FIELDS = [
|
|
"caption",
|
|
"parentId",
|
|
"pageLayout",
|
|
"enabled",
|
|
"visible",
|
|
"orderNum",
|
|
"iconImage",
|
|
"iconColor",
|
|
"pageHeadline",
|
|
"pageTeaser",
|
|
"pageSpecial",
|
|
"pageText1",
|
|
"pageText2",
|
|
"pageTextDetails",
|
|
"pageTextTeaser",
|
|
] as const;
|
|
|
|
const BC_ITEM_FIELDS = [
|
|
"itemIds",
|
|
"catalogName",
|
|
"orderNumber",
|
|
"extradata",
|
|
"pageId",
|
|
] as const;
|
|
|
|
function pickAllowed(
|
|
fields: Record<string, unknown>,
|
|
allowed: readonly string[],
|
|
) {
|
|
const out: Record<string, unknown> = {};
|
|
for (const key of allowed) {
|
|
if (Object.hasOwn(fields, key) && fields[key] !== undefined) {
|
|
out[key] = fields[key];
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function updateBcPage({
|
|
id,
|
|
...fields
|
|
}: { id: number } & Record<string, unknown>) {
|
|
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
|
const data = pickAllowed(fields, BC_PAGE_FIELDS);
|
|
if (Object.keys(data).length === 0) {
|
|
return { ok: false as const, error: "No valid fields to update" };
|
|
}
|
|
await prisma.catalogPagesBc.update({
|
|
where: { id },
|
|
data: data as any,
|
|
});
|
|
await rcon.updateCatalog();
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "bc_page_update",
|
|
description: `Updated BC catalog page #${id}`,
|
|
targetType: "catalog_page_bc",
|
|
targetId: id,
|
|
});
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export async function deleteBcItem({ id }: { id: number }) {
|
|
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
|
await prisma.catalogItemsBc.delete({ where: { id } });
|
|
await rcon.updateCatalog();
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "bc_item_delete",
|
|
description: `Deleted BC catalog item #${id}`,
|
|
targetType: "catalog_item_bc",
|
|
targetId: id,
|
|
});
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export async function updateBcItem({
|
|
id,
|
|
...data
|
|
}: {
|
|
id: number;
|
|
itemIds?: string;
|
|
catalogName?: string;
|
|
orderNumber?: number;
|
|
extradata?: string;
|
|
}) {
|
|
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
|
const safe = pickAllowed(data as Record<string, unknown>, BC_ITEM_FIELDS);
|
|
if (Object.keys(safe).length === 0) {
|
|
return { ok: false as const, error: "No valid fields to update" };
|
|
}
|
|
await prisma.catalogItemsBc.update({
|
|
where: { id },
|
|
data: safe as any,
|
|
});
|
|
await rcon.updateCatalog();
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "bc_item_update",
|
|
description: `Updated BC catalog item #${id}`,
|
|
targetType: "catalog_item_bc",
|
|
targetId: id,
|
|
});
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export async function createBcItem({
|
|
pageId,
|
|
...data
|
|
}: {
|
|
pageId: number;
|
|
itemIds: string;
|
|
catalogName: string;
|
|
orderNumber: number;
|
|
extradata: string;
|
|
}) {
|
|
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
|
const created = await prisma.catalogItemsBc.create({
|
|
data: { pageId, ...data },
|
|
});
|
|
await rcon.updateCatalog();
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "bc_item_create",
|
|
description: `Created BC catalog item #${created.id}`,
|
|
targetType: "catalog_item_bc",
|
|
targetId: created.id,
|
|
});
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const, data: { id: created.id } };
|
|
}
|
|
|
|
export async function toggleBcPage({
|
|
id,
|
|
field,
|
|
}: {
|
|
id: number;
|
|
field: "enabled" | "visible";
|
|
}) {
|
|
await requirePermission(PERMS.CATALOG_EDIT);
|
|
const page = await prisma.catalogPagesBc.findUnique({
|
|
where: { id },
|
|
select: { enabled: true, visible: true },
|
|
});
|
|
if (!page) return { ok: false as const, error: "Page not found" };
|
|
await prisma.catalogPagesBc.update({
|
|
where: { id },
|
|
data: { [field]: page[field] === "1" ? "0" : "1" },
|
|
});
|
|
await rcon.updateCatalog();
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export async function createBcPage(input: {
|
|
caption: string;
|
|
parentId: number;
|
|
pageLayout?: string;
|
|
iconImage?: number;
|
|
iconColor?: number;
|
|
enabled?: "0" | "1";
|
|
visible?: "0" | "1";
|
|
orderNum?: number;
|
|
}) {
|
|
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
|
const created = await prisma.catalogPagesBc.create({
|
|
data: {
|
|
caption: input.caption,
|
|
parentId: input.parentId,
|
|
pageLayout: input.pageLayout ?? "default_3x3",
|
|
iconColor: input.iconColor ?? 0,
|
|
iconImage: input.iconImage ?? 0,
|
|
orderNum: input.orderNum ?? 0,
|
|
visible: input.visible ?? "1",
|
|
enabled: input.enabled ?? "1",
|
|
pageHeadline: "",
|
|
pageTeaser: "",
|
|
},
|
|
});
|
|
await rcon.updateCatalog();
|
|
await logStaffActivity({
|
|
staffId: staff.id,
|
|
action: "bc_page_create",
|
|
description: `Created BC catalog page "${input.caption}"`,
|
|
targetType: "catalog_page_bc",
|
|
targetId: created.id,
|
|
});
|
|
revalidatePath("/admin/catalog");
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const, data: { id: created.id } };
|
|
}
|
|
|
|
async function moveBcPage(pageId: number, newParentId: number): Promise<void> {
|
|
if (newParentId > 0) {
|
|
let currentId = newParentId;
|
|
for (let i = 0; i < 50; i++) {
|
|
if (currentId === pageId) {
|
|
throw new Error("Cannot move page: would create a circular hierarchy");
|
|
}
|
|
const parent = await prisma.catalogPagesBc.findUnique({
|
|
where: { id: currentId },
|
|
select: { parentId: true },
|
|
});
|
|
if (!parent || parent.parentId <= 0) break;
|
|
currentId = parent.parentId;
|
|
}
|
|
}
|
|
await prisma.catalogPagesBc.update({
|
|
where: { id: pageId },
|
|
data: { parentId: newParentId },
|
|
});
|
|
}
|
|
|
|
export async function reorderBcTreePage(input: {
|
|
pageId: number;
|
|
newParentId?: number;
|
|
newOrderNum: number;
|
|
}) {
|
|
await requirePermission(PERMS.CATALOG_EDIT);
|
|
if (input.newParentId !== undefined) {
|
|
try {
|
|
await moveBcPage(input.pageId, input.newParentId);
|
|
} catch (err) {
|
|
return {
|
|
ok: false as const,
|
|
error: err instanceof Error ? err.message : "Invalid move",
|
|
};
|
|
}
|
|
}
|
|
await prisma.catalogPagesBc.update({
|
|
where: { id: input.pageId },
|
|
data: { orderNum: input.newOrderNum },
|
|
});
|
|
await rcon.updateCatalog();
|
|
revalidatePath("/admin/catalog");
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const, data: {} };
|
|
}
|
|
|
|
export async function deleteBcTreePage(input: {
|
|
pageId: number;
|
|
mode: "reparent" | "cascade";
|
|
}) {
|
|
await requirePermission(PERMS.CATALOG_EDIT);
|
|
const page = await prisma.catalogPagesBc.findUnique({
|
|
where: { id: input.pageId },
|
|
select: { parentId: true },
|
|
});
|
|
if (!page) return { ok: false as const, error: "Page not found" };
|
|
|
|
if (input.mode === "reparent") {
|
|
await prisma.$transaction([
|
|
prisma.catalogPagesBc.updateMany({
|
|
where: { parentId: input.pageId },
|
|
data: { parentId: page.parentId },
|
|
}),
|
|
prisma.catalogItemsBc.deleteMany({ where: { pageId: input.pageId } }),
|
|
prisma.catalogPagesBc.delete({ where: { id: input.pageId } }),
|
|
]);
|
|
} else {
|
|
const toDelete: number[] = [input.pageId];
|
|
const queue: number[] = [input.pageId];
|
|
while (queue.length > 0) {
|
|
const children = await prisma.catalogPagesBc.findMany({
|
|
where: { parentId: { in: queue } },
|
|
select: { id: true },
|
|
});
|
|
queue.length = 0;
|
|
for (const child of children) {
|
|
toDelete.push(child.id);
|
|
queue.push(child.id);
|
|
}
|
|
}
|
|
await prisma.$transaction([
|
|
prisma.catalogItemsBc.deleteMany({
|
|
where: { pageId: { in: toDelete } },
|
|
}),
|
|
prisma.catalogPagesBc.deleteMany({
|
|
where: { id: { in: toDelete } },
|
|
}),
|
|
]);
|
|
}
|
|
|
|
await rcon.updateCatalog();
|
|
revalidatePath("/admin/catalog");
|
|
revalidatePath("/admin/catalog/builder-club");
|
|
return { ok: true as const, data: {} };
|
|
}
|