144 lines
5.5 KiB
JavaScript
144 lines
5.5 KiB
JavaScript
// 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 "<fieldname>__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
|
|
};
|