lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
//! User repository - Database adapter (outbound port)
//!
//! This is where all database interactions happen. The repository pattern
//! isolates database concerns from business logic.
//!
//! In hexagonal architecture, this is an "outbound adapter" - it adapts
//! the core application logic to external systems (the database).

use crate::adapters::entities::{prelude::Users, users};
use crate::error::{AppError, Result};
use crate::features::examples::models::{CreateUserRequest, UpdateUserRequest, User};
use chrono::Utc;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter,
    QueryOrder, Set,
};
use uuid::Uuid;

/// Repository for user data access
#[derive(Clone)]
pub struct UserRepository {
    db: DatabaseConnection,
}

impl UserRepository {
    /// Create a new user repository
    pub fn new(db: DatabaseConnection) -> Self {
        Self { db }
    }

    /// Find a user by ID
    pub async fn find_by_id(&self, id: Uuid) -> Result<Option<User>> {
        let user = Users::find_by_id(id).one(&self.db).await?;

        Ok(user.map(|u| User {
            id: u.id,
            email: u.email,
            name: u.name,
            created_at: u.created_at,
            updated_at: u.updated_at,
        }))
    }

    /// Find a user by email
    pub async fn find_by_email(&self, email: &str) -> Result<Option<User>> {
        let user = Users::find()
            .filter(users::Column::Email.eq(email))
            .one(&self.db)
            .await?;

        Ok(user.map(|u| User {
            id: u.id,
            email: u.email,
            name: u.name,
            created_at: u.created_at,
            updated_at: u.updated_at,
        }))
    }

    /// List users with pagination
    pub async fn list(&self, page: u64, per_page: u64) -> Result<(Vec<User>, u64)> {
        let paginator = Users::find()
            .order_by_desc(users::Column::CreatedAt)
            .paginate(&self.db, per_page);

        let total = paginator.num_items().await?;
        let users = paginator.fetch_page(page.saturating_sub(1)).await?;

        let users = users
            .into_iter()
            .map(|u| User {
                id: u.id,
                email: u.email,
                name: u.name,
                created_at: u.created_at,
                updated_at: u.updated_at,
            })
            .collect();

        Ok((users, total))
    }

    /// Create a new user
    pub async fn create(&self, request: &CreateUserRequest) -> Result<User> {
        let now = Utc::now();
        let id = Uuid::new_v4();

        let user = users::ActiveModel {
            id: Set(id),
            email: Set(request.email.clone()),
            name: Set(request.name.clone()),
            created_at: Set(now),
            updated_at: Set(now),
        };

        let user = user.insert(&self.db).await?;

        Ok(User {
            id: user.id,
            email: user.email,
            name: user.name,
            created_at: user.created_at,
            updated_at: user.updated_at,
        })
    }

    /// Update an existing user
    pub async fn update(&self, id: Uuid, request: &UpdateUserRequest) -> Result<User> {
        let user = Users::find_by_id(id)
            .one(&self.db)
            .await?
            .ok_or_else(|| AppError::NotFound(format!("User with id {} not found", id)))?;

        let mut user: users::ActiveModel = user.into();

        if let Some(email) = &request.email {
            user.email = Set(email.clone());
        }

        if let Some(name) = &request.name {
            user.name = Set(name.clone());
        }

        user.updated_at = Set(Utc::now());

        let user = user.update(&self.db).await?;

        Ok(User {
            id: user.id,
            email: user.email,
            name: user.name,
            created_at: user.created_at,
            updated_at: user.updated_at,
        })
    }

    /// Delete a user by ID
    pub async fn delete(&self, id: Uuid) -> Result<()> {
        let result = Users::delete_by_id(id).exec(&self.db).await?;

        if result.rows_affected == 0 {
            return Err(AppError::NotFound(format!("User with id {} not found", id)));
        }

        Ok(())
    }

    /// Check if email already exists
    pub async fn email_exists(&self, email: &str) -> Result<bool> {
        let count = Users::find()
            .filter(users::Column::Email.eq(email))
            .count(&self.db)
            .await?;

        Ok(count > 0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Note: Integration tests with real database are in tests/integration/
    // These are just compilation tests

    #[test]
    fn test_repository_compiles() {
        // Ensure the repository API is sound
    }
}