1
0
Fork 0
Code Issues Pull requests Projects Releases Packages Wiki Activity Actions Pages

extract server transform and validation helpers

This commit is contained in:
super-jalawii 2026-06-27 11:10:23 -04:00
parent aea2c9d56b
commit e79bc2e339
10 changed files with 373 additions and 139 deletions

160
server.js
View file

@ -3,6 +3,23 @@ import { spawn } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import {
areRowsOnlyFillChar,
normalizeBackgroundTileId,
} from "./server/contentTransforms.js";
import {
buildWorldChunkFileName,
defaultWorldDirRel,
getWorldStoragePaths as buildWorldStoragePaths,
normalizeWorldBookmark,
normalizeWorldIndexEntry,
normalizeWorldIndexPayload,
sanitizeWorldId,
} from "./server/worldTransforms.js";
import {
validateCatalogMetaPayload as validateCatalogMetaPayloadShape,
validatePayload as validatePayloadShape,
} from "./server/validation.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -961,27 +978,6 @@ function writeLauncherRequestsPayload(payload) {
});
}
function normalizeBackgroundTileId(value, idToSymbol = null) {
const normalizedId = String(value || "").trim();
if (!normalizedId) {
return "";
}
if (idToSymbol instanceof Map && idToSymbol.size > 0 && !idToSymbol.has(normalizedId)) {
return "";
}
return normalizedId;
}
function areRowsOnlyFillChar(rows, fillChar = ".") {
if (!Array.isArray(rows) || rows.length === 0) {
return true;
}
return rows.every((row) => {
const normalizedRow = String(row || "");
return normalizedRow.length === 0 || normalizedRow.split("").every((ch) => ch === fillChar);
});
}
function createDefaultColorCatalogEntries() {
return DEFAULT_COLOR_HEXES_ORDERED.map((hex, index) => {
const symbol = DEFAULT_COLOR_SYMBOLS_ORDERED[index] || `X${index}`;
@ -1030,70 +1026,14 @@ function readJsonSafe(fullPath, fallback) {
}
}
function toContentAbs(relPath) {
const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
return path.resolve(contentRoot, normalized);
}
function sanitizeWorldId(worldId) {
const raw = String(worldId || "").trim();
if (!raw) {
return "world";
}
return raw.replace(/[^a-zA-Z0-9_-]/g, "_");
}
function defaultWorldDirRel(worldId) {
return `worlds/${sanitizeWorldId(worldId)}`;
}
function buildWorldChunkFileName(chunkX, chunkY) {
return `${Math.floor(Number(chunkX) || 0)}_${Math.floor(Number(chunkY) || 0)}.json`;
}
function getWorldStoragePaths(worldEntryOrId) {
const worldId = typeof worldEntryOrId === "string"
? String(worldEntryOrId || "").trim()
: String(worldEntryOrId?.id || "").trim();
const worldDirRel = typeof worldEntryOrId === "string"
? defaultWorldDirRel(worldId)
: String(worldEntryOrId?.worldDir || defaultWorldDirRel(worldId));
const worldDirAbs = toContentAbs(worldDirRel);
const chunksDirRel = `${worldDirRel}/chunks`;
return {
worldId,
worldDirRel,
worldDirAbs,
worldJsonRel: `${worldDirRel}/world.json`,
worldJsonAbs: path.join(worldDirAbs, "world.json"),
bookmarksRel: `${worldDirRel}/bookmarks.json`,
bookmarksAbs: path.join(worldDirAbs, "bookmarks.json"),
chunksDirRel,
chunksDirAbs: path.join(worldDirAbs, "chunks"),
};
}
function normalizeWorldIndexEntry(entry) {
const id = sanitizeWorldId(entry?.id || "");
return {
id,
name: String(entry?.name || id || "World"),
worldDir: String(entry?.worldDir || defaultWorldDirRel(id)),
};
return buildWorldStoragePaths(contentRoot, worldEntryOrId);
}
function readWorldIndexPayload() {
const fallback = { schemaVersion: 1, worlds: [] };
const payload = readJsonSafe(worldsIndexPath, fallback);
const worlds = Array.isArray(payload?.worlds)
? payload.worlds
.filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
.map((entry) => normalizeWorldIndexEntry(entry))
: [];
return {
schemaVersion: typeof payload?.schemaVersion === "number" ? payload.schemaVersion : 1,
worlds,
};
return normalizeWorldIndexPayload(payload);
}
function normalizeWorldDefinitionPayload(payload, fallbackId = "") {
@ -1155,16 +1095,6 @@ function readWorldDefinitionPayload(worldId) {
);
}
function normalizeWorldBookmark(entry, index = 0) {
const fallbackId = `bookmark_${index + 1}`;
return {
id: String(entry?.id || fallbackId).trim() || fallbackId,
label: String(entry?.label || entry?.id || fallbackId).trim() || fallbackId,
x: Math.floor(Number(entry?.x) || 0),
y: Math.floor(Number(entry?.y) || 0),
};
}
function readWorldBookmarksPayload(worldId) {
const normalizedId = sanitizeWorldId(worldId);
const storage = getWorldStoragePaths(normalizedId);
@ -2164,59 +2094,11 @@ function injectNpcNodeDescriptions(payload, meta) {
}
function validatePayload(payload, type, rootKey) {
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
return "Payload must be an object";
}
if (typeof payload.schemaVersion !== "number") {
return "schemaVersion must be a number";
}
const allowedTopLevel = new Set(["schemaVersion", rootKey]);
const unknownTopLevel = Object.keys(payload).filter((key) => !allowedTopLevel.has(key));
if (unknownTopLevel.length > 0) {
return `Unsupported top-level keys for ${type}: ${unknownTopLevel.join(", ")}`;
}
if (!Array.isArray(payload[rootKey])) {
return `Missing array root: ${rootKey}`;
}
const idKey = REQUIRED_ID_KEY_BY_TYPE[type];
if (!idKey) {
return null;
}
const list = payload[rootKey];
for (let index = 0; index < list.length; index += 1) {
const entry = list[index];
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return `${rootKey}[${index}] must be an object`;
}
const idValue = String(entry[idKey] ?? "").trim();
if (!idValue) {
return `${rootKey}[${index}] is missing required key: ${idKey}`;
}
}
return null;
return validatePayloadShape(payload, type, rootKey, REQUIRED_ID_KEY_BY_TYPE);
}
function validateCatalogMetaPayload(payload) {
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
return "Catalog payload must be an object";
}
if (typeof payload.schemaVersion !== "number") {
return "schemaVersion must be a number";
}
const allowedTopLevel = new Set(["schemaVersion", ...FROZEN_CATALOG_KEYS]);
const unknownTopLevel = Object.keys(payload).filter((key) => !allowedTopLevel.has(key));
if (unknownTopLevel.length > 0) {
return `Unsupported catalog keys: ${unknownTopLevel.join(", ")}`;
}
for (const key of FROZEN_CATALOG_KEYS) {
if (!Array.isArray(payload[key])) {
return `${key} must be an array`;
}
}
return null;
return validateCatalogMetaPayloadShape(payload, FROZEN_CATALOG_KEYS);
}
function writeJsonAtomic(fullPath, data) {