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>
This commit is contained in:
115
backend/app/api/endpoints/target_lists.py
Normal file
115
backend/app/api/endpoints/target_lists.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""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()
|
||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter
|
||||
from app.api.endpoints import auth, trees, sessions, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown
|
||||
from app.api.endpoints import admin_dashboard, admin_audit, admin_plan_limits, admin_feature_flags, admin_settings, admin_categories
|
||||
from app.api.endpoints import ratings, analytics
|
||||
from app.api.endpoints import target_lists
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -28,3 +29,4 @@ api_router.include_router(shared.router) # Public endpoints (no auth)
|
||||
api_router.include_router(tree_markdown.router)
|
||||
api_router.include_router(ratings.router)
|
||||
api_router.include_router(analytics.router)
|
||||
api_router.include_router(target_lists.router)
|
||||
|
||||
Reference in New Issue
Block a user