"""Saved searches — zapisane słowa kluczowe per urządzenie. User-report mobilism: sceny słabo opisane → user chce zapisywać często używane zapytania i szybko je odpalać. Scope po device_id (X-Device-Id), jak reszta stanu. Endpointy: GET /saved-searches — lista zapisów tego urządzenia (najnowsze pierwsze) POST /saved-searches — dodaj {query} (idempotent na device+query) DELETE /saved-searches/{id} — usuń """ from __future__ import annotations import uuid from datetime import datetime from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, field_validator from sqlalchemy import select from sqlalchemy.orm import Session from app.api.device import get_device_id from app.auth import require_api_key from app.db import get_session from app.models.saved_search import SavedSearch router = APIRouter( prefix="/saved-searches", tags=["saved-searches"], dependencies=[Depends(require_api_key)], ) # Limit zapisów per urządzenie — to wygodny skrót, nie nieograniczona kolekcja. MAX_PER_DEVICE = 50 class SavedSearchOut(BaseModel): id: uuid.UUID query: str created_at: datetime class SavedSearchIn(BaseModel): query: str @field_validator("query") @classmethod def _clean(cls, v: str) -> str: v = (v or "").strip() if not v: raise ValueError("query empty") return v[:256] @router.get("", response_model=list[SavedSearchOut]) def list_saved_searches( session: Annotated[Session, Depends(get_session)], device_id: Annotated[str, Depends(get_device_id)], ) -> list[SavedSearch]: rows = session.execute( select(SavedSearch) .where(SavedSearch.device_id == device_id) .order_by(SavedSearch.created_at.desc()) ).scalars().all() return list(rows) @router.post("", response_model=SavedSearchOut, status_code=status.HTTP_200_OK) def add_saved_search( payload: SavedSearchIn, session: Annotated[Session, Depends(get_session)], device_id: Annotated[str, Depends(get_device_id)], ) -> SavedSearch: # Idempotent — ten sam (device, query) zwraca istniejący wiersz. existing = session.execute( select(SavedSearch).where( SavedSearch.device_id == device_id, SavedSearch.query == payload.query ) ).scalar_one_or_none() if existing is not None: return existing count = session.execute( select(SavedSearch.id).where(SavedSearch.device_id == device_id) ).scalars().all() if len(count) >= MAX_PER_DEVICE: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"saved-search limit ({MAX_PER_DEVICE}) reached", ) row = SavedSearch(device_id=device_id, query=payload.query) session.add(row) session.commit() session.refresh(row) return row @router.delete("/{search_id}", status_code=status.HTTP_204_NO_CONTENT) def remove_saved_search( search_id: uuid.UUID, session: Annotated[Session, Depends(get_session)], device_id: Annotated[str, Depends(get_device_id)], ) -> None: row = session.get(SavedSearch, search_id) # Tylko własne urządzenie może usuwać; cudze/nieistniejące → idempotentny no-op. if row is None or row.device_id != device_id: return session.delete(row) session.commit()