feat: social graph features
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
DROP TABLE IF EXISTS notifications;
|
||||
DROP TABLE IF EXISTS user_reports;
|
||||
DROP TABLE IF EXISTS user_blocks;
|
||||
DROP TABLE IF EXISTS follows;
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Social graph: follows, blocks, reports, plus a generic notifications
|
||||
-- table that backs the "notification plugin" (email today; websocket +
|
||||
-- push will read the same rows later).
|
||||
--
|
||||
-- Follows are REQUEST-BASED. Clicking "Follow" inserts a row with
|
||||
-- status='pending'; the target later accepts (status→'accepted',
|
||||
-- responded_at stamped) or rejects (row deleted). A user is "following"
|
||||
-- another only once the edge is 'accepted'. The UNIQUE(follower,
|
||||
-- following) pair makes a repeated request idempotent, and the CHECK
|
||||
-- stops anyone following themselves.
|
||||
CREATE TABLE IF NOT EXISTS follows (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
follower_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
following_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending | accepted
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
responded_at TIMESTAMPTZ, -- set when accepted
|
||||
UNIQUE (follower_id, following_id),
|
||||
CHECK (follower_id <> following_id)
|
||||
);
|
||||
|
||||
-- "Who follows me / who has requested me" — the incoming side. Filtered
|
||||
-- by status so accepted-follower counts and pending-request lists are
|
||||
-- both index-served.
|
||||
CREATE INDEX IF NOT EXISTS follows_following_status_idx
|
||||
ON follows (following_id, status);
|
||||
|
||||
-- "Who I follow / who I've requested" — the outgoing side.
|
||||
CREATE INDEX IF NOT EXISTS follows_follower_status_idx
|
||||
ON follows (follower_id, status);
|
||||
|
||||
-- Blocks. blocker_id has blocked blocked_id. One row per direction; the
|
||||
-- service treats a block in EITHER direction as "these two can't follow
|
||||
-- each other" and tears down any existing follow edges both ways.
|
||||
CREATE TABLE IF NOT EXISTS user_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
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(),
|
||||
UNIQUE (blocker_id, blocked_id),
|
||||
CHECK (blocker_id <> blocked_id)
|
||||
);
|
||||
|
||||
-- Reverse lookup: "has anyone blocked me?" — used to compute blocked_by
|
||||
-- when rendering a profile.
|
||||
CREATE INDEX IF NOT EXISTS user_blocks_blocked_idx
|
||||
ON user_blocks (blocked_id);
|
||||
|
||||
-- Reports. A reporter flags another user with a category + free-text
|
||||
-- reason. resolved_at is one-way (admins clear it later); the partial
|
||||
-- index keeps the open-reports queue cheap to list.
|
||||
CREATE TABLE IF NOT EXISTS user_reports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reporter_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
reported_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
category VARCHAR(32) NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
resolved_at TIMESTAMPTZ,
|
||||
CHECK (reporter_id <> reported_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS user_reports_reported_idx
|
||||
ON user_reports (reported_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS user_reports_unresolved_idx
|
||||
ON user_reports (created_at DESC) WHERE resolved_at IS NULL;
|
||||
|
||||
-- Notifications back the in-app/email/push fan-out. Today the
|
||||
-- NotificationService persists a row here AND sends an email; websocket
|
||||
-- + push delivery will later read unread rows for a user. actor_id is
|
||||
-- the user who triggered the event (nullable for system notifications);
|
||||
-- kind is a short machine string ('follow_request', 'follow_accepted').
|
||||
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, -- recipient
|
||||
actor_id UUID REFERENCES users(id) ON DELETE CASCADE, -- who caused it
|
||||
kind VARCHAR(32) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
read_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- "My notifications, newest first" + an unread filter for badge counts.
|
||||
CREATE INDEX IF NOT EXISTS notifications_user_created_idx
|
||||
ON notifications (user_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS notifications_user_unread_idx
|
||||
ON notifications (user_id) WHERE read_at IS NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS country;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Account country (ISO-3166-1 alpha-2), shown in the profile "About this
|
||||
-- account" panel. Captured best-effort at signup (Cloudflare's
|
||||
-- CF-IPCountry header when present, else a GeoIP lookup on the client IP)
|
||||
-- and editable by the user from the profile edit form. NULL until known.
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS country CHAR(2);
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_daily_chanting_rounds_range;
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS gender,
|
||||
DROP COLUMN IF EXISTS initiated,
|
||||
DROP COLUMN IF EXISTS initiated_at,
|
||||
DROP COLUMN IF EXISTS daily_chanting_rounds,
|
||||
DROP COLUMN IF EXISTS preferred_chanting_time,
|
||||
DROP COLUMN IF EXISTS peak_active_time;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Devotional profile details, plus two system-managed fields reserved for
|
||||
-- the (not-yet-built) automated-notification scheduler.
|
||||
--
|
||||
-- gender / initiated / initiated_at / daily_chanting_rounds are
|
||||
-- user-editable from the profile form. preferred_chanting_time and
|
||||
-- peak_active_time are written silently by the backend later (no UI yet) —
|
||||
-- both store a local clock time as "HH:MM" text so the scheduler can pick
|
||||
-- a send window without timezone math at write time.
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS gender VARCHAR(16),
|
||||
ADD COLUMN IF NOT EXISTS initiated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS initiated_at DATE,
|
||||
ADD COLUMN IF NOT EXISTS daily_chanting_rounds INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS preferred_chanting_time VARCHAR(5),
|
||||
ADD COLUMN IF NOT EXISTS peak_active_time VARCHAR(5);
|
||||
|
||||
-- Guard the chanting-round count to the meaningful range (1 round up to a
|
||||
-- full lakh of names = 1008). NULL stays allowed (not specified).
|
||||
ALTER TABLE users
|
||||
DROP CONSTRAINT IF EXISTS users_daily_chanting_rounds_range;
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT users_daily_chanting_rounds_range
|
||||
CHECK (daily_chanting_rounds IS NULL
|
||||
OR (daily_chanting_rounds BETWEEN 1 AND 1008));
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS sampradaya;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Devotee's sampradāya (spiritual lineage). Every account defaults to the
|
||||
-- Gaudiya sampradāya; the column is NOT NULL so the value is always
|
||||
-- present. Stored as a lowercase code; the frontend maps it to a display
|
||||
-- label. Existing rows are backfilled to 'gaudiya' by the DEFAULT.
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS sampradaya VARCHAR(32) NOT NULL DEFAULT 'gaudiya';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Re-imposing NOT NULL requires backfilling any NULLs first.
|
||||
UPDATE users SET sampradaya = 'gaudiya' WHERE sampradaya IS NULL;
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN sampradaya SET NOT NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Sampradāya is optional. Keep the 'gaudiya' DEFAULT so new accounts still
|
||||
-- start in the Gaudiya lineage, but allow NULL so a devotee can clear it.
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN sampradaya DROP NOT NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS post_reports;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Post reports — a viewer flags a specific post (not its author) with a
|
||||
-- category + optional free-text reason. Mirrors user_reports but keyed to
|
||||
-- a post; resolved_at is one-way (admins clear it later). The partial
|
||||
-- index keeps the open-reports queue cheap to list.
|
||||
CREATE TABLE IF NOT EXISTS post_reports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reporter_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
||||
category VARCHAR(32) NOT NULL,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS post_reports_post_idx
|
||||
ON post_reports (post_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS post_reports_unresolved_idx
|
||||
ON post_reports (created_at DESC) WHERE resolved_at IS NULL;
|
||||
Reference in New Issue
Block a user