// Recursive walker for op payloads: translate file references between this // instance's local form (numeric id or relative path) and a portable // placeholder ("__dd_file_ref::"). Promote-time wraps call // toPlaceholders to journal portable payloads; apply-time handlers call // fromPlaceholders to resolve back to the target's local file path. // // Saltcorn's /files/serve route accepts either a numeric file id or a // relative path, so writing the path back on apply works regardless of which // form the source used. const db = require("@saltcorn/data/db"); const { lookupByUuid } = require("./entityIds"); // Common keys in page/view layout JSON that reference a file. const FILE_REF_KEYS = new Set([ "fileid", "file_id", "bgFileId", "image_id" ]); const PLACEHOLDER_PREFIX = "__dd_file_ref::"; // Look up a file entity by value (string path or numeric id). Returns the // entity_ids row or null. const lookupFileByValue = async (v) => { if (v === null || v === undefined || v === "") return null; // Match by current_name (relative path string) let row = await db.selectMaybeOne("_dd_entity_ids", { kind: "file", current_name: String(v) }); if (row) return row; // Also try numeric id, in case the source stored an integer id const asNum = Number(v); if (Number.isFinite(asNum)) { row = await db.selectMaybeOne("_dd_entity_ids", { kind: "file", current_id: asNum }); if (row) return row; } return null; }; // Walk obj, mutating in place. For each key in FILE_REF_KEYS with a non-empty // value, replace with the result of `transform(key, value)`. Async-aware. const transformFileRefs = async (obj, transform) => { if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { if (obj[i] && typeof obj[i] === "object") { await transformFileRefs(obj[i], transform); } } return; } if (!obj || typeof obj !== "object") return; for (const k of Object.keys(obj)) { const v = obj[k]; if (FILE_REF_KEYS.has(k) && v !== null && v !== undefined && v !== "" && typeof v !== "object") { obj[k] = await transform(k, v); } else if (v && typeof v === "object") { await transformFileRefs(v, transform); } } }; // Convert local file refs (id or path) -> portable placeholders. const toPlaceholders = async (payload) => { await transformFileRefs(payload, async (k, v) => { const ent = await lookupFileByValue(v); if (ent) return PLACEHOLDER_PREFIX + ent.uuid; return v; }); return payload; }; // Convert portable placeholders -> local file refs (write back as relative // path, which Saltcorn's /files/serve accepts). const fromPlaceholders = async (payload) => { await transformFileRefs(payload, async (k, v) => { if (typeof v !== "string") return v; if (!v.startsWith(PLACEHOLDER_PREFIX)) return v; const uuid = v.substring(PLACEHOLDER_PREFIX.length); const ent = await lookupByUuid(uuid); return ent ? ent.current_name : v; }); return payload; }; module.exports = { toPlaceholders, fromPlaceholders, PLACEHOLDER_PREFIX, FILE_REF_KEYS };