feat: add validation for minimum length on various fields and update type definitions

This commit is contained in:
Abhimanyu Saharan
2026-02-06 16:12:04 +05:30
parent ca614328ac
commit d86fe0a7a6
157 changed files with 12340 additions and 2977 deletions

View File

@@ -2,24 +2,26 @@ from __future__ import annotations
import asyncio
import json
from collections.abc import AsyncIterator
from datetime import datetime, timezone
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy import asc, or_
from sqlmodel import Session, col, select
from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from sse_starlette.sse import EventSourceResponse
from starlette.concurrency import run_in_threadpool
from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent
from app.db.session import engine, get_session
from app.core.auth import AuthContext
from app.core.time import utcnow
from app.db.session import async_session_maker, get_session
from app.models.approvals import Approval
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalUpdate
from app.models.boards import Board
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus, ApprovalUpdate
router = APIRouter(prefix="/boards/{board_id}/approvals", tags=["approvals"])
ALLOWED_STATUSES = {"pending", "approved", "rejected"}
def _parse_since(value: str | None) -> datetime | None:
if not value:
@@ -45,30 +47,30 @@ def _serialize_approval(approval: Approval) -> dict[str, object]:
return ApprovalRead.model_validate(approval, from_attributes=True).model_dump(mode="json")
def _fetch_approval_events(
async def _fetch_approval_events(
session: AsyncSession,
board_id: UUID,
since: datetime,
) -> list[Approval]:
with Session(engine) as session:
statement = (
select(Approval)
.where(col(Approval.board_id) == board_id)
.where(
or_(
col(Approval.created_at) >= since,
col(Approval.resolved_at) >= since,
)
statement = (
select(Approval)
.where(col(Approval.board_id) == board_id)
.where(
or_(
col(Approval.created_at) >= since,
col(Approval.resolved_at) >= since,
)
.order_by(asc(col(Approval.created_at)))
)
return list(session.exec(statement))
.order_by(asc(col(Approval.created_at)))
)
return list(await session.exec(statement))
@router.get("", response_model=list[ApprovalRead])
def list_approvals(
status_filter: str | None = Query(default=None, alias="status"),
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
async def list_approvals(
status_filter: ApprovalStatus | None = Query(default=None, alias="status"),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent),
) -> list[Approval]:
if actor.actor_type == "agent" and actor.agent:
@@ -76,32 +78,31 @@ def list_approvals(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
statement = select(Approval).where(col(Approval.board_id) == board.id)
if status_filter:
if status_filter not in ALLOWED_STATUSES:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
statement = statement.where(col(Approval.status) == status_filter)
statement = statement.order_by(col(Approval.created_at).desc())
return list(session.exec(statement))
return list(await session.exec(statement))
@router.get("/stream")
async def stream_approvals(
request: Request,
board=Depends(get_board_or_404),
board: Board = Depends(get_board_or_404),
actor: ActorContext = Depends(require_admin_or_agent),
since: str | None = Query(default=None),
) -> EventSourceResponse:
if actor.actor_type == "agent" and actor.agent:
if actor.agent.board_id and actor.agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
since_dt = _parse_since(since) or datetime.utcnow()
since_dt = _parse_since(since) or utcnow()
last_seen = since_dt
async def event_generator():
async def event_generator() -> AsyncIterator[dict[str, str]]:
nonlocal last_seen
while True:
if await request.is_disconnected():
break
approvals = await run_in_threadpool(_fetch_approval_events, board.id, last_seen)
async with async_session_maker() as session:
approvals = await _fetch_approval_events(session, board.id, last_seen)
for approval in approvals:
updated_at = _approval_updated_at(approval)
if updated_at > last_seen:
@@ -114,10 +115,10 @@ async def stream_approvals(
@router.post("", response_model=ApprovalRead)
def create_approval(
async def create_approval(
payload: ApprovalCreate,
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent),
) -> Approval:
if actor.actor_type == "agent" and actor.agent:
@@ -133,30 +134,28 @@ def create_approval(
status=payload.status,
)
session.add(approval)
session.commit()
session.refresh(approval)
await session.commit()
await session.refresh(approval)
return approval
@router.patch("/{approval_id}", response_model=ApprovalRead)
def update_approval(
async def update_approval(
approval_id: str,
payload: ApprovalUpdate,
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
auth=Depends(require_admin_auth),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> Approval:
approval = session.get(Approval, approval_id)
approval = await session.get(Approval, approval_id)
if approval is None or approval.board_id != board.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
updates = payload.model_dump(exclude_unset=True)
if "status" in updates:
if updates["status"] not in ALLOWED_STATUSES:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
approval.status = updates["status"]
if approval.status != "pending":
approval.resolved_at = datetime.utcnow()
approval.resolved_at = utcnow()
session.add(approval)
session.commit()
session.refresh(approval)
await session.commit()
await session.refresh(approval)
return approval