fix(catalog): stream single imports and preserve response errors
This commit is contained in:
1 parent
f0dcbee162
commit
33b6d1520e
5 files changed
+351
-142
No files matched your search
@@ -39,6 +39,7 @@ import {
|
||||
} from "@/lib/services/furni-import";
|
||||
import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { listFurniture } from "@/lib/services/habbofurni";
|
||||
import { streamImportResponse } from "@/lib/services/import/core/single-response";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
|
||||
|
||||
@@ -682,144 +683,150 @@ export const POST = withAdmin(
|
||||
});
|
||||
}
|
||||
|
||||
// ── Single import ────────────────────────────────────────────────
|
||||
const {
|
||||
id,
|
||||
classname,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
revision,
|
||||
category,
|
||||
sourceId,
|
||||
updateExisting,
|
||||
} = body;
|
||||
|
||||
if (!classname || !name) return apiError("Missing classname or name", 400);
|
||||
|
||||
let source: Awaited<ReturnType<typeof getSource>> | null = null;
|
||||
if (sourceId) source = await getSource(sourceId);
|
||||
|
||||
const result = await importSingleFurni({
|
||||
id: id ?? 0,
|
||||
classname,
|
||||
name,
|
||||
description: description ?? "",
|
||||
type: type ?? "flooritem",
|
||||
revision: revision ?? 0,
|
||||
category: category ?? "unknown",
|
||||
updateExisting: updateExisting === true,
|
||||
repairExisting: body.repairExisting === true,
|
||||
sourceSwfBaseUrl: source?.sourceSwfBaseUrl,
|
||||
nitroBaseUrl: source?.nitroBaseUrl || undefined,
|
||||
iconBaseUrl: source?.iconBaseUrl || undefined,
|
||||
});
|
||||
|
||||
if (!result.ok) return apiError(result.error ?? "Import failed", 409);
|
||||
|
||||
// Refresh game server caches
|
||||
try {
|
||||
await rcon.updateCatalog();
|
||||
await rcon.updateItems();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] RCON update failed:",
|
||||
(err as Error).message,
|
||||
);
|
||||
result.warnings.push("RCON cache refresh failed");
|
||||
}
|
||||
|
||||
// Force offer_id to mirror its catalog row id across the tree
|
||||
const reconciled = await reconcileImportedOfferIds();
|
||||
|
||||
// Keep asset + furnidata folders readable by the web server
|
||||
const ownership = await ensureFurniOwnership();
|
||||
|
||||
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
|
||||
const assetsSynced = await syncAssetsToGamedataBundle();
|
||||
|
||||
// Verify the furnidata matches the DB ids
|
||||
const furniReconcile = await reconcileFurniDataWithItemsBase();
|
||||
|
||||
// Fix database consistency (catalog_name + have_offer + cost_credits) after import
|
||||
const { catalogNameFixed, haveOfferFixed, costCreditsFixed } =
|
||||
await fixDatabaseConsistencyAfterImport();
|
||||
|
||||
// Force every catalog offer_id to equal its own row id
|
||||
const offerRebuild = await rebuildCatalogOfferIds();
|
||||
|
||||
// Repair any sprite_id drift so the client renders the right item
|
||||
const spriteVerify = await verifyAndFixSpriteIds();
|
||||
|
||||
// Verify + fix interaction_modes_count (single consolidated pass)
|
||||
const verification = await verifyAndFixInteractionModesCount();
|
||||
|
||||
// Patch localized furnidata files for this single furni (best effort).
|
||||
// Same `translate` / `langs` contract as the batch path above.
|
||||
let languagesPatch: Awaited<
|
||||
ReturnType<typeof patchLocalizedFurniDataEntries>
|
||||
> = [];
|
||||
try {
|
||||
if (body.translate !== false) {
|
||||
const langs = Array.isArray(body.langs)
|
||||
? (body.langs as unknown[]).filter(
|
||||
(l): l is string => typeof l === "string",
|
||||
)
|
||||
: undefined;
|
||||
languagesPatch = await patchLocalizedFurniDataEntries(
|
||||
[{ classname, name, description: description ?? "" }],
|
||||
body.translate === true,
|
||||
langs,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Localized patch failed:",
|
||||
(err as Error).message,
|
||||
);
|
||||
}
|
||||
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
action: "furni_import",
|
||||
target: "ItemsBase",
|
||||
targetId: result.itemId ?? 0,
|
||||
after: {
|
||||
const runSingle = async () => {
|
||||
// ── Single import ────────────────────────────────────────────────
|
||||
const {
|
||||
id,
|
||||
classname,
|
||||
name,
|
||||
spriteId: id ?? 0,
|
||||
type: type === "wallitem" ? "i" : "s",
|
||||
catalogItemId: result.catalogItemId,
|
||||
dimensions: result.dimensions,
|
||||
spriteCount: result.spriteCount,
|
||||
warnings: result.warnings.length > 0 ? result.warnings : undefined,
|
||||
},
|
||||
});
|
||||
description,
|
||||
type,
|
||||
revision,
|
||||
category,
|
||||
sourceId,
|
||||
updateExisting,
|
||||
} = body;
|
||||
|
||||
return apiOk({
|
||||
id: result.itemId,
|
||||
catalogItemId: result.catalogItemId,
|
||||
offerIdsFixed: reconciled.fixed,
|
||||
ownershipFixed: ownership.fixed,
|
||||
nitrosSynced: assetsSynced.copiedNitros,
|
||||
iconsSynced: assetsSynced.copiedIcons,
|
||||
furniDataFixedIds: furniReconcile.fixedIds,
|
||||
furniDataFixedOfferIds: furniReconcile.fixedOfferIds,
|
||||
furniDataMissing: furniReconcile.missing,
|
||||
furniDataFixedConflicts: furniReconcile.fixedConflicts,
|
||||
furniDataConflicts: furniReconcile.conflicts,
|
||||
offerRebuildChecked: offerRebuild.checked,
|
||||
offerRebuildFixed: offerRebuild.fixed,
|
||||
spriteChecked: spriteVerify.checked,
|
||||
spriteFixed: spriteVerify.fixed,
|
||||
interactionFixed: verification.fixed,
|
||||
catalogNameFixed,
|
||||
haveOfferFixed,
|
||||
costCreditsFixed,
|
||||
languages: languagesPatch,
|
||||
spriteCount: result.spriteCount,
|
||||
warnings: result.warnings,
|
||||
});
|
||||
if (!classname || !name)
|
||||
return apiError("Missing classname or name", 400);
|
||||
|
||||
let source: Awaited<ReturnType<typeof getSource>> | null = null;
|
||||
if (sourceId) source = await getSource(sourceId);
|
||||
|
||||
const result = await importSingleFurni({
|
||||
id: id ?? 0,
|
||||
classname,
|
||||
name,
|
||||
description: description ?? "",
|
||||
type: type ?? "flooritem",
|
||||
revision: revision ?? 0,
|
||||
category: category ?? "unknown",
|
||||
updateExisting: updateExisting === true,
|
||||
repairExisting: body.repairExisting === true,
|
||||
sourceSwfBaseUrl: source?.sourceSwfBaseUrl,
|
||||
nitroBaseUrl: source?.nitroBaseUrl || undefined,
|
||||
iconBaseUrl: source?.iconBaseUrl || undefined,
|
||||
});
|
||||
|
||||
if (!result.ok) return apiError(result.error ?? "Import failed", 409);
|
||||
|
||||
// Refresh game server caches
|
||||
try {
|
||||
await rcon.updateCatalog();
|
||||
await rcon.updateItems();
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] RCON update failed:",
|
||||
(err as Error).message,
|
||||
);
|
||||
result.warnings.push("RCON cache refresh failed");
|
||||
}
|
||||
|
||||
// Force offer_id to mirror its catalog row id across the tree
|
||||
const reconciled = await reconcileImportedOfferIds();
|
||||
|
||||
// Keep asset + furnidata folders readable by the web server
|
||||
const ownership = await ensureFurniOwnership();
|
||||
|
||||
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
|
||||
const assetsSynced = await syncAssetsToGamedataBundle();
|
||||
|
||||
// Verify the furnidata matches the DB ids
|
||||
const furniReconcile = await reconcileFurniDataWithItemsBase();
|
||||
|
||||
// Fix database consistency (catalog_name + have_offer + cost_credits) after import
|
||||
const { catalogNameFixed, haveOfferFixed, costCreditsFixed } =
|
||||
await fixDatabaseConsistencyAfterImport();
|
||||
|
||||
// Force every catalog offer_id to equal its own row id
|
||||
const offerRebuild = await rebuildCatalogOfferIds();
|
||||
|
||||
// Repair any sprite_id drift so the client renders the right item
|
||||
const spriteVerify = await verifyAndFixSpriteIds();
|
||||
|
||||
// Verify + fix interaction_modes_count (single consolidated pass)
|
||||
const verification = await verifyAndFixInteractionModesCount();
|
||||
|
||||
// Patch localized furnidata files for this single furni (best effort).
|
||||
// Same `translate` / `langs` contract as the batch path above.
|
||||
let languagesPatch: Awaited<
|
||||
ReturnType<typeof patchLocalizedFurniDataEntries>
|
||||
> = [];
|
||||
try {
|
||||
if (body.translate !== false) {
|
||||
const langs = Array.isArray(body.langs)
|
||||
? (body.langs as unknown[]).filter(
|
||||
(l): l is string => typeof l === "string",
|
||||
)
|
||||
: undefined;
|
||||
languagesPatch = await patchLocalizedFurniDataEntries(
|
||||
[{ classname, name, description: description ?? "" }],
|
||||
body.translate === true,
|
||||
langs,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[import-furni] Localized patch failed:",
|
||||
(err as Error).message,
|
||||
);
|
||||
}
|
||||
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
action: "furni_import",
|
||||
target: "ItemsBase",
|
||||
targetId: result.itemId ?? 0,
|
||||
after: {
|
||||
classname,
|
||||
name,
|
||||
spriteId: id ?? 0,
|
||||
type: type === "wallitem" ? "i" : "s",
|
||||
catalogItemId: result.catalogItemId,
|
||||
dimensions: result.dimensions,
|
||||
spriteCount: result.spriteCount,
|
||||
warnings: result.warnings.length > 0 ? result.warnings : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return apiOk({
|
||||
id: result.itemId,
|
||||
catalogItemId: result.catalogItemId,
|
||||
offerIdsFixed: reconciled.fixed,
|
||||
ownershipFixed: ownership.fixed,
|
||||
nitrosSynced: assetsSynced.copiedNitros,
|
||||
iconsSynced: assetsSynced.copiedIcons,
|
||||
furniDataFixedIds: furniReconcile.fixedIds,
|
||||
furniDataFixedOfferIds: furniReconcile.fixedOfferIds,
|
||||
furniDataMissing: furniReconcile.missing,
|
||||
furniDataFixedConflicts: furniReconcile.fixedConflicts,
|
||||
furniDataConflicts: furniReconcile.conflicts,
|
||||
offerRebuildChecked: offerRebuild.checked,
|
||||
offerRebuildFixed: offerRebuild.fixed,
|
||||
spriteChecked: spriteVerify.checked,
|
||||
spriteFixed: spriteVerify.fixed,
|
||||
interactionFixed: verification.fixed,
|
||||
catalogNameFixed,
|
||||
haveOfferFixed,
|
||||
costCreditsFixed,
|
||||
languages: languagesPatch,
|
||||
spriteCount: result.spriteCount,
|
||||
warnings: result.warnings,
|
||||
});
|
||||
};
|
||||
return request.headers.get("accept")?.includes("text/event-stream")
|
||||
? streamImportResponse(runSingle)
|
||||
: runSingle();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user