Stand up the full SQLite content layer: all 7 tables from the authoritative schema with WAL + foreign-keys enforced per-connection, entity dataclasses plus row mappers, hand-rolled versioned migrations tracked in schema_migrations, and an idempotent Python seed (system user + welcome post + About page). Add a Markdown->HTML service using markdown-it-py with a strict bleach allowlist (tables intentionally omitted on both sides). Add a typed in-process TTLCache[K,V] and wire it into real DB-backed PostService and PageService, both exposing invalidate_all() for Phase 4 admin writes. Rewire / and /about to read from the DB; homepage renders the seeded welcome post, About renders page.title + sanitized body_html_cached. Update the Phase 1 route tests accordingly. Mark Phase 2 complete in docs/ROADMAP.md.
109 lines
3.3 KiB
SQL
109 lines
3.3 KiB
SQL
-- 001_init.sql
|
|
--
|
|
-- Initial schema for Chicken Babies R Us. Authoritative copy of the
|
|
-- tables + indexes + check constraints documented in
|
|
-- ``docs/ROADMAP.md`` (see "SQLite Schema (authoritative)").
|
|
--
|
|
-- Idempotency: every statement uses IF NOT EXISTS so re-running the
|
|
-- file on a partially-migrated database is still safe. The migration
|
|
-- runner also gates execution via the schema_migrations tracker, so
|
|
-- this belt-and-braces approach is defensive only.
|
|
--
|
|
-- No PRAGMA statements here: journal_mode = WAL and foreign_keys = ON
|
|
-- are applied per-connection via the SQLAlchemy connect-event
|
|
-- listener in ``app/db.py``. Setting them inside a migration file
|
|
-- would be a no-op on every connection except the one that ran the
|
|
-- migration, which is the opposite of what we want.
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
display_name TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
last_login_at TEXT,
|
|
active INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS magic_link_tokens (
|
|
id INTEGER PRIMARY KEY,
|
|
email TEXT NOT NULL,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
used_at TEXT,
|
|
request_ip TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_magic_email_created
|
|
ON magic_link_tokens(email, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id INTEGER PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
ip TEXT NOT NULL,
|
|
user_agent TEXT NOT NULL,
|
|
revoked_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pages (
|
|
id INTEGER PRIMARY KEY,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
body_md TEXT NOT NULL,
|
|
body_html_cached TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
published INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id INTEGER PRIMARY KEY,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
body_md TEXT NOT NULL,
|
|
body_html_cached TEXT NOT NULL,
|
|
status TEXT NOT NULL CHECK (status IN ('draft','published')),
|
|
published_at TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
author_user_id INTEGER NOT NULL REFERENCES users(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_posts_status_pub
|
|
ON posts(status, published_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS media (
|
|
id INTEGER PRIMARY KEY,
|
|
filename TEXT NOT NULL UNIQUE,
|
|
original_filename TEXT NOT NULL,
|
|
content_type TEXT NOT NULL,
|
|
size_bytes INTEGER NOT NULL,
|
|
stored_path TEXT NOT NULL,
|
|
alt_text TEXT NOT NULL DEFAULT '',
|
|
uploaded_by INTEGER NOT NULL REFERENCES users(id),
|
|
uploaded_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS contact_submissions (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
ip TEXT NOT NULL,
|
|
user_agent TEXT NOT NULL,
|
|
submitted_at TEXT NOT NULL,
|
|
handled INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_events (
|
|
id INTEGER PRIMARY KEY,
|
|
event_type TEXT NOT NULL,
|
|
email TEXT,
|
|
user_id INTEGER REFERENCES users(id),
|
|
ip TEXT NOT NULL,
|
|
user_agent TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
detail TEXT NOT NULL DEFAULT '{}'
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_auth_events_created
|
|
ON auth_events(created_at DESC);
|