aro-fletch 1.0.0

Fletch ORM persistence adapter for the Aro web framework
Documentation
//! Extension trait for registering a fletch database connection with [`App`].
//!
//! # Example
//!
//! ```ignore
//! use aro_web::App;
//! use aro_fletch::AppDatabaseExt;
//!
//! let app = App::new()
//!     .database::<sqlx::Sqlite>("sqlite::memory:")
//!     .await?
//!     .migrate::<sqlx::Sqlite>("migrations/")
//!     .await?
//!     .health_check_db::<sqlx::Sqlite>()
//!     .routes(router)
//!     .build();
//! ```

use aro_web::App;
use axum::Router;
use axum::routing::get;
use sqlx::Database;
use std::sync::Arc;

use crate::health::health_db_handler;

/// Extension trait that adds database connection and migration methods to [`App`].
pub trait AppDatabaseExt: Sized {
    /// Connect to a database using the given URL and register the pool.
    ///
    /// The [`fletch_orm::Pool<DB>`] is registered as a dependency and can be
    /// extracted in actions via `Dep<fletch_orm::Pool<DB>>`.
    fn database<DB: Database>(
        self,
        url: &str,
    ) -> impl std::future::Future<Output = Result<Self, fletch_orm::FletchError>> + Send;

    /// Connect to a database using the given URL and a pre-configured
    /// [`fletch_orm::PoolBuilder`], then register the pool.
    ///
    /// This allows customizing pool settings such as max connections,
    /// idle timeout, and acquire timeout.
    fn database_with<DB: Database>(
        self,
        url: &str,
        builder: fletch_orm::PoolBuilder<DB>,
    ) -> impl std::future::Future<Output = Result<Self, fletch_orm::FletchError>> + Send;

    /// Register a `GET /health/db` route that checks database connectivity.
    ///
    /// Returns 200 with pool stats when healthy, or 503 when unreachable.
    fn health_check_db<DB: Database>(self) -> Self;

    /// Run SQLx migrations from the given directory path.
    ///
    /// This registers a startup hook that runs all pending migrations
    /// when the app starts (via [`serve()`](App::serve) or
    /// [`build_with_hooks()`](App::build_with_hooks)).
    ///
    /// The `path` should point to a directory containing `.sql` migration files
    /// following SQLx naming conventions (e.g. `20230101000000_create_users.sql`).
    fn migrate<DB>(
        self,
        path: &str,
    ) -> impl std::future::Future<Output = Result<Self, sqlx::migrate::MigrateError>> + Send
    where
        DB: Database,
        <DB as Database>::Connection: sqlx::migrate::Migrate;
}

impl AppDatabaseExt for App {
    async fn database<DB: Database>(self, url: &str) -> Result<Self, fletch_orm::FletchError> {
        let pool = fletch_orm::Pool::<DB>::connect(url).await?;
        Ok(self.register::<fletch_orm::Pool<DB>>(Arc::new(pool)))
    }

    async fn database_with<DB: Database>(
        self,
        url: &str,
        builder: fletch_orm::PoolBuilder<DB>,
    ) -> Result<Self, fletch_orm::FletchError> {
        let pool = builder.connect(url).await?;
        Ok(self.register::<fletch_orm::Pool<DB>>(Arc::new(pool)))
    }

    fn health_check_db<DB: Database>(self) -> Self {
        let router = Router::new().route("/health/db", get(health_db_handler::<DB>));
        self.routes_with_state(router)
    }

    async fn migrate<DB>(self, path: &str) -> Result<Self, sqlx::migrate::MigrateError>
    where
        DB: Database,
        <DB as Database>::Connection: sqlx::migrate::Migrate,
    {
        let migrator = sqlx::migrate::Migrator::new(std::path::Path::new(path)).await?;
        Ok(self.on_startup(move |state| {
            Box::pin(async move {
                let Some(pool) = state.get::<fletch_orm::Pool<DB>>() else {
                    return Err(
                        "fletch_orm::Pool not registered — call .database() before .migrate()"
                            .into(),
                    );
                };
                migrator.run(pool.inner()).await?;
                Ok(())
            })
        }))
    }
}