use sqlx::sqlite::SqlitePool; use super::{ exercise_repository::ExerciseRepository, metadata_repository::MetadataRepository, node_repository::NodeRepository, path_repository::PathRepository, }; /// Repository manager that coordinates access to all repositories /// and provides a single entry point for database operations pub struct RepositoryManager<'a> { pool: &'a SqlitePool, path_repo: PathRepository<'a>, metadata_repo: MetadataRepository<'a>, node_repo: NodeRepository<'a>, exercise_repo: ExerciseRepository<'a>, } impl<'a> RepositoryManager<'a> { pub fn new(pool: &'a SqlitePool) -> Self { Self { pool, path_repo: PathRepository::new(pool), metadata_repo: MetadataRepository::new(pool), node_repo: NodeRepository::new(pool), exercise_repo: ExerciseRepository::new(pool), } } /// Get the path repository pub fn paths(&self) -> &PathRepository<'a> { &self.path_repo } /// Get the metadata repository pub fn metadata(&self) -> &MetadataRepository<'a> { &self.metadata_repo } /// Get the node repository pub fn nodes(&self) -> &NodeRepository<'a> { &self.node_repo } /// Get the exercises repository pub fn exercises(&self) -> &ExerciseRepository<'a> { &self.exercise_repo } /// Get the database pool pub fn pool(&self) -> &SqlitePool { self.pool } /// Check database health by performing a simple query pub async fn health_check(&self) -> Result { let result = sqlx::query("SELECT 1") .fetch_optional(self.pool) .await .map_err(|e| format!("Database health check failed: {}", e))?; Ok(result.is_some()) } /// Begin a database transaction /// This is useful for operations that need to be atomic across multiple repositories pub async fn begin_transaction(&self) -> Result, String> { self.pool .begin() .await .map_err(|e| format!("Failed to begin transaction: {}", e)) } /// Get database statistics pub async fn get_stats(&self) -> Result { let path_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM path") .fetch_one(self.pool) .await .map_err(|e| format!("Failed to count paths: {}", e))?; let node_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM node") .fetch_one(self.pool) .await .map_err(|e| format!("Failed to count nodes: {}", e))?; let exercise_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM exercise") .fetch_one(self.pool) .await .map_err(|e| format!("Failed to count exercises: {}", e))?; let metadata_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM pathMetadata") .fetch_one(self.pool) .await .map_err(|e| format!("Failed to count metadata: {}", e))?; Ok(DatabaseStats { path_count: path_count.0, node_count: node_count.0, exercise_count: exercise_count.0, metadata_count: metadata_count.0, }) } } /// Database statistics structure #[derive(Debug, Clone)] pub struct DatabaseStats { pub path_count: i64, pub node_count: i64, pub exercise_count: i64, pub metadata_count: i64, } impl DatabaseStats { pub fn total_records(&self) -> i64 { self.path_count + self.node_count + self.exercise_count + self.metadata_count } pub fn is_empty(&self) -> bool { self.total_records() == 0 } } impl<'a> RepositoryManager<'a> { /// Advanced operations combining multiple repositories /// Import a path from JSON string with full validation pub async fn import_path_from_json(&self, json_content: &str) -> Result { let json_utils = super::path_json_utils::PathJsonUtils::new(&self.path_repo); json_utils.import_from_json(json_content).await } /// Export a path to JSON string pub async fn export_path_to_json(&self, path_id: i32) -> Result { let json_utils = super::path_json_utils::PathJsonUtils::new(&self.path_repo); json_utils.export_to_json(path_id).await } /// Clone a path with all its dependencies pub async fn clone_path_complete( &self, source_path_id: i32, new_path_id: &str, new_title: &str, ) -> Result { self.path_repo .clone_path(source_path_id, new_path_id, new_title) .await } /// Get comprehensive path statistics pub async fn get_path_statistics(&self, path_id: i32) -> Result { let path = self.path_repo.get_path_by_id(path_id).await?; let total_exercises = path.nodes.iter().map(|n| n.exercises.len()).sum(); let exercise_types: std::collections::HashMap = path .nodes .iter() .flat_map(|n| &n.exercises) .fold(std::collections::HashMap::new(), |mut acc, ex| { *acc.entry(ex.ex_type.clone()).or_insert(0) += 1; acc }); let avg_exercises_per_node = if path.nodes.is_empty() { 0.0 } else { total_exercises as f64 / path.nodes.len() as f64 }; Ok(PathStatistics { path_id: path.id, title: path.title, description: path.description, node_count: path.nodes.len(), total_exercises, exercise_types, metadata_count: path.metadata.len(), avg_exercises_per_node, }) } /// Validate path integrity across all repositories pub async fn validate_path_integrity(&self, path_id: i32) -> Result, String> { let mut issues = Vec::new(); // Check if path exists if !self.path_repo.path_exists(path_id).await? { issues.push(format!("Path with ID {} does not exist", path_id)); return Ok(issues); } let path = self.path_repo.get_path_by_id(path_id).await?; // Check metadata consistency if path.metadata.is_empty() { issues.push("Path has no metadata".to_string()); } else { for metadata in &path.metadata { if metadata.path_id != path.id { issues.push(format!( "Metadata path_id '{}' doesn't match path ID '{}'", metadata.path_id, path.id )); } } } // Check nodes consistency if path.nodes.is_empty() { issues.push("Path has no nodes".to_string()); } else { for node in &path.nodes { if node.path_id != path.id { issues.push(format!( "Node {} path_id '{}' doesn't match path ID '{}'", node.id, node.path_id, path.id )); } // Check exercises consistency for exercise in &node.exercises { if exercise.node_id != node.id { issues.push(format!( "Exercise {} node_id {} doesn't match node ID {}", exercise.id, exercise.node_id, node.id )); } // Validate exercise content is valid JSON if let Err(e) = serde_json::from_str::(&exercise.content) { issues.push(format!( "Exercise {} has invalid JSON content: {}", exercise.id, e )); } } } } Ok(issues) } /// Bulk operations for multiple paths pub async fn validate_all_paths( &self, ) -> Result>, String> { let paths = self.path_repo.get_all_paths().await?; let mut results = std::collections::HashMap::new(); for path in paths { if let Ok(path_id) = path.id.parse::() { match self.validate_path_integrity(path_id).await { Ok(issues) => { if !issues.is_empty() { results.insert(path.id, issues); } } Err(e) => { results.insert(path.id, vec![format!("Validation failed: {}", e)]); } } } else { results.insert(path.id.clone(), vec!["Invalid path ID format".to_string()]); } } Ok(results) } /// Search paths by content pub async fn search_paths(&self, query: &str) -> Result, String> { let paths = self.path_repo.get_all_paths().await?; let mut results = Vec::new(); let query_lower = query.to_lowercase(); for path in paths { let mut relevance_score = 0; let mut matching_content = Vec::new(); // Check title if path.title.to_lowercase().contains(&query_lower) { relevance_score += 10; matching_content.push(format!("Title: {}", path.title)); } // Check description if path.description.to_lowercase().contains(&query_lower) { relevance_score += 5; matching_content.push(format!("Description: {}", path.description)); } // Check nodes for node in &path.nodes { if node.title.to_lowercase().contains(&query_lower) { relevance_score += 3; matching_content.push(format!("Node: {}", node.title)); } if node.description.to_lowercase().contains(&query_lower) { relevance_score += 2; matching_content.push(format!("Node description: {}", node.description)); } // Check exercises for exercise in &node.exercises { if exercise.content.to_lowercase().contains(&query_lower) { relevance_score += 1; matching_content .push(format!("Exercise ({}): {}", exercise.ex_type, exercise.id)); } } } if relevance_score > 0 { results.push(SearchResult { path_id: path.id, title: path.title, relevance_score, matching_content, }); } } // Sort by relevance score (descending) results.sort_by(|a, b| b.relevance_score.cmp(&a.relevance_score)); Ok(results) } } /// Comprehensive path statistics #[derive(Debug, Clone)] pub struct PathStatistics { pub path_id: String, pub title: String, pub description: String, pub node_count: usize, pub total_exercises: usize, pub exercise_types: std::collections::HashMap, pub metadata_count: usize, pub avg_exercises_per_node: f64, } impl PathStatistics { pub fn print_detailed_summary(&self) { println!("=== Detailed Path Statistics ==="); println!("ID: {}", self.path_id); println!("Title: {}", self.title); println!("Description: {}", self.description); println!("Nodes: {}", self.node_count); println!("Total Exercises: {}", self.total_exercises); println!( "Average Exercises per Node: {:.2}", self.avg_exercises_per_node ); println!("Metadata Records: {}", self.metadata_count); println!("Exercise Types:"); for (ex_type, count) in &self.exercise_types { println!( " {}: {} ({:.1}%)", ex_type, count, (*count as f64 / self.total_exercises as f64) * 100.0 ); } } } /// Search result for path content search #[derive(Debug, Clone)] pub struct SearchResult { pub path_id: String, pub title: String, pub relevance_score: i32, pub matching_content: Vec, } impl SearchResult { pub fn print_summary(&self) { println!("=== Search Result ==="); println!("Path: {} - {}", self.path_id, self.title); println!("Relevance Score: {}", self.relevance_score); println!("Matching Content:"); for content in &self.matching_content { println!(" - {}", content); } } }