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

161 lines
6 KiB
JavaScript

// DDL for dev-deploy's six plugin tables.
//
// Saltcorn supports SQLite and PostgreSQL. We use a portable subset:
// - TEXT for all strings, JSON payloads, ISO 8601 timestamps
// - INTEGER for booleans (0/1) and surrogate ids
// - No JSONB; payloads are TEXT containing JSON, parsed/stringified at the
// application layer
//
// Tables are created idempotently in onLoad. Re-running is safe.
const db = require("@saltcorn/data/db");
const createDdEnv = async () => {
const schema = db.getTenantSchemaPrefix();
await db.query(`
CREATE TABLE IF NOT EXISTS ${schema}_dd_env (
env_id TEXT PRIMARY KEY,
env_label TEXT,
on_destructive_op TEXT NOT NULL DEFAULT 'confirm',
require_tls INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
bootstrapped_at TEXT
)
`);
};
const createDdPeers = async () => {
// Secret components stored as hex TEXT rather than BLOB: Saltcorn's
// SQLite insert layer JSON-stringifies any object value, which would
// mangle Buffer columns. Hex is portable and survives that path intact.
const schema = db.getTenantSchemaPrefix();
// Portable auto-increment PK: sqlite "integer primary key" auto-assigns
// rowids; postgres uses "serial" (AUTOINCREMENT is sqlite-only syntax).
const serial = db.isSQLite ? "integer" : "serial";
await db.query(`
CREATE TABLE IF NOT EXISTS ${schema}_dd_peers (
peer_id ${serial} PRIMARY KEY,
env_id TEXT NOT NULL UNIQUE,
label TEXT,
base_url TEXT NOT NULL,
peer_secret_ciphertext TEXT,
peer_secret_iv TEXT,
peer_secret_tag TEXT,
require_tls INTEGER,
created_at TEXT NOT NULL,
last_seen_at TEXT
)
`);
};
const createDdEntityIds = async () => {
const schema = db.getTenantSchemaPrefix();
await db.query(`
CREATE TABLE IF NOT EXISTS ${schema}_dd_entity_ids (
uuid TEXT PRIMARY KEY,
kind TEXT NOT NULL,
current_name TEXT NOT NULL,
current_id INTEGER NOT NULL,
parent_uuid TEXT,
created_at TEXT NOT NULL,
UNIQUE (kind, current_id)
)
`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_entity_ids_kind_name ON ${schema}_dd_entity_ids (kind, current_name)`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_entity_ids_parent ON ${schema}_dd_entity_ids (parent_uuid)`);
};
const createDdOps = async () => {
const schema = db.getTenantSchemaPrefix();
await db.query(`
CREATE TABLE IF NOT EXISTS ${schema}_dd_ops (
op_id TEXT PRIMARY KEY,
source_env_id TEXT NOT NULL,
op_type TEXT NOT NULL,
entity_kind TEXT,
entity_uuid TEXT,
payload TEXT NOT NULL,
parent_op_id TEXT,
correlation_id TEXT,
schema_version INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
applied_at TEXT,
status TEXT NOT NULL DEFAULT 'committed',
conflict_with_op_id TEXT
)
`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_ops_created ON ${schema}_dd_ops (created_at)`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_ops_source ON ${schema}_dd_ops (source_env_id, created_at)`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_ops_entity ON ${schema}_dd_ops (entity_uuid)`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_ops_correlation ON ${schema}_dd_ops (correlation_id)`);
await db.query(`CREATE INDEX IF NOT EXISTS _dd_ops_status ON ${schema}_dd_ops (status) WHERE status = 'conflict'`).catch(() => {});
// Idempotent migration for instances installed before conflict_with_op_id
// existed. On PG a failed statement poisons the surrounding transaction, so a
// bare ALTER caught in JS is not enough -- use ADD COLUMN IF NOT EXISTS there;
// sqlite lacks that clause, so run the bare ALTER and swallow the error.
if (db.isSQLite) {
try {
await db.query(`ALTER TABLE ${schema}_dd_ops ADD COLUMN conflict_with_op_id TEXT`);
} catch (e) {
// column already exists; ignore
}
} else {
await db.query(`ALTER TABLE ${schema}_dd_ops ADD COLUMN IF NOT EXISTS conflict_with_op_id TEXT`);
}
};
const createDdAnchors = async () => {
const schema = db.getTenantSchemaPrefix();
await db.query(`
CREATE TABLE IF NOT EXISTS ${schema}_dd_anchors (
peer_id INTEGER NOT NULL,
direction TEXT NOT NULL,
last_op_id TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (peer_id, direction)
)
`);
};
const createDdTableModes = async () => {
const schema = db.getTenantSchemaPrefix();
await db.query(`
CREATE TABLE IF NOT EXISTS ${schema}_dd_table_modes (
table_uuid TEXT PRIMARY KEY,
data_mode TEXT NOT NULL,
updated_at TEXT NOT NULL,
starter_shipped_at TEXT
)
`);
// Idempotent migration for older installs that lack starter_shipped_at.
// (See createDdOps: PG needs IF NOT EXISTS or a failed ALTER poisons the txn.)
if (db.isSQLite) {
try {
await db.query(`ALTER TABLE ${schema}_dd_table_modes ADD COLUMN starter_shipped_at TEXT`);
} catch (e) { /* column exists */ }
} else {
await db.query(`ALTER TABLE ${schema}_dd_table_modes ADD COLUMN IF NOT EXISTS starter_shipped_at TEXT`);
}
};
const createAllTables = async () => {
await createDdEnv();
await createDdPeers();
await createDdEntityIds();
await createDdOps();
await createDdAnchors();
await createDdTableModes();
};
module.exports = {
createAllTables
};