import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from api.database import get_session from api.models.task import Task from api.models.workstream import Workstream from api.models.workstream_dependency import WorkstreamDependency from api.schemas.workstream_dependency import WorkstreamDependencyCreate, WorkstreamDependencyRead router = APIRouter(prefix="/workstreams", tags=["dependencies"]) @router.post( "/{workstream_id}/dependencies/", response_model=WorkstreamDependencyRead, status_code=status.HTTP_201_CREATED, ) async def create_dependency( workstream_id: uuid.UUID, body: WorkstreamDependencyCreate, session: AsyncSession = Depends(get_session), ) -> WorkstreamDependency: """Record that workstream_id depends on another workstream or a task.""" if await session.get(Workstream, workstream_id) is None: raise HTTPException(status_code=404, detail="from workstream not found") has_workstream_target = body.to_workstream_id is not None has_task_target = body.to_task_id is not None if has_workstream_target == has_task_target: raise HTTPException(status_code=422, detail="provide exactly one dependency target") if body.to_workstream_id and await session.get(Workstream, body.to_workstream_id) is None: raise HTTPException(status_code=404, detail="target workstream not found") if body.to_task_id and await session.get(Task, body.to_task_id) is None: raise HTTPException(status_code=404, detail="target task not found") if workstream_id == body.to_workstream_id: raise HTTPException(status_code=422, detail="a workstream cannot depend on itself") dep = WorkstreamDependency( from_workstream_id=workstream_id, to_workstream_id=body.to_workstream_id, to_task_id=body.to_task_id, relationship_type=body.relationship_type, description=body.description, ) session.add(dep) await session.commit() await session.refresh(dep) return dep @router.get( "/{workstream_id}/dependencies/", response_model=list[WorkstreamDependencyRead], ) async def list_dependencies( workstream_id: uuid.UUID, session: AsyncSession = Depends(get_session), ) -> list[WorkstreamDependency]: """Return all dependency edges touching this workstream (both directions).""" if await session.get(Workstream, workstream_id) is None: raise HTTPException(status_code=404, detail="workstream not found") rows = await session.execute( select(WorkstreamDependency).where( (WorkstreamDependency.from_workstream_id == workstream_id) | (WorkstreamDependency.to_workstream_id == workstream_id) ) ) return list(rows.scalars().all()) @router.delete( "/{workstream_id}/dependencies/{dep_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_dependency( workstream_id: uuid.UUID, dep_id: uuid.UUID, session: AsyncSession = Depends(get_session), ) -> None: """Hard-delete a dependency edge. Removing a constraint is safe — no information is lost.""" dep = await session.get(WorkstreamDependency, dep_id) if dep is None: raise HTTPException(status_code=404, detail="dependency not found") if dep.from_workstream_id != workstream_id: raise HTTPException(status_code=403, detail="dependency does not belong to this workstream") await session.delete(dep) await session.commit()