use sea_orm::{prelude::*, Condition, DatabaseConnection, EntityTrait, QueryFilter, SelectorTrait};
use serde::{Deserialize, Serialize};
const fn default_page_size() -> u64 {
25
}
const fn default_page() -> u64 {
1
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PaginationQuery {
#[serde(
default = "default_page_size",
rename = "page_size",
deserialize_with = "deserialize_pagination_filter"
)]
pub page_size: u64,
#[serde(
default = "default_page",
rename = "page",
deserialize_with = "deserialize_pagination_filter"
)]
pub page: u64,
}
impl PaginationQuery {
#[must_use]
pub fn page(page: u64) -> Self {
Self {
page,
..Default::default()
}
}
}
impl Default for PaginationQuery {
fn default() -> Self {
Self {
page_size: default_page_size(),
page: default_page(),
}
}
}
fn deserialize_pagination_filter<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
use crate::controller::views::pagination::PagerMeta;
#[derive(Debug, Serialize, Deserialize)]
pub struct PageResponse<T> {
pub page: Vec<T>,
pub meta: PagerMeta,
}
use crate::Result as LocoResult;
pub async fn paginate<E>(
db: &DatabaseConnection,
entity: Select<E>,
condition: Option<Condition>,
pagination_query: &PaginationQuery,
) -> LocoResult<PageResponse<E::Model>>
where
E: EntityTrait,
<E as EntityTrait>::Model: Sync,
{
let entity = if let Some(condition) = condition {
entity.filter(condition)
} else {
entity
};
fetch_page(db, entity, pagination_query).await
}
pub async fn fetch_page<'db, C, S>(
db: &'db C,
selector: S,
pagination_query: &PaginationQuery,
) -> LocoResult<PageResponse<<<S as PaginatorTrait<'db, C>>::Selector as SelectorTrait>::Item>>
where
C: ConnectionTrait + Sync,
S: PaginatorTrait<'db, C> + Send,
{
let page = pagination_query.page.saturating_sub(1);
let page_size = pagination_query.page_size.max(1);
let query = selector.paginate(db, page_size);
let total_pages_and_items = query.num_items_and_pages().await?;
let page = query.fetch_page(page).await?;
Ok(PageResponse {
page,
meta: PagerMeta {
page: pagination_query.page,
page_size,
total_pages: total_pages_and_items.number_of_pages,
total_items: total_pages_and_items.number_of_items,
},
})
}
#[cfg(test)]
mod tests {
use axum::extract::Query;
use serde::Deserialize;
use super::*;
fn parse<T: serde::de::DeserializeOwned>(query: &str) -> T {
let uri: axum::http::Uri = format!("http://localhost/?{query}")
.parse()
.expect("a valid URI");
Query::try_from_uri(&uri)
.unwrap_or_else(|err| panic!("`?{query}` should deserialize: {err}"))
.0
}
#[test]
fn serializes_to_page_and_meta() {
let res = PageResponse {
page: vec![1, 2, 3],
meta: PagerMeta {
page: 2,
page_size: 3,
total_pages: 4,
total_items: 10,
},
};
let json = serde_json::to_value(&res).expect("PageResponse serializes");
assert_eq!(
json,
serde_json::json!({
"page": [1, 2, 3],
"meta": {
"page": 2,
"page_size": 3,
"total_pages": 4,
"total_items": 10
}
})
);
let back: PageResponse<i32> =
serde_json::from_value(json).expect("PageResponse reads its own output");
assert_eq!(back.page, vec![1, 2, 3]);
assert_eq!(back.meta.total_items, 10);
}
#[test]
fn reads_page_and_page_size_from_a_query_string() {
let q: PaginationQuery = parse("page=2&page_size=10");
assert_eq!(q.page, 2);
assert_eq!(q.page_size, 10);
}
#[test]
fn falls_back_to_defaults_when_absent() {
let q: PaginationQuery = parse("");
assert_eq!(q.page, default_page());
assert_eq!(q.page_size, default_page_size());
}
#[test]
fn survives_being_flattened_into_a_filter_struct() {
#[derive(Debug, Deserialize)]
struct ListParams {
status: Option<String>,
#[serde(flatten)]
pagination: PaginationQuery,
}
let params: ListParams = parse("status=draft&page=3&page_size=5");
assert_eq!(params.status.as_deref(), Some("draft"));
assert_eq!(params.pagination.page, 3);
assert_eq!(params.pagination.page_size, 5);
let defaults: ListParams = parse("status=draft");
assert_eq!(defaults.pagination.page, default_page());
assert_eq!(defaults.pagination.page_size, default_page_size());
}
}