feat(catalog): undo bulk offer edits with guarded history restoration

This commit is contained in:
Simo committed 2026-09-11 10:47:46 +02:00
1 parent 13bd3695cb
commit c5a2b44807
33 files changed
+1399 -116

No files matched your search

+36 -2
View File
@@ -10,7 +10,12 @@ import {
import type { db } from "@/lib/db";
import { getOperationContext } from "@/lib/foundation/request-context";
import { type HistorySnapshot, sameSnapshot } from "./model";
export type HistoryKind = "category" | "category_bc" | "prices" | "news";
export type HistoryKind =
| "category"
| "category_bc"
| "prices"
| "catalog_offer"
| "news";
export type HistoryTransaction = Parameters<
Parameters<typeof db.transaction>[0]
>[0];
@@ -35,6 +40,10 @@ const categoryFields = [
"includes",
];
const configs = {
catalog_offer: {
table: CatalogItems,
fields: ["pageId", "costCredits", "costPoints", "pointsType"],
},
category: { table: CatalogPages, fields: categoryFields },
category_bc: { table: CatalogPagesBc, fields: categoryFields },
prices: {
@@ -113,7 +122,7 @@ export async function recordHistory(
Buffer.byteLength(serializedAfter, "utf8") > 16_000_000
)
throw Error("History snapshot is too large");
await tx.insert(AdminAuditLog).values({
const inserted = await tx.insert(AdminAuditLog).values({
userId,
action,
target: kind,
@@ -126,6 +135,7 @@ export async function recordHistory(
after: serializedAfter,
createdAt: new Date().toISOString(),
});
return inserted?.[0]?.insertId;
}
export async function readHistory(tx: HistoryTransaction, id: number) {
const [entry] = await tx
@@ -176,6 +186,7 @@ export async function applyHistory(
entry: Awaited<ReturnType<typeof readHistory>>,
userId: number,
) {
if (entry.kind === "catalog_offer") await lockOfferHistoryPages(tx, [entry]);
const current = await historySnapshot(tx, entry.kind, entry.targetId);
if (!sameSnapshot(current, entry.after)) throw Error("conflict");
const { table } = historyConfig(entry.kind);
@@ -197,3 +208,26 @@ export async function applyHistory(
"history_restore",
);
}
export async function lockOfferHistoryPages(
tx: HistoryTransaction,
entries: Array<Awaited<ReturnType<typeof readHistory>>>,
) {
const ids = [
...new Set(
entries.flatMap((entry) => [
Number(entry.before.pageId),
Number(entry.after.pageId),
]),
),
].sort((a, b) => a - b);
if (ids.some((id) => !Number.isSafeInteger(id) || id < 1))
throw Error("unavailable");
const [rows] = await tx.execute(
sql`SELECT id FROM catalog_pages WHERE id IN (${sql.join(ids, sql`, `)}) ORDER BY id FOR UPDATE`,
);
const found = new Set(
(rows as unknown as Array<{ id: number }>).map((row) => Number(row.id)),
);
if (ids.some((id) => !found.has(id))) throw Error("unavailable");
}