"""Saved searches — stored queries shown as virtual mailboxes (SPEC §12 M2).""" from __future__ import annotations import uuid from typing import Annotated from fastapi import APIRouter, Depends from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.errors import raise_api_error from app.db import get_session from app.identity.deps import CurrentUser, require_csrf from app.models import SavedSearch router = APIRouter(prefix="/saved-searches", tags=["search"], dependencies=[Depends(require_csrf)]) DbSession = Annotated[AsyncSession, Depends(get_session)] class SavedSearchOut(BaseModel): id: uuid.UUID name: str query: str pinned: bool class SavedSearchIn(BaseModel): name: str = Field(min_length=1, max_length=80) query: str = Field(min_length=1, max_length=500) pinned: bool = True def _out(s: SavedSearch) -> SavedSearchOut: return SavedSearchOut(id=s.id, name=s.name, query=s.query, pinned=s.pinned) @router.get("", response_model=list[SavedSearchOut]) async def list_saved_searches(user: CurrentUser, db: DbSession) -> list[SavedSearchOut]: rows = await db.scalars( select(SavedSearch).where(SavedSearch.user_id == user.id).order_by(SavedSearch.name) ) return [_out(s) for s in rows] @router.post("", response_model=SavedSearchOut) async def create_saved_search( body: SavedSearchIn, user: CurrentUser, db: DbSession ) -> SavedSearchOut: name = body.name.strip() query = body.query.strip() if not name or not query: raise_api_error(400, "invalid", "name and query are required") # Upsert by (user, name): re-saving under an existing name updates its query. existing = await db.scalar( select(SavedSearch).where(SavedSearch.user_id == user.id, SavedSearch.name == name) ) if existing is not None: existing.query = query existing.pinned = body.pinned saved = existing else: saved = SavedSearch(user_id=user.id, name=name, query=query, pinned=body.pinned) db.add(saved) await db.commit() return _out(saved) @router.delete("/{search_id}") async def delete_saved_search( search_id: uuid.UUID, user: CurrentUser, db: DbSession ) -> dict[str, str]: saved = await db.scalar( select(SavedSearch).where(SavedSearch.id == search_id, SavedSearch.user_id == user.id) ) if saved is None: raise_api_error(404, "not_found", "saved search not found") await db.delete(saved) await db.commit() return {"status": "deleted"}