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

3
server/contentTransforms.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export function normalizeBackgroundTileId(value: unknown, idToSymbol?: Map<string, unknown> | null): string;
export function areRowsOnlyFillChar(rows: unknown, fillChar?: string): boolean;
export function resolveContentPath(contentRoot: string, relativePath: string): string;

View file

@ -0,0 +1,27 @@
import path from "path";
export 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;
}
export 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);
});
}
export function resolveContentPath(contentRoot, relativePath) {
const normalized = String(relativePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
return path.resolve(contentRoot, normalized);
}

10
server/validation.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
export function validatePayload(
payload: unknown,
type: string,
rootKey: string,
requiredIdKeyByType: Record<string, string>,
): string | null;
export function validateCatalogMetaPayload(
payload: unknown,
frozenCatalogKeys: string[],
): string | null;

55
server/validation.js Normal file
View file

@ -0,0 +1,55 @@
export function validatePayload(payload, type, rootKey, requiredIdKeyByType) {
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 = requiredIdKeyByType[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;
}
export function validateCatalogMetaPayload(payload, frozenCatalogKeys) {
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", ...frozenCatalogKeys]);
const unknownTopLevel = Object.keys(payload).filter((key) => !allowedTopLevel.has(key));
if (unknownTopLevel.length > 0) {
return `Unsupported catalog keys: ${unknownTopLevel.join(", ")}`;
}
for (const key of frozenCatalogKeys) {
if (!Array.isArray(payload[key])) {
return `${key} must be an array`;
}
}
return null;
}

36
server/worldTransforms.d.ts vendored Normal file
View file

@ -0,0 +1,36 @@
export function sanitizeWorldId(worldId: unknown): string;
export function defaultWorldDirRel(worldId: unknown): string;
export function buildWorldChunkFileName(chunkX: unknown, chunkY: unknown): string;
export function getWorldStoragePaths(
contentRoot: string,
worldEntryOrId: string | { id?: unknown; worldDir?: unknown },
): {
worldId: string;
worldDirRel: string;
worldDirAbs: string;
worldJsonRel: string;
worldJsonAbs: string;
bookmarksRel: string;
bookmarksAbs: string;
chunksDirRel: string;
chunksDirAbs: string;
};
export function normalizeWorldIndexEntry(entry: { id?: unknown; name?: unknown; worldDir?: unknown } | null | undefined): {
id: string;
name: string;
worldDir: string;
};
export function normalizeWorldIndexPayload(payload: unknown): {
schemaVersion: number;
worlds: Array<{
id: string;
name: string;
worldDir: string;
}>;
};
export function normalizeWorldBookmark(entry: { id?: unknown; label?: unknown; x?: unknown; y?: unknown } | null | undefined, index?: number): {
id: string;
label: string;
x: number;
y: number;
};

71
server/worldTransforms.js Normal file
View file

@ -0,0 +1,71 @@
import path from "path";
import { resolveContentPath } from "./contentTransforms.js";
export function sanitizeWorldId(worldId) {
const raw = String(worldId || "").trim();
if (!raw) {
return "world";
}
return raw.replace(/[^a-zA-Z0-9_-]/g, "_");
}
export function defaultWorldDirRel(worldId) {
return `worlds/${sanitizeWorldId(worldId)}`;
}
export function buildWorldChunkFileName(chunkX, chunkY) {
return `${Math.floor(Number(chunkX) || 0)}_${Math.floor(Number(chunkY) || 0)}.json`;
}
export function getWorldStoragePaths(contentRoot, 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 = resolveContentPath(contentRoot, 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"),
};
}
export function normalizeWorldIndexEntry(entry) {
const id = sanitizeWorldId(entry?.id || "");
return {
id,
name: String(entry?.name || id || "World"),
worldDir: String(entry?.worldDir || defaultWorldDirRel(id)),
};
}
export function normalizeWorldIndexPayload(payload) {
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,
};
}
export 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),
};
}