151 lines
6.3 KiB
JavaScript
151 lines
6.3 KiB
JavaScript
// Schema setup for site-sync's plugin tables.
|
|
//
|
|
// Three storage strategies, chosen by WHEN the state is written, so each survives
|
|
// Saltcorn backup/restore without fighting the plugin lifecycle:
|
|
//
|
|
// * ENV IDENTITY -> _sc_config (lib/env.js + lib/common/configStore.js). Created during
|
|
// onLoad; _sc_config is restored BEFORE onLoad, so a restored host keeps its
|
|
// env_id and onLoad does not create a duplicate.
|
|
//
|
|
// * PEERS + TABLE MODES -> REGISTERED Saltcorn tables (auto `id` PK + the logical
|
|
// keys kept as unique fields). Written only at runtime (not during
|
|
// onLoad/restore), so being in _sc_tables they ride backup. Peer SECRETS must
|
|
// survive backup, so they belong here, not in a raw table.
|
|
//
|
|
// * NONCE STORE (_ss_seen_nonces), PERSISTED PLANS (_ss_plans), rollback
|
|
// RESTORE POINTS (_ss_rollback) and the APPLY LOCK (_ss_apply_lock) stay RAW:
|
|
// they are a replay cache / short-lived plan tokens / short-lived restore points
|
|
// / a lease, so losing them on a restored host is acceptable, and they must
|
|
// never carry must-survive state.
|
|
|
|
const db = require("@saltcorn/data/db");
|
|
|
|
const { PLUGIN_NAME, PEERS_TABLE, SEEN_NONCES_TABLE, APPLY_LOCK_TABLE } = require("./constants");
|
|
const { PEER_TABLE_FIELDS } = require("./common/peerStore");
|
|
const { createNonceStore } = require("./common/peerServer");
|
|
const { ensureRegisteredTable, physicalColumns } = require("./common/registeredTable");
|
|
|
|
|
|
// Field definitions (excluding the auto `id` PK) of the registered table-modes
|
|
// table (the peers table's come from lib/common/peerStore.js).
|
|
const TABLE_MODES_FIELDS = [
|
|
{ name: "table_name", type: "String", is_unique: true, required: true },
|
|
{ name: "data_mode", type: "String", required: true },
|
|
{ name: "updated_at", type: "String", required: true },
|
|
{ name: "starter_shipped_at", type: "String" }
|
|
];
|
|
|
|
|
|
// Make `name` a registered Saltcorn table with an auto `id` PK plus `fieldDefs`
|
|
// (lib/common/registeredTable.js: decided from _sc_tables, never the stale
|
|
// per-process cache; an unregistered stray table of that name is adopted with
|
|
// its rows, keyed by keyField; racing workers are serialized). A cheap no-op
|
|
// once registered. site-sync has no CRUD wrap layer, so (unlike dev-deploy)
|
|
// there is nothing to suppress here.
|
|
const registerTable = async (name, fieldDefs, keyField) => {
|
|
await ensureRegisteredTable({ owner: PLUGIN_NAME, name: name, fields: fieldDefs, keyField: keyField });
|
|
};
|
|
|
|
|
|
// Idempotent migration: add `column` to a raw table created before it existed (the
|
|
// CREATE TABLE IF NOT EXISTS leaves such a table as it was). Workers booting
|
|
// together race here; losing the race (the column exists now) is fine.
|
|
const addColumnIfMissing = async (table, column, sqlType) => {
|
|
if ((await physicalColumns(table)).has(column)) {
|
|
return;
|
|
}
|
|
try {
|
|
await db.query(`ALTER TABLE ${db.getTenantSchemaPrefix()}${table} ADD COLUMN ${column} ${sqlType}`);
|
|
} catch (e) {
|
|
if (!(await physicalColumns(table)).has(column)) {
|
|
throw e;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// Persisted plans for the two-phase preview -> apply flow. A plan is computed on
|
|
// the mutating (target) side, previewed, then applied by plan_id. Raw + regenerable
|
|
// (a lost plan just has to be re-previewed). desired_pack + plan_json are TEXT JSON.
|
|
// claimed_from = the status the last apply / rollback claimed the plan from: where
|
|
// lib/siteLock.js returns a plan whose apply or rollback was abandoned.
|
|
const createPlans = async () => {
|
|
const schema = db.getTenantSchemaPrefix();
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS ${schema}_ss_plans (
|
|
plan_id TEXT PRIMARY KEY,
|
|
peer_id INTEGER,
|
|
source_env_id TEXT,
|
|
direction TEXT,
|
|
desired_pack TEXT,
|
|
plan_json TEXT,
|
|
precondition_hash TEXT,
|
|
source_hash TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
claimed_from TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
await db.query(`CREATE INDEX IF NOT EXISTS _ss_plans_created ON ${schema}_ss_plans (created_at)`);
|
|
await addColumnIfMissing("_ss_plans", "claimed_from", "TEXT");
|
|
};
|
|
|
|
|
|
// The site-wide apply lock (lib/siteLock.js): at most one row per tenant schema,
|
|
// the holder's lease ending at expires_ms (epoch milliseconds).
|
|
const createApplyLock = async () => {
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS ${db.getTenantSchemaPrefix()}${APPLY_LOCK_TABLE} (
|
|
lock_name TEXT PRIMARY KEY,
|
|
holder TEXT NOT NULL,
|
|
operation TEXT NOT NULL,
|
|
plan_id TEXT,
|
|
acquired_at TEXT NOT NULL,
|
|
expires_ms BIGINT NOT NULL
|
|
)
|
|
`);
|
|
};
|
|
|
|
|
|
// 0.0.1 created a per-peer last-applied-hash table that nothing ever read; drop it.
|
|
const dropLegacyPeerState = async () => {
|
|
await db.query(`DROP TABLE IF EXISTS ${db.getTenantSchemaPrefix()}_ss_peer_state`);
|
|
};
|
|
|
|
|
|
// Pre-apply restore points for rollback: the pre-apply metadata pack + raw rows of
|
|
// the tables an apply will touch/drop, keyed by plan_id. Raw + short-lived (a lost
|
|
// restore point just means that apply can no longer be one-click reverted).
|
|
const createRollback = async () => {
|
|
const schema = db.getTenantSchemaPrefix();
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS ${schema}_ss_rollback (
|
|
plan_id TEXT PRIMARY KEY,
|
|
restore_pack TEXT,
|
|
restore_rows TEXT,
|
|
restore_files TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
`);
|
|
// Without the column (an install created before it) every saveRestorePoint
|
|
// INSERT would throw and silently disable ALL rollback.
|
|
await addColumnIfMissing("_ss_rollback", "restore_files", "TEXT");
|
|
};
|
|
|
|
|
|
const createAllTables = async () => {
|
|
// Backed-up state -> registered Saltcorn tables.
|
|
await registerTable(PEERS_TABLE, PEER_TABLE_FIELDS, "env_id");
|
|
await registerTable("_ss_table_modes", TABLE_MODES_FIELDS, "table_name");
|
|
// Regenerable -> raw tables.
|
|
await createNonceStore(SEEN_NONCES_TABLE);
|
|
await createPlans();
|
|
await createRollback();
|
|
await createApplyLock();
|
|
await dropLegacyPeerState();
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
createAllTables
|
|
};
|