From 1888e185f78814a43076d8e8dbcbba3218e0ea5f Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 21:53:21 +0200 Subject: [PATCH] refactor(api): share Pagination across admin/public; cover get-by-id auth Co-Authored-By: Claude Sonnet 4.6 --- crates/api/src/admin_objects.rs | 23 ++--------------------- crates/api/src/lib.rs | 1 + crates/api/src/pagination.rs | 23 +++++++++++++++++++++++ crates/api/src/public.rs | 24 ++---------------------- crates/api/tests/admin_objects.rs | 12 ++++++++++++ 5 files changed, 40 insertions(+), 43 deletions(-) create mode 100644 crates/api/src/pagination.rs diff --git a/crates/api/src/admin_objects.rs b/crates/api/src/admin_objects.rs index e0a1bf3..24b5209 100644 --- a/crates/api/src/admin_objects.rs +++ b/crates/api/src/admin_objects.rs @@ -10,10 +10,10 @@ use axum::{ routing::get, }; use domain::{CatalogueObject, ObjectId}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use utoipa::ToSchema; -use crate::AppState; +use crate::{AppState, pagination::Pagination}; /// A localized label `{ lang, label }` (shared across admin views). #[derive(Serialize, ToSchema)] @@ -69,25 +69,6 @@ pub(crate) struct AdminObjectPage { pub offset: i64, } -#[derive(Deserialize)] -pub(crate) struct Pagination { - limit: Option, - offset: Option, -} - -const DEFAULT_LIMIT: i64 = 50; -const MAX_LIMIT: i64 = 200; - -impl Pagination { - fn limit(&self) -> i64 { - self.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT) - } - - fn offset(&self) -> i64 { - self.offset.unwrap_or(0).max(0) - } -} - /// Format a `time::Date` as `YYYY-MM-DD`. pub(crate) fn format_date(d: time::Date) -> String { let fmt = time::macros::format_description!("[year]-[month]-[day]"); diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 74b081e..5656210 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -4,6 +4,7 @@ mod admin; mod admin_objects; mod health; mod openapi; +mod pagination; mod public; use axum::Router; diff --git a/crates/api/src/pagination.rs b/crates/api/src/pagination.rs new file mode 100644 index 0000000..a45711e --- /dev/null +++ b/crates/api/src/pagination.rs @@ -0,0 +1,23 @@ +//! Shared pagination query parameters used by both admin and public handlers. + +use serde::Deserialize; + +pub(crate) const DEFAULT_LIMIT: i64 = 50; +pub(crate) const MAX_LIMIT: i64 = 200; + +/// Pagination query parameters with sane defaults and a hard cap. +#[derive(Deserialize)] +pub(crate) struct Pagination { + pub(crate) limit: Option, + pub(crate) offset: Option, +} + +impl Pagination { + pub(crate) fn limit(&self) -> i64 { + self.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT) + } + + pub(crate) fn offset(&self) -> i64 { + self.offset.unwrap_or(0).max(0) + } +} diff --git a/crates/api/src/public.rs b/crates/api/src/public.rs index 3c61db5..b36f2c7 100644 --- a/crates/api/src/public.rs +++ b/crates/api/src/public.rs @@ -14,10 +14,10 @@ use axum::{ routing::get, }; use domain::{CatalogueObject, ObjectId}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use utoipa::ToSchema; -use crate::AppState; +use crate::{AppState, pagination::Pagination}; /// A catalogue object as exposed on the public surface (public-safe fields only). #[derive(Serialize, ToSchema)] @@ -50,26 +50,6 @@ pub(crate) struct PublicObjectPage { pub offset: i64, } -/// Pagination query parameters with sane defaults and a hard cap. -#[derive(Deserialize)] -pub(crate) struct Pagination { - limit: Option, - offset: Option, -} - -const DEFAULT_LIMIT: i64 = 50; -const MAX_LIMIT: i64 = 200; - -impl Pagination { - fn limit(&self) -> i64 { - self.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT) - } - - fn offset(&self) -> i64 { - self.offset.unwrap_or(0).max(0) - } -} - /// List public objects (paginated). #[utoipa::path( get, diff --git a/crates/api/tests/admin_objects.rs b/crates/api/tests/admin_objects.rs index 1dcd6b3..7aeb492 100644 --- a/crates/api/tests/admin_objects.rs +++ b/crates/api/tests/admin_objects.rs @@ -97,7 +97,19 @@ async fn list_and_get_require_auth(pool: PgPool) { ) .await .unwrap(); + + let get = app + .oneshot( + Request::builder() + .uri(format!("/api/admin/objects/{}", domain::ObjectId::new())) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(list.status(), StatusCode::UNAUTHORIZED); + assert_eq!(get.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "../db/migrations")]