use axum::{ Json, extract::{Path, State}, }; use serde::{Deserialize, Serialize}; use crate::{error::ApiError, state::AppState}; #[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct CreateSavedSearchRequest { pub name: String, pub query: String, pub sort_order: Option, } #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct SavedSearchResponse { pub id: String, pub name: String, pub query: String, pub sort_order: Option, pub created_at: chrono::DateTime, } const VALID_SORT_ORDERS: &[&str] = &[ "date_asc", "date_desc", "name_asc", "name_desc", "size_asc", "size_desc", ]; #[utoipa::path( post, path = "/api/v1/searches", tag = "saved_searches", request_body = CreateSavedSearchRequest, responses( (status = 200, description = "Search saved", body = SavedSearchResponse), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error"), ), security(("bearer_auth" = [])) )] pub async fn create_saved_search( State(state): State, Json(req): Json, ) -> Result, ApiError> { let name_len = req.name.chars().count(); if name_len == 0 || name_len > 255 { return Err(ApiError( pinakes_core::error::PinakesError::InvalidOperation( "name must be 1-255 characters".into(), ), )); } if req.query.is_empty() || req.query.len() > 2048 { return Err(ApiError( pinakes_core::error::PinakesError::InvalidOperation( "query must be 1-2048 bytes".into(), ), )); } if let Some(ref sort) = req.sort_order && !VALID_SORT_ORDERS.contains(&sort.as_str()) { return Err(ApiError( pinakes_core::error::PinakesError::InvalidOperation(format!( "sort_order must be one of: {}", VALID_SORT_ORDERS.join(", ") )), )); } let id = uuid::Uuid::now_v7(); state .storage .save_search(id, &req.name, &req.query, req.sort_order.as_deref()) .await .map_err(ApiError)?; Ok(Json(SavedSearchResponse { id: id.to_string(), name: req.name, query: req.query, sort_order: req.sort_order, created_at: chrono::Utc::now(), })) } #[utoipa::path( get, path = "/api/v1/searches", tag = "saved_searches", responses( (status = 200, description = "List of saved searches", body = Vec), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error"), ), security(("bearer_auth" = [])) )] pub async fn list_saved_searches( State(state): State, ) -> Result>, ApiError> { let searches = state .storage .list_saved_searches() .await .map_err(ApiError)?; Ok(Json( searches .into_iter() .map(|s| { SavedSearchResponse { id: s.id.to_string(), name: s.name, query: s.query, sort_order: s.sort_order, created_at: s.created_at, } }) .collect(), )) } #[utoipa::path( delete, path = "/api/v1/searches/{id}", tag = "saved_searches", params(("id" = uuid::Uuid, Path, description = "Saved search ID")), responses( (status = 200, description = "Saved search deleted"), (status = 401, description = "Unauthorized"), (status = 404, description = "Not found"), (status = 500, description = "Internal server error"), ), security(("bearer_auth" = [])) )] pub async fn delete_saved_search( State(state): State, Path(id): Path, ) -> Result, ApiError> { state .storage .delete_saved_search(id) .await .map_err(ApiError)?; Ok(Json(serde_json::json!({ "deleted": true }))) }