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 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", () => ({
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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(() => ({
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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) };
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user