Files
gerbeur/api/db/schema.sql
khannurien 134d1a0b97 v4: a moderator's or an admin's @everyone in the chat notifies every user — there was no way to reach everyone at once. The new chat:everyone permission, held by moderators and admins, lets a chat message saying @everyone notify every user but its author and those reading the chat, through notifyEveryone, after notifyMentions, so a user also named is told once, by name; the notification is a user_mentioned with everyone set, which the notifications page and the digest word as "mentioned everyone in the chat". chat_messages.mentions_everyone, sent as ChatMessage.mentionsEveryone, is set from the author's role each time the body is written, so a user's @everyone reaches no one, a moderator's edit of a user's message can't make it reach everyone, and a demoted author's next edit loses it. Only an edit that adds @everyone notifies, so fixing an announcement's typo reaches no one new. The client highlights and chimes by the flag, never by the text; Markdown shows @everyone as a mention only in a flagged message, and never links it to a profile. The chat composer offers @everyone first to those who may say it. No account may be named everyone, in any case: migration 0023 adds the column, flags the moderators' and admins' past @everyone messages, renames a user named everyone to everyone_N and drops an alias everyone. 265 backend tests and 681 frontend tests pass, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean; checked in screenshots of the chat at 1280 and 390px, with a moderator's and a user's @everyone and the suggestions open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:54:09 +00:00

273 lines
9.1 KiB
SQL

CREATE TABLE dumps (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
title TEXT NOT NULL,
comment TEXT,
user_id TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT,
url TEXT,
-- Lossy lookup key for "has this been dumped already?" — see api/lib/canonical-url.ts
url_canonical TEXT,
slug TEXT,
rich_content TEXT,
file_name TEXT,
file_mime TEXT,
file_size INTEGER,
vote_count INTEGER NOT NULL DEFAULT 0,
is_private INTEGER NOT NULL DEFAULT 0,
custom_thumbnail_mime TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL,
updated_at TEXT,
avatar_mime TEXT,
description TEXT,
invited_by TEXT REFERENCES users(id),
email TEXT NOT NULL,
chat_read_at TEXT,
tokens_valid_after TEXT
);
CREATE TABLE votes (
dump_id TEXT NOT NULL,
user_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (dump_id, user_id),
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE playlists (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
slug TEXT,
description TEXT,
is_public INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT,
image_mime TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE playlist_dumps (
playlist_id TEXT NOT NULL,
dump_id TEXT NOT NULL,
position INTEGER NOT NULL,
added_at TEXT NOT NULL,
PRIMARY KEY (playlist_id, dump_id),
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE
);
CREATE TABLE categories (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL UNIQUE,
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT
);
CREATE TABLE dump_categories (
dump_id TEXT NOT NULL,
category_id TEXT NOT NULL,
PRIMARY KEY (dump_id, category_id),
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
CREATE TABLE comments (
id TEXT PRIMARY KEY,
dump_id TEXT NOT NULL,
user_id TEXT NOT NULL,
parent_id TEXT,
body TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT,
deleted INTEGER NOT NULL DEFAULT 0,
like_count INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
);
CREATE TABLE comment_likes (
comment_id TEXT NOT NULL,
user_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (comment_id, user_id),
FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE dump_backlinks (
source_type TEXT NOT NULL,
source_id TEXT NOT NULL,
from_dump_id TEXT NOT NULL,
to_dump_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (source_type, source_id, to_dump_id),
FOREIGN KEY (from_dump_id) REFERENCES dumps(id) ON DELETE CASCADE,
FOREIGN KEY (to_dump_id) REFERENCES dumps(id) ON DELETE CASCADE
);
CREATE INDEX idx_dumps_created ON dumps(created_at);
CREATE INDEX idx_dumps_user_created ON dumps(user_id, created_at);
CREATE UNIQUE INDEX idx_dumps_user_slug ON dumps(user_id, slug);
CREATE INDEX idx_dumps_url ON dumps(url);
CREATE INDEX idx_dumps_url_canonical ON dumps(url_canonical);
CREATE INDEX idx_votes_user_created ON votes(user_id, created_at);
CREATE INDEX idx_playlists_user ON playlists(user_id);
CREATE UNIQUE INDEX idx_playlists_user_slug ON playlists(user_id, slug);
CREATE INDEX idx_playlist_dumps_order ON playlist_dumps(playlist_id, position);
CREATE INDEX idx_playlist_dumps_dump ON playlist_dumps(dump_id);
CREATE INDEX idx_comments_dump ON comments(dump_id, created_at);
CREATE INDEX idx_comments_parent ON comments(parent_id);
CREATE INDEX idx_comments_user ON comments(user_id);
CREATE INDEX idx_comment_likes_user ON comment_likes(user_id);
CREATE INDEX idx_dump_backlinks_to ON dump_backlinks(to_dump_id);
CREATE INDEX idx_dump_backlinks_from ON dump_backlinks(from_dump_id);
CREATE INDEX idx_dump_categories_category ON dump_categories(category_id);
CREATE TABLE follows (
id TEXT PRIMARY KEY,
follower_id TEXT NOT NULL,
followed_user_id TEXT,
followed_playlist_id TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (followed_user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (followed_playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
CHECK (
(followed_user_id IS NOT NULL AND followed_playlist_id IS NULL)
OR
(followed_user_id IS NULL AND followed_playlist_id IS NOT NULL)
)
);
CREATE UNIQUE INDEX idx_follows_user
ON follows(follower_id, followed_user_id)
WHERE followed_user_id IS NOT NULL;
CREATE UNIQUE INDEX idx_follows_playlist
ON follows(follower_id, followed_playlist_id)
WHERE followed_playlist_id IS NOT NULL;
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_followed_user ON follows(followed_user_id);
CREATE INDEX idx_follows_followed_playlist ON follows(followed_playlist_id);
CREATE TABLE invites (
token TEXT PRIMARY KEY,
inviter_id TEXT NOT NULL,
used_at TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_invites_inviter ON invites(inviter_id);
CREATE INDEX idx_users_invited_by ON users(invited_by);
CREATE UNIQUE INDEX idx_users_username_lower ON users(lower(username));
CREATE UNIQUE INDEX idx_users_email_lower ON users(lower(email));
CREATE TABLE attachments (
id TEXT PRIMARY KEY,
resource_id TEXT,
mime TEXT NOT NULL,
created_at TEXT NOT NULL,
resource_type TEXT
);
CREATE INDEX idx_attachments_resource ON attachments(resource_id);
CREATE TABLE password_reset_tokens (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
expires_at TEXT NOT NULL,
used_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_password_reset_tokens_user ON password_reset_tokens(user_id);
CREATE TABLE notifications (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
data TEXT NOT NULL,
read INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
source_key TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_notifications_user ON notifications(user_id, created_at);
CREATE UNIQUE INDEX idx_notifications_dedup
ON notifications(user_id, source_key)
WHERE source_key IS NOT NULL;
CREATE TABLE user_settings (
user_id TEXT PRIMARY KEY,
style TEXT,
color_scheme TEXT,
digest_weekly INTEGER NOT NULL DEFAULT 1,
digest_monthly INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE digest_schedule (
user_id TEXT NOT NULL,
cadence TEXT NOT NULL,
window_start TEXT NOT NULL,
PRIMARY KEY (user_id, cadence),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE dump_slug_aliases (
user_id TEXT NOT NULL,
slug TEXT NOT NULL,
dump_id TEXT NOT NULL REFERENCES dumps(id) ON DELETE CASCADE,
created_at TEXT NOT NULL,
PRIMARY KEY (user_id, slug)
);
CREATE INDEX idx_dump_slug_aliases_dump ON dump_slug_aliases(dump_id, created_at);
CREATE TABLE playlist_slug_aliases (
user_id TEXT NOT NULL,
slug TEXT NOT NULL,
playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
created_at TEXT NOT NULL,
PRIMARY KEY (user_id, slug)
);
CREATE INDEX idx_playlist_slug_aliases_playlist ON playlist_slug_aliases(playlist_id, created_at);
CREATE TABLE user_aliases (
username TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TEXT NOT NULL,
PRIMARY KEY (username)
);
CREATE UNIQUE INDEX idx_user_aliases_lower ON user_aliases(lower(username));
CREATE INDEX idx_user_aliases_user ON user_aliases(user_id, created_at);
CREATE TABLE chat_messages (
id TEXT NOT NULL PRIMARY KEY,
user_id TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT,
reply_to_id TEXT,
mentions_everyone INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_chat_messages_created_id ON chat_messages(created_at, id);
CREATE INDEX idx_chat_messages_user ON chat_messages(user_id);