161 lines
6 KiB
JavaScript
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
|
|
};
|