Files
resolutionflow/backend/app/api/endpoints/target_lists.py
chihlasm 0c2d4ba685 feat: add target_lists table, schema, and CRUD endpoints
Introduces the TargetList model (team-scoped JSONB target arrays),
Pydantic schemas, and full CRUD REST API at /target-lists/.
Includes Alembic migration and 5 integration tests (TDD).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 11:16:40 -05:00

116 lines
3.8 KiB
Python

"""Target lists CRUD endpoints."""
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_active_user, get_db
from app.models.target_list import TargetList
from app.models.user import User
from app.schemas.target_list import TargetListCreate, TargetListUpdate, TargetListResponse
router = APIRouter(prefix="/target-lists", tags=["target-lists"])
@router.get("/", response_model=list[TargetListResponse])
async def list_target_lists(
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""List all target lists for the current user's team."""
if not current_user.team_id:
return []
result = await db.execute(
select(TargetList)
.where(TargetList.team_id == current_user.team_id)
.order_by(TargetList.name)
)
return result.scalars().all()
@router.post("/", response_model=TargetListResponse, status_code=201)
async def create_target_list(
data: TargetListCreate,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Create a new target list for the current team."""
if not current_user.team_id:
raise HTTPException(status_code=400, detail="User must belong to a team")
target_list = TargetList(
team_id=current_user.team_id,
created_by=current_user.id,
name=data.name,
description=data.description,
targets=[t.model_dump() for t in data.targets],
)
db.add(target_list)
await db.commit()
await db.refresh(target_list)
return target_list
@router.get("/{list_id}", response_model=TargetListResponse)
async def get_target_list(
list_id: UUID,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
result = await db.execute(
select(TargetList).where(
TargetList.id == list_id,
TargetList.team_id == current_user.team_id,
)
)
target_list = result.scalar_one_or_none()
if not target_list:
raise HTTPException(status_code=404, detail="Target list not found")
return target_list
@router.put("/{list_id}", response_model=TargetListResponse)
async def update_target_list(
list_id: UUID,
data: TargetListUpdate,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
result = await db.execute(
select(TargetList).where(
TargetList.id == list_id,
TargetList.team_id == current_user.team_id,
)
)
target_list = result.scalar_one_or_none()
if not target_list:
raise HTTPException(status_code=404, detail="Target list not found")
if data.name is not None:
target_list.name = data.name
if data.description is not None:
target_list.description = data.description
if data.targets is not None:
target_list.targets = [t.model_dump() for t in data.targets]
await db.commit()
await db.refresh(target_list)
return target_list
@router.delete("/{list_id}", status_code=204)
async def delete_target_list(
list_id: UUID,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
result = await db.execute(
select(TargetList).where(
TargetList.id == list_id,
TargetList.team_id == current_user.team_id,
)
)
target_list = result.scalar_one_or_none()
if not target_list:
raise HTTPException(status_code=404, detail="Target list not found")
await db.delete(target_list)
await db.commit()