Pipeline/backend/app/api/board_repository_links.py

117 lines
4.0 KiB
Python
Raw Normal View History

"""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",
)