sc-dev-deploy/lib/entityIds.js
2026-06-01 16:43:43 -05:00

349 lines
11 KiB
JavaScript

// Stable UUIDs for Saltcorn metadata entities.
//
// Saltcorn core identifies entities by integer id and human name. dev-deploy
// needs a stable identity that survives across environments and renames, so we
// maintain a side-table _dd_entity_ids mapping (kind, current_id) -> uuid.
//
// First-run "backfill" assigns deterministic UUIDs (hash of namespace + kind
// + canonical name) so two environments installed from the same pack converge
// on the same UUIDs without a coordination step.
const db = require("@saltcorn/data/db");
const Table = require("@saltcorn/data/models/table");
const Field = require("@saltcorn/data/models/field");
const View = require("@saltcorn/data/models/view");
const Page = require("@saltcorn/data/models/page");
const Trigger = require("@saltcorn/data/models/trigger");
const Role = require("@saltcorn/data/models/role");
const Library = require("@saltcorn/data/models/library");
const Tag = require("@saltcorn/data/models/tag");
const TableConstraint = require("@saltcorn/data/models/table_constraints");
const PageGroup = require("@saltcorn/data/models/page_group");
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
const { deterministicUuid, randomUuid } = require("./ids");
const { ENTITY_KINDS } = require("./constants");
const assignNewUuid = async (kind, currentId, currentName, parentUuid) => {
const row = {
uuid: randomUuid(),
kind: kind,
current_name: currentName,
current_id: currentId,
parent_uuid: parentUuid || null,
created_at: new Date().toISOString()
};
await db.insert("_dd_entity_ids", row, { noid: true });
return row.uuid;
};
const canonicalKey = (kind, entity, parentName) => {
if (kind === ENTITY_KINDS.FIELD) {
return `${parentName || "?"}.${entity.name}`;
}
return entity.name;
};
// Stable fingerprint for a constraint, scoped to its parent table's UUID so
// it's globally unique across the journal. Two instances installed from the
// same base agree on this fingerprint and so derive identical UUIDs.
const constraintFingerprint = (con, parentUuid) => {
const cfg = con.configuration || {};
let detail;
if (con.type === "Unique" && cfg.fields) {
detail = [...cfg.fields].sort().join(",");
} else if (con.type === "Index") {
detail = cfg.field || "";
} else if (con.type === "Formula") {
detail = cfg.formula || "";
} else {
detail = JSON.stringify(cfg);
}
return `${parentUuid || "?"}|${con.type}|${detail}`;
};
// Friendly display name for _dd_entity_ids.current_name. Not used for identity.
const constraintDisplayName = (con) => {
const cfg = con.configuration || {};
if (con.type === "Unique") return `unique(${(cfg.fields || []).join(",")})`;
if (con.type === "Index") return `index(${cfg.field || ""})`;
if (con.type === "Formula") return `formula`;
return `${con.type}_${con.id || "new"}`;
};
const ensureUuid = async (kind, currentId, currentName, parentUuid, canonical) => {
const existing = await db.selectMaybeOne("_dd_entity_ids", { kind: kind, current_id: currentId });
if (existing) {
return existing.uuid;
}
const uuid = deterministicUuid(kind, canonical || currentName);
const row = {
uuid: uuid,
kind: kind,
current_name: currentName,
current_id: currentId,
parent_uuid: parentUuid || null,
created_at: new Date().toISOString()
};
await db.insert("_dd_entity_ids", row, { noid: true });
return uuid;
};
const lookupByCurrent = async (kind, currentId) => {
return await db.selectMaybeOne("_dd_entity_ids", { kind: kind, current_id: currentId });
};
const lookupByUuid = async (uuid) => {
return await db.selectMaybeOne("_dd_entity_ids", { uuid: uuid });
};
// Insert a row with a specific UUID (used by apply.js when ingesting an op
// that created an entity on a peer — we want to preserve the peer's UUID).
const adoptUuid = async (uuid, kind, currentId, currentName, parentUuid) => {
const row = {
uuid: uuid,
kind: kind,
current_name: currentName,
current_id: currentId,
parent_uuid: parentUuid || null,
created_at: new Date().toISOString()
};
await db.insert("_dd_entity_ids", row, { noid: true });
return uuid;
};
const updateName = async (kind, currentId, newName) => {
await db.updateWhere("_dd_entity_ids", { current_name: newName }, { kind: kind, current_id: currentId });
};
const removeEntityRow = async (kind, currentId) => {
await db.deleteWhere("_dd_entity_ids", { kind: kind, current_id: currentId });
};
// Backfill helpers: each returns the count of new UUIDs inserted.
const backfillTables = async () => {
const tables = await Table.find({}, { cached: false });
let added = 0;
for (const t of tables) {
const before = await lookupByCurrent(ENTITY_KINDS.TABLE, t.id);
await ensureUuid(ENTITY_KINDS.TABLE, t.id, t.name, null, t.name);
if (!before) {
added += 1;
}
}
return added;
};
const backfillFields = async () => {
const tables = await Table.find({}, { cached: false });
let added = 0;
for (const t of tables) {
const tableUuidRow = await lookupByCurrent(ENTITY_KINDS.TABLE, t.id);
if (!tableUuidRow) {
continue;
}
const fields = t.getFields ? t.getFields() : (await Field.find({ table_id: t.id }));
for (const f of fields) {
const before = await lookupByCurrent(ENTITY_KINDS.FIELD, f.id);
await ensureUuid(ENTITY_KINDS.FIELD, f.id, f.name, tableUuidRow.uuid, `${t.name}.${f.name}`);
if (!before) {
added += 1;
}
}
}
return added;
};
const backfillViews = async () => {
const views = await View.find({});
let added = 0;
for (const v of views) {
const before = await lookupByCurrent(ENTITY_KINDS.VIEW, v.id);
let parentUuid = null;
if (v.table_id) {
const t = await lookupByCurrent(ENTITY_KINDS.TABLE, v.table_id);
parentUuid = t ? t.uuid : null;
}
await ensureUuid(ENTITY_KINDS.VIEW, v.id, v.name, parentUuid, v.name);
if (!before) {
added += 1;
}
}
return added;
};
const backfillPages = async () => {
const pages = await Page.find({});
let added = 0;
for (const p of pages) {
const before = await lookupByCurrent(ENTITY_KINDS.PAGE, p.id);
await ensureUuid(ENTITY_KINDS.PAGE, p.id, p.name, null, p.name);
if (!before) {
added += 1;
}
}
return added;
};
const backfillTriggers = async () => {
const triggers = Trigger.find({});
let added = 0;
for (const tr of triggers) {
const before = await lookupByCurrent(ENTITY_KINDS.TRIGGER, tr.id);
let parentUuid = null;
if (tr.table_id) {
const t = await lookupByCurrent(ENTITY_KINDS.TABLE, tr.table_id);
parentUuid = t ? t.uuid : null;
}
await ensureUuid(ENTITY_KINDS.TRIGGER, tr.id, tr.name || `trigger_${tr.id}`, parentUuid, tr.name || `trigger_${tr.id}`);
if (!before) {
added += 1;
}
}
return added;
};
const backfillRoles = async () => {
const roles = await Role.find({});
let added = 0;
for (const r of roles) {
const before = await lookupByCurrent(ENTITY_KINDS.ROLE, r.id);
await ensureUuid(ENTITY_KINDS.ROLE, r.id, r.role, null, r.role);
if (!before) {
added += 1;
}
}
return added;
};
const backfillLibrary = async () => {
const items = await Library.find({});
let added = 0;
for (const li of items) {
const before = await lookupByCurrent(ENTITY_KINDS.LIBRARY, li.id);
await ensureUuid(ENTITY_KINDS.LIBRARY, li.id, li.name, null, li.name);
if (!before) {
added += 1;
}
}
return added;
};
const backfillTags = async () => {
const tags = await Tag.find({});
let added = 0;
for (const tg of tags) {
const before = await lookupByCurrent(ENTITY_KINDS.TAG, tg.id);
await ensureUuid(ENTITY_KINDS.TAG, tg.id, tg.name, null, tg.name);
if (!before) {
added += 1;
}
}
return added;
};
const backfillTableConstraints = async () => {
const cons = await TableConstraint.find({});
let added = 0;
for (const c of cons) {
const before = await lookupByCurrent(ENTITY_KINDS.CONSTRAINT, c.id);
let parentUuid = null;
if (c.table_id) {
const t = await lookupByCurrent(ENTITY_KINDS.TABLE, c.table_id);
parentUuid = t ? t.uuid : null;
}
const canonical = constraintFingerprint(c, parentUuid);
await ensureUuid(ENTITY_KINDS.CONSTRAINT, c.id, constraintDisplayName(c), parentUuid, canonical);
if (!before) {
added += 1;
}
}
return added;
};
const backfillPageGroups = async () => {
const groups = await PageGroup.find({});
let added = 0;
for (const g of groups) {
const before = await lookupByCurrent(ENTITY_KINDS.PAGE_GROUP, g.id);
await ensureUuid(ENTITY_KINDS.PAGE_GROUP, g.id, g.name, null, g.name);
if (!before) {
added += 1;
}
}
return added;
};
const backfillWorkflowSteps = async () => {
const steps = await WorkflowStep.find({});
let added = 0;
for (const s of steps) {
const before = await lookupByCurrent(ENTITY_KINDS.WORKFLOW_STEP, s.id);
let parentUuid = null;
if (s.trigger_id) {
const tr = await lookupByCurrent(ENTITY_KINDS.TRIGGER, s.trigger_id);
parentUuid = tr ? tr.uuid : null;
}
const canonical = `${parentUuid || "?"}|${s.name || s.id}`;
await ensureUuid(ENTITY_KINDS.WORKFLOW_STEP, s.id, s.name || `step_${s.id}`, parentUuid, canonical);
if (!before) {
added += 1;
}
}
return added;
};
const backfillAll = async () => {
const counts = {};
counts.tables = await backfillTables();
counts.fields = await backfillFields();
counts.views = await backfillViews();
counts.pages = await backfillPages();
counts.triggers = await backfillTriggers();
counts.roles = await backfillRoles();
counts.library = await backfillLibrary();
counts.tags = await backfillTags();
counts.constraints = await backfillTableConstraints();
counts.page_groups = await backfillPageGroups();
counts.workflow_steps = await backfillWorkflowSteps();
return counts;
};
module.exports = {
backfillAll,
ensureUuid,
assignNewUuid,
lookupByCurrent,
lookupByUuid,
adoptUuid,
updateName,
removeEntityRow,
canonicalKey,
constraintFingerprint,
constraintDisplayName
};