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:
openhands
2026-09-17 21:02:57 +02:00
parent 2e25b39364
commit 8638e81444
116 changed files with 866 additions and 897 deletions
+32 -32
View File
@@ -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";