Files
EpicNext-Cms/src/lib/services/furniture-inspection.ts
T
openhands 8638e81444
CI / check (push) Successful in 4m10s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m4s
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.
2026-09-17 21:02:57 +02:00

144 lines
4.2 KiB
TypeScript

import { promises as fs } from "node:fs";
import path from "node:path";
import { sql } from "drizzle-orm";
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";
import { readFurniData } from "@/lib/services/furni-data";
async function asset(
directory: string,
name: string,
): Promise<{ exists: boolean | null; bytes: number }> {
try {
const stat = await fs.stat(path.join(directory, name));
return {
exists: stat.isFile() ? (stat.size > 0 ? true : null) : false,
bytes: stat.size,
};
} catch (error) {
return {
exists: (error as NodeJS.ErrnoException).code === "ENOENT" ? false : null,
bytes: 0,
};
}
}
async function assetCandidates(directory: string, names: string[]) {
const results = await Promise.all(
[...new Set(names)].map((name) => asset(directory, name)),
);
return (
results.find((result) => result.exists === true) ??
results.find((result) => result.exists === null) ??
results[0]
);
}
export async function inspectFurniture(
names: string[],
): Promise<FurnitureInspection[]> {
const namesSql = sql.join(
names.map((name) => sql`${normalizeClassname(name)}`),
sql`, `,
);
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})`,
);
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})`,
);
let readable = true;
let data: Record<string, unknown> = {};
try {
data = await readFurniData();
} catch {
readable = false;
}
const entries = new Map<string, FurnitureInspection["furnidata"]>();
for (const [section, type] of [
["roomitemtypes", "flooritem"],
["wallitemtypes", "wallitem"],
] as const) {
const list = (
data[section] as
| { furnitype?: Array<Record<string, unknown>> }
| undefined
)?.furnitype;
if (!Array.isArray(list)) {
readable = false;
continue;
}
for (const entry of list) {
if (!entry || typeof entry !== "object") {
readable = false;
continue;
}
const name = String(entry.classname ?? "");
const key = normalizeClassname(name);
if (!names.some((value) => normalizeClassname(value) === key)) continue;
const values = entries.get(key) ?? [];
values.push({
id: Number(entry.id),
name: String(entry.name ?? ""),
description: String(entry.description ?? ""),
revision: Number(entry.revision ?? 0),
type,
});
entries.set(key, values);
}
}
const dirs = await getFurniAssetDirs();
const items: FurnitureInspection[] = [];
for (let offset = 0; offset < names.length; offset += 20) {
items.push(
...(await Promise.all(
names.slice(offset, offset + 20).map(async (classname) => {
const variants = [
classname,
...rows
.filter(
(row) =>
normalizeClassname(row.classname) ===
normalizeClassname(classname),
)
.map((row) => row.classname),
];
const [nitro, icon] = await Promise.all([
assetCandidates(
dirs.nitroDir,
variants.map((name) => `${name.split("*")[0]}.nitro`),
),
assetCandidates(
dirs.iconDir,
variants.map((name) => `${name.replaceAll("*", "_")}_icon.png`),
),
]);
return {
classname,
sql: rows.filter(
(row) =>
normalizeClassname(row.classname) ===
normalizeClassname(classname),
),
catalog: offers.filter(
(row) =>
normalizeClassname(row.classname) ===
normalizeClassname(classname),
),
furnidata: entries.get(normalizeClassname(classname)) ?? [],
furnidataReadable: readable,
nitro,
icon,
};
}),
)),
);
}
return items;
}