Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

forum.ts16.6 kB
'use server'; /** * Forum Server Actions * Next.js Server Actions for forum CRUD operations */ import { createServerClient, createAdminClient } from '@/lib/supabase-server'; import { auth } from '@/auth'; import type { ForumPost, ForumCategory, CreatePostInput, UpdatePostInput, GetPostsParams, ApiResponse, PaginatedResponse, VoteType, } from '@/types/forum'; // ============================================ // CATEGORIES // ============================================ export async function getCategories(): Promise<ApiResponse<ForumCategory[]>> { try { const supabase = await createServerClient(); const { data, error } = await supabase .from('forum_categories') .select('*') .eq('is_active', true) .order('display_order', { ascending: true }); if (error) throw error; return { success: true, data: data || [] }; } catch (error) { console.error('Error fetching categories:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to fetch categories', }; } } export async function getCategoryBySlug( slug: string ): Promise<ApiResponse<ForumCategory>> { try { const supabase = await createServerClient(); const { data, error } = await supabase .from('forum_categories') .select('*') .eq('slug', slug) .eq('is_active', true) .single(); if (error) throw error; if (!data) throw new Error('Category not found'); return { success: true, data }; } catch (error) { console.error('Error fetching category:', error); return { success: false, error: error instanceof Error ? error.message : 'Category not found', }; } } // ============================================ // POSTS - READ // ============================================ export async function getPosts( params: GetPostsParams = {} ): Promise<ApiResponse<PaginatedResponse<ForumPost>>> { try { const supabase = await createServerClient(); const { category_id, bill_number, bill_session, author_id, limit = 20, offset = 0, sort = 'recent', } = params; let query = supabase .from('forum_posts') .select('*, author:user_profiles(display_name, avatar_url)', { count: 'exact' }) .eq('is_deleted', false) .eq('depth', 0); // Only top-level posts // Filters if (category_id) { query = query.eq('category_id', category_id); } if (bill_number && bill_session) { query = query.eq('bill_number', bill_number).eq('bill_session', bill_session); } if (author_id) { query = query.eq('author_id', author_id); } // Sorting switch (sort) { case 'hot': query = query.order('last_reply_at', { ascending: false }); break; case 'top': query = query.order('upvotes_count', { ascending: false }); break; case 'recent': default: query = query.order('created_at', { ascending: false }); break; } // Pagination query = query.range(offset, offset + limit - 1); const { data, error, count } = await query; if (error) throw error; return { success: true, data: { data: data || [], total: count || 0, limit, offset, has_more: count ? offset + limit < count : false, }, }; } catch (error) { console.error('Error fetching posts:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to fetch posts', }; } } export async function getPost(postId: string): Promise<ApiResponse<ForumPost>> { try { const supabase = await createServerClient(); const session = await auth(); const { data, error } = await supabase .from('forum_posts') .select('*, author:user_profiles(display_name, avatar_url), category:forum_categories(*)') .eq('id', postId) .single(); if (error) throw error; if (!data) throw new Error('Post not found'); // Get user's vote if authenticated if (session?.user?.id) { const { data: voteData } = await supabase .from('forum_votes') .select('vote_type') .eq('post_id', postId) .eq('user_id', session.user.id) .maybeSingle(); if (voteData) { data.user_vote = voteData.vote_type; } } return { success: true, data }; } catch (error) { console.error('Error fetching post:', error); return { success: false, error: error instanceof Error ? error.message : 'Post not found', }; } } export async function getPostThread( postId: string, maxDepth: number = 10 ): Promise<ApiResponse<ForumPost[]>> { try { const supabase = await createServerClient(); const session = await auth(); // Get the root post const rootPost = await getPost(postId); if (!rootPost.success || !rootPost.data) { throw new Error('Post not found'); } // Get all replies in the thread const threadRootId = rootPost.data.thread_root_id || postId; const { data, error } = await supabase .from('forum_posts') .select('*, author:user_profiles(display_name, avatar_url)') .eq('thread_root_id', threadRootId) .lte('depth', maxDepth) .order('created_at', { ascending: true }); if (error) throw error; // Get user votes if authenticated if (session?.user?.id && data) { const postIds = data.map((p) => p.id); const { data: votes } = await supabase .from('forum_votes') .select('post_id, vote_type') .eq('user_id', session.user.id) .in('post_id', postIds); if (votes) { const voteMap = new Map(votes.map((v) => [v.post_id, v.vote_type])); data.forEach((post) => { post.user_vote = voteMap.get(post.id) || null; }); } } // Build threaded structure const postsWithReplies = buildThreadTree(data || []); return { success: true, data: postsWithReplies }; } catch (error) { console.error('Error fetching thread:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to fetch thread', }; } } // Helper function to build thread tree function buildThreadTree(posts: ForumPost[]): ForumPost[] { const postMap = new Map<string, ForumPost>(); const rootPosts: ForumPost[] = []; // Initialize all posts with empty replies array posts.forEach((post) => { postMap.set(post.id, { ...post, replies: [] }); }); // Build tree structure posts.forEach((post) => { const postWithReplies = postMap.get(post.id)!; if (post.parent_post_id) { const parent = postMap.get(post.parent_post_id); if (parent) { parent.replies = parent.replies || []; parent.replies.push(postWithReplies); } } else { rootPosts.push(postWithReplies); } }); return rootPosts; } // ============================================ // POSTS - CREATE // ============================================ export async function createPost( input: CreatePostInput ): Promise<ApiResponse<ForumPost>> { try { const supabase = await createServerClient(); const session = await auth(); if (!session?.user?.id) { throw new Error('You must be logged in to create a post'); } const userId = session.user.id; // Get user profile for denormalized data const { data: profile } = await supabase .from('user_profiles') .select('display_name, avatar_url') .eq('id', userId) .maybeSingle(); // Calculate depth and thread_root_id for replies let depth = 0; let thread_root_id: string | null = null; if (input.parent_post_id) { const { data: parentPost } = await supabase .from('forum_posts') .select('depth, thread_root_id, id, is_locked') .eq('id', input.parent_post_id) .single(); if (!parentPost) { throw new Error('Parent post not found'); } if (parentPost.is_locked) { throw new Error('This post is locked and cannot receive replies'); } depth = parentPost.depth + 1; thread_root_id = parentPost.thread_root_id || parentPost.id; if (depth > 10) { throw new Error('Maximum reply depth exceeded'); } } // Validate required fields if (depth === 0 && !input.title) { throw new Error('Title is required for top-level posts'); } if (input.post_type === 'discussion' && !input.category_id) { throw new Error('Category is required for discussion posts'); } if ( input.post_type === 'bill_comment' && (!input.bill_number || !input.bill_session) ) { throw new Error('Bill number and session are required for bill comments'); } // Check rate limit const { data: canPost } = await supabase.rpc('check_post_rate_limit', { p_user_id: userId, }); if (canPost === false) { throw new Error( 'Rate limit exceeded. You can only create 10 posts per hour. Please try again later.' ); } // Create post const postData: any = { post_type: input.post_type, content: input.content, author_id: userId, author_name: profile?.display_name || session.user.email?.split('@')[0] || 'Anonymous', author_avatar_url: profile?.avatar_url, depth, thread_root_id, parent_post_id: input.parent_post_id || null, }; if (input.title) postData.title = input.title; if (input.category_id) postData.category_id = input.category_id; if (input.bill_number) postData.bill_number = input.bill_number; if (input.bill_session) postData.bill_session = input.bill_session; if (input.entity_metadata) postData.entity_metadata = input.entity_metadata; const { data, error } = await supabase .from('forum_posts') .insert(postData) .select() .single(); if (error) throw error; return { success: true, data }; } catch (error) { console.error('Error creating post:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to create post', }; } } // ============================================ // POSTS - UPDATE // ============================================ export async function updatePost( postId: string, input: UpdatePostInput ): Promise<ApiResponse<ForumPost>> { try { const supabase = await createServerClient(); const session = await auth(); if (!session?.user?.id) { throw new Error('You must be logged in to update a post'); } const userId = session.user.id; // Check ownership const { data: post } = await supabase .from('forum_posts') .select('author_id, is_locked') .eq('id', postId) .single(); if (!post) { throw new Error('Post not found'); } if (post.author_id !== userId) { throw new Error('You can only edit your own posts'); } if (post.is_locked) { throw new Error('This post is locked and cannot be edited'); } // Update post const { data, error } = await supabase .from('forum_posts') .update(input) .eq('id', postId) .select() .single(); if (error) throw error; return { success: true, data }; } catch (error) { console.error('Error updating post:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to update post', }; } } // ============================================ // POSTS - DELETE (Soft Delete) // ============================================ export async function deletePost(postId: string): Promise<ApiResponse> { try { const supabase = await createServerClient(); const session = await auth(); if (!session?.user?.id) { throw new Error('You must be logged in to delete a post'); } const userId = session.user.id; // Check ownership const { data: post } = await supabase .from('forum_posts') .select('author_id') .eq('id', postId) .single(); if (!post) { throw new Error('Post not found'); } if (post.author_id !== userId) { throw new Error('You can only delete your own posts'); } // Soft delete const { error } = await supabase .from('forum_posts') .update({ is_deleted: true, deleted_at: new Date().toISOString(), deleted_by: userId, }) .eq('id', postId); if (error) throw error; return { success: true }; } catch (error) { console.error('Error deleting post:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to delete post', }; } } // ============================================ // VOTING // ============================================ export async function votePost( postId: string, voteType: VoteType ): Promise<ApiResponse<{ upvotes: number; downvotes: number }>> { try { const supabase = await createServerClient(); const session = await auth(); if (!session?.user?.id) { throw new Error('You must be logged in to vote'); } const userId = session.user.id; // Check if post exists const { data: post } = await supabase .from('forum_posts') .select('id') .eq('id', postId) .single(); if (!post) { throw new Error('Post not found'); } // Check existing vote const { data: existingVote } = await supabase .from('forum_votes') .select('vote_type') .eq('post_id', postId) .eq('user_id', userId) .maybeSingle(); if (existingVote) { if (existingVote.vote_type === voteType) { // Remove vote (toggle off) await supabase .from('forum_votes') .delete() .eq('post_id', postId) .eq('user_id', userId); } else { // Change vote await supabase .from('forum_votes') .update({ vote_type: voteType }) .eq('post_id', postId) .eq('user_id', userId); } } else { // Create new vote await supabase.from('forum_votes').insert({ post_id: postId, user_id: userId, vote_type: voteType, }); } // Get updated vote counts const { data: updatedPost } = await supabase .from('forum_posts') .select('upvotes_count, downvotes_count') .eq('id', postId) .single(); return { success: true, data: { upvotes: updatedPost?.upvotes_count || 0, downvotes: updatedPost?.downvotes_count || 0, }, }; } catch (error) { console.error('Error voting:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to vote', }; } } // ============================================ // USER PROFILE // ============================================ export async function getUserProfile( userId: string ): Promise<ApiResponse<any>> { try { const supabase = await createServerClient(); const { data, error } = await supabase .from('user_profiles') .select('*') .eq('id', userId) .maybeSingle(); if (error) throw error; return { success: true, data }; } catch (error) { console.error('Error fetching profile:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to fetch profile', }; } } export async function ensureUserProfile(): Promise<ApiResponse> { try { const supabase = await createServerClient(); const session = await auth(); if (!session?.user?.id) { throw new Error('Not authenticated'); } const userId = session.user.id; // Check if profile exists const { data: existingProfile } = await supabase .from('user_profiles') .select('id') .eq('id', userId) .maybeSingle(); if (!existingProfile) { // Create profile const { error } = await supabase.from('user_profiles').insert({ id: userId, display_name: session.user.email?.split('@')[0] || 'User', }); if (error) throw error; } return { success: true }; } catch (error) { console.error('Error ensuring profile:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to create profile', }; } }

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