You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
83 lines
4.3 KiB
SQL
83 lines
4.3 KiB
SQL
-- pages.elijah.run — D1 schema
|
|
--
|
|
-- Mirrors docs/plans/2026-07-12-pages-design.md §4.2 exactly. All statements
|
|
-- use `IF NOT EXISTS` so this file can be re-applied safely (e.g. re-running
|
|
-- `wrangler d1 execute` against a database that already has these objects).
|
|
--
|
|
-- Apply with:
|
|
-- wrangler d1 execute <DB_NAME> --file=migrations/schema.sql (remote)
|
|
-- wrangler d1 execute <DB_NAME> --local --file=migrations/schema.sql (local dev)
|
|
|
|
-- Registered users, upserted on every successful GitHub OAuth login
|
|
-- (src/db.rs::Db::upsert_user_on_login). `github_id` is the stable GitHub
|
|
-- account id (not the login, which can change) and is the natural
|
|
-- conflict key for the upsert. `can_upload` and `is_admin` are booleans
|
|
-- stored as INTEGER 0/1 (SQLite/D1 has no native boolean type) — see the
|
|
-- `UserRow` -> `User` conversion in src/db.rs for how these are turned
|
|
-- into real `bool`s on read. Both default to 0: new OAuth users can log
|
|
-- in but cannot publish until an admin grants `can_upload` (design §2.4).
|
|
-- `ADMIN_GITHUB_LOGIN` logins are auto-promoted (both flags forced to 1)
|
|
-- on every login by the upsert, independent of this default. `trusted` is a
|
|
-- separate, admin-only-granted flag (pages-shqc): it is never touched by
|
|
-- admin auto-promotion or by any login upsert — see the "Trust escape
|
|
-- hatch" addendum in docs/plans/2026-07-12-pages-design.md §2.1. A user's
|
|
-- `trusted` flag contributes to a page's *effective* trust (alongside the
|
|
-- page's own `trusted` flag below) via `p.trusted OR u.trusted`.
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
github_id INTEGER NOT NULL UNIQUE,
|
|
login TEXT NOT NULL,
|
|
avatar_url TEXT,
|
|
can_upload INTEGER NOT NULL DEFAULT 0,
|
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
|
trusted INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Server-side session records. The cookie sent to the browser holds the raw
|
|
-- session token; only its SHA-256 hex hash is ever persisted here, so a D1
|
|
-- read leak does not yield a usable session token (design §2.4).
|
|
-- `ON DELETE CASCADE` means deleting a user row (none currently does this in
|
|
-- v0, but future admin tooling might) also removes their sessions.
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
token_hash TEXT PRIMARY KEY, -- SHA-256 hex of the cookie token
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- One row per published page. `slug` is the primary key, so a duplicate
|
|
-- upload name surfaces as a UNIQUE/PK constraint violation, which src/db.rs
|
|
-- maps to a typed `DbError::NameTaken` (HTTP 409 at the routes layer,
|
|
-- design §2.3). `file_count`/`total_bytes` are denormalized summaries of the
|
|
-- R2 objects stored under the `<slug>/` prefix (design §4.3), populated at
|
|
-- upload time so the page list and per-user quota check (design §2.5) don't
|
|
-- need to touch R2. `trusted` is an admin-only-granted flag (pages-shqc,
|
|
-- design §2.1 addendum): a page's *effective* trust is
|
|
-- `pages.trusted OR <owner's> users.trusted`, computed at serve time —
|
|
-- see src/db.rs's `PageRow::effective_trust`.
|
|
CREATE TABLE IF NOT EXISTS pages (
|
|
slug TEXT PRIMARY KEY,
|
|
owner_id INTEGER NOT NULL REFERENCES users(id),
|
|
file_count INTEGER NOT NULL,
|
|
total_bytes INTEGER NOT NULL,
|
|
trusted INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Per-user page quota (design §2.5) is enforced by counting
|
|
-- `pages` rows WHERE owner_id = ? before an upload; without this index that
|
|
-- scan is a full table scan on every upload.
|
|
CREATE INDEX IF NOT EXISTS idx_pages_owner_id ON pages(owner_id);
|
|
|
|
-- get_user_by_session filters `WHERE expires_at > datetime('now')` on every
|
|
-- authenticated request; this index keeps that lookup (and any future
|
|
-- expired-session cleanup job) from scanning the whole table.
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
|
|
-- Not required by any current query pattern (session lookups go through
|
|
-- `token_hash`, the primary key) but cheap to maintain and speeds up the
|
|
-- `ON DELETE CASCADE` scan when a user row is deleted, plus any future
|
|
-- "list my sessions" / "log out everywhere" feature keyed on user_id.
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|