puerto 0.10.1

Puerto — Rust full-stack DDD framework CLI. Scaffolds Clean Architecture projects with auto-wired DI, SQLx support, and zero manual wiring. Formerly puerto-framework.
use async_trait::async_trait;
use sqlx::PgPool;
use uuid::Uuid;
{{ map_imports }}
use business::domain::{{ snake }}::{
    errors::{{ pascal }}Error,
    model::{{ pascal }},
    repository::{{ pascal }}RepositoryTrait,
};

use super::entity::{{ pascal }}Db;

pub struct Pg{{ pascal }}Repository {
    pub pool: PgPool,
}

impl Pg{{ pascal }}Repository {
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

#[async_trait]
impl {{ pascal }}RepositoryTrait for Pg{{ pascal }}Repository {
    async fn find_all(&self) -> Result<Vec<{{ pascal }}>, {{ pascal }}Error> {
        tracing::debug!("find_all");
        let rows = sqlx::query_as!(
            {{ pascal }}Db,
            "SELECT {{ select_cols }}
             FROM {{ snake }}s WHERE deleted = false ORDER BY created_at DESC"
        )
        .fetch_all(&self.pool)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "find_all failed");
            {{ pascal }}Error::RepositoryError
        })?;

        rows.into_iter().map(|r| r.try_into()).collect()
    }

    async fn find_by_id(&self, id: Uuid) -> Result<Option<{{ pascal }}>, {{ pascal }}Error> {
        tracing::debug!(%id, "find_by_id");
        let row = sqlx::query_as!(
            {{ pascal }}Db,
            "SELECT {{ select_cols }}
             FROM {{ snake }}s WHERE id = $1 AND deleted = false",
            id
        )
        .fetch_optional(&self.pool)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "find_by_id failed");
            {{ pascal }}Error::RepositoryError
        })?;

        row.map(|r| r.try_into()).transpose()
    }

    async fn save(&self, entity: &{{ pascal }}) -> Result<(), {{ pascal }}Error> {
        tracing::debug!(id = %entity.id, "save");
        let db = {{ pascal }}Db::from(entity);
        sqlx::query!(
            "INSERT INTO {{ snake }}s ({{ all_cols }})
             VALUES ({{ all_params }})
             ON CONFLICT (id) DO UPDATE
             SET {{ all_updates }}",
{{ all_bindings }}
        )
        .execute(&self.pool)
        .await
        .map_err(|e| {
            // 23505 = unique_violation: a `unique` field already taken.
            if let Some(db_err) = e.as_database_error()
                && db_err.code().is_some_and(|c| c == "23505")
            {
                return {{ pascal }}Error::Duplicate;
            }
            tracing::error!(error = %e, "save failed");
            {{ pascal }}Error::RepositoryError
        })?;
        Ok(())
    }
}

#[cfg(test)]
mod integration_tests {
    use super::*;
    use business::domain::{{ snake }}::model::{{ pascal }}Props;
{{ vo_test_imports }}

    fn test_repo(pool: PgPool) -> Pg{{ pascal }}Repository {
        Pg{{ pascal }}Repository::new(pool)
    }

{{ seed_fn }}
    #[sqlx::test(migrations = "./migrations")]
    async fn should_persist_and_retrieve_by_id(pool: PgPool) {
        // Arrange
        let entity = seed(&pool).await;

        // Act
        let found = test_repo(pool).find_by_id(entity.id).await.unwrap().unwrap();

        // Assert
        assert_eq!(found.id, entity.id);
{{ field_asserts }}
        assert!(!found.deleted);
    }

    #[sqlx::test(migrations = "./migrations")]
    async fn should_return_none_for_nonexistent_id(pool: PgPool) {
        // Act
        let result = test_repo(pool).find_by_id(Uuid::new_v4()).await.unwrap();

        // Assert
        assert!(result.is_none());
    }

    #[sqlx::test(migrations = "./migrations")]
    async fn should_update_entity_on_save_conflict(pool: PgPool) {
        // Arrange
{{ update_test }}
    }

    #[sqlx::test(migrations = "./migrations")]
    async fn should_list_all_{{ snake }}s_excluding_deleted(pool: PgPool) {
        // Arrange
        seed(&pool).await;
        seed(&pool).await;

        // Act
        let results = test_repo(pool).find_all().await.unwrap();

        // Assert
        assert_eq!(results.len(), 2);
    }
}