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 committed 2026-09-17 21:02:57 +02:00
1 parent 2e25b39364
commit 8638e81444
116 files changed
+866 -897

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -8,7 +8,10 @@ const mocks = vi.hoisted(() => ({
repair: vi.fn(),
}));
vi.mock("node:fs/promises", () => ({ readdir: mocks.readdir }));
vi.mock("@/lib/db", () => ({ db: { execute: mocks.execute } }));
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(mocks.execute),
db: { execute: mocks.execute },
}));
vi.mock("@/lib/services/clone-import", () => ({}));
vi.mock("@/lib/services/clone-sources", () => ({}));
vi.mock("@/lib/services/furni-asset-dirs", () => ({
+10 -15
View File
@@ -1,7 +1,7 @@
import { promises as fs } from "node:fs";
import { asc, sql } from "drizzle-orm";
import { numericValue } from "@/features/catalog/domain/offer-input";
import { CatalogPages, db } from "@/lib/db";
import { CatalogPages, db, queryRows } from "@/lib/db";
import { getFurnitureDataPath } from "@/lib/services/furni-data";
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
@@ -140,12 +140,12 @@ export async function loadCatalogItemsData(
// Load items via raw query to work around pageId Int vs VARCHAR mismatch
const pageIdStr = String(pageId);
// CAST: live Habbo DBs often store page_id as VARCHAR while schema maps Int.
const [rawItems] = (await db.execute(sql`
const rawItems = await queryRows<Record<string, unknown>>(sql`
SELECT * FROM catalog_items
WHERE CAST(page_id AS CHAR) = ${pageIdStr}
ORDER BY order_number ASC, id ASC
`)) as unknown as [Record<string, unknown>[], unknown[]];
const items: RawItem[] = rawItems.map((r: Record<string, unknown>) => ({
`);
const items: RawItem[] = rawItems.map((r) => ({
id: Number(r.id),
itemIds: String(r.item_ids ?? ""),
pageId: numericValue(r.page_id, 0),
@@ -169,14 +169,9 @@ export async function loadCatalogItemsData(
.select({ id: CatalogPages.id, caption: CatalogPages.caption })
.from(CatalogPages)
.orderBy(asc(CatalogPages.caption)),
db
.execute(sql`
queryRows<{ interaction_type: string }>(sql`
SELECT DISTINCT interaction_type FROM items_base ORDER BY interaction_type ASC
`)
.then(
(result) =>
(result as unknown as [{ interaction_type: string }[], unknown[]])[0],
),
`),
getHabboGamedataHotel(),
]);
@@ -199,9 +194,9 @@ export async function loadCatalogItemsData(
// Raw query to work around ENUM('0','1') columns returning numeric values.
// baseItemIds is built from numeric splits + filter(n > 0); joined via sql.join.
const [baseItemRows] =
const baseItemRows =
baseItemIds.length > 0
? ((await db.execute(sql`
? await queryRows<Record<string, unknown>>(sql`
SELECT id, sprite_id, public_name, item_name, type, width, length,
stack_height, allow_stack, allow_sit, allow_lay, allow_walk,
allow_gift, allow_trade, allow_recycle, allow_marketplace_sell,
@@ -209,8 +204,8 @@ export async function loadCatalogItemsData(
vending_ids, customparams, effect_id_male, effect_id_female,
clothing_on_walk
FROM items_base WHERE id IN (${sql.join(baseItemIds, sql`, `)})
`)) as unknown as [Record<string, unknown>[], unknown[]])
: [[], []];
`)
: [];
const baseItems: BaseItem[] = baseItemRows.map((r) => ({
id: Number(r.id),
@@ -7,7 +7,8 @@ const mocks = vi.hoisted(() => ({
insert: vi.fn(),
values: vi.fn(),
}));
vi.mock("@/lib/db", () => ({
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(mocks.execute),
AdminAuditLog: "audit",
db: { execute: mocks.execute, transaction: mocks.transaction },
}));
File diff suppressed because it is too large. Load diff
+11 -11
View File
@@ -3,7 +3,7 @@ import {
deletePageCommand,
updatePageCommand,
} from "@/features/catalog/server/page-commands";
import { CatalogPages, db } from "@/lib/db";
import { CatalogPages, db, queryRows } from "@/lib/db";
export type { NestedTreeNode, TreeNode } from "@/types/catalog";
@@ -29,27 +29,27 @@ export async function getCatalogItemCounts(
if (pageIds) {
const idStrs = pageIds.map(String);
const [rows] = (await db.execute(sql`
const rows = await queryRows<{
page_id: string | number;
cnt: number | bigint;
}>(sql`
SELECT page_id, COUNT(*) as cnt
FROM catalog_items
WHERE page_id IN (${sql.join(idStrs, sql`, `)})
GROUP BY page_id
`)) as unknown as [
{ page_id: string | number; cnt: number | bigint }[],
unknown[],
];
`);
for (const r of rows) {
map.set(toInt(r.page_id), toInt(r.cnt));
}
return map;
}
const [rows] = (await db.execute(sql`
const rows = await queryRows<{
page_id: string | number;
cnt: number | bigint;
}>(sql`
SELECT page_id, COUNT(*) as cnt FROM catalog_items GROUP BY page_id
`)) as unknown as [
{ page_id: string | number; cnt: number | bigint }[],
unknown[],
];
`);
for (const r of rows) {
map.set(toInt(r.page_id), toInt(r.cnt));
}
+2 -1
View File
@@ -65,7 +65,8 @@ vi.mock("node:fs", async (orig) => {
},
};
});
vi.mock("@/lib/db", () => ({
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(executeFn),
db: {
execute: executeFn,
select: vi.fn(() => ({
+5 -5
View File
@@ -1,6 +1,6 @@
import { promises as fs } from "node:fs";
import { eq, inArray, sql } from "drizzle-orm";
import { db, ItemsBase } from "@/lib/db";
import { db, ItemsBase, queryRows } from "@/lib/db";
import { autoDetectInteraction } from "@/lib/furni/auto-interaction";
import { normalizeClassname } from "@/lib/furni/classname";
import { HABBO_GAMEDATA_HOTEL_SETTING_KEY } from "@/lib/habbo-gamedata-hotel";
@@ -222,9 +222,9 @@ async function allocateItemsBaseId<T>(
itemsBaseNextId === null ||
Date.now() - itemsBaseIdLastUsed > ITEMS_BASE_ID_REFRESH_MS
) {
const [idRows] = (await db.execute(sql`
const idRows = await queryRows<{ next: number }>(sql`
SELECT COALESCE(MAX(id), 0) + 1 AS next FROM items_base
`)) as unknown as [Array<{ next: number }>, unknown];
`);
itemsBaseNextId = Number(idRows[0]?.next ?? 1);
}
} finally {
@@ -450,9 +450,9 @@ export async function cloneSingleFurni(params: {
// Skip when the item already has a catalog row — re-imports must never
// create a second row for the same item_ids.
try {
const [existingCatalog] = (await db.execute(sql`
const existingCatalog = await queryRows<{ id: number }>(sql`
SELECT id FROM catalog_items WHERE item_ids = ${String(newId)} LIMIT 1
`)) as unknown as [Array<{ id: number }>, unknown];
`);
if (existingCatalog.length > 0) {
warnings.push(
`catalog entry already exists (#${existingCatalog[0].id}) — skipped duplicate`,
+5 -5
View File
@@ -1,7 +1,7 @@
import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { eq, sql } from "drizzle-orm";
import { db, ItemsBase } from "@/lib/db";
import { db, execResult, ItemsBase, queryRows } from "@/lib/db";
import {
getGamedataRoot,
getNitroFilesRoot,
@@ -698,14 +698,14 @@ export interface CatalogOfferRebuildResult {
*/
export async function rebuildCatalogOfferIds(): Promise<CatalogOfferRebuildResult> {
return withFurniDataLock(async () => {
const [countRows] = (await db.execute(
const countRows = await queryRows<{ total: number }>(
sql`SELECT COUNT(*) AS total FROM catalog_items`,
)) as unknown as [Array<{ total: number }>, unknown];
);
const checked = Number(countRows?.[0]?.total ?? 0);
const [result] = (await db.execute(sql`
const result = await execResult(sql`
UPDATE catalog_items SET offer_id = id WHERE offer_id <> id
`)) as unknown as [Record<string, unknown>, unknown];
`);
return { checked, fixed: Number(result.affectedRows ?? 0) };
});
+19 -28
View File
@@ -15,7 +15,7 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { sql } from "drizzle-orm";
import { db, ItemsBase } from "@/lib/db";
import { db, ItemsBase, queryRows } from "@/lib/db";
import { normalizeClassname } from "@/lib/furni/classname";
import { indexLocalFurniture } from "@/lib/furni/local-presence";
import { readFurniData } from "./furni-data";
@@ -89,10 +89,10 @@ export async function getCatalogIdSet(): Promise<Set<number>> {
}
const set = new Set<number>();
try {
const rows = await db.execute(sql`SELECT item_ids FROM catalog_items`);
for (const row of (
rows as unknown as [Array<{ item_ids: string | null }>, unknown]
)[0]) {
const rows = await queryRows<{ item_ids: string | null }>(
sql`SELECT item_ids FROM catalog_items`,
);
for (const row of rows) {
for (const part of String(row.item_ids ?? "").split(/[;,]/)) {
const n = Number(part.trim());
if (Number.isFinite(n) && n > 0) set.add(n);
@@ -138,48 +138,39 @@ export async function getFurniImportStats(
return statsSnapshot.stats;
}
try {
const [totalResult, catalogResult, allIdsResult, referencedResult] =
const [totalInDbRaw, inCatalogRaw, allIds, referencedList] =
await Promise.all([
db.execute(sql`SELECT COUNT(*) as cnt FROM items_base`),
db.execute(
queryRows<{ cnt: bigint }>(sql`SELECT COUNT(*) as cnt FROM items_base`),
queryRows<{ cnt: bigint }>(
sql`SELECT COUNT(DISTINCT item_ids) as cnt FROM catalog_items`,
),
db.execute(sql`SELECT id FROM items_base`),
db.execute(sql`SELECT item_ids FROM catalog_items`),
queryRows<{ id: number }>(sql`SELECT id FROM items_base`),
queryRows<{ item_ids: string | null }>(
sql`SELECT item_ids FROM catalog_items`,
),
]);
const totalInDb = Number(
(totalResult as unknown as [[{ cnt: bigint }], unknown])[0][0].cnt,
);
const inCatalog = Number(
(catalogResult as unknown as [[{ cnt: bigint }], unknown])[0][0].cnt,
);
const totalInDb = Number(totalInDbRaw[0]?.cnt ?? 0);
const inCatalog = Number(inCatalogRaw[0]?.cnt ?? 0);
const allIds = (
allIdsResult as unknown as [Array<{ id: number }>, unknown]
)[0].map((r) => Number(r.id));
const allIdsNum = allIds.map((r) => Number(r.id));
const referenced = new Set<number>();
for (const row of (
referencedResult as unknown as [
Array<{ item_ids: string | null }>,
unknown,
]
)[0]) {
for (const row of referencedList) {
for (const part of String(row.item_ids ?? "").split(/[;,]/)) {
const n = Number(part.trim());
if (Number.isFinite(n) && n > 0) referenced.add(n);
}
}
const notInCatalog = allIds.filter((id) => !referenced.has(id)).length;
const notInCatalog = allIdsNum.filter((id) => !referenced.has(id)).length;
const [importedItems] = (await db.execute(sql`
const importedItems = await queryRows<{ item_name: string }>(sql`
SELECT ib.item_name FROM items_base ib
WHERE EXISTS (
SELECT 1 FROM catalog_items ci
WHERE FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ',')) > 0
)
LIMIT 10000
`)) as unknown as [Array<{ item_name: string }>, unknown];
`);
let missingNitro = 0;
for (const item of importedItems) {
+2 -1
View File
@@ -15,7 +15,8 @@ const mocks = vi.hoisted(() => ({
build: vi.fn(),
append: vi.fn(),
}));
vi.mock("@/lib/db", () => ({
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(mocks.execute),
db: { select: mocks.select, execute: mocks.execute },
ItemsBase: {
id: "id",
+20 -15
View File
@@ -3,7 +3,7 @@ import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { and, eq, type SQL, sql } from "drizzle-orm";
import { CatalogPages, db, ItemsBase } from "@/lib/db";
import { CatalogPages, db, execResult, ItemsBase, queryRows } from "@/lib/db";
import { officialHabboEnrichmentWarning } from "@/lib/habbo-gamedata-hotel";
import { logger } from "@/lib/logger";
import { logServerError } from "@/lib/server-log";
@@ -146,10 +146,10 @@ async function dedupeCatalogPage(
parentId: number,
justCreatedId: number,
): Promise<number> {
const [rows] = (await db.execute(sql`
const rows = await queryRows<{ id: number }>(sql`
SELECT MIN(id) AS id FROM catalog_pages
WHERE caption_save = ${captionSave} AND parent_id = ${parentId}
`)) as unknown as [Array<{ id: number }>, unknown];
`);
const winner = rows[0]?.id ?? justCreatedId;
if (winner !== justCreatedId) {
// Never delete a page that already received catalog items.
@@ -304,13 +304,13 @@ export async function reconcileImportedOfferIds(): Promise<{
fixed: number;
}> {
const parentId = await getOrCreateImportedParentPage();
const [result] = (await db.execute(sql`
const result = await execResult(sql`
UPDATE catalog_items ci
JOIN catalog_pages cp ON cp.id = ci.page_id
SET ci.offer_id = ci.id
WHERE cp.parent_id = ${parentId}
AND ci.offer_id <> ci.id
`)) as unknown as [Record<string, unknown>, unknown];
`);
return { fixed: Number(result.affectedRows ?? 0) };
}
@@ -486,9 +486,11 @@ export async function allocateCatalogItemId<T>(
catalogNextId === null ||
Date.now() - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
) {
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];
`);
catalogNextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
}
} finally {
@@ -1067,9 +1069,12 @@ export async function importSingleFurni(params: {
const itemIdStr = String(itemId);
// Check if this item already exists anywhere in the catalog
const [existingCatalog] = (await db.execute(sql`
const existingCatalog = await queryRows<{
id: number;
page_id: string;
}>(sql`
SELECT id, page_id FROM catalog_items WHERE FIND_IN_SET(${itemIdStr}, REPLACE(item_ids, ';', ',')) > 0 LIMIT 1
`)) as unknown as [Array<{ id: number; page_id: string }>, unknown];
`);
if (existingCatalog.length > 0) {
catalogItemId = existingCatalog[0].id;
@@ -1177,7 +1182,7 @@ export async function fixDatabaseConsistencyAfterImport(): Promise<{
costCreditsFixed: number;
}> {
// 1. Fill empty catalog_name from items_base.public_name
const [nameResult] = (await db.execute(sql`
const nameResult = await execResult(sql`
UPDATE catalog_items c
JOIN items_base i ON c.item_ids = i.id
SET c.catalog_name = CASE
@@ -1185,23 +1190,23 @@ export async function fixDatabaseConsistencyAfterImport(): Promise<{
ELSE i.public_name
END
WHERE c.catalog_name IS NULL OR c.catalog_name = ''
`)) as unknown as [Record<string, unknown>, unknown];
`);
const catalogNameFixed = Number(nameResult.affectedRows ?? 0);
// 2. Set have_offer = '1' where it was '0'
const [offerResult] = (await db.execute(sql`
const offerResult = await execResult(sql`
UPDATE catalog_items
SET have_offer = '1'
WHERE have_offer = '0'
`)) as unknown as [Record<string, unknown>, unknown];
`);
const haveOfferFixed = Number(offerResult.affectedRows ?? 0);
// 3. Set cost_credits = 3 where it was <= 0
const [creditResult] = (await db.execute(sql`
const creditResult = await execResult(sql`
UPDATE catalog_items
SET cost_credits = 3
WHERE cost_credits <= 0
`)) as unknown as [Record<string, unknown>, unknown];
`);
const costCreditsFixed = Number(creditResult.affectedRows ?? 0);
return { catalogNameFixed, haveOfferFixed, costCreditsFixed };
+8 -4
View File
@@ -12,8 +12,8 @@ const state = vi.hoisted(() => ({
queries: [] as { sql: string; params: unknown[] }[],
writes: 0,
}));
vi.mock("@/lib/db", () => ({
db: {
vi.mock("@/lib/db", async () => {
const db = {
execute: async (query: SQL) => {
const compiled = new MySqlDialect().sqlToQuery(query);
if (compiled.sql.includes("FROM catalog_items ci"))
@@ -50,8 +50,12 @@ vi.mock("@/lib/db", () => ({
},
});
},
},
}));
};
return {
db,
...(await import("@/test/db-helpers")).createDbHelpers(db.execute),
};
});
import { fixEmulatorIssues, listEmulatorIssues } from "./furni-maintenance";
File diff suppressed because it is too large. Load diff
@@ -3,7 +3,10 @@ import { MySqlDialect } from "drizzle-orm/mysql-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({ execute: vi.fn(), conflict: vi.fn() }));
vi.mock("@/lib/db", () => ({ db: { execute: mocks.execute } }));
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(mocks.execute),
db: { execute: mocks.execute },
}));
vi.mock("./furni-data", () => ({ findFurniDataIdConflict: mocks.conflict }));
import { reserveFurnitureId } from "./furniture-id-reservation";
+5 -5
View File
@@ -1,5 +1,5 @@
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { queryRows } from "@/lib/db";
import { findFurniDataIdConflict } from "./furni-data";
let reservation: Promise<void> = Promise.resolve();
@@ -26,9 +26,9 @@ export async function reserveFurnitureId(
candidate > 2147483647
)
throw new Error("No valid furniture ID is available");
const [owners] = (await db.execute(
const owners = await queryRows<{ item_name: string }>(
sql`SELECT item_name FROM items_base WHERE id = ${candidate} OR sprite_id = ${candidate} LIMIT 1`,
)) as unknown as [Array<{ item_name: string }>, unknown];
);
const occupied =
owners.length > 0 ||
!!(await findFurniDataIdConflict(candidate, classname));
@@ -44,9 +44,9 @@ export async function reserveFurnitureId(
throw error;
}
}
const [rows] = (await db.execute(
const rows = await queryRows<{ maximum: number | string }>(
sql`SELECT GREATEST(COALESCE(MAX(id), 0), COALESCE(MAX(sprite_id), 0)) AS maximum FROM items_base`,
)) as unknown as [Array<{ maximum: number | string }>, unknown];
);
candidate = Math.max(candidate + 1, Number(rows[0]?.maximum ?? 0) + 1);
}
throw new Error("Cannot reserve a free furniture ID; retry the import");
+9 -11
View File
@@ -1,7 +1,7 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { queryRows } from "@/lib/db";
import { normalizeClassname } from "@/lib/furni/classname";
import type { FurnitureInspection } from "@/lib/furni/studio-inspection";
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
@@ -42,18 +42,16 @@ export async function inspectFurniture(
names.map((name) => sql`${normalizeClassname(name)}`),
sql`, `,
);
const [rows] = (await db.execute(
const rows = await queryRows<
FurnitureInspection["sql"][number] & { classname: string }
>(
sql`SELECT id, sprite_id AS spriteId, item_name AS classname, public_name AS name, type FROM items_base WHERE LOWER(TRIM(item_name)) IN (${namesSql})`,
)) as unknown as [
Array<FurnitureInspection["sql"][number] & { classname: string }>,
unknown,
];
const [offers] = (await db.execute(
);
const offers = await queryRows<
FurnitureInspection["catalog"][number] & { classname: string }
>(
sql`SELECT ib.item_name AS classname, ci.id, ci.page_id AS pageId, ci.cost_credits AS credits, ci.cost_points AS points FROM items_base ib JOIN catalog_items ci ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ',')) > 0 WHERE LOWER(TRIM(ib.item_name)) IN (${namesSql})`,
)) as unknown as [
Array<FurnitureInspection["catalog"][number] & { classname: string }>,
unknown,
];
);
let readable = true;
let data: Record<string, unknown> = {};
try {
+2 -1
View File
@@ -22,7 +22,7 @@ const state = vi.hoisted(() => ({
}));
vi.mock("@/lib/logger", () => ({ logger: { error: vi.fn() } }));
vi.mock("./news-cache", () => ({ invalidateNewsCache: state.refresh }));
vi.mock("@/lib/db", () => {
vi.mock("@/lib/db", async () => {
const dialect = new MySqlDialect();
const timestamp = (value: unknown) =>
value instanceof Date
@@ -68,6 +68,7 @@ vi.mock("@/lib/db", () => {
},
};
return {
rowsFrom: (await import("@/test/db-helpers")).rowsFrom,
db: {
transaction: async (work: (tx: unknown) => Promise<unknown>) => {
state.transactionCalls++;
+8 -10
View File
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
import { sql } from "drizzle-orm";
import { operationHash } from "@/features/operations/model";
import { enqueueEffect } from "@/features/operations/server";
import { db } from "@/lib/db";
import { db, rowsFrom } from "@/lib/db";
import { logger } from "@/lib/logger";
import { invalidateNewsCache } from "./news-cache";
@@ -35,23 +35,21 @@ export async function publishDueArticles(now = new Date()): Promise<number> {
for (let attempt = 0; ; attempt++) {
try {
published = await db.transaction(async (tx) => {
const [rows] = await tx.execute(
sql`SELECT CAST(id AS CHAR) AS id,user_id AS userId,title,publish_at AS publishAt FROM website_articles WHERE status='scheduled' AND publish_at<=${utcNow} ORDER BY publish_at,id LIMIT 100 FOR UPDATE`,
);
let published = 0;
for (const article of rows as unknown as Array<{
for (const article of rowsFrom<{
id: string;
userId: number | null;
title: string | null;
publishAt: Date | string;
}>) {
}>(
await tx.execute(
sql`SELECT CAST(id AS CHAR) AS id,user_id AS userId,title,publish_at AS publishAt FROM website_articles WHERE status='scheduled' AND publish_at<=${utcNow} ORDER BY publish_at,id LIMIT 100 FOR UPDATE`,
),
)) {
const [result] = await tx.execute(
sql`UPDATE website_articles SET status='published',published_at=${utcNow},updated_at=${utcNow} WHERE id=${article.id} AND status='scheduled' AND publish_at<=${utcNow}`,
);
if (
(result as unknown as { affectedRows: number }).affectedRows !== 1
)
continue;
if (result.affectedRows !== 1) continue;
const operationId = randomUUID();
const hash = operationHash({
articleId: article.id,
+4 -1
View File
@@ -52,7 +52,10 @@ const {
downloadFileFn: vi.fn<AnyFn>(async () => ({ ok: true, size: 100 })),
}));
vi.mock("@/lib/db", () => ({ db: { execute: executeFn } }));
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(executeFn),
db: { execute: executeFn },
}));
vi.mock("@/lib/services/furni-asset-dirs", () => ({
getFurniAssetWriteTargets: getTargetsFn,
}));
+6 -6
View File
@@ -1,7 +1,7 @@
import { existsSync, promises as fs } from "node:fs";
import os from "node:os";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { queryRows } from "@/lib/db";
import { getBaseClassname, getSafeClassnameStem } from "@/lib/furni/classname";
import {
DEFAULT_NITRO_SOURCES,
@@ -272,12 +272,12 @@ async function loadValidStems(): Promise<Set<string>> {
// fallback), mirroring repair-nitros.ts classname resolution.
const validStems = new Set<string>();
try {
const [result] = (await db.execute(sql`
const result = await queryRows<{
item_name: string;
public_name: string;
}>(sql`
SELECT item_name, public_name FROM items_base
`)) as unknown as [
Array<{ item_name: string; public_name: string }>,
unknown,
];
`);
for (const row of result) {
addStems(validStems, row.item_name);
if (
File diff suppressed because it is too large. Load diff
+3 -13
View File
@@ -1,7 +1,7 @@
import { existsSync, promises as fs } from "node:fs";
import os from "node:os";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { queryRows } from "@/lib/db";
import { logServerError } from "@/lib/server-log";
import { extractFurniIconPng } from "@/lib/services/clone-icon";
import {
@@ -174,19 +174,9 @@ export async function repairMissingIcons(
interaction_type: string;
}>;
try {
const [rows] = (await db.execute(sql`
items = await queryRows<(typeof items)[number]>(sql`
SELECT id, item_name, public_name, type, interaction_type FROM items_base ORDER BY id
`)) as unknown as [
Array<{
id: number;
item_name: string;
public_name: string;
type: string;
interaction_type: string;
}>,
unknown,
];
items = rows;
`);
} catch (err) {
onEvent?.({
type: "error",
+3 -13
View File
@@ -1,7 +1,7 @@
import { existsSync, promises as fs } from "node:fs";
import os from "node:os";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { queryRows } from "@/lib/db";
import {
type CloneSource,
DEFAULT_NITRO_SOURCES,
@@ -167,19 +167,9 @@ export async function repairMissingNitros(
interaction_type: string;
}>;
try {
const [rows] = (await db.execute(sql`
items = await queryRows<(typeof items)[number]>(sql`
SELECT id, item_name, public_name, type, interaction_type FROM items_base ORDER BY id
`)) as unknown as [
Array<{
id: number;
item_name: string;
public_name: string;
type: string;
interaction_type: string;
}>,
unknown,
];
items = rows;
`);
} catch (err) {
onEvent?.({
type: "error",
+2 -1
View File
@@ -33,7 +33,8 @@ vi.mock("drizzle-orm", () => ({
}),
}));
vi.mock("@/lib/db", () => ({
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(dbExecuteMock),
ItemsBase: { id: "id", itemName: "item_name" },
db: {
execute: dbExecuteMock,
+5 -5
View File
@@ -1,7 +1,7 @@
import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { eq, sql } from "drizzle-orm";
import { db, ItemsBase } from "@/lib/db";
import { db, ItemsBase, queryRows } from "@/lib/db";
import { autoDetectInteraction } from "@/lib/furni/auto-interaction";
import { logServerError } from "@/lib/server-log";
import { getFurniAssetWriteTargets } from "@/lib/services/furni-asset-dirs";
@@ -105,9 +105,9 @@ async function allocateItemsBaseId<T>(
itemsBaseNextId === null ||
Date.now() - itemsBaseIdLastUsed > ITEMS_BASE_ID_REFRESH_MS
) {
const [idRows] = (await db.execute(sql`
const idRows = await queryRows<{ next: number }>(sql`
SELECT COALESCE(MAX(id), 0) + 1 AS next FROM items_base
`)) as unknown as [Array<{ next: number }>, unknown];
`);
itemsBaseNextId = Number(idRows[0]?.next ?? 1);
}
} finally {
@@ -410,9 +410,9 @@ export async function uploadSingleFurni(params: {
try {
// Skip when the item already has a catalog row — re-uploads must never
// create a second row for the same item_ids.
const [existingCatalog] = (await db.execute(sql`
const existingCatalog = await queryRows<{ id: number }>(sql`
SELECT id FROM catalog_items WHERE item_ids = ${String(newId)} LIMIT 1
`)) as unknown as [Array<{ id: number }>, unknown];
`);
if (existingCatalog.length > 0) {
catalogItemId = existingCatalog[0].id;
warnings.push(