// Row payload translation: FK ids -> row uuids (on journal) and back (on apply). // // For each FK field on the row's table (field.is_fkey), look up the referenced // row's _dd_row_uuid. Store under "__uuid" to avoid colliding with // the original field name. On apply, resolve back to the local row id. // // FKs to user-mode tables are NULLed on the target, with a warning attached // to the payload so it surfaces in the admin UI. const db = require("@saltcorn/data/db"); const { getRowUuid, findIdByRowUuid, COLUMN_NAME } = require("./rowIdentity"); const { lookupByCurrent } = require("./entityIds"); const UUID_SUFFIX = "__uuid"; // Returns data_mode for the table with given current_id (Saltcorn table id). // 'user' (default), 'starter', or 'managed'. const tableModeByCurrentId = async (tableId) => { const ent = await lookupByCurrent("table", tableId); if (!ent) return "user"; const row = await db.selectMaybeOne("_dd_table_modes", { table_uuid: ent.uuid }); return row ? row.data_mode : "user"; }; const tableModeByUuid = async (tableUuid) => { if (!tableUuid) return "user"; const row = await db.selectMaybeOne("_dd_table_modes", { table_uuid: tableUuid }); return row ? row.data_mode : "user"; }; // rowData: a plain row from the table. // table: the Saltcorn Table instance whose fields define the FK shape. // Returns { portable: {...with __uuid keys for FKs}, warnings: [...] }. const rowToPortable = async (rowData, table) => { const portable = {}; const warnings = []; for (const field of table.fields || []) { if (field.name === COLUMN_NAME || field.name === "id") continue; const v = rowData[field.name]; if (field.is_fkey && field.reftable_name && v !== null && v !== undefined) { const refMode = await tableModeByCurrentId_byName(field.reftable_name); if (refMode === "managed" || refMode === "starter") { const refUuid = await getRowUuid(field.reftable_name, v); if (refUuid) { portable[field.name + UUID_SUFFIX] = refUuid; } else { portable[field.name + UUID_SUFFIX] = null; warnings.push(`${field.name}: source row references ${field.reftable_name}.id=${v} but it has no _dd_row_uuid yet`); } } else { // FK into a user-mode table — can't translate. Drop and warn. portable[field.name + UUID_SUFFIX] = null; warnings.push(`${field.name} → user-mode table ${field.reftable_name}; FK will be null on target`); } } else { portable[field.name] = v; } } return { portable: portable, warnings: warnings }; }; // portable: payload from a journaled row op. // table: the Saltcorn Table instance on the local (target) side. const portableToRow = async (portable, table) => { const row = {}; for (const field of table.fields || []) { if (field.name === COLUMN_NAME || field.name === "id") continue; const uuidKey = field.name + UUID_SUFFIX; if (uuidKey in portable) { const refUuid = portable[uuidKey]; if (!refUuid) { row[field.name] = null; } else if (field.is_fkey && field.reftable_name) { const localId = await findIdByRowUuid(field.reftable_name, refUuid); row[field.name] = localId; // may be null if target doesn't have that row yet } else { row[field.name] = null; } } else if (field.name in portable) { row[field.name] = portable[field.name]; } } return row; }; // Helper: lookup mode by table name (via entity_ids). const tableModeByCurrentId_byName = async (tableName) => { const Table = require("@saltcorn/data/models/table"); const t = Table.findOne({ name: tableName }); if (!t) return "user"; return await tableModeByCurrentId(t.id); }; // True if a starter table has already had its initial-ship completed; managed // tables ignore this (they always journal). const isStarterShipped = async (tableUuid) => { const row = await db.selectMaybeOne("_dd_table_modes", { table_uuid: tableUuid }); return !!(row && row.starter_shipped_at); }; const markStarterShipped = async (tableUuid) => { await db.updateWhere("_dd_table_modes", { starter_shipped_at: new Date().toISOString() }, { table_uuid: tableUuid }); }; // For a given Saltcorn Table id, returns { mode, tableUuid, shouldJournal }. // shouldJournal is the wrap-level decision: true → record the row op; false → // pass through silently. const journalDecision = async (tableId) => { const { lookupByCurrent } = require("./entityIds"); const ent = await lookupByCurrent("table", tableId); if (!ent) return { mode: "user", tableUuid: null, shouldJournal: false }; const mode = await tableModeByUuid(ent.uuid); if (mode === "managed") { return { mode: mode, tableUuid: ent.uuid, shouldJournal: true }; } if (mode === "starter") { const shipped = await isStarterShipped(ent.uuid); return { mode: mode, tableUuid: ent.uuid, shouldJournal: !shipped }; } return { mode: "user", tableUuid: ent.uuid, shouldJournal: false }; }; module.exports = { rowToPortable, portableToRow, tableModeByCurrentId, tableModeByUuid, tableModeByCurrentId_byName, isStarterShipped, markStarterShipped, journalDecision, UUID_SUFFIX };