Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

20250107000001_forum_system_v2.sql15.8 kB
-- ============================================ -- CanadaGPT Forum System Migration (v2 - Fixed) -- ============================================ -- Creates tables for threaded discussions, bill debates, and moderation -- Version: 2.0.0 -- Date: 2025-01-07 -- This version handles existing tables and adds missing columns -- ============================================ -- 1. USER PROFILES (Update existing or create) -- ============================================ -- Drop and recreate user_profiles to avoid conflicts DROP TABLE IF EXISTS public.user_profiles CASCADE; CREATE TABLE public.user_profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, display_name VARCHAR(100), avatar_url TEXT, bio TEXT, location VARCHAR(100), -- Forum statistics posts_count INT DEFAULT 0 CHECK (posts_count >= 0), replies_count INT DEFAULT 0 CHECK (replies_count >= 0), reputation_score INT DEFAULT 0, -- Settings email_notifications BOOLEAN DEFAULT TRUE, show_email BOOLEAN DEFAULT FALSE, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); -- Index for reputation leaderboard CREATE INDEX idx_user_profiles_reputation ON public.user_profiles(reputation_score DESC); -- ============================================ -- 2. FORUM CATEGORIES -- ============================================ DROP TABLE IF EXISTS public.forum_categories CASCADE; CREATE TABLE public.forum_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, slug VARCHAR(100) NOT NULL UNIQUE, description TEXT, icon VARCHAR(50), color VARCHAR(7), display_order INT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, post_count INT DEFAULT 0 CHECK (post_count >= 0), created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); CREATE INDEX idx_forum_categories_active ON public.forum_categories(is_active, display_order); -- ============================================ -- 3. FORUM POSTS -- ============================================ DROP TABLE IF EXISTS public.forum_posts CASCADE; CREATE TABLE public.forum_posts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_type VARCHAR(20) NOT NULL CHECK (post_type IN ('discussion', 'bill_comment')), -- For generic discussions category_id UUID REFERENCES public.forum_categories(id) ON DELETE CASCADE, -- For bill-specific comments bill_id INT, bill_number VARCHAR(20), bill_session VARCHAR(20), -- Content title VARCHAR(255), content TEXT NOT NULL CHECK (LENGTH(content) > 0), -- Ownership author_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, author_name VARCHAR(255), author_avatar_url TEXT, -- Threading structure parent_post_id UUID REFERENCES public.forum_posts(id) ON DELETE CASCADE, thread_root_id UUID REFERENCES public.forum_posts(id) ON DELETE CASCADE, depth INT DEFAULT 0 CHECK (depth >= 0 AND depth <= 10), -- Engagement metrics upvotes_count INT DEFAULT 0 CHECK (upvotes_count >= 0), downvotes_count INT DEFAULT 0 CHECK (downvotes_count >= 0), reply_count INT DEFAULT 0 CHECK (reply_count >= 0), -- Moderation is_deleted BOOLEAN DEFAULT FALSE, is_pinned BOOLEAN DEFAULT FALSE, is_locked BOOLEAN DEFAULT FALSE, deleted_at TIMESTAMPTZ, deleted_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, last_reply_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, -- Full-text search vector search_vector TSVECTOR GENERATED ALWAYS AS ( to_tsvector('english', COALESCE(title, '') || ' ' || content) ) STORED, -- Constraints CONSTRAINT forum_posts_category_check CHECK ( (post_type = 'discussion' AND category_id IS NOT NULL) OR (post_type = 'bill_comment' AND bill_number IS NOT NULL AND bill_session IS NOT NULL) ), CONSTRAINT forum_posts_title_check CHECK ( (depth = 0 AND title IS NOT NULL) OR (depth > 0) ) ); -- Indexes CREATE INDEX idx_forum_posts_category ON public.forum_posts(category_id, created_at DESC) WHERE is_deleted = FALSE; CREATE INDEX idx_forum_posts_bill ON public.forum_posts(bill_number, bill_session, created_at DESC) WHERE is_deleted = FALSE; CREATE INDEX idx_forum_posts_author ON public.forum_posts(author_id, created_at DESC); CREATE INDEX idx_forum_posts_parent ON public.forum_posts(parent_post_id, created_at); CREATE INDEX idx_forum_posts_thread ON public.forum_posts(thread_root_id, depth, created_at); CREATE INDEX idx_forum_posts_search ON public.forum_posts USING GIN(search_vector); CREATE INDEX idx_forum_posts_hot ON public.forum_posts(last_reply_at DESC) WHERE is_deleted = FALSE AND is_pinned = FALSE; -- ============================================ -- 4. FORUM VOTES -- ============================================ DROP TABLE IF EXISTS public.forum_votes CASCADE; CREATE TABLE public.forum_votes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_id UUID REFERENCES public.forum_posts(id) ON DELETE CASCADE NOT NULL, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, vote_type VARCHAR(10) NOT NULL CHECK (vote_type IN ('upvote', 'downvote')), created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, UNIQUE(post_id, user_id) ); CREATE INDEX idx_forum_votes_post ON public.forum_votes(post_id); CREATE INDEX idx_forum_votes_user ON public.forum_votes(user_id); -- ============================================ -- 5. MODERATION REPORTS -- ============================================ DROP TABLE IF EXISTS public.moderation_reports CASCADE; CREATE TABLE public.moderation_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_id UUID REFERENCES public.forum_posts(id) ON DELETE CASCADE NOT NULL, reporter_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, reason TEXT NOT NULL CHECK (LENGTH(reason) > 0), status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'resolved', 'dismissed')), resolved_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, resolved_at TIMESTAMPTZ, admin_notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, UNIQUE(post_id, reporter_id) ); CREATE INDEX idx_moderation_reports_status ON public.moderation_reports(status, created_at DESC); CREATE INDEX idx_moderation_reports_post ON public.moderation_reports(post_id); -- ============================================ -- 6. MODERATION ACTIONS -- ============================================ DROP TABLE IF EXISTS public.moderation_actions CASCADE; CREATE TABLE public.moderation_actions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_id UUID REFERENCES public.forum_posts(id) ON DELETE CASCADE NOT NULL, moderator_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, action VARCHAR(20) NOT NULL CHECK (action IN ('delete', 'lock', 'unlock', 'pin', 'unpin', 'warn')), reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); CREATE INDEX idx_moderation_actions_post ON public.moderation_actions(post_id, created_at DESC); CREATE INDEX idx_moderation_actions_moderator ON public.moderation_actions(moderator_id, created_at DESC); -- ============================================ -- 7. TRIGGERS & FUNCTIONS -- ============================================ -- Trigger: Update updated_at timestamp CREATE OR REPLACE FUNCTION public.update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_user_profiles_updated_at BEFORE UPDATE ON public.user_profiles FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER update_forum_categories_updated_at BEFORE UPDATE ON public.forum_categories FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); CREATE TRIGGER update_forum_posts_updated_at BEFORE UPDATE ON public.forum_posts FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); -- Function: Update post vote counts CREATE OR REPLACE FUNCTION public.update_post_vote_counts() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT') THEN IF NEW.vote_type = 'upvote' THEN UPDATE public.forum_posts SET upvotes_count = upvotes_count + 1 WHERE id = NEW.post_id; ELSIF NEW.vote_type = 'downvote' THEN UPDATE public.forum_posts SET downvotes_count = downvotes_count + 1 WHERE id = NEW.post_id; END IF; ELSIF (TG_OP = 'DELETE') THEN IF OLD.vote_type = 'upvote' THEN UPDATE public.forum_posts SET upvotes_count = upvotes_count - 1 WHERE id = OLD.post_id; ELSIF OLD.vote_type = 'downvote' THEN UPDATE public.forum_posts SET downvotes_count = downvotes_count - 1 WHERE id = OLD.post_id; END IF; ELSIF (TG_OP = 'UPDATE' AND OLD.vote_type != NEW.vote_type) THEN IF OLD.vote_type = 'upvote' AND NEW.vote_type = 'downvote' THEN UPDATE public.forum_posts SET upvotes_count = upvotes_count - 1, downvotes_count = downvotes_count + 1 WHERE id = NEW.post_id; ELSIF OLD.vote_type = 'downvote' AND NEW.vote_type = 'upvote' THEN UPDATE public.forum_posts SET upvotes_count = upvotes_count + 1, downvotes_count = downvotes_count - 1 WHERE id = NEW.post_id; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_vote_counts AFTER INSERT OR UPDATE OR DELETE ON public.forum_votes FOR EACH ROW EXECUTE FUNCTION public.update_post_vote_counts(); -- Function: Update category post counts CREATE OR REPLACE FUNCTION public.update_category_post_count() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT' AND NEW.depth = 0 AND NEW.is_deleted = FALSE) THEN UPDATE public.forum_categories SET post_count = post_count + 1 WHERE id = NEW.category_id; ELSIF (TG_OP = 'UPDATE') THEN IF OLD.is_deleted = FALSE AND NEW.is_deleted = TRUE AND NEW.depth = 0 THEN UPDATE public.forum_categories SET post_count = post_count - 1 WHERE id = NEW.category_id; ELSIF OLD.is_deleted = TRUE AND NEW.is_deleted = FALSE AND NEW.depth = 0 THEN UPDATE public.forum_categories SET post_count = post_count + 1 WHERE id = NEW.category_id; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_category_counts AFTER INSERT OR UPDATE ON public.forum_posts FOR EACH ROW EXECUTE FUNCTION public.update_category_post_count(); -- Function: Update parent post reply counts and last_reply_at CREATE OR REPLACE FUNCTION public.update_parent_reply_stats() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT' AND NEW.parent_post_id IS NOT NULL AND NEW.is_deleted = FALSE) THEN UPDATE public.forum_posts SET reply_count = reply_count + 1, last_reply_at = NEW.created_at WHERE id = NEW.parent_post_id OR id = NEW.thread_root_id; ELSIF (TG_OP = 'UPDATE') THEN IF OLD.is_deleted = FALSE AND NEW.is_deleted = TRUE AND NEW.parent_post_id IS NOT NULL THEN UPDATE public.forum_posts SET reply_count = reply_count - 1 WHERE id = NEW.parent_post_id; ELSIF OLD.is_deleted = TRUE AND NEW.is_deleted = FALSE AND NEW.parent_post_id IS NOT NULL THEN UPDATE public.forum_posts SET reply_count = reply_count + 1 WHERE id = NEW.parent_post_id; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_reply_stats AFTER INSERT OR UPDATE ON public.forum_posts FOR EACH ROW EXECUTE FUNCTION public.update_parent_reply_stats(); -- Function: Rate limiting check (max 10 posts per hour) CREATE OR REPLACE FUNCTION public.check_post_rate_limit(p_user_id UUID) RETURNS BOOLEAN AS $$ DECLARE v_recent_posts INT; BEGIN SELECT COUNT(*) INTO v_recent_posts FROM public.forum_posts WHERE author_id = p_user_id AND created_at > NOW() - INTERVAL '1 hour'; RETURN v_recent_posts < 10; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- ============================================ -- 8. ROW-LEVEL SECURITY (RLS) -- ============================================ ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY; ALTER TABLE public.forum_categories ENABLE ROW LEVEL SECURITY; ALTER TABLE public.forum_posts ENABLE ROW LEVEL SECURITY; ALTER TABLE public.forum_votes ENABLE ROW LEVEL SECURITY; ALTER TABLE public.moderation_reports ENABLE ROW LEVEL SECURITY; ALTER TABLE public.moderation_actions ENABLE ROW LEVEL SECURITY; -- User Profiles CREATE POLICY user_profiles_select ON public.user_profiles FOR SELECT USING (TRUE); CREATE POLICY user_profiles_insert ON public.user_profiles FOR INSERT WITH CHECK (auth.uid() = id); CREATE POLICY user_profiles_update ON public.user_profiles FOR UPDATE USING (auth.uid() = id); -- Categories CREATE POLICY forum_categories_select ON public.forum_categories FOR SELECT USING (is_active = TRUE); -- Posts CREATE POLICY forum_posts_select ON public.forum_posts FOR SELECT USING (is_deleted = FALSE OR author_id = auth.uid()); CREATE POLICY forum_posts_insert ON public.forum_posts FOR INSERT WITH CHECK (auth.uid() = author_id AND public.check_post_rate_limit(auth.uid())); CREATE POLICY forum_posts_update ON public.forum_posts FOR UPDATE USING (auth.uid() = author_id AND is_locked = FALSE) WITH CHECK (auth.uid() = author_id); CREATE POLICY forum_posts_delete ON public.forum_posts FOR DELETE USING (auth.uid() = author_id); -- Votes CREATE POLICY forum_votes_select ON public.forum_votes FOR SELECT USING (TRUE); CREATE POLICY forum_votes_insert ON public.forum_votes FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY forum_votes_update ON public.forum_votes FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); CREATE POLICY forum_votes_delete ON public.forum_votes FOR DELETE USING (auth.uid() = user_id); -- Reports CREATE POLICY moderation_reports_select ON public.moderation_reports FOR SELECT USING (reporter_id = auth.uid()); CREATE POLICY moderation_reports_insert ON public.moderation_reports FOR INSERT WITH CHECK (auth.uid() = reporter_id); -- Actions CREATE POLICY moderation_actions_select ON public.moderation_actions FOR SELECT USING (TRUE); -- ============================================ -- 9. SEED DATA -- ============================================ INSERT INTO public.forum_categories (name, slug, description, icon, color, display_order) VALUES ('General Discussion', 'general', 'General political discussions and current events', 'MessageSquare', '#6B7280', 1), ('Healthcare', 'healthcare', 'Discuss healthcare policy, funding, and reforms', 'Heart', '#EF4444', 2), ('Climate & Environment', 'climate', 'Climate change, environmental policy, and sustainability', 'Leaf', '#10B981', 3), ('Economy & Finance', 'economy', 'Economic policy, budgets, taxes, and fiscal matters', 'DollarSign', '#F59E0B', 4), ('Justice & Law', 'justice', 'Legal system, justice reform, and law enforcement', 'Scale', '#8B5CF6', 5), ('Indigenous Affairs', 'indigenous', 'Indigenous rights, reconciliation, and treaty issues', 'Users', '#F97316', 6), ('Immigration', 'immigration', 'Immigration policy, refugees, and citizenship', 'Globe', '#3B82F6', 7), ('Education', 'education', 'Education policy, funding, and curriculum', 'GraduationCap', '#EC4899', 8), ('Defence & Security', 'defence', 'National defence, cybersecurity, and public safety', 'Shield', '#DC2626', 9), ('Housing', 'housing', 'Housing affordability, development, and policy', 'Home', '#14B8A6', 10); -- ============================================ -- SUCCESS MESSAGE -- ============================================ DO $$ BEGIN RAISE NOTICE 'Forum system migration completed successfully!'; RAISE NOTICE 'Created 6 tables: user_profiles, forum_categories, forum_posts, forum_votes, moderation_reports, moderation_actions'; RAISE NOTICE 'Seeded 10 forum categories'; RAISE NOTICE 'Enabled RLS on all tables'; END $$;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server