feat(cms): add recovery history and operational reliability tools
This commit is contained in:
1 parent
89526af344
commit
1ef405be0a
92 files changed
+6384
-267
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { AdminAuditLog, db } from "@/lib/db";
|
||||
import {
|
||||
readFurniData,
|
||||
withFurniDataLock,
|
||||
@@ -784,3 +785,89 @@ export async function repairFurniData(
|
||||
return { added: added.length, removedDuplicates, removedIdConflicts };
|
||||
});
|
||||
}
|
||||
|
||||
export interface CatalogParentRepairPreview {
|
||||
fingerprint: string;
|
||||
catalog: "normal" | "bc";
|
||||
changes: import("./catalog-integrity").ParentRepair[];
|
||||
}
|
||||
|
||||
function parentStateFingerprint(
|
||||
catalog: "normal" | "bc",
|
||||
pages: import("./catalog-integrity").IntegrityPage[],
|
||||
) {
|
||||
return createHash("sha256")
|
||||
.update(JSON.stringify({ catalog, pages }))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
export async function previewCatalogParentRepair(
|
||||
catalog: "normal" | "bc",
|
||||
): Promise<CatalogParentRepairPreview> {
|
||||
const { integrityCatalog, planParentRepairs } = await import(
|
||||
"./catalog-integrity"
|
||||
);
|
||||
integrityCatalog(catalog);
|
||||
const table = sql.raw(
|
||||
catalog === "bc" ? "catalog_pages_bc" : "catalog_pages",
|
||||
);
|
||||
const [rows] = await db.execute(
|
||||
sql`SELECT id, parent_id AS parentId, caption FROM ${table} ORDER BY id`,
|
||||
);
|
||||
const pages =
|
||||
rows as unknown as import("./catalog-integrity").IntegrityPage[];
|
||||
return {
|
||||
catalog,
|
||||
fingerprint: parentStateFingerprint(catalog, pages),
|
||||
changes: planParentRepairs(pages),
|
||||
};
|
||||
}
|
||||
|
||||
/** Locks and revalidates the complete parent graph before making the previewed changes. */
|
||||
export async function applyCatalogParentRepair(
|
||||
catalog: "normal" | "bc",
|
||||
fingerprint: string,
|
||||
userId: number,
|
||||
): Promise<{ applied: number }> {
|
||||
const { integrityCatalog, planParentRepairs } = await import(
|
||||
"./catalog-integrity"
|
||||
);
|
||||
integrityCatalog(catalog);
|
||||
if (!/^[a-f0-9]{64}$/.test(fingerprint))
|
||||
throw new Error("Invalid repair preview");
|
||||
const tableName = catalog === "bc" ? "catalog_pages_bc" : "catalog_pages";
|
||||
const table = sql.raw(tableName);
|
||||
return db.transaction(async (tx) => {
|
||||
const [rows] = await tx.execute(
|
||||
sql`SELECT id, parent_id AS parentId, caption FROM ${table} ORDER BY id FOR UPDATE`,
|
||||
);
|
||||
const pages =
|
||||
rows as unknown as import("./catalog-integrity").IntegrityPage[];
|
||||
if (parentStateFingerprint(catalog, pages) !== fingerprint) {
|
||||
const error = new Error("Catalog changed since preview");
|
||||
error.name = "CatalogPreviewConflict";
|
||||
throw error;
|
||||
}
|
||||
const changes = planParentRepairs(pages);
|
||||
for (const change of changes) {
|
||||
await tx.execute(
|
||||
sql`UPDATE ${table} SET parent_id = ${change.to} WHERE id = ${change.id} AND parent_id = ${change.from}`,
|
||||
);
|
||||
await tx
|
||||
.insert(AdminAuditLog)
|
||||
.values({
|
||||
userId,
|
||||
action: "catalog_parent_repair",
|
||||
target: tableName,
|
||||
targetId: change.id,
|
||||
before: JSON.stringify({ parentId: change.from }),
|
||||
after: JSON.stringify({ parentId: change.to }),
|
||||
diff: JSON.stringify({
|
||||
parentId: { from: change.from, to: change.to },
|
||||
}),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
return { applied: changes.length };
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user