refactor(catalog): unify commands and embed guarded catalog workspace
CI / check (push) Successful in 1m16s
CI / deploy (push) Successful in 28s

This commit is contained in:
Simo committed 2026-09-06 19:40:52 +02:00
1 parent 16f35568f5
commit 193b6686a9
57 files changed
+3268 -728

No files matched your search

+60 -118
View File
@@ -1,12 +1,24 @@
"use server";
import { eq, inArray } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { catalogFailure } from "@/features/catalog/server/errors";
import {
createBcOfferCommand,
updateBcOfferCommand,
} from "@/features/catalog/server/offer-commands";
import {
createPageCommand,
deletePageCommand,
reorderPagesCommand,
togglePageCommand,
updatePageCommand,
} from "@/features/catalog/server/page-commands";
import { sendCatalogUpdate } from "@/features/catalog/server/sync-status";
import { requirePermission } from "@/lib/admin/guard";
import { CatalogItemsBc, CatalogPagesBc, db } from "@/lib/db";
import { CatalogItemsBc, db } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
const BC_PAGE_FIELDS = [
@@ -50,19 +62,24 @@ function pickAllowed(
export async function updateBcPage({
id,
expected,
...fields
}: { id: number } & Record<string, unknown>) {
}: { id: number; expected?: Record<string, unknown> } & Record<
string,
unknown
>) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
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 db
.update(CatalogPagesBc)
.set(data as Partial<typeof CatalogPagesBc.$inferInsert>)
.where(eq(CatalogPagesBc.id, id));
await rcon.updateCatalog();
try {
await updatePageCommand("bc", id, data, expected);
} catch (error) {
return { ok: false as const, error: catalogFailure(error).message };
}
await sendCatalogUpdate();
await logStaffActivity({
staffId: staff.id,
action: "bc_page_update",
@@ -79,7 +96,7 @@ export async function deleteBcItem({ id }: { id: number }) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
await db.delete(CatalogItemsBc).where(eq(CatalogItemsBc.id, id));
await rcon.updateCatalog();
await sendCatalogUpdate();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_delete",
@@ -108,11 +125,12 @@ export async function updateBcItem({
if (Object.keys(safe).length === 0) {
return { ok: false as const, error: "No valid fields to update" };
}
await db
.update(CatalogItemsBc)
.set(safe as Partial<typeof CatalogItemsBc.$inferInsert>)
.where(eq(CatalogItemsBc.id, id));
await rcon.updateCatalog();
try {
await updateBcOfferCommand(id, safe);
} catch (error) {
return { ok: false as const, error: catalogFailure(error).message };
}
await sendCatalogUpdate();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_update",
@@ -137,11 +155,8 @@ export async function createBcItem({
}) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
const [result] = await db
.insert(CatalogItemsBc)
.values({ pageId, ...data });
const createdId = Number(result.insertId);
await rcon.updateCatalog();
const createdId = await createBcOfferCommand({ pageId, ...data });
await sendCatalogUpdate();
await logStaffActivity({
staffId: staff.id,
action: "bc_item_create",
@@ -163,20 +178,8 @@ export async function toggleBcPage({
}) {
await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
const [page] = await db
.select({
enabled: CatalogPagesBc.enabled,
visible: CatalogPagesBc.visible,
})
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, id))
.limit(1);
if (!page) return { ok: false as const, error: "Page not found" };
await db
.update(CatalogPagesBc)
.set({ [field]: page[field] === "1" ? "0" : "1" })
.where(eq(CatalogPagesBc.id, id));
await rcon.updateCatalog();
await togglePageCommand("bc", id, field);
await sendCatalogUpdate();
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const };
});
@@ -194,7 +197,7 @@ export async function createBcPage(input: {
}) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
const [result] = await db.insert(CatalogPagesBc).values({
const createdId = await createPageCommand("bc", {
caption: input.caption,
parentId: input.parentId,
pageLayout: input.pageLayout ?? "default_3x3",
@@ -206,8 +209,7 @@ export async function createBcPage(input: {
pageHeadline: "",
pageTeaser: "",
});
const createdId = Number(result.insertId);
await rcon.updateCatalog();
await sendCatalogUpdate();
await logStaffActivity({
staffId: staff.id,
action: "bc_page_create",
@@ -221,28 +223,6 @@ export async function createBcPage(input: {
});
}
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 db
.select({ parentId: CatalogPagesBc.parentId })
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, currentId))
.limit(1);
if (!parent || parent.parentId <= 0) break;
currentId = parent.parentId;
}
}
await db
.update(CatalogPagesBc)
.set({ parentId: newParentId })
.where(eq(CatalogPagesBc.id, pageId));
}
export async function reorderBcTreePage(input: {
pageId: number;
newParentId?: number;
@@ -250,21 +230,11 @@ export async function reorderBcTreePage(input: {
}) {
await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
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 db
.update(CatalogPagesBc)
.set({ orderNum: input.newOrderNum })
.where(eq(CatalogPagesBc.id, input.pageId));
await rcon.updateCatalog();
await updatePageCommand("bc", input.pageId, {
parentId: input.newParentId,
orderNum: input.newOrderNum,
});
await sendCatalogUpdate();
revalidatePath("/admin/catalog");
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: {} };
@@ -277,51 +247,23 @@ export async function deleteBcTreePage(input: {
}) {
await requirePermission(PERMS.CATALOG_EDIT);
return await withCatalogExport(async () => {
const [page] = await db
.select({ parentId: CatalogPagesBc.parentId })
.from(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, input.pageId))
.limit(1);
if (!page) return { ok: false as const, error: "Page not found" };
await deletePageCommand("bc", input.pageId, input.mode);
await sendCatalogUpdate();
revalidatePath("/admin/catalog");
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: {} };
});
}
if (input.mode === "reparent") {
await db.transaction(async (tx) => {
await tx
.update(CatalogPagesBc)
.set({ parentId: page.parentId })
.where(eq(CatalogPagesBc.parentId, input.pageId));
await tx
.delete(CatalogItemsBc)
.where(eq(CatalogItemsBc.pageId, input.pageId));
await tx
.delete(CatalogPagesBc)
.where(eq(CatalogPagesBc.id, input.pageId));
});
} else {
const toDelete: number[] = [input.pageId];
const queue: number[] = [input.pageId];
while (queue.length > 0) {
const children = await db
.select({ id: CatalogPagesBc.id })
.from(CatalogPagesBc)
.where(inArray(CatalogPagesBc.parentId, queue));
queue.length = 0;
for (const child of children) {
toDelete.push(child.id);
queue.push(child.id);
}
}
await db.transaction(async (tx) => {
await tx
.delete(CatalogItemsBc)
.where(inArray(CatalogItemsBc.pageId, toDelete));
await tx
.delete(CatalogPagesBc)
.where(inArray(CatalogPagesBc.id, toDelete));
});
}
await rcon.updateCatalog();
export async function reorderBcCatalogPages(input: {
parentId: number;
ids: number[];
expectedIds: number[];
}) {
await requirePermission(PERMS.CATALOG_EDIT);
return withCatalogExport(async () => {
await reorderPagesCommand("bc", input);
await sendCatalogUpdate();
revalidatePath("/admin/catalog");
revalidatePath("/admin/catalog/builder-club");
return { ok: true as const, data: {} };