117 lines
4.6 KiB
JavaScript
117 lines
4.6 KiB
JavaScript
// dev-deploy: Saltcorn plugin for migrating metadata changes across
|
|
// Dev/Test/Prod environments via an ops journal with stable UUIDs.
|
|
|
|
const db = require("@saltcorn/data/db");
|
|
|
|
const { PLUGIN_NAME, PLUGIN_VERSION, DATA_MODES } = require("./lib/constants");
|
|
const { createAllTables } = require("./lib/schema");
|
|
const { getEnv, initEnvIfMissing, markBootstrapped } = require("./lib/env");
|
|
const { backfillAll } = require("./lib/entityIds");
|
|
const { ensureManagedSchema } = require("./lib/rowIdentity");
|
|
const { installAllWraps } = require("./lib/wrap");
|
|
const { routes } = require("./lib/routes");
|
|
const { configurationWorkflow } = require("./lib/configWorkflow");
|
|
|
|
|
|
const log = (msg) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[${PLUGIN_NAME}] ${msg}`);
|
|
};
|
|
|
|
|
|
const ensureCsrfBypass = async () => {
|
|
try {
|
|
const { getState } = require("@saltcorn/data/db/state");
|
|
const current = getState().getConfig("disable_csrf_routes", "");
|
|
const want = "/dev-deploy/api/";
|
|
const entries = current.split(",").map((s) => s.trim()).filter(Boolean);
|
|
if (!entries.includes(want)) {
|
|
entries.push(want);
|
|
await getState().setConfig("disable_csrf_routes", entries.join(","));
|
|
}
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`[${PLUGIN_NAME}] failed to register csrf bypass:`, e);
|
|
}
|
|
};
|
|
|
|
|
|
// One-time migration: managed/starter tables created by an older plugin version
|
|
// carry a RAW _dd_row_uuid column (added via ALTER, not registered in _sc_fields).
|
|
// Such a column is dumped by backup but absent from the restored table's field
|
|
// list, which fails the row import and rolls back the whole table (app-data
|
|
// loss). Adopt every existing managed table's column into _sc_fields so it
|
|
// round-trips. ensureManagedSchema is idempotent: a no-op once registered.
|
|
const migrateRowUuidFields = async () => {
|
|
const schema = db.getTenantSchemaPrefix();
|
|
let rows;
|
|
try {
|
|
rows = await db.query(
|
|
`SELECT e.current_name AS name
|
|
FROM ${schema}_dd_table_modes m
|
|
JOIN ${schema}_dd_entity_ids e ON e.uuid = m.table_uuid
|
|
WHERE e.kind = 'table'
|
|
AND m.data_mode IN ($1, $2)`,
|
|
[DATA_MODES.MANAGED, DATA_MODES.STARTER]
|
|
);
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
let adopted = 0;
|
|
for (const r of rows.rows) {
|
|
try {
|
|
const res = await ensureManagedSchema(r.name);
|
|
if (res && res.adopted) {
|
|
adopted++;
|
|
}
|
|
} catch (e) {
|
|
console.error(`[${PLUGIN_NAME}] row-uuid field migration failed for ${r.name}:`, e.message);
|
|
}
|
|
}
|
|
return adopted;
|
|
};
|
|
|
|
|
|
const onLoad = async (cfg) => {
|
|
try {
|
|
await createAllTables();
|
|
const env = await initEnvIfMissing();
|
|
// Reconcile the entity-id cache on EVERY load, not just first bootstrap.
|
|
// _dd_entity_ids is a raw, un-backed-up cache; after a restore it is empty
|
|
// while the restored env reports bootstrapped, so a gated backfill would
|
|
// leave the map empty. backfillAll is idempotent (ensureUuid keys on
|
|
// (kind,current_id) and derives deterministic UUIDs from name), so running
|
|
// it always rebuilds the map against the restored host's live catalog.
|
|
const counts = await backfillAll();
|
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
if (!env.bootstrapped_at) {
|
|
await markBootstrapped(env.env_id);
|
|
log(`v${PLUGIN_VERSION} bootstrapped env_id=${env.env_id} backfilled ${total} entities ${JSON.stringify(counts)}`);
|
|
} else if (total > 0) {
|
|
log(`v${PLUGIN_VERSION} loaded env_id=${env.env_id}; reconciled entity-id cache (+${total})`);
|
|
} else {
|
|
log(`v${PLUGIN_VERSION} loaded env_id=${env.env_id}`);
|
|
}
|
|
installAllWraps();
|
|
const adopted = await migrateRowUuidFields();
|
|
if (adopted > 0) {
|
|
log(`migrated ${adopted} managed table(s) to a registered _dd_row_uuid field`);
|
|
}
|
|
await ensureCsrfBypass();
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`[${PLUGIN_NAME}] onLoad failed:`, err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
sc_plugin_api_version: 1,
|
|
plugin_name: PLUGIN_NAME,
|
|
configuration_workflow: configurationWorkflow,
|
|
onLoad: onLoad,
|
|
// With configuration_workflow present, Saltcorn invokes capability keys as
|
|
// (cfg)=>value (state.ts withCfg), so routes must be a function, not the array.
|
|
routes: () => routes
|
|
};
|