diff --git a/src/lib/services/import-badge.ts b/src/lib/services/import-badge.ts index e83cb203..b4352780 100644 --- a/src/lib/services/import-badge.ts +++ b/src/lib/services/import-badge.ts @@ -17,24 +17,42 @@ export async function writeBadgeToExternalTexts( name: string, description: string, ): Promise { + const nameKey = `badge_name_${code}`; + const descKey = `badge_desc_${code}`; + + let texts: Record; try { const raw = await fs.readFile(EXTERNAL_TEXTS_PATH, "utf-8"); - const texts = JSON.parse(raw) as Record; - - const nameKey = `badge_name_${code}`; - const descKey = `badge_desc_${code}`; - - if (texts[nameKey] !== undefined) { + texts = JSON.parse(raw) as Record; + } catch (err) { + // File not present yet (fresh deployment) — start with an empty object. + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + console.warn( + "[import-badges] Failed to update ExternalTexts.json:", + (err as Error).message, + ); return { ok: false, - error: "Badge already exists in ExternalTexts.json", - status: 409, + error: "Failed to update ExternalTexts.json", + status: 500, }; } + texts = {}; + } - texts[nameKey] = name; - texts[descKey] = description || ""; + if (texts[nameKey] !== undefined) { + return { + ok: false, + error: "Badge already exists in ExternalTexts.json", + status: 409, + }; + } + texts[nameKey] = name; + texts[descKey] = description || ""; + + try { + await fs.mkdir(path.dirname(EXTERNAL_TEXTS_PATH), { recursive: true }); await fs.writeFile( EXTERNAL_TEXTS_PATH, JSON.stringify(texts, null, 4), diff --git a/src/lib/services/upload-import.test.ts b/src/lib/services/upload-import.test.ts index 5a1d7f01..0f9a179e 100644 --- a/src/lib/services/upload-import.test.ts +++ b/src/lib/services/upload-import.test.ts @@ -62,6 +62,8 @@ vi.mock("@/lib/services/furni-import", () => ({ async (insertFn: (nextId: number) => Promise) => insertFn(701), ), autoPriceFurni: vi.fn(() => ({ credits: 3, points: 0, pointsType: 0 })), + CATEGORY_PAGE: {}, + classifyFurni: vi.fn(() => "other"), ensureDirectories: vi.fn(async () => { const dirs = [ assetTargets.swfDir, @@ -76,6 +78,8 @@ vi.mock("@/lib/services/furni-import", () => ({ await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true }))); }), getOrCreateCategoryPage: getOrCreateCategoryPageMock, + IMPORTED_PAGE_CAPTION: "Imported Furniture", + IMPORTED_PAGE_CAPTION_SAVE: "imported_furni", })); vi.mock("@/lib/services/swf/nitro-builder", () => ({ diff --git a/src/lib/services/upload-import.ts b/src/lib/services/upload-import.ts index 9aa50a2f..708b1a39 100644 --- a/src/lib/services/upload-import.ts +++ b/src/lib/services/upload-import.ts @@ -7,8 +7,12 @@ import { appendFurniEntry, buildFurniEntry } from "@/lib/services/furni-data"; import { allocateCatalogItemId, autoPriceFurni, + CATEGORY_PAGE, + classifyFurni, ensureDirectories, getOrCreateCategoryPage, + IMPORTED_PAGE_CAPTION, + IMPORTED_PAGE_CAPTION_SAVE, } from "@/lib/services/furni-import"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; @@ -153,6 +157,7 @@ async function writeSqlMigration(params: { interactionType: string; customparams: string; price: { credits: number; points: number; pointsType: number }; + categoryKey: string; }): Promise { const num = await findNextMigrationNumber(); const padded = String(num).padStart(4, "0"); @@ -163,11 +168,29 @@ async function writeSqlMigration(params: { fileName, ); + const catKey = params.categoryKey; + const captionSave = `imp_${catKey}`; + const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other; + const lines: string[] = [ `-- Migration: ${fileName}`, `-- Description: Import furniture "${params.classname}"`, `-- Generated: ${new Date().toISOString()}`, "", + `-- Ensure the "Imported Furniture" parent page exists`, + `INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)`, + `SELECT ${escapeSql(IMPORTED_PAGE_CAPTION_SAVE)}, ${escapeSql(IMPORTED_PAGE_CAPTION)}, 'default_3x3', -1, '1', 9999, 263, '1', '1', '', '', '', '', '', '', '', ''`, + `WHERE NOT EXISTS (SELECT 1 FROM catalog_pages WHERE caption_save = ${escapeSql(IMPORTED_PAGE_CAPTION_SAVE)});`, + "", + `-- Ensure the category sub-page exists (${catKey})`, + `INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)`, + `SELECT ${escapeSql(captionSave)}, ${escapeSql(catInfo.label)}, 'default_3x3', p.id, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', ''`, + `FROM catalog_pages p`, + `WHERE p.caption_save = ${escapeSql(IMPORTED_PAGE_CAPTION_SAVE)}`, + ` AND NOT EXISTS (SELECT 1 FROM catalog_pages c WHERE c.caption_save = ${escapeSql(captionSave)} AND c.parent_id = p.id);`, + "", + `SET @next_id = (SELECT COALESCE(MAX(id), 0) + 1 FROM items_base);`, + "", `INSERT INTO items_base`, ` (id, sprite_id, public_name, item_name, type, width, length, stack_height,`, ` allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams)`, @@ -177,13 +200,12 @@ async function writeSqlMigration(params: { ` ${params.allowStack}, ${params.cansiton ? "1" : "0"}, ${params.canlayon ? "1" : "0"}, ${params.canstandon ? "1" : "0"},`, ` ${escapeSql(params.interactionType)}, ${escapeSql(params.customparams)});`, "", - `SET @next_id = LAST_INSERT_ID();`, - "", `INSERT INTO catalog_items`, ` (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)`, - `VALUES`, - ` (NULL, ${escapeSql("imp_other")}, @next_id, ${escapeSql(params.classname)},`, - ` ${params.price.credits}, ${params.price.points}, ${params.price.pointsType}, 1, 1, '-1', '');`, + `SELECT (SELECT COALESCE(MAX(id), 0) + 1 FROM catalog_items), p.id, @next_id, ${escapeSql(params.classname)},`, + ` ${params.price.credits}, ${params.price.points}, ${params.price.pointsType}, 1, 1, '-1', ''`, + `FROM catalog_pages p`, + `WHERE p.caption_save = ${escapeSql(captionSave)};`, "", ]; @@ -379,6 +401,7 @@ export async function uploadSingleFurni(params: { interactionType: userInteractionType, customparams, price, + categoryKey: classifyFurni(classname, itemType), }); warnings.push(`SQL migration written: ${sqlFile}`); } catch (err) {