feat(catalog): undo bulk offer edits with guarded history restoration
This commit is contained in:
1 parent
13bd3695cb
commit
c5a2b44807
33 files changed
+1399
-116
No files matched your search
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user