refactor(db): typed query helpers, shared test FormData helper
Replace raw db.execute tuple casts with queryRows/rowsFrom/execResult/ affectedRows helpers from lib/db, drop redundant mysql2 casts on typed query builders, and centralize per-test fakeForm into test/fake-form. Update db mocks in tests so helpers resolve against mocked execute.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { AdminAuditLog, db } from "@/lib/db";
|
||||
import { AdminAuditLog, db, execResult, queryRows, rowsFrom } from "@/lib/db";
|
||||
import {
|
||||
readFurniData,
|
||||
withFurniDataLock,
|
||||
@@ -329,26 +329,28 @@ export async function generateCatalogSql(
|
||||
let nextPageId = 0;
|
||||
if (organizeSql) {
|
||||
parentPageId = await getOrCreateImportedParentPage();
|
||||
const [maxPageResult] = (await db.execute(sql`
|
||||
const maxPageResult = await queryRows<{
|
||||
maxId: number | bigint | null;
|
||||
}>(sql`
|
||||
SELECT MAX(id) as maxId FROM catalog_pages
|
||||
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
|
||||
`);
|
||||
nextPageId = Number(maxPageResult[0]?.maxId ?? 0) + 1;
|
||||
}
|
||||
|
||||
// The generation pass does not INSERT, so MAX(id) would never advance and
|
||||
// per-entry allocateCatalogItemId would hand out the same id every time.
|
||||
// Read the current max once and hand out sequential ids from there.
|
||||
const [maxIdResult] = (await db.execute(sql`
|
||||
const maxIdResult = await queryRows<{ maxId: number | bigint | null }>(sql`
|
||||
SELECT MAX(id) as maxId FROM catalog_items
|
||||
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
|
||||
`);
|
||||
let nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
||||
|
||||
// Skip entries whose sprite id no longer exists in items_base (e.g. because
|
||||
// a duplicate-classname merge ran between the audit snapshot and this
|
||||
// generation). Inserting them would create orphaned catalog_items rows.
|
||||
const [validIds] = (await db.execute(sql`
|
||||
const validIds = await queryRows<{ id: number }>(sql`
|
||||
SELECT id FROM items_base
|
||||
`)) as unknown as [Array<{ id: number }>, unknown];
|
||||
`);
|
||||
const validItemIds = new Set(validIds.map((r) => Number(r.id)));
|
||||
let skipped = 0;
|
||||
|
||||
@@ -365,9 +367,9 @@ export async function generateCatalogSql(
|
||||
// Re-check against the live table: the audit snapshot may be stale and
|
||||
// an import may have created the entry in the meantime. Inserting would
|
||||
// create a duplicate catalog_items row for the same item_ids.
|
||||
const [existing] = (await db.execute(sql`
|
||||
const existing = await queryRows<{ id: number }>(sql`
|
||||
SELECT id FROM catalog_items WHERE item_ids = ${String(entry.itemId)} LIMIT 1
|
||||
`)) as unknown as [Array<{ id: number }>, unknown];
|
||||
`);
|
||||
if (existing.length > 0) {
|
||||
skipped++;
|
||||
continue;
|
||||
@@ -383,9 +385,9 @@ export async function generateCatalogSql(
|
||||
);
|
||||
let existing = pageMap.get(captionSave);
|
||||
if (!existing) {
|
||||
const [found] = (await db.execute(sql`
|
||||
const found = await queryRows<{ id: number }>(sql`
|
||||
SELECT id FROM catalog_pages WHERE caption_save = ${captionSave} AND parent_id = ${parentPageId}
|
||||
`)) as unknown as [Array<{ id: number }>, unknown];
|
||||
`);
|
||||
if (found?.[0]?.id) {
|
||||
pageMap.set(captionSave, found[0].id);
|
||||
} else {
|
||||
@@ -466,15 +468,15 @@ export interface DuplicateClassnameResult {
|
||||
* new orphans are left behind.
|
||||
*/
|
||||
export async function repairOrphanedCatalog(): Promise<OrphanedCatalogResult> {
|
||||
const [itemRows] = (await db.execute(sql`
|
||||
const itemRows = await queryRows<{ id: number }>(sql`
|
||||
SELECT id FROM items_base
|
||||
`)) as unknown as [Array<{ id: number }>, unknown];
|
||||
`);
|
||||
const valid = new Set<number>();
|
||||
for (const row of itemRows) valid.add(Number(row.id));
|
||||
|
||||
const [catalogRows] = (await db.execute(sql`
|
||||
const catalogRows = await queryRows<{ id: number; item_ids: string }>(sql`
|
||||
SELECT id, item_ids FROM catalog_items
|
||||
`)) as unknown as [Array<{ id: number; item_ids: string }>, unknown];
|
||||
`);
|
||||
|
||||
let removed = 0;
|
||||
let cleaned = 0;
|
||||
@@ -513,9 +515,9 @@ export async function repairOrphanedCatalog(): Promise<OrphanedCatalogResult> {
|
||||
* catalog_items when present, otherwise the lowest id.
|
||||
*/
|
||||
export async function repairDuplicateClassnames(): Promise<DuplicateClassnameResult> {
|
||||
const [itemRows] = (await db.execute(sql`
|
||||
const itemRows = await queryRows<{ id: number; item_name: string }>(sql`
|
||||
SELECT id, item_name FROM items_base
|
||||
`)) as unknown as [Array<{ id: number; item_name: string }>, unknown];
|
||||
`);
|
||||
|
||||
const byName = new Map<string, number[]>();
|
||||
for (const row of itemRows) {
|
||||
@@ -524,9 +526,9 @@ export async function repairDuplicateClassnames(): Promise<DuplicateClassnameRes
|
||||
byName.set(row.item_name, arr);
|
||||
}
|
||||
|
||||
const [catalogRows] = (await db.execute(sql`
|
||||
const catalogRows = await queryRows<{ item_ids: string }>(sql`
|
||||
SELECT item_ids FROM catalog_items
|
||||
`)) as unknown as [Array<{ item_ids: string }>, unknown];
|
||||
`);
|
||||
const referenced = new Set<number>();
|
||||
for (const row of catalogRows) {
|
||||
for (const part of (row.item_ids ?? "").split(";")) {
|
||||
@@ -558,9 +560,9 @@ export async function repairDuplicateClassnames(): Promise<DuplicateClassnameRes
|
||||
{ table: "logs_shop_purchases", idCol: "id" },
|
||||
];
|
||||
for (const { table, idCol } of listTables) {
|
||||
const [rows] = (await db.execute(
|
||||
const rows = await queryRows<{ pk: number; v: string | null }>(
|
||||
sql`SELECT ${sql.raw(quoteIdentifier(idCol))} AS pk, item_ids AS v FROM ${sql.raw(quoteIdentifier(table))}`,
|
||||
)) as unknown as [Array<{ pk: number; v: string | null }>, unknown];
|
||||
);
|
||||
for (const row of rows) {
|
||||
if (!row.v) continue;
|
||||
const parts = row.v
|
||||
@@ -625,11 +627,11 @@ export async function repairDuplicateClassnames(): Promise<DuplicateClassnameRes
|
||||
);
|
||||
const dupList = chunk.map(([dup]) => dup);
|
||||
try {
|
||||
const [result] = (await db.execute(sql`
|
||||
const result = await execResult(sql`
|
||||
UPDATE ${sql.raw(quoteIdentifier(table))}
|
||||
SET item_id = CASE item_id ${sql.join(cases, sql` `)} ELSE item_id END
|
||||
WHERE item_id IN (${sql.join(dupList, sql`, `)})
|
||||
`)) as unknown as [Record<string, unknown>, unknown];
|
||||
`);
|
||||
remapped += Number(result.affectedRows ?? 0);
|
||||
} catch {
|
||||
// Table may not exist on some hotel schemas — skip it.
|
||||
@@ -811,15 +813,13 @@ export async function previewCatalogParentRepair(
|
||||
const table = sql.raw(
|
||||
catalog === "bc" ? "catalog_pages_bc" : "catalog_pages",
|
||||
);
|
||||
const [rows] = await db.execute(
|
||||
const rows = await queryRows<import("./catalog-integrity").IntegrityPage>(
|
||||
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),
|
||||
fingerprint: parentStateFingerprint(catalog, rows),
|
||||
changes: planParentRepairs(rows),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -838,11 +838,11 @@ export async function applyCatalogParentRepair(
|
||||
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 = rowsFrom<import("./catalog-integrity").IntegrityPage>(
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user