import type { Db } from './driver.js'; interface Migration { version: number; name: string; up: string; } /** * Forward-only migrations for one workspace's database. Each runs once, inside * a transaction, recorded in `_wellformed_migration`. Never edit a migration that * has shipped — add another. * * That rule is why version 1 still creates `_wellformed_user` and `_wellformed_session` * only for version 2 to drop them. Accounts and sessions moved to the control * database when Wellformed became multi-tenant, and an install that predates the * move needs its users read out of the old tables before they go — see * `db/adopt.ts`. A fresh workspace pays two redundant CREATEs for that. */ const MIGRATIONS: Migration[] = [ { version: 1, name: 'core metadata', up: ` CREATE TABLE _wellformed_table ( name TEXT PRIMARY KEY, label TEXT NOT NULL, display_column TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE _wellformed_column ( table_name TEXT NOT NULL REFERENCES _wellformed_table(name) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, options TEXT, target_table TEXT, expr TEXT, format TEXT, required INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (table_name, name) ); CREATE TABLE _wellformed_view ( id TEXT PRIMARY KEY, table_name TEXT NOT NULL REFERENCES _wellformed_table(name) ON DELETE CASCADE, name TEXT NOT NULL, filters TEXT NOT NULL DEFAULT '[]', sort TEXT, position INTEGER NOT NULL DEFAULT 0, system INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX idx_view_table ON _wellformed_view(table_name, position); CREATE TABLE _wellformed_rule ( id TEXT PRIMARY KEY, table_name TEXT NOT NULL REFERENCES _wellformed_table(name) ON DELETE CASCADE, column_name TEXT NOT NULL, equals TEXT NOT NULL, label TEXT NOT NULL, actions TEXT NOT NULL DEFAULT '[]', enabled INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX idx_rule_table ON _wellformed_rule(table_name, column_name); CREATE TABLE _wellformed_user ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'viewer', password_hash TEXT NOT NULL, disabled INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_seen_at TEXT ); CREATE TABLE _wellformed_session ( token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES _wellformed_user(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (datetime('now')), expires_at TEXT NOT NULL, user_agent TEXT ); CREATE INDEX idx_session_user ON _wellformed_session(user_id); CREATE TABLE _wellformed_audit ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL DEFAULT (datetime('now')), user_id TEXT, who TEXT NOT NULL, kind TEXT NOT NULL, table_name TEXT, row_id TEXT, row_label TEXT, column_name TEXT, old_value TEXT, new_value TEXT ); CREATE INDEX idx_audit_recent ON _wellformed_audit(id DESC); CREATE INDEX idx_audit_table ON _wellformed_audit(table_name, id DESC); CREATE INDEX idx_audit_row ON _wellformed_audit(table_name, row_id, id DESC); `, }, { version: 2, name: 'identity moves to the control plane', up: ` DROP TABLE IF EXISTS _wellformed_session; DROP TABLE IF EXISTS _wellformed_user; `, }, { version: 3, name: 'audit records which agent a change came through', up: ` /* * The label of the agent key a change arrived on, or null for a person in * the browser. It sits beside 'who' rather than replacing it: an * assistant acts as its holder, with their role, on a key they made — so * the account stays accountable and this is the disclosure. * * Null on every row written before this, which is the honest reading of * "there were no agents yet". */ ALTER TABLE _wellformed_audit ADD COLUMN via TEXT; `, }, { version: 4, name: 'views carry an aggregate row', up: ` /* * Column name → aggregate function, for the row pinned under the grid. * It belongs to the view for the same reason the filters do: the total * of "Unpaid" is a different number from the total of everything, and * whoever saved the view meant the one they were looking at. * * '{}' on every view that predates this, which is "no summaries" — the * footer stays empty until somebody asks for something. */ ALTER TABLE _wellformed_view ADD COLUMN summaries TEXT NOT NULL DEFAULT '{}'; `, }, { version: 5, name: 'columns can be unique', up: ` /* * Whether a column refuses a value another row already holds. * * Only the catalogue's record of it lives here; the constraint itself is * a real UNIQUE INDEX on the user table, named "_uq_