"""Follow system — users follow creators. Endpoints: POST /follows/{creator_id} — follow a creator DELETE /follows/{creator_id} — unfollow a creator GET /follows/{creator_id}/status — check if following GET /follows/me — list followed creators """ from __future__ import annotations import uuid from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import delete, func, select from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from auth import get_current_user from database import get_session from models import Creator, CreatorFollow, User from schemas import FollowResponse, FollowStatusResponse, FollowedCreatorItem router = APIRouter(prefix="/follows", tags=["follows"]) async def _follower_count(session: AsyncSession, creator_id: uuid.UUID) -> int: result = await session.execute( select(func.count()).select_from(CreatorFollow).where( CreatorFollow.creator_id == creator_id ) ) return result.scalar_one() @router.post("/{creator_id}", response_model=FollowResponse) async def follow_creator( creator_id: uuid.UUID, current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_session)], ): """Follow a creator. Idempotent — following twice is a no-op.""" # Verify creator exists result = await session.execute(select(Creator).where(Creator.id == creator_id)) if result.scalar_one_or_none() is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Creator not found") # Upsert — idempotent stmt = pg_insert(CreatorFollow).values( user_id=current_user.id, creator_id=creator_id, ).on_conflict_do_nothing(constraint="uq_creator_follow_user_creator") await session.execute(stmt) await session.commit() count = await _follower_count(session, creator_id) return FollowResponse(followed=True, creator_id=creator_id, follower_count=count) @router.delete("/{creator_id}", response_model=FollowResponse) async def unfollow_creator( creator_id: uuid.UUID, current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_session)], ): """Unfollow a creator. Idempotent — unfollowing when not following is a no-op.""" await session.execute( delete(CreatorFollow).where( CreatorFollow.user_id == current_user.id, CreatorFollow.creator_id == creator_id, ) ) await session.commit() count = await _follower_count(session, creator_id) return FollowResponse(followed=False, creator_id=creator_id, follower_count=count) @router.get("/me", response_model=list[FollowedCreatorItem]) async def my_follows( current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_session)], ): """List all creators the current user follows.""" result = await session.execute( select( CreatorFollow.creator_id, Creator.name.label("creator_name"), Creator.slug.label("creator_slug"), CreatorFollow.created_at.label("followed_at"), ) .join(Creator, CreatorFollow.creator_id == Creator.id) .where(CreatorFollow.user_id == current_user.id) .order_by(CreatorFollow.created_at.desc()) ) return [FollowedCreatorItem(**row._mapping) for row in result.all()] @router.get("/{creator_id}/status", response_model=FollowStatusResponse) async def follow_status( creator_id: uuid.UUID, current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_session)], ): """Check if the current user follows a specific creator.""" result = await session.execute( select(CreatorFollow.id).where( CreatorFollow.user_id == current_user.id, CreatorFollow.creator_id == creator_id, ) ) return FollowStatusResponse( following=result.scalar_one_or_none() is not None, creator_id=creator_id, )