Implement 13 endpoints: read-only reference data (games, routes, pokemon), run CRUD with cascading deletes, and encounter management. Uses Pydantic v2 with camelCase alias generation to match frontend types, and nested response schemas for detail views. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Response
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import joinedload, selectinload
|
|
|
|
from app.core.database import get_session
|
|
from app.models.encounter import Encounter
|
|
from app.models.game import Game
|
|
from app.models.nuzlocke_run import NuzlockeRun
|
|
from app.schemas.run import RunCreate, RunDetailResponse, RunResponse, RunUpdate
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("", response_model=RunResponse, status_code=201)
|
|
async def create_run(
|
|
data: RunCreate, session: AsyncSession = Depends(get_session)
|
|
):
|
|
# Validate game exists
|
|
game = await session.get(Game, data.game_id)
|
|
if game is None:
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
run = NuzlockeRun(
|
|
game_id=data.game_id,
|
|
name=data.name,
|
|
status="active",
|
|
rules=data.rules,
|
|
)
|
|
session.add(run)
|
|
await session.commit()
|
|
await session.refresh(run)
|
|
return run
|
|
|
|
|
|
@router.get("", response_model=list[RunResponse])
|
|
async def list_runs(session: AsyncSession = Depends(get_session)):
|
|
result = await session.execute(
|
|
select(NuzlockeRun).order_by(NuzlockeRun.started_at.desc())
|
|
)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/{run_id}", response_model=RunDetailResponse)
|
|
async def get_run(run_id: int, session: AsyncSession = Depends(get_session)):
|
|
result = await session.execute(
|
|
select(NuzlockeRun)
|
|
.where(NuzlockeRun.id == run_id)
|
|
.options(
|
|
joinedload(NuzlockeRun.game),
|
|
selectinload(NuzlockeRun.encounters)
|
|
.joinedload(Encounter.pokemon),
|
|
selectinload(NuzlockeRun.encounters)
|
|
.joinedload(Encounter.route),
|
|
)
|
|
)
|
|
run = result.scalar_one_or_none()
|
|
if run is None:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
return run
|
|
|
|
|
|
@router.patch("/{run_id}", response_model=RunResponse)
|
|
async def update_run(
|
|
run_id: int,
|
|
data: RunUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
run = await session.get(NuzlockeRun, run_id)
|
|
if run is None:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(run, field, value)
|
|
|
|
await session.commit()
|
|
await session.refresh(run)
|
|
return run
|
|
|
|
|
|
@router.delete("/{run_id}", status_code=204)
|
|
async def delete_run(
|
|
run_id: int, session: AsyncSession = Depends(get_session)
|
|
):
|
|
run = await session.get(NuzlockeRun, run_id)
|
|
if run is None:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
|
|
# Delete associated encounters first
|
|
encounters = await session.execute(
|
|
select(Encounter).where(Encounter.run_id == run_id)
|
|
)
|
|
for enc in encounters.scalars():
|
|
await session.delete(enc)
|
|
|
|
await session.delete(run)
|
|
await session.commit()
|
|
return Response(status_code=204)
|