Skip to main content

sova_db/
page.rs

1//! Offset pagination helpers (`Page` / `PageParams` / SeaORM `paginate`).
2
3use crate::error::DbError;
4use crate::handle::DbHandle;
5use sea_orm::{EntityTrait, FromQueryResult, PaginatorTrait, Select};
6use serde::Serialize;
7use sova_core::Request;
8
9const DEFAULT_PER_PAGE: u64 = 15;
10const MAX_PER_PAGE: u64 = 100;
11
12/// 1-based page + page size from query (`?page=&per_page=`).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct PageParams {
15    pub page: u64,
16    pub per_page: u64,
17}
18
19impl Default for PageParams {
20    fn default() -> Self {
21        Self {
22            page: 1,
23            per_page: DEFAULT_PER_PAGE,
24        }
25    }
26}
27
28impl PageParams {
29    pub fn new(page: u64, per_page: u64) -> Self {
30        Self {
31            page: page.max(1),
32            per_page: per_page.clamp(1, MAX_PER_PAGE),
33        }
34    }
35
36    pub fn from_request(req: &Request) -> Self {
37        let page = req.query("page").and_then(|s| s.parse().ok()).unwrap_or(1);
38        let per_page = req
39            .query("per_page")
40            .and_then(|s| s.parse().ok())
41            .unwrap_or(DEFAULT_PER_PAGE);
42        Self::new(page, per_page)
43    }
44
45    pub fn offset(&self) -> u64 {
46        self.page.saturating_sub(1).saturating_mul(self.per_page)
47    }
48}
49
50/// One page of results + meta.
51#[derive(Debug, Clone, Serialize)]
52pub struct Page<T> {
53    pub data: Vec<T>,
54    pub total: u64,
55    pub page: u64,
56    pub per_page: u64,
57    pub last_page: u64,
58}
59
60impl<T> Page<T> {
61    pub fn has_more(&self) -> bool {
62        self.page < self.last_page
63    }
64}
65
66/// `req.page_params()`.
67pub trait PageExt {
68    fn page_params(&self) -> PageParams;
69}
70
71impl PageExt for Request {
72    fn page_params(&self) -> PageParams {
73        PageParams::from_request(self)
74    }
75}
76
77/// Paginate a SeaORM [`Select`].
78pub trait PaginateExt<E>
79where
80    E: EntityTrait,
81{
82    fn paginate_sova(
83        self,
84        db: &DbHandle,
85        params: PageParams,
86    ) -> impl std::future::Future<Output = Result<Page<E::Model>, DbError>> + Send;
87}
88
89impl<E> PaginateExt<E> for Select<E>
90where
91    E: EntityTrait + Send,
92    E::Model: FromQueryResult + Sized + Send + Sync,
93{
94    async fn paginate_sova(
95        self,
96        db: &DbHandle,
97        params: PageParams,
98    ) -> Result<Page<E::Model>, DbError> {
99        let params = PageParams::new(params.page, params.per_page);
100        let paginator = self.paginate(db, params.per_page);
101        let total = paginator.num_items().await.map_err(DbError::from)?;
102        let last_page = if total == 0 {
103            1
104        } else {
105            total.div_ceil(params.per_page).max(1)
106        };
107        let page = params.page.min(last_page);
108        let data = paginator
109            .fetch_page(page.saturating_sub(1))
110            .await
111            .map_err(DbError::from)?;
112        Ok(Page {
113            data,
114            total,
115            page,
116            per_page: params.per_page,
117            last_page,
118        })
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use sova_core::Request;
126
127    #[test]
128    fn page_params_from_query() {
129        let req = Request::builder()
130            .path("/notes")
131            .query_param("page", "3")
132            .query_param("per_page", "10")
133            .build();
134        let p = PageParams::from_request(&req);
135        assert_eq!(p.page, 3);
136        assert_eq!(p.per_page, 10);
137        assert_eq!(p.offset(), 20);
138    }
139
140    #[test]
141    fn page_params_clamp() {
142        let p = PageParams::new(0, 999);
143        assert_eq!(p.page, 1);
144        assert_eq!(p.per_page, 100);
145    }
146}