""" Heaps API router """ from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select, func, update, insert, delete from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import Heap from app.models.heaps import heap_photos router = APIRouter() # ── Schemas ─────────────────────────────────────────────────────────────── class HeapCreate(BaseModel): name: str class HeapUpdate(BaseModel): name: Optional[str] = None is_active: Optional[bool] = None class HeapPhotosBody(BaseModel): photo_ids: list[str] # ── Endpoints ───────────────────────────────────────────────────────────── @router.get("") async def list_heaps(db: AsyncSession = Depends(get_db)): """List all heaps with photo counts.""" # LEFT JOIN heap_photos and group so we can return counts in one query. count_subq = ( select( heap_photos.c.heap_id, func.count(heap_photos.c.photo_id).label("photo_count"), ) .group_by(heap_photos.c.heap_id) .subquery() ) stmt = ( select(Heap, count_subq.c.photo_count) .outerjoin(count_subq, Heap.id == count_subq.c.heap_id) .order_by(Heap.created_at.asc()) ) result = await db.execute(stmt) rows = result.all() return [ { "id": h.id, "name": h.name, "is_active": bool(h.is_active), "created_at": h.created_at, "updated_at": h.updated_at, "photo_count": int(count or 0), } for h, count in rows ] @router.post("", status_code=201) async def create_heap(body: HeapCreate, db: AsyncSession = Depends(get_db)): """Create a new heap.""" name = (body.name or "").strip() if not name: raise HTTPException(status_code=400, detail="Heap name is required") heap = Heap(name=name) db.add(heap) await db.commit() await db.refresh(heap) return { "id": heap.id, "name": heap.name, "is_active": bool(heap.is_active), "created_at": heap.created_at, "updated_at": heap.updated_at, "photo_count": 0, } @router.patch("/{heap_id}") async def update_heap( heap_id: str, body: HeapUpdate, db: AsyncSession = Depends(get_db) ): """Rename a heap and/or toggle active state. Setting is_active=true on one heap deactivates all others (single-active invariant).""" result = await db.execute(select(Heap).where(Heap.id == heap_id)) heap = result.scalar_one_or_none() if not heap: raise HTTPException(status_code=404, detail="Heap not found") if body.name is not None: name = body.name.strip() if not name: raise HTTPException(status_code=400, detail="Heap name is required") heap.name = name if body.is_active is not None: if body.is_active: # Clear active flag on all other heaps in one statement await db.execute(update(Heap).values(is_active=False)) heap.is_active = True else: heap.is_active = False await db.commit() await db.refresh(heap) return { "id": heap.id, "name": heap.name, "is_active": bool(heap.is_active), "created_at": heap.created_at, "updated_at": heap.updated_at, } @router.delete("/{heap_id}", status_code=204) async def delete_heap(heap_id: str, db: AsyncSession = Depends(get_db)): """Delete a heap. Photos themselves are unaffected — only the membership rows in heap_photos cascade-delete.""" result = await db.execute(select(Heap).where(Heap.id == heap_id)) heap = result.scalar_one_or_none() if not heap: raise HTTPException(status_code=404, detail="Heap not found") await db.delete(heap) await db.commit() return None @router.post("/{heap_id}/photos") async def add_photos_to_heap( heap_id: str, body: HeapPhotosBody, db: AsyncSession = Depends(get_db) ): """Add photos to a heap. Idempotent: re-adding existing members is a no-op (handled by an INSERT OR IGNORE-style filter on duplicates).""" result = await db.execute(select(Heap).where(Heap.id == heap_id)) heap = result.scalar_one_or_none() if not heap: raise HTTPException(status_code=404, detail="Heap not found") if not body.photo_ids: return {"status": "success", "added": 0} # Find which ids are already members so we don't violate the PK. existing = await db.execute( select(heap_photos.c.photo_id).where( heap_photos.c.heap_id == heap_id, heap_photos.c.photo_id.in_(body.photo_ids), ) ) existing_ids = {row[0] for row in existing.all()} new_ids = [pid for pid in body.photo_ids if pid not in existing_ids] if new_ids: await db.execute( insert(heap_photos), [{"heap_id": heap_id, "photo_id": pid} for pid in new_ids], ) await db.commit() return {"status": "success", "added": len(new_ids), "already_present": len(existing_ids)} @router.delete("/{heap_id}/photos") async def remove_photos_from_heap( heap_id: str, body: HeapPhotosBody, db: AsyncSession = Depends(get_db) ): """Remove photos from a heap. Removing a non-member is a no-op.""" result = await db.execute(select(Heap).where(Heap.id == heap_id)) heap = result.scalar_one_or_none() if not heap: raise HTTPException(status_code=404, detail="Heap not found") if not body.photo_ids: return {"status": "success", "removed": 0} res = await db.execute( delete(heap_photos).where( heap_photos.c.heap_id == heap_id, heap_photos.c.photo_id.in_(body.photo_ids), ) ) await db.commit() return {"status": "success", "removed": res.rowcount or 0}