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