"""API routes for board-to-repository linking operations.""" from __future__ import annotations from typing import TYPE_CHECKING from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel import select from sqlalchemy.exc import IntegrityError from app.api.deps import ( get_board_for_actor_read, get_board_for_actor_write, ) from app.core.logging import get_logger from app.core.time import utcnow from app.db import crud from app.db.session import get_session from app.models.forgejo_repositories import ForgejoRepository from app.models.board_repository_links import BoardRepositoryLink from app.schemas.board_repository_links import ( BoardRepositoryLinkCreate, BoardRepositoryLinkDeleteResponse, BoardRepositoryLinkRead, BoardRepositoryLinkResponse, ) if TYPE_CHECKING: from sqlalchemy.ext.asyncio.session import AsyncSession router = APIRouter(prefix="/boards/{board_id}/forgejo/repositories", tags=["board-repositories"]) logger = get_logger(__name__) SESSION_DEP = Depends(get_session) BOARD_READ_DEP = Depends(get_board_for_actor_read) BOARD_WRITE_DEP = Depends(get_board_for_actor_write) @router.get("", response_model=list[BoardRepositoryLinkRead]) async def list_board_repositories( board_id: UUID, session: AsyncSession = SESSION_DEP, board: BoardRepositoryLink = BOARD_READ_DEP, ) -> list[BoardRepositoryLinkRead]: """List repositories linked to a board.""" statement = select(BoardRepositoryLink).where( BoardRepositoryLink.board_id == board_id ) links = (await session.exec(statement)).all() return [BoardRepositoryLinkRead.model_validate(link) for link in links] @router.post("", response_model=BoardRepositoryLinkResponse) async def link_repository_to_board( board_id: UUID, payload: BoardRepositoryLinkCreate, session: AsyncSession = SESSION_DEP, board: BoardRepositoryLink = BOARD_WRITE_DEP, ) -> BoardRepositoryLinkResponse: """Link a Forgejo repository to a board.""" # Verify repository belongs to same organization as board repository = await crud.get_by_id(session, ForgejoRepository, payload.repository_id) if repository is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found or access denied", ) if repository.organization_id != board.organization_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Repository must belong to the same organization as the board", ) # Create the link link = BoardRepositoryLink( board_id=board_id, repository_id=payload.repository_id, organization_id=board.organization_id, ) try: await crud.create(session, BoardRepositoryLink, **link.model_dump()) await session.flush() link_read = BoardRepositoryLinkRead.model_validate(link) return BoardRepositoryLinkResponse(link=link_read) except IntegrityError: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Repository is already linked to this board", ) @router.delete("/{repository_id}", response_model=BoardRepositoryLinkDeleteResponse) async def unlink_repository_from_board( board_id: UUID, repository_id: UUID, session: AsyncSession = SESSION_DEP, board: BoardRepositoryLink = BOARD_WRITE_DEP, ) -> BoardRepositoryLinkDeleteResponse: """Remove a repository link from a board.""" statement = select(BoardRepositoryLink).where( BoardRepositoryLink.board_id == board_id, BoardRepositoryLink.repository_id == repository_id, ) link = (await session.exec(statement)).first() if link is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Repository link not found", ) await session.delete(link) await session.commit() return BoardRepositoryLinkDeleteResponse( success=True, message="Repository unlinked successfully", )