// 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 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, onLoad: onLoad, routes: routes };