// 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 };