by_loco/model/query/paginate/
mod.rs

1use sea_orm::{prelude::*, Condition, DatabaseConnection, EntityTrait, QueryFilter, SelectorTrait};
2use serde::{Deserialize, Serialize};
3
4/// Set the default pagination page size.
5const fn default_page_size() -> u64 {
6    25
7}
8
9/// Set the default pagination page.
10const fn default_page() -> u64 {
11    1
12}
13
14/// Structure representing the pagination query parameters.
15/// This struct allows to get the struct parameters from the query parameters.
16///
17/// # Example
18///
19/// ```
20/// use serde::{Deserialize, Serialize};
21/// use loco_rs::prelude::model::*;
22///
23/// #[derive(Debug, Deserialize)]
24/// pub struct ListQueryParams {
25///     pub title: Option<String>,
26///     pub content: Option<String>,
27///     #[serde(flatten)]
28///     pub pagination: query::PaginationQuery,
29/// }
30/// ````
31#[derive(Debug, Deserialize, Serialize)]
32pub struct PaginationQuery {
33    #[serde(
34        default = "default_page_size",
35        rename = "page_size",
36        deserialize_with = "deserialize_pagination_filter"
37    )]
38    pub page_size: u64,
39    #[serde(
40        default = "default_page",
41        rename = "page",
42        deserialize_with = "deserialize_pagination_filter"
43    )]
44    pub page: u64,
45}
46
47impl PaginationQuery {
48    #[must_use]
49    pub fn page(page: u64) -> Self {
50        Self {
51            page,
52            ..Default::default()
53        }
54    }
55}
56
57/// Default implementation for `PaginationQuery`.
58impl Default for PaginationQuery {
59    fn default() -> Self {
60        Self {
61            page_size: default_page_size(),
62            page: default_page(),
63        }
64    }
65}
66
67/// Deserialize pagination filter from string to u64 following a bug in
68/// `serde_urlencoded`.
69fn deserialize_pagination_filter<'de, D>(deserializer: D) -> Result<u64, D::Error>
70where
71    D: serde::Deserializer<'de>,
72{
73    let s: String = Deserialize::deserialize(deserializer)?;
74    s.parse().map_err(serde::de::Error::custom)
75}
76
77#[derive(Debug)]
78pub struct PageResponse<T> {
79    pub page: Vec<T>,
80    pub total_pages: u64,
81    pub total_items: u64,
82}
83
84use crate::Result as LocoResult;
85
86/// Paginate function for fetching paginated data from the database.
87///
88/// # Examples
89///
90/// Without conditions
91/// ```
92/// use loco_rs::tests_cfg::db;
93/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};
94/// use loco_rs::prelude::*;
95///
96/// async fn example() {
97///     let db = db::dummy_connection().await;
98///     let pagination_query = query::PaginationQuery {
99///         page_size: 100,
100///         page: 1,
101///     };
102///     
103///     let res = query::paginate(&db, db::test_db::Entity::find(), None, &pagination_query).await;
104/// }
105/// ````
106/// With conditions
107/// ```
108/// use loco_rs::tests_cfg::db;
109/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};
110/// use loco_rs::prelude::*;
111///
112/// async fn example() {
113///     let db = db::dummy_connection().await;
114///     let pagination_query = query::PaginationQuery {
115///         page_size: 100,
116///         page: 1,
117///     };
118///     let condition = query::condition().contains(db::test_db::Column::Name, "loco").build();
119///     let res = query::paginate(&db, db::test_db::Entity::find(), Some(condition), &pagination_query).await;
120/// }
121/// ````
122/// With Order By
123/// ```
124/// use loco_rs::tests_cfg::db;
125/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait, sea_query::Order, QueryOrder};
126/// use loco_rs::prelude::*;
127///
128/// async fn example() {
129///     let db = db::dummy_connection().await;
130///     let pagination_query = query::PaginationQuery {
131///         page_size: 100,
132///         page: 1,
133///     };
134///     
135///     let condition = query::condition().contains(db::test_db::Column::Name, "loco").build();
136///     let entity = db::test_db::Entity::find().order_by(db::test_db::Column::Name, Order::Desc);
137///     let res = query::paginate(&db, entity, Some(condition), &pagination_query).await;
138/// }
139/// ````
140///
141/// # Errors
142///
143/// Returns a `LocoResult` indicating any errors that occur
144/// during pagination.
145pub async fn paginate<E>(
146    db: &DatabaseConnection,
147    entity: Select<E>,
148    condition: Option<Condition>,
149    pagination_query: &PaginationQuery,
150) -> LocoResult<PageResponse<E::Model>>
151where
152    E: EntityTrait,
153    <E as EntityTrait>::Model: Sync,
154{
155    let page = pagination_query.page.saturating_sub(1);
156    let entity = if let Some(condition) = condition {
157        entity.filter(condition)
158    } else {
159        entity
160    };
161
162    let query = entity.paginate(db, pagination_query.page_size);
163    let total_pages_and_items = query.num_items_and_pages().await?;
164    let page: Vec<<E as EntityTrait>::Model> = query.fetch_page(page).await?;
165
166    let paginated_response = PageResponse {
167        page,
168        total_pages: total_pages_and_items.number_of_pages,
169        total_items: total_pages_and_items.number_of_items,
170    };
171
172    Ok(paginated_response)
173}
174
175/// Fetching a page from a selector.
176///
177/// # Examples
178///
179/// From Entity
180/// ```
181/// use loco_rs::tests_cfg::db;
182/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};
183/// use loco_rs::prelude::*;
184///
185/// async fn example() {
186///     let db = db::dummy_connection().await;
187///     let pagination_query = query::PaginationQuery {
188///         page_size: 100,
189///         page: 1,
190///     };
191///     let res = query::fetch_page(&db, db::test_db::Entity::find(), &query::PaginationQuery::page(2)).await;
192/// }
193/// ``````
194///
195/// # Errors
196///
197/// Returns a `LocoResult` indicating any errors that occur
198/// during the fetch.
199pub async fn fetch_page<'db, C, S>(
200    db: &'db C,
201    selector: S,
202    pagination_query: &PaginationQuery,
203) -> LocoResult<PageResponse<<<S as PaginatorTrait<'db, C>>::Selector as SelectorTrait>::Item>>
204where
205    C: ConnectionTrait + Sync,
206    S: PaginatorTrait<'db, C> + Send,
207{
208    let page = pagination_query.page.saturating_sub(1);
209
210    let query = selector.paginate(db, pagination_query.page_size);
211    let total_pages_and_items = query.num_items_and_pages().await?;
212    let page = query.fetch_page(page).await?;
213
214    Ok(PageResponse {
215        page,
216        total_pages: total_pages_and_items.number_of_pages,
217        total_items: total_pages_and_items.number_of_items,
218    })
219}