349 lines
11 KiB
JavaScript
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
|
|
};
|