Files
website/server/db/schema.sql
whitlocktech 7bb992f58d Wiki Phase 4: full-text search + revision history
Final phase of the wiki upgrade (see WIKI_UPGRADE.md).

Schema (additive): wiki_revisions table (per-save content snapshots).
The FULLTEXT index on wiki_pages(title, body) shipped in Phase 1.

Search:
- MATCH ... AGAINST natural-language search over title + body, ordered by
  relevance
- public: GET /public/wiki?q= (published only); admin: GET /admin/wiki?q=
  (all statuses)
- public wiki index gains a search box; admin list gains a search field

Revision history:
- every create/update snapshots the page into wiki_revisions
- admin endpoints: list revisions, get one, and restore (restore overwrites
  the page, rebuilds links, and appends a new revision — history stays
  append-only); logged as wiki.revision.restore
- editor gains a History modal: revision list + word-level diff (jsdiff) of a
  chosen revision against the current page, with one-click restore

Verified end-to-end: search matches body and title; two edits produce three
revisions; diff renders added/removed words; restore reverts and records a new
revision. No console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:49:05 -05:00

131 lines
6.0 KiB
SQL

-- UOMysticmoon database schema (MariaDB)
-- Run automatically by the MariaDB container (docker-entrypoint-initdb.d) on a
-- fresh volume, and idempotently by ensureSchema() on every server boot.
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(32) NOT NULL UNIQUE,
password_hash VARCHAR(72) NOT NULL,
role ENUM('admin','editor') NOT NULL DEFAULT 'admin',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS posts (
id INT AUTO_INCREMENT PRIMARY KEY,
category ENUM('news','five_on_friday','newsletter','screenshot') NOT NULL,
title VARCHAR(200) NOT NULL,
slug VARCHAR(220) NULL,
excerpt VARCHAR(400) NULL,
body MEDIUMTEXT NULL,
image_url VARCHAR(500) NULL,
published TINYINT(1) NOT NULL DEFAULT 0,
author_id INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
published_at DATETIME NULL,
CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_posts_feed (category, published, published_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Wiki categories / sections. Defined before wiki_pages so the FK resolves on a
-- fresh install. Pages reference a category (nullable = "Uncategorized").
CREATE TABLE IF NOT EXISTS wiki_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(120) NOT NULL UNIQUE,
title VARCHAR(200) NOT NULL,
description VARCHAR(400) NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS wiki_pages (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(120) NOT NULL UNIQUE,
title VARCHAR(200) NOT NULL,
body MEDIUMTEXT NULL,
excerpt VARCHAR(400) NULL,
category_id INT NULL,
published TINYINT(1) NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
updated_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
published_at DATETIME NULL,
CONSTRAINT fk_wiki_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_wiki_category FOREIGN KEY (category_id) REFERENCES wiki_categories(id) ON DELETE SET NULL,
FULLTEXT INDEX idx_wiki_search (title, body)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Wiki tags (many-to-many with pages).
CREATE TABLE IF NOT EXISTS wiki_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(120) NOT NULL UNIQUE,
label VARCHAR(120) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS wiki_page_tags (
page_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (page_id, tag_id),
CONSTRAINT fk_wpt_page FOREIGN KEY (page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE,
CONSTRAINT fk_wpt_tag FOREIGN KEY (tag_id) REFERENCES wiki_tags(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Internal-link index, rebuilt on each save. target_slug may point at a page
-- that does not exist yet (a "red link").
CREATE TABLE IF NOT EXISTS wiki_links (
source_page_id INT NOT NULL,
target_slug VARCHAR(120) NOT NULL,
CONSTRAINT fk_wiki_links_src FOREIGN KEY (source_page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE,
INDEX idx_wiki_links_target (target_slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Per-save content snapshots for history / diff / restore.
CREATE TABLE IF NOT EXISTS wiki_revisions (
id INT AUTO_INCREMENT PRIMARY KEY,
page_id INT NOT NULL,
title VARCHAR(200) NOT NULL,
body MEDIUMTEXT NULL,
excerpt VARCHAR(400) NULL,
category_id INT NULL,
editor_id INT NULL,
change_note VARCHAR(280) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_wiki_rev_page FOREIGN KEY (page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE,
CONSTRAINT fk_wiki_rev_editor FOREIGN KEY (editor_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_wiki_rev_page (page_id, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS settings (
`key` VARCHAR(64) PRIMARY KEY,
value TEXT NULL,
updated_by INT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_settings_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS activity_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
action VARCHAR(64) NOT NULL,
detail TEXT NULL,
ip VARCHAR(45) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_activity_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_activity_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Migrations for databases created before the wiki upgrade. Each statement uses
-- IF NOT EXISTS so re-running on every boot is a harmless no-op. New installs get
-- these columns from the CREATE TABLE above; existing installs get them here.
-- (The category foreign key is only added on fresh installs; on upgraded databases
-- referential integrity for category_id is enforced in application code.)
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS excerpt VARCHAR(400) NULL;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS category_id INT NULL;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published TINYINT(1) NOT NULL DEFAULT 1;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published_at DATETIME NULL;
ALTER TABLE wiki_pages ADD FULLTEXT INDEX IF NOT EXISTS idx_wiki_search (title, body);