use crate::models::{Connection, ConnectionInput}; use crate::store::Store; use std::sync::Mutex; const VALID_DB_TYPES: [&str; 4] = ["postgresql", "mysql", "sqlite", "redis"]; fn validate(input: &ConnectionInput) -> Result<(), String> { if input.name.is_empty() || input.name.chars().count() > 100 { return Err("name is required and must be 100 chars or fewer".into()); } if !VALID_DB_TYPES.contains(&input.db_type.as_str()) { return Err(format!( "db_type must be one of: {}", VALID_DB_TYPES.join(", ") )); } if input.host.is_empty() || input.host.chars().count() > 255 { return Err("host is required and must be 255 chars or fewer".into()); } if input.db_type != "sqlite" { match input.port { Some(p) if (1..=65535).contains(&p) => {} _ => return Err("port must be an integer between 1 and 65535 for this db_type".into()), } } if let Some(u) = &input.username { if u.chars().count() > 100 { return Err("username must be 100 chars or fewer".into()); } } Ok(()) } pub fn get_connections_inner(state: &Mutex) -> Result, String> { let store = state.lock().map_err(|e| e.to_string())?; store.get_connections() } pub fn create_connection_inner( state: &Mutex, input: ConnectionInput, ) -> Result { validate(&input)?; let store = state.lock().map_err(|e| e.to_string())?; store.create_connection(input) } pub fn update_connection_inner( state: &Mutex, id: String, input: ConnectionInput, ) -> Result { validate(&input)?; let store = state.lock().map_err(|e| e.to_string())?; store.update_connection(&id, input) } pub fn delete_connection_inner(state: &Mutex, id: &str) -> Result<(), String> { let store = state.lock().map_err(|e| e.to_string())?; store.delete_connection(id) } pub fn add_connection_tags_inner( state: &Mutex, connection_id: String, tag_ids: Vec, ) -> Result<(), String> { let store = state.lock().map_err(|e| e.to_string())?; store.add_connection_tags(&connection_id, &tag_ids) } #[tauri::command] pub fn get_connections(state: tauri::State) -> Result, String> { get_connections_inner(&state.db_store) } #[tauri::command] pub fn create_connection( state: tauri::State, input: ConnectionInput, ) -> Result { create_connection_inner(&state.db_store, input) } #[tauri::command] pub fn update_connection( state: tauri::State, id: String, input: ConnectionInput, ) -> Result { update_connection_inner(&state.db_store, id, input) } #[tauri::command] pub fn delete_connection( state: tauri::State, id: String, app: tauri::AppHandle, ) -> Result<(), String> { delete_connection_inner(&state.db_store, &id)?; // Purge keychain secrets (missing entries are no-ops) and close any // SSH tunnel associated with the deleted connection. let _ = crate::commands::keychain::delete_connection_password_internal(&app, &id); let _ = crate::commands::keychain::delete_connection_ssh_password_internal(&app, &id); let _ = crate::commands::keychain::delete_connection_ssh_passphrase_internal(&app, &id); if let Ok(mut mgr) = state.ssh_manager.lock() { mgr.close_tunnel(&id); } Ok(()) } #[tauri::command] pub fn add_connection_tags( state: tauri::State, connection_id: String, tag_ids: Vec, ) -> Result<(), String> { add_connection_tags_inner(&state.db_store, connection_id, tag_ids) } pub fn set_connection_favorite_inner( state: &Mutex, connection_id: String, favorite: bool, ) -> Result<(), String> { let store = state.lock().map_err(|e| e.to_string())?; store.set_connection_favorite(&connection_id, favorite) } pub fn record_recent_connection_inner( state: &Mutex, connection_id: String, ) -> Result<(), String> { let store = state.lock().map_err(|e| e.to_string())?; store.record_recent_connection(&connection_id) } pub fn get_recent_connections_inner( state: &Mutex, limit: i64, ) -> Result, String> { let store = state.lock().map_err(|e| e.to_string())?; store.get_recent_connections(limit) } pub fn clear_recent_connections_inner(state: &Mutex) -> Result<(), String> { let store = state.lock().map_err(|e| e.to_string())?; store.clear_recent_connections() } #[tauri::command] pub fn set_connection_favorite( state: tauri::State, connection_id: String, favorite: bool, ) -> Result<(), String> { set_connection_favorite_inner(&state.db_store, connection_id, favorite) } #[tauri::command] pub fn record_recent_connection( state: tauri::State, connection_id: String, ) -> Result<(), String> { record_recent_connection_inner(&state.db_store, connection_id) } #[tauri::command] pub fn get_recent_connections( state: tauri::State, limit: i64, ) -> Result, String> { get_recent_connections_inner(&state.db_store, limit) } #[tauri::command] pub fn clear_recent_connections(state: tauri::State) -> Result<(), String> { clear_recent_connections_inner(&state.db_store) } #[cfg(test)] mod tests { use super::*; use crate::models::ConnectionInput; use crate::store::Store; fn state() -> std::sync::Mutex { let conn = rusqlite::Connection::open_in_memory().unwrap(); crate::store::migrations::run_migrations(&conn).unwrap(); std::sync::Mutex::new(Store::from_connection(conn)) } #[test] fn get_connections_returns_list() { let st = state(); let result = get_connections_inner(&st); assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 0); } #[test] fn create_connection_command_returns_connection() { let st = state(); let input = ConnectionInput { name: "Prod".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, password: None, database: None, environment: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_password: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, tag_ids: vec![], use_keychain: true, }; let result = create_connection_inner(&st, input.clone()).unwrap(); assert_eq!(result.name, "Prod"); assert_eq!(get_connections_inner(&st).unwrap().len(), 1); } #[test] fn create_connection_rejects_invalid_db_type() { let st = state(); let input = ConnectionInput { name: "X".into(), db_type: "mongodb".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, password: None, database: None, environment: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_password: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, tag_ids: vec![], use_keychain: true, }; assert!(create_connection_inner(&st, input).is_err()); } #[test] fn delete_connection_command_removes_it() { let st = state(); let input = ConnectionInput { name: "X".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, password: None, database: None, environment: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_password: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, tag_ids: vec![], use_keychain: true, }; let conn = create_connection_inner(&st, input).unwrap(); delete_connection_inner(&st, &conn.id).unwrap(); assert_eq!(get_connections_inner(&st).unwrap().len(), 0); } #[test] fn set_connection_favorite_command_persists() { let st = state(); let input = ConnectionInput { name: "P".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, tag_ids: vec![], use_keychain: true, password: None, database: None, environment: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_password: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, }; let conn = create_connection_inner(&st, input).unwrap(); set_connection_favorite_inner(&st, conn.id.clone(), true).unwrap(); assert_eq!(get_connections_inner(&st).unwrap()[0].favorite, true); } #[test] fn record_recent_command_upserts() { let st = state(); let input = ConnectionInput { name: "P".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, tag_ids: vec![], use_keychain: true, password: None, database: None, environment: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_password: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, }; let conn = create_connection_inner(&st, input).unwrap(); record_recent_connection_inner(&st, conn.id.clone()).unwrap(); record_recent_connection_inner(&st, conn.id.clone()).unwrap(); // idempotent upsert let recent = get_recent_connections_inner(&st, 10).unwrap(); assert_eq!(recent.len(), 1); clear_recent_connections_inner(&st).unwrap(); assert_eq!(get_recent_connections_inner(&st, 10).unwrap().len(), 0); } }