feat: social graph features

This commit is contained in:
Krispire Admin
2026-06-04 19:29:48 +05:30
parent d9678ebb3b
commit efa1eaf23b
28 changed files with 1882 additions and 78 deletions
@@ -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;
+6
View File
@@ -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;