144 lines
6 KiB
MySQL
144 lines
6 KiB
MySQL
|
|
-- go-bananas schema.
|
||
|
|
--
|
||
|
|
-- Design notes
|
||
|
|
-- ------------
|
||
|
|
-- * Timestamps are TEXT in RFC 3339 (see domain.TimeFormat). SQLite has no
|
||
|
|
-- native date type, and storing a sortable string removes every date-parsing
|
||
|
|
-- ambiguity from ORDER BY and range filters.
|
||
|
|
-- * JSON columns are TEXT holding a JSON document. The token document is read
|
||
|
|
-- and written as a whole value, never queried into, so a JSON column is the
|
||
|
|
-- correct shape: it avoids a join table whose only purpose would be to
|
||
|
|
-- reassemble a tree the application always wants whole.
|
||
|
|
-- * Every table a consumer might read carries an explicit covering index.
|
||
|
|
|
||
|
|
PRAGMA foreign_keys = ON;
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------- themes ----
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS themes (
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
slug TEXT NOT NULL,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT NOT NULL DEFAULT '',
|
||
|
|
status TEXT NOT NULL DEFAULT 'draft'
|
||
|
|
CHECK (status IN ('draft', 'published', 'archived')),
|
||
|
|
default_appearance TEXT NOT NULL DEFAULT 'light'
|
||
|
|
CHECK (default_appearance IN ('light', 'dark')),
|
||
|
|
tokens_json TEXT NOT NULL,
|
||
|
|
meta_json TEXT NOT NULL DEFAULT '{}',
|
||
|
|
is_system INTEGER NOT NULL DEFAULT 0 CHECK (is_system IN (0, 1)),
|
||
|
|
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
||
|
|
created_at TEXT NOT NULL,
|
||
|
|
updated_at TEXT NOT NULL,
|
||
|
|
created_by TEXT NOT NULL DEFAULT '',
|
||
|
|
updated_by TEXT NOT NULL DEFAULT ''
|
||
|
|
);
|
||
|
|
|
||
|
|
-- The runtime API resolves by slug on every page load, so this index is the
|
||
|
|
-- single most important one in the schema.
|
||
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_themes_slug ON themes (slug);
|
||
|
|
|
||
|
|
-- Listing is always ordered by recency within a status filter.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_themes_status_updated
|
||
|
|
ON themes (status, updated_at DESC);
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_themes_updated ON themes (updated_at DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_themes_created ON themes (created_at DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_themes_name ON themes (name COLLATE NOCASE);
|
||
|
|
|
||
|
|
-- Free-text search over slug, name and description. SQLite's LIKE with a
|
||
|
|
-- leading wildcard cannot use an index, so this stays a scan; the table is
|
||
|
|
-- small by construction (themes are hand-authored, not machine-generated) and
|
||
|
|
-- the scan is served from the page cache.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_themes_search
|
||
|
|
ON themes (name COLLATE NOCASE, slug, description);
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------- theme_versions --
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS theme_versions (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
theme_id TEXT NOT NULL REFERENCES themes (id) ON DELETE CASCADE,
|
||
|
|
version INTEGER NOT NULL,
|
||
|
|
slug TEXT NOT NULL,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT NOT NULL DEFAULT '',
|
||
|
|
status TEXT NOT NULL,
|
||
|
|
default_appearance TEXT NOT NULL,
|
||
|
|
tokens_json TEXT NOT NULL,
|
||
|
|
message TEXT NOT NULL DEFAULT '',
|
||
|
|
author TEXT NOT NULL DEFAULT '',
|
||
|
|
created_at TEXT NOT NULL,
|
||
|
|
UNIQUE (theme_id, version)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Rollback resolves a theme's history newest-first.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_versions_theme
|
||
|
|
ON theme_versions (theme_id, version DESC);
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------- api_keys --
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
-- prefix is the non-secret leading segment shown in the UI so a key can be
|
||
|
|
-- identified after its secret has been forgotten.
|
||
|
|
prefix TEXT NOT NULL,
|
||
|
|
-- secret_hash is a SHA-256 of the full key. The plaintext is never stored,
|
||
|
|
-- so a database disclosure does not yield usable credentials.
|
||
|
|
secret_hash TEXT NOT NULL,
|
||
|
|
scopes TEXT NOT NULL,
|
||
|
|
identity TEXT NOT NULL DEFAULT '',
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TEXT NOT NULL,
|
||
|
|
last_used_at TEXT,
|
||
|
|
expires_at TEXT,
|
||
|
|
revoked_at TEXT
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Lookup by secret hash happens on every machine request.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (secret_hash);
|
||
|
|
-- The UI lists live keys newest-first and hides revoked ones by default.
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_live ON api_keys (revoked_at, created_at DESC);
|
||
|
|
|
||
|
|
-- ----------------------------------------------------------------- users ----
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS users (
|
||
|
|
subject TEXT PRIMARY KEY,
|
||
|
|
email TEXT NOT NULL DEFAULT '',
|
||
|
|
display_name TEXT NOT NULL DEFAULT '',
|
||
|
|
roles TEXT NOT NULL DEFAULT '[]',
|
||
|
|
method TEXT NOT NULL DEFAULT '',
|
||
|
|
first_seen TEXT NOT NULL,
|
||
|
|
last_login TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
|
||
|
|
|
||
|
|
-- ------------------------------------------------------------- audit_log ----
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
at TEXT NOT NULL,
|
||
|
|
actor TEXT NOT NULL DEFAULT '',
|
||
|
|
actor_email TEXT NOT NULL DEFAULT '',
|
||
|
|
action TEXT NOT NULL,
|
||
|
|
entity_type TEXT NOT NULL DEFAULT '',
|
||
|
|
entity_id TEXT NOT NULL DEFAULT '',
|
||
|
|
summary TEXT NOT NULL DEFAULT '',
|
||
|
|
detail_json TEXT NOT NULL DEFAULT '{}',
|
||
|
|
remote_ip TEXT NOT NULL DEFAULT ''
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_audit_at ON audit_log (at DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log (actor, at DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log (entity_type, entity_id, at DESC);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action, at DESC);
|
||
|
|
|
||
|
|
-- -------------------------------------------------------------- settings ----
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS settings (
|
||
|
|
key TEXT PRIMARY KEY,
|
||
|
|
value TEXT NOT NULL,
|
||
|
|
updated_at TEXT NOT NULL,
|
||
|
|
updated_by TEXT NOT NULL DEFAULT ''
|
||
|
|
);
|