aster_forge_db/
pagination.rs

1//! `SeaORM` repository helpers for offset pagination.
2//!
3//! The helper executes the same select query for total count and page items, letting service code
4//! build filters once and reuse them consistently. It stays generic over entities and connections
5//! so product repositories can keep their own model types.
6
7use sea_orm::{ConnectionTrait, EntityTrait, PaginatorTrait, QuerySelect, Select};
8
9use crate::DbError;
10
11/// Fetches an offset page and total count from a `SeaORM` select query.
12///
13/// # Errors
14///
15/// Returns an error when the database operation fails.
16pub async fn fetch_offset_page<C, Entity, Error>(
17    db: &C,
18    query: Select<Entity>,
19    limit: u64,
20    offset: u64,
21) -> std::result::Result<(Vec<Entity::Model>, u64), Error>
22where
23    C: ConnectionTrait,
24    Entity: EntityTrait,
25    Error: From<DbError>,
26    Select<Entity>: QuerySelect,
27    for<'db> Select<Entity>: PaginatorTrait<'db, C>,
28{
29    let total = query
30        .clone()
31        .count(db)
32        .await
33        .map_err(DbError::from)
34        .map_err(Error::from)?;
35    let items = query
36        .limit(limit)
37        .offset(offset)
38        .all(db)
39        .await
40        .map_err(DbError::from)
41        .map_err(Error::from)?;
42    Ok((items, total))
43}
44
45#[cfg(test)]
46mod tests {
47    use super::fetch_offset_page;
48    use sea_orm::{
49        ActiveModelBehavior, ConnectionTrait, Database, DeriveEntityModel, DerivePrimaryKey,
50        DeriveRelation, EntityTrait, EnumIter, PrimaryKeyTrait, QueryOrder,
51    };
52
53    #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
54    #[sea_orm(table_name = "pagination_items")]
55    pub struct Model {
56        #[sea_orm(primary_key)]
57        pub id: i32,
58        pub name: String,
59    }
60
61    #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
62    pub enum Relation {}
63
64    impl ActiveModelBehavior for ActiveModel {}
65
66    #[tokio::test]
67    async fn fetch_offset_page_returns_items_and_total_count() {
68        let db = Database::connect("sqlite::memory:")
69            .await
70            .expect("sqlite memory database should connect");
71        db.execute_unprepared(
72            "CREATE TABLE pagination_items (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
73        )
74        .await
75        .expect("table should be created");
76        db.execute_unprepared(
77            "INSERT INTO pagination_items (id, name) VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma');",
78        )
79        .await
80        .expect("rows should be inserted");
81
82        let (items, total) = fetch_offset_page::<_, _, crate::DbError>(
83            &db,
84            Entity::find().order_by_asc(Column::Id),
85            2,
86            1,
87        )
88        .await
89        .expect("page should load");
90
91        assert_eq!(total, 3);
92        assert_eq!(
93            items.into_iter().map(|item| item.name).collect::<Vec<_>>(),
94            vec!["beta".to_string(), "gamma".to_string()]
95        );
96    }
97}