116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
import { promises as fs } from "node:fs";
|
||
import path from "node:path";
|
||
import { sql } from "drizzle-orm";
|
||
import { withAdmin } from "@/lib/api-handler";
|
||
import { apiError, apiOk } from "@/lib/api-response";
|
||
import { db } from "@/lib/db";
|
||
import {
|
||
type FurnitureInspection,
|
||
validateClassnames,
|
||
} from "@/lib/furni/studio-inspection";
|
||
import { PERMS } from "@/lib/permission-slugs";
|
||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||
import { readFurniData } from "@/lib/services/furni-data";
|
||
|
||
async function asset(directory: string, name: string) {
|
||
try {
|
||
const stat = await fs.stat(path.join(directory, name));
|
||
return { exists: stat.isFile(), bytes: stat.size };
|
||
} catch (error) {
|
||
return {
|
||
exists: (error as NodeJS.ErrnoException).code === "ENOENT" ? false : null,
|
||
bytes: 0,
|
||
};
|
||
}
|
||
}
|
||
// Read-only inspection lives outside import routes so it cannot enqueue a Git export.
|
||
export const POST = withAdmin(
|
||
{ permission: PERMS.ASSETS_IMPORT },
|
||
async (request) => {
|
||
const body = await request.json().catch(() => null);
|
||
const names = validateClassnames(body?.classnames);
|
||
if (!names)
|
||
return apiError("Provide 1–500 valid furniture classnames", 400);
|
||
const namesSql = sql.join(
|
||
names.map((name) => sql`${name}`),
|
||
sql`, `,
|
||
);
|
||
const [rows] = (await db.execute(
|
||
sql`SELECT id, sprite_id AS spriteId, item_name AS classname, public_name AS name, type FROM items_base WHERE item_name IN (${namesSql})`,
|
||
)) as unknown as [
|
||
Array<FurnitureInspection["sql"][number] & { classname: string }>,
|
||
unknown,
|
||
];
|
||
const [offers] = (await db.execute(
|
||
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 ib.item_name IN (${namesSql})`,
|
||
)) as unknown as [
|
||
Array<FurnitureInspection["catalog"][number] & { classname: string }>,
|
||
unknown,
|
||
];
|
||
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 ?? "");
|
||
if (!names.includes(name)) continue;
|
||
const values = entries.get(name) ?? [];
|
||
values.push({
|
||
id: Number(entry.id),
|
||
name: String(entry.name ?? ""),
|
||
description: String(entry.description ?? ""),
|
||
revision: Number(entry.revision ?? 0),
|
||
type,
|
||
});
|
||
entries.set(name, 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 base = classname.split("*")[0];
|
||
const [nitro, icon] = await Promise.all([
|
||
asset(dirs.nitroDir, `${base}.nitro`),
|
||
asset(dirs.iconDir, `${classname.replace(/\*/g, "_")}_icon.png`),
|
||
]);
|
||
return {
|
||
classname,
|
||
sql: rows.filter((row) => row.classname === classname),
|
||
catalog: offers.filter((row) => row.classname === classname),
|
||
furnidata: entries.get(classname) ?? [],
|
||
furnidataReadable: readable,
|
||
nitro,
|
||
icon,
|
||
};
|
||
}),
|
||
)),
|
||
);
|
||
}
|
||
return apiOk({ items });
|
||
},
|
||
);
|