feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready (#1)
This commit was merged in pull request #1.
This commit is contained in:
55
crates/adapters/postgres/migrations/001_initial_schema.sql
Normal file
55
crates/adapters/postgres/migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(32) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name VARCHAR(50),
|
||||
bio VARCHAR(160),
|
||||
avatar_url TEXT,
|
||||
header_url TEXT,
|
||||
custom_css TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS thoughts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
content VARCHAR(128) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS follows (
|
||||
follower_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
following_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (follower_id, following_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS top_friends (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
friend_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
position SMALLINT NOT NULL,
|
||||
PRIMARY KEY (user_id, friend_id),
|
||||
UNIQUE (user_id, position)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS thought_tags (
|
||||
thought_id UUID NOT NULL REFERENCES thoughts(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (thought_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
key_hash TEXT NOT NULL UNIQUE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS ap_id TEXT UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS inbox_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS public_key TEXT,
|
||||
ADD COLUMN IF NOT EXISTS private_key TEXT,
|
||||
ADD COLUMN IF NOT EXISTS local BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
ALTER TABLE thoughts
|
||||
ADD COLUMN IF NOT EXISTS in_reply_to_id UUID REFERENCES thoughts(id),
|
||||
ADD COLUMN IF NOT EXISTS in_reply_to_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS ap_id TEXT UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS visibility TEXT NOT NULL DEFAULT 'public',
|
||||
ADD COLUMN IF NOT EXISTS content_warning TEXT,
|
||||
ADD COLUMN IF NOT EXISTS sensitive BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS local BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;
|
||||
|
||||
ALTER TABLE follows
|
||||
ADD COLUMN IF NOT EXISTS state TEXT NOT NULL DEFAULT 'accepted',
|
||||
ADD COLUMN IF NOT EXISTS ap_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
49
crates/adapters/postgres/migrations/003_new_tables.sql
Normal file
49
crates/adapters/postgres/migrations/003_new_tables.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
CREATE TABLE IF NOT EXISTS likes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
thought_id UUID NOT NULL REFERENCES thoughts(id) ON DELETE CASCADE,
|
||||
ap_id TEXT UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (user_id, thought_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS boosts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
thought_id UUID NOT NULL REFERENCES thoughts(id) ON DELETE CASCADE,
|
||||
ap_id TEXT UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (user_id, thought_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
blocker_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
blocked_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (blocker_id, blocked_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remote_actors (
|
||||
url TEXT PRIMARY KEY,
|
||||
handle TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
inbox_url TEXT NOT NULL,
|
||||
shared_inbox_url TEXT,
|
||||
public_key TEXT NOT NULL,
|
||||
last_fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL,
|
||||
from_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
thought_id UUID REFERENCES thoughts(id) ON DELETE CASCADE,
|
||||
read BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_thoughts_user_id ON thoughts(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_thoughts_created_at ON thoughts(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_follows_following_id ON follows(following_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id, read);
|
||||
11
crates/adapters/postgres/migrations/004_search_indexes.sql
Normal file
11
crates/adapters/postgres/migrations/004_search_indexes.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_thoughts_content_trgm
|
||||
ON thoughts USING GIN(content gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username_trgm
|
||||
ON users USING GIN(username gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_display_name_trgm
|
||||
ON users USING GIN(display_name gin_trgm_ops)
|
||||
WHERE display_name IS NOT NULL;
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Add avatar_url and outbox_url to remote_actors (FederationRepository::RemoteActor needs them)
|
||||
ALTER TABLE remote_actors
|
||||
ADD COLUMN IF NOT EXISTS avatar_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS outbox_url TEXT;
|
||||
|
||||
-- Federation followers: remote actors following local users
|
||||
CREATE TABLE IF NOT EXISTS federation_followers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
local_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
remote_actor_url TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
follow_activity_id TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (local_user_id, remote_actor_url)
|
||||
);
|
||||
|
||||
-- Federation following: local users following remote actors
|
||||
CREATE TABLE IF NOT EXISTS federation_following (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
local_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
remote_actor_url TEXT NOT NULL,
|
||||
follow_activity_id TEXT NOT NULL,
|
||||
outbox_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (local_user_id, remote_actor_url)
|
||||
);
|
||||
|
||||
-- Announces (boosts of remote objects via AP)
|
||||
CREATE TABLE IF NOT EXISTS federation_announces (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
activity_id TEXT NOT NULL UNIQUE,
|
||||
object_url TEXT NOT NULL,
|
||||
actor_url TEXT NOT NULL,
|
||||
announced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Blocked domains (instance-level)
|
||||
CREATE TABLE IF NOT EXISTS federation_blocked_domains (
|
||||
domain TEXT PRIMARY KEY,
|
||||
reason TEXT,
|
||||
blocked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Blocked actors (per local user)
|
||||
CREATE TABLE IF NOT EXISTS federation_blocked_actors (
|
||||
local_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
actor_url TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (local_user_id, actor_url)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fed_followers_user ON federation_followers(local_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fed_following_user ON federation_following(local_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_fed_announces_object ON federation_announces(object_url);
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE remote_actor_connections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_url TEXT NOT NULL,
|
||||
connection_type TEXT NOT NULL,
|
||||
page INT NOT NULL,
|
||||
connected_actor_url TEXT NOT NULL,
|
||||
connected_handle TEXT NOT NULL,
|
||||
connected_display_name TEXT,
|
||||
connected_avatar_url TEXT,
|
||||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(actor_url, connection_type, page, connected_actor_url)
|
||||
);
|
||||
CREATE INDEX ON remote_actor_connections(actor_url, connection_type, page, fetched_at);
|
||||
3
crates/adapters/postgres/migrations/007_content_text.sql
Normal file
3
crates/adapters/postgres/migrations/007_content_text.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Remote ActivityPub posts can exceed 128 characters.
|
||||
-- The 128-char limit is enforced at the application layer for local posts only.
|
||||
ALTER TABLE thoughts ALTER COLUMN content TYPE TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE notifications RENAME COLUMN "type" TO notification_type;
|
||||
15
crates/adapters/postgres/migrations/009_failed_events.sql
Normal file
15
crates/adapters/postgres/migrations/009_failed_events.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE failed_events (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
event_type TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
failed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
retry_at TIMESTAMPTZ NOT NULL,
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
last_error TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT failed_events_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX failed_events_due_idx
|
||||
ON failed_events (retry_at)
|
||||
WHERE retry_count < 3;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Change in_reply_to_id FK from RESTRICT (default) to SET NULL.
|
||||
-- Previously, deleting a thought that had replies raised a FK violation.
|
||||
-- With SET NULL, deleting a thought orphans its replies (they survive but
|
||||
-- lose their parent reference), which is the correct semantic for a
|
||||
-- threaded social app.
|
||||
ALTER TABLE thoughts
|
||||
DROP CONSTRAINT IF EXISTS thoughts_in_reply_to_id_fkey;
|
||||
|
||||
ALTER TABLE thoughts
|
||||
ADD CONSTRAINT thoughts_in_reply_to_id_fkey
|
||||
FOREIGN KEY (in_reply_to_id) REFERENCES thoughts(id) ON DELETE SET NULL;
|
||||
10
crates/adapters/postgres/migrations/011_outbox_events.sql
Normal file
10
crates/adapters/postgres/migrations/011_outbox_events.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE outbox_events (
|
||||
seq BIGSERIAL PRIMARY KEY,
|
||||
aggregate_id UUID NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
delivered BOOLEAN NOT NULL DEFAULT false,
|
||||
delivered_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX outbox_events_pending_idx ON outbox_events (seq) WHERE delivered = false;
|
||||
Reference in New Issue
Block a user