feat: saved searches (bookmarks)
All checks were successful
CI / lint-and-test (push) Successful in 23s
All checks were successful
CI / lint-and-test (push) Successful in 23s
- Add saved_searches_collection to database.py with index on created_by+created_at - New routes/saved_searches.py: GET /api/saved-searches, POST, DELETE - Saved searches are scoped per user (created_by = token sub) - Mount router in main.py - Frontend: Save filters button, saved search pills with load/delete - loadSavedSearches called on initApp - applySavedSearch restores filters and validates services against current options - Add CSS for saved-searches row - Add tests for CRUD, delete 404, and name validation
This commit is contained in:
60
backend/routes/saved_searches.py
Normal file
60
backend/routes/saved_searches.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""CRUD for saved filter searches (bookmarks)."""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import structlog
|
||||
from auth import require_auth
|
||||
from database import saved_searches_collection
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
router = APIRouter(dependencies=[Depends(require_auth)])
|
||||
logger = structlog.get_logger("aoc.saved_searches")
|
||||
|
||||
|
||||
def _user_sub(user: dict) -> str:
|
||||
return user.get("sub", "anonymous")
|
||||
|
||||
|
||||
@router.get("/saved-searches")
|
||||
async def list_saved_searches(user: dict = Depends(require_auth)):
|
||||
"""Return saved searches for the current user."""
|
||||
sub = _user_sub(user)
|
||||
cursor = saved_searches_collection.find({"created_by": sub}).sort("created_at", -1)
|
||||
items = []
|
||||
for doc in cursor:
|
||||
doc["id"] = doc.pop("_id")
|
||||
items.append(doc)
|
||||
return items
|
||||
|
||||
|
||||
@router.post("/saved-searches")
|
||||
async def create_saved_search(body: dict, user: dict = Depends(require_auth)):
|
||||
"""Save the current filter set."""
|
||||
name = (body.get("name") or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Name is required")
|
||||
|
||||
filters = body.get("filters") or {}
|
||||
doc = {
|
||||
"_id": str(uuid.uuid4()),
|
||||
"name": name,
|
||||
"filters": filters,
|
||||
"created_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
||||
"created_by": _user_sub(user),
|
||||
}
|
||||
saved_searches_collection.insert_one(doc)
|
||||
logger.info("Saved search created", name=name, user=doc["created_by"])
|
||||
doc["id"] = doc.pop("_id")
|
||||
return doc
|
||||
|
||||
|
||||
@router.delete("/saved-searches/{search_id}")
|
||||
async def delete_saved_search(search_id: str, user: dict = Depends(require_auth)):
|
||||
"""Delete a saved search (only if owned by current user)."""
|
||||
sub = _user_sub(user)
|
||||
result = saved_searches_collection.delete_one({"_id": search_id, "created_by": sub})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=404, detail="Saved search not found")
|
||||
logger.info("Saved search deleted", search_id=search_id, user=sub)
|
||||
return {"status": "deleted"}
|
||||
Reference in New Issue
Block a user