Files

83 lines
2.5 KiB
Python
Raw Permalink Normal View History

"""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"}