shared-framework 0.0.17

Reusable building blocks for HTTP services — Hyper routing, SeaORM data layer, validation, OpenAPI docs, jobs, queues, cache.
Documentation
//! Database seeding with execution tracking.
//!
//! Provides the [`DatabaseSeeder`] trait, the [`DatabaseSeederRunner`] that
//! runs seeders in dependency order inside a single transaction, the
//! [`EntitySeeder`] helper for entity-specific seeders, and the
//! `__database_seeders` tracking table (see [`ensure_table`] and [`entity`]).
//!
//! Each seeder runs once: completed names are recorded in
//! `__database_seeders` and skipped on later runs. Deletion runs in reverse
//! order and removes the tracking rows.
//!
//! ```ignore
//! use shared_framework::data::seed::DatabaseSeederRunner;
//!
//! let runner = DatabaseSeederRunner::new().add(MySeeder);
//! runner.seed(&db).await?;
//! ```

pub mod entity;

pub use entity::Entity as SeederEntryEntity;
pub use entity::ModelEx as SeederEntryModel;

use sea_orm::{
    ActiveValue::Set, ColumnTrait, ConnectionTrait, DatabaseConnection, DatabaseTransaction,
    EntityTrait, QueryFilter, TransactionTrait,
};
use std::collections::HashSet;
use std::sync::Arc;

// Re-export entity for external use
pub use entity::{ActiveModel as SeederEntryActiveModel, ModelEx as SeederEntry};

/// Creates the `__database_seeders` tracking table when missing.
///
/// Idempotent `CREATE TABLE IF NOT EXISTS`, callable from the library without
/// a separate migration run. Returns an error when the DDL fails.
pub async fn ensure_table(db: &DatabaseConnection) -> anyhow::Result<()> {
    db.execute_unprepared(
        r#"CREATE TABLE IF NOT EXISTS "__database_seeders" (
            "id" BIGSERIAL PRIMARY KEY,
            "uid" UUID NOT NULL UNIQUE,
            "seederName" VARCHAR(255) NOT NULL UNIQUE,
            "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
            "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
        )"#,
    )
    .await?;
    Ok(())
}

/// Trait for database seeders.
///
/// Implementations receive an `Arc<DatabaseTransaction>` so all work runs
/// inside the runner's single transaction while the handle stays freely
/// shareable (e.g., for nested repository calls via `RepositoryOptions`).
/// Do not retain clones beyond the call: committing requires sole ownership.
#[async_trait::async_trait]
pub trait DatabaseSeeder: Send + Sync {
    /// Returns the unique tracking name; defaults to the type name.
    fn name(&self) -> &str {
        std::any::type_name::<Self>()
    }
    /// Returns the execution order (lower runs first, default 0).
    fn order(&self) -> i32 {
        0
    }
    /// Inserts seeded data inside the given transaction.
    async fn seed(&self, txn: Arc<DatabaseTransaction>) -> anyhow::Result<()>;
    /// Removes seeded data inside the given transaction.
    async fn delete(&self, txn: Arc<DatabaseTransaction>) -> anyhow::Result<()>;
}

/// Alias for [`DatabaseSeeder`] kept for existing imports.
pub use DatabaseSeeder as RepositorySeeder;

/// Helper marking an entity-specific seeder.
///
/// `E` is the SeaORM entity being seeded.
pub struct EntitySeeder<E>
where
    E: EntityTrait,
    E::ModelEx: crate::data::BaseEntity + Send + Sync,
{
    _marker: std::marker::PhantomData<E>,
}

impl<E> EntitySeeder<E>
where
    E: EntityTrait,
    E::ModelEx: crate::data::BaseEntity + Send + Sync,
{
    /// Creates an empty helper for the entity type.
    pub fn new() -> Self {
        Self { _marker: std::marker::PhantomData }
    }
}

/// Runs registered seeders in order inside a single transaction.
///
/// Ensures the `__database_seeders` table exists before querying it.
pub struct DatabaseSeederRunner {
    seeders: Vec<Box<dyn DatabaseSeeder>>,
}

impl DatabaseSeederRunner {
    /// Creates a runner with no seeders.
    pub fn new() -> Self {
        Self { seeders: Vec::new() }
    }

    /// Registers a seeder and returns the runner for chaining.
    pub fn add<S: DatabaseSeeder + 'static>(mut self, seeder: S) -> Self {
        self.seeders.push(Box::new(seeder));
        self
    }

    /// Registers an already-boxed seeder.
    pub fn add_boxed(&mut self, seeder: Box<dyn DatabaseSeeder>) {
        self.seeders.push(seeder);
    }

    /// Runs all seeders in ascending `order` inside one transaction, recording each name.
    ///
    /// Already-recorded seeders are skipped. Does nothing when no seeders are registered.
    pub async fn seed(&self, db: &DatabaseConnection) -> anyhow::Result<()> {
        if self.seeders.is_empty() {
            tracing::info!("No seeders registered; nothing to run");
            return Ok(());
        }
        // Ensure table exists — clean bundling without external migration step
        ensure_table(db).await?;

        let mut ordered: Vec<&Box<dyn DatabaseSeeder>> = self.seeders.iter().collect();
        ordered.sort_by_key(|s| s.order());

        let existing = entity::Entity::load().all(db).await?;
        let executed: HashSet<String> = existing.into_iter().map(|e| e.seeder_name).collect();

        // Single transaction across ALL seeder runs, shared by handle so
        // seeders can fan the same transaction out to repository calls.
        let txn = Arc::new(db.begin().await?);
        let mut in_txn_executed = executed.clone();

        for seeder in ordered {
            let name = seeder.name().to_string();
            if in_txn_executed.contains(&name) {
                tracing::info!(seeder = %name, action = "seed", "Skipping already executed seeder");
                continue;
            }
            tracing::info!(seeder = %name, action = "seed", "Running seeder");
            seeder.seed(txn.clone()).await.map_err(|e| anyhow::anyhow!("seeder {} failed: {}", name, e))?;
            let active = entity::ActiveModel {
                seeder_name: Set(name.clone()),
                uid: Set(uuid::Uuid::new_v4()),
                created_at: Set(chrono::Utc::now().into()),
                updated_at: Set(chrono::Utc::now().into()),
                ..Default::default()
            };
            entity::Entity::insert(active).exec(txn.as_ref()).await.map_err(|e| anyhow::anyhow!("failed to track seeder {}: {}", name, e))?;
            in_txn_executed.insert(name);
        }

        let owned = Arc::try_unwrap(txn).map_err(|_| anyhow::anyhow!("seeder transaction handle still shared; refusing to commit"))?;
        owned.commit().await?;
        tracing::info!(action = "seed", seeder_count = in_txn_executed.len(), "Seeder transaction committed");
        Ok(())
    }

    /// Deletes seeded data in descending `order` inside one transaction, removing each tracking row.
    ///
    /// Seeders without a tracking row are skipped. Does nothing when no seeders are registered.
    pub async fn delete(&self, db: &DatabaseConnection) -> anyhow::Result<()> {
        if self.seeders.is_empty() {
            tracing::info!("No seeders registered; nothing to delete");
            return Ok(());
        }
        ensure_table(db).await?;

        let mut ordered: Vec<&Box<dyn DatabaseSeeder>> = self.seeders.iter().collect();
        ordered.sort_by_key(|s| std::cmp::Reverse(s.order()));

        let existing = entity::Entity::load().all(db).await?;
        let executed: HashSet<String> = existing.into_iter().map(|e| e.seeder_name).collect();

        let txn = Arc::new(db.begin().await?);
        let mut in_txn_executed = executed.clone();

        for seeder in ordered {
            let name = seeder.name().to_string();
            if !in_txn_executed.contains(&name) {
                tracing::info!(seeder = %name, action = "delete", "Skipping seeder that has not been executed");
                continue;
            }
            tracing::info!(seeder = %name, action = "delete", "Deleting seeded data");
            seeder.delete(txn.clone()).await.map_err(|e| anyhow::anyhow!("seeder delete {} failed: {}", name, e))?;
            entity::Entity::delete_many()
                .filter(entity::Column::SeederName.eq(name.clone()))
                .exec(txn.as_ref())
                .await?;
            in_txn_executed.remove(&name);
        }

        let owned = Arc::try_unwrap(txn).map_err(|_| anyhow::anyhow!("seeder transaction handle still shared; refusing to commit"))?;
        owned.commit().await?;
        tracing::info!(action = "delete", "Seeder deletion transaction committed");
        Ok(())
    }
}

impl Default for DatabaseSeederRunner {
    fn default() -> Self { Self::new() }
}

/// Alias for [`DatabaseSeederRunner`].
pub type DatabaseSeederHelper = DatabaseSeederRunner;

#[deprecated(note = "Use DatabaseSeederRunner or DatabaseSeederHelper")]
/// Deprecated alias for [`DatabaseSeederRunner`].
pub type SeederRunner = DatabaseSeederRunner;