diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index 7e3474cb..0741fd33 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -1,7 +1,7 @@ //! Definición de entidades y acceso a la base de datos. //! //! Agrupa los *traits*, macros y tipos del sistema de entidades de SeaORM, junto con las funciones -//! [`dbconn`], [`execute`], [`fetch_all`] y [`fetch_one`], en una sola importación: +//! [`dbconn`], [`execute`], [`fetch_all`], [`fetch_one`] y [`paginate`], en una sola importación: //! //! ```rust,no_run //! use pagetop_seaorm::db::*; @@ -30,7 +30,8 @@ //! - **Macros de derivación**: [`DeriveEntityModel`], [`DeriveColumn`], [`DerivePrimaryKey`], //! [`DeriveRelation`], [`EnumIter`]. //! - **Errores**: [`DbErr`]. -//! - **Resultados**: [`QueryResult`] (filas sin tipar), [`ExecResult`] (INSERT/UPDATE/DELETE). +//! - **Resultados**: [`QueryResult`] (filas sin tipar), [`ExecResult`] (INSERT/UPDATE/DELETE), +//! [`Paginated`] (página de resultados). //! //! # Definir una entidad //! @@ -106,6 +107,17 @@ //! //! Para migraciones y definición de esquemas usa [`migration`](crate::migration). //! +//! # Paginación +//! +//! [`paginate`] ejecuta una consulta paginada sobre una entidad y devuelve un [`Paginated`] con los +//! elementos de la página junto con su metadata (`total`, `page`, `per_page`, `total_pages`). Es el +//! camino habitual para listados administrables (usuarios, roles...). +//! +//! Cuando cada elemento necesita enriquecerse con datos de otra tabla que no vienen incluidos en +//! la propia consulta paginada (una colección asociada, un conteo relacionado...), +//! [`Paginated::map_items`] aplica esa transformación de forma asíncrona y falible sin perder la +//! metadata de paginación ya calculada. +//! //! # Acceso completo a SeaORM //! //! Este módulo re-exporta el crate `sea_orm` íntegro. Úsalo cuando necesites un tipo o función que @@ -316,3 +328,110 @@ pub async fn fetch_one( )) .await } + +// **< Paginated / paginate >*********************************************************************** + +/// Página de resultados de una consulta paginada. +pub struct Paginated { + /// Elementos de esta página. + pub items: Vec, + /// Número total de registros que cumplen la consulta, sin paginar. + pub total: u64, + /// Página actual, empezando en `1`. + pub page: u64, + /// Número de elementos por página. + pub per_page: u64, + /// Número total de páginas. + pub total_pages: u64, +} + +// Implementación manual en lugar de `#[derive(Default)]`, que exigiría un `T: Default` innecesario, +// ya que una página vacía no requiere que el tipo de elemento lo sea. +impl Default for Paginated { + fn default() -> Self { + Paginated { + items: Vec::new(), + total: 0, + page: 1, + per_page: 1, + total_pages: 1, + } + } +} + +impl Paginated { + /// Transforma los elementos de la página con una función asíncrona y falible, conservando el + /// resto de la metadata de paginación (`total`, `page`, `per_page`, `total_pages`). + /// + /// * `f` - función que recibe los elementos actuales (`Vec`) y devuelve, de forma + /// asíncrona, el resultado de la transformación (`Result, E>`). + pub async fn map_items( + self, + f: impl FnOnce(Vec) -> Fut, + ) -> Result, E> + where + Fut: std::future::Future, E>>, + { + let items = f(self.items).await?; + Ok(Paginated { + items, + total: self.total, + page: self.page, + per_page: self.per_page, + total_pages: self.total_pages, + }) + } +} + +/// Ejecuta una consulta paginada con el sistema de entidades y devuelve la página solicitada. +/// +/// Añade la metadata de paginación (`total`, `total_pages`); `page` y `per_page` se ajustan a un +/// mínimo de `1`, ya que no existe la página `0` ni un tamaño de página vacío. +/// +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// +/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +/// #[sea_orm(table_name = "users")] +/// pub struct Model { +/// #[sea_orm(primary_key)] +/// pub id: i32, +/// pub email: String, +/// } +/// +/// #[derive(Clone, Copy, Debug, EnumIter, DeriveRelation)] +/// pub enum Relation {} +/// +/// impl ActiveModelBehavior for ActiveModel {} +/// +/// async fn example() -> Result<(), DbErr> { +/// let page = paginate(Entity::find(), 1, 20).await?; +/// println!("{} usuarios en {} páginas", page.total, page.total_pages); +/// Ok(()) +/// } +/// ``` +pub async fn paginate( + select: Select, + page: u64, + per_page: u64, +) -> Result, DbErr> +where + E: EntityTrait, + E::Model: sea_orm::FromQueryResult + Send + Sync, +{ + let per_page = per_page.max(1); + let page = page.max(1); + let paginator = select.paginate(dbconn(), per_page); + let sea_orm::ItemsAndPagesNumber { + number_of_items: total, + number_of_pages: total_pages, + } = paginator.num_items_and_pages().await?; + let items = paginator.fetch_page(page.saturating_sub(1)).await?; + Ok(Paginated { + items, + total, + page, + per_page, + total_pages, + }) +} diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index eff88193..bb23c366 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -174,13 +174,13 @@ async fn example() -> Result<(), DbErr> { use pagetop::prelude::*; +include_locales!(LOCALES_SEAORM); + use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use url::Url; use std::sync::OnceLock; -include_locales!(LOCALES_SEAORM); - pub mod config; pub mod db;