use axum::extract::{Path, Query, State}; use axum::routing::get; use axum::{Json, Router}; use serde::Deserialize; use uuid::Uuid; use crate::api::pagination::PagedResponse; use crate::app::AppState; use crate::domain::author::{Author, AuthorWithCount}; use crate::domain::manga::Manga; use crate::error::AppResult; use crate::repo; pub fn routes() -> Router { Router::new() .route("/authors", get(list)) .route("/authors/:id", get(get_one)) .route("/authors/:id/mangas", get(list_mangas)) } #[derive(Debug, Deserialize)] pub struct ListParams { #[serde(default)] pub search: Option, #[serde(default = "default_limit")] pub limit: i64, #[serde(default)] pub offset: i64, } fn default_limit() -> i64 { 10 } #[derive(Debug, Deserialize)] pub struct MangaListParams { #[serde(default = "default_manga_limit")] pub limit: i64, #[serde(default)] pub offset: i64, } fn default_manga_limit() -> i64 { 50 } async fn list( State(state): State, Query(params): Query, ) -> AppResult>> { let limit = params.limit.clamp(1, 50); let offset = params.offset.max(0); let search = params.search.as_deref().and_then(|s| { let t = s.trim(); if t.is_empty() { None } else { Some(t) } }); Ok(Json(repo::author::list(&state.db, search, limit, offset).await?)) } async fn get_one( State(state): State, Path(id): Path, ) -> AppResult> { Ok(Json(repo::author::find_with_count(&state.db, id).await?)) } async fn list_mangas( State(state): State, Path(id): Path, Query(params): Query, ) -> AppResult>> { let limit = params.limit.clamp(1, 200); let offset = params.offset.max(0); // Intentionally does NOT 404 on unknown id — the join naturally // returns zero rows, and the page envelope already conveys that. // Saves a round-trip and matches the shape of GET /mangas. let (items, total) = repo::author::list_mangas_for_author(&state.db, id, limit, offset).await?; Ok(Json(PagedResponse::with_total(items, limit, offset, total))) }