Files
EpicNext-Cms/src/app/api/admin/studio/inspect/route.ts
T
Simo 26f071117b
CI / check (push) Successful in 1m39s
CI / deploy (push) Successful in 1m18s
CI / e2e (push) Successful in 25s
Add Catalog Studio inspection and guided import review
2026-09-05 13:10:43 +02:00

116 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
},
);