use crate::database::{lock_conn, Database}; use crate::error::AppError; use crate::provider::{Provider, ProviderMeta}; use indexmap::IndexMap; use rusqlite::params; use std::collections::HashMap; type OmoProviderRow = ( String, String, String, Option, Option, Option, Option, String, ); impl Database { pub fn get_all_providers( &self, app_type: &str, ) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( "SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue FROM providers WHERE app_type = ?1 ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC" ).map_err(|e| AppError::Database(e.to_string()))?; let provider_iter = stmt .query_map(params![app_type], |row| { let id: String = row.get(0)?; let name: String = row.get(1)?; let settings_config_str: String = row.get(2)?; let website_url: Option = row.get(3)?; let category: Option = row.get(4)?; let created_at: Option = row.get(5)?; let sort_index: Option = row.get(6)?; let notes: Option = row.get(7)?; let icon: Option = row.get(8)?; let icon_color: Option = row.get(9)?; let meta_str: String = row.get(10)?; let in_failover_queue: bool = row.get(11)?; let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null); let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default(); Ok(( id, Provider { id: "".to_string(), // Placeholder, set below name, settings_config, website_url, category, created_at, sort_index, notes, meta: Some(meta), icon, icon_color, in_failover_queue, }, )) }) .map_err(|e| AppError::Database(e.to_string()))?; let mut providers = IndexMap::new(); for provider_res in provider_iter { let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?; provider.id = id.clone(); let mut stmt_endpoints = conn.prepare( "SELECT url, added_at FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 ORDER BY added_at ASC, url ASC" ).map_err(|e| AppError::Database(e.to_string()))?; let endpoints_iter = stmt_endpoints .query_map(params![id, app_type], |row| { let url: String = row.get(0)?; let added_at: Option = row.get(1)?; Ok(( url, crate::settings::CustomEndpoint { url: "".to_string(), added_at: added_at.unwrap_or(0), last_used: None, }, )) }) .map_err(|e| AppError::Database(e.to_string()))?; let mut custom_endpoints = HashMap::new(); for ep_res in endpoints_iter { let (url, mut ep) = ep_res.map_err(|e| AppError::Database(e.to_string()))?; ep.url = url.clone(); custom_endpoints.insert(url, ep); } if let Some(meta) = &mut provider.meta { meta.custom_endpoints = custom_endpoints; } providers.insert(id, provider); } Ok(providers) } pub fn get_current_provider(&self, app_type: &str) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn .prepare("SELECT id FROM providers WHERE app_type = ?1 AND is_current = 1 LIMIT 1") .map_err(|e| AppError::Database(e.to_string()))?; let mut rows = stmt .query(params![app_type]) .map_err(|e| AppError::Database(e.to_string()))?; if let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? { Ok(Some( row.get(0).map_err(|e| AppError::Database(e.to_string()))?, )) } else { Ok(None) } } pub fn get_provider_by_id( &self, id: &str, app_type: &str, ) -> Result, AppError> { let conn = lock_conn!(self.conn); let result = conn.query_row( "SELECT name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2", params![id, app_type], |row| { let name: String = row.get(0)?; let settings_config_str: String = row.get(1)?; let website_url: Option = row.get(2)?; let category: Option = row.get(3)?; let created_at: Option = row.get(4)?; let sort_index: Option = row.get(5)?; let notes: Option = row.get(6)?; let icon: Option = row.get(7)?; let icon_color: Option = row.get(8)?; let meta_str: String = row.get(9)?; let in_failover_queue: bool = row.get(10)?; let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null); let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default(); Ok(Provider { id: id.to_string(), name, settings_config, website_url, category, created_at, sort_index, notes, meta: Some(meta), icon, icon_color, in_failover_queue, }) }, ); match result { Ok(provider) => Ok(Some(provider)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(AppError::Database(e.to_string())), } } pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> { let mut conn = lock_conn!(self.conn); let tx = conn .transaction() .map_err(|e| AppError::Database(e.to_string()))?; let mut meta_clone = provider.meta.clone().unwrap_or_default(); let endpoints = std::mem::take(&mut meta_clone.custom_endpoints); let existing: Option<(bool, bool)> = tx .query_row( "SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2", params![provider.id, app_type], |row| Ok((row.get(0)?, row.get(1)?)), ) .ok(); let is_update = existing.is_some(); let (is_current, in_failover_queue) = existing.unwrap_or((false, provider.in_failover_queue)); if is_update { tx.execute( "UPDATE providers SET name = ?1, settings_config = ?2, website_url = ?3, category = ?4, created_at = ?5, sort_index = ?6, notes = ?7, icon = ?8, icon_color = ?9, meta = ?10, is_current = ?11, in_failover_queue = ?12 WHERE id = ?13 AND app_type = ?14", params![ provider.name, serde_json::to_string(&provider.settings_config).map_err(|e| { AppError::Database(format!("Failed to serialize settings_config: {e}")) })?, provider.website_url, provider.category, provider.created_at, provider.sort_index, provider.notes, provider.icon, provider.icon_color, serde_json::to_string(&meta_clone).map_err(|e| AppError::Database(format!( "Failed to serialize meta: {e}" )))?, is_current, in_failover_queue, provider.id, app_type, ], ) .map_err(|e| AppError::Database(e.to_string()))?; } else { tx.execute( "INSERT INTO providers ( id, app_type, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", params![ provider.id, app_type, provider.name, serde_json::to_string(&provider.settings_config) .map_err(|e| AppError::Database(format!("Failed to serialize settings_config: {e}")))?, provider.website_url, provider.category, provider.created_at, provider.sort_index, provider.notes, provider.icon, provider.icon_color, serde_json::to_string(&meta_clone) .map_err(|e| AppError::Database(format!("Failed to serialize meta: {e}")))?, is_current, in_failover_queue, ], ) .map_err(|e| AppError::Database(e.to_string()))?; for (url, endpoint) in endpoints { tx.execute( "INSERT INTO provider_endpoints (provider_id, app_type, url, added_at) VALUES (?1, ?2, ?3, ?4)", params![provider.id, app_type, url, endpoint.added_at], ) .map_err(|e| AppError::Database(e.to_string()))?; } } tx.commit().map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> { let conn = lock_conn!(self.conn); conn.execute( "DELETE FROM providers WHERE id = ?1 AND app_type = ?2", params![id, app_type], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> { let mut conn = lock_conn!(self.conn); let tx = conn .transaction() .map_err(|e| AppError::Database(e.to_string()))?; tx.execute( "UPDATE providers SET is_current = 0 WHERE app_type = ?1", params![app_type], ) .map_err(|e| AppError::Database(e.to_string()))?; tx.execute( "UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2", params![id, app_type], ) .map_err(|e| AppError::Database(e.to_string()))?; tx.commit().map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn update_provider_settings_config( &self, app_type: &str, provider_id: &str, settings_config: &serde_json::Value, ) -> Result<(), AppError> { let conn = lock_conn!(self.conn); conn.execute( "UPDATE providers SET settings_config = ?1 WHERE id = ?2 AND app_type = ?3", params![ serde_json::to_string(settings_config).map_err(|e| AppError::Database(format!( "Failed to serialize settings_config: {e}" )))?, provider_id, app_type ], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn add_custom_endpoint( &self, app_type: &str, provider_id: &str, url: &str, ) -> Result<(), AppError> { let conn = lock_conn!(self.conn); let added_at = chrono::Utc::now().timestamp_millis(); conn.execute( "INSERT INTO provider_endpoints (provider_id, app_type, url, added_at) VALUES (?1, ?2, ?3, ?4)", params![provider_id, app_type, url, added_at], ).map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn remove_custom_endpoint( &self, app_type: &str, provider_id: &str, url: &str, ) -> Result<(), AppError> { let conn = lock_conn!(self.conn); conn.execute( "DELETE FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 AND url = ?3", params![provider_id, app_type, url], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn set_omo_provider_current( &self, app_type: &str, provider_id: &str, category: &str, ) -> Result<(), AppError> { let mut conn = lock_conn!(self.conn); let tx = conn .transaction() .map_err(|e| AppError::Database(e.to_string()))?; tx.execute( "UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = ?2", params![app_type, category], ) .map_err(|e| AppError::Database(e.to_string()))?; let updated = tx .execute( "UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3", params![provider_id, app_type, category], ) .map_err(|e| AppError::Database(e.to_string()))?; if updated != 1 { return Err(AppError::Database(format!( "Failed to set {category} provider current: provider '{provider_id}' not found in app '{app_type}'" ))); } tx.commit().map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn is_omo_provider_current( &self, app_type: &str, provider_id: &str, category: &str, ) -> Result { let conn = lock_conn!(self.conn); match conn.query_row( "SELECT is_current FROM providers WHERE id = ?1 AND app_type = ?2 AND category = ?3", params![provider_id, app_type, category], |row| row.get(0), ) { Ok(is_current) => Ok(is_current), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), Err(e) => Err(AppError::Database(e.to_string())), } } pub fn clear_omo_provider_current( &self, app_type: &str, provider_id: &str, category: &str, ) -> Result<(), AppError> { let conn = lock_conn!(self.conn); conn.execute( "UPDATE providers SET is_current = 0 WHERE id = ?1 AND app_type = ?2 AND category = ?3", params![provider_id, app_type, category], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } pub fn get_current_omo_provider( &self, app_type: &str, category: &str, ) -> Result, AppError> { let conn = lock_conn!(self.conn); let row_data: Result = conn.query_row( "SELECT id, name, settings_config, category, created_at, sort_index, notes, meta FROM providers WHERE app_type = ?1 AND category = ?2 AND is_current = 1 LIMIT 1", params![app_type, category], |row| { Ok(( row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?, )) }, ); let (id, name, settings_config_str, _row_category, created_at, sort_index, notes, meta_str) = match row_data { Ok(v) => v, Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None), Err(e) => return Err(AppError::Database(e.to_string())), }; let settings_config = serde_json::from_str(&settings_config_str).map_err(|e| { AppError::Database(format!( "Failed to parse {category} provider settings_config (provider_id={id}): {e}" )) })?; let meta: crate::provider::ProviderMeta = if meta_str.trim().is_empty() { crate::provider::ProviderMeta::default() } else { serde_json::from_str(&meta_str).map_err(|e| { AppError::Database(format!( "Failed to parse {category} provider meta (provider_id={id}): {e}" )) })? }; Ok(Some(Provider { id, name, settings_config, website_url: None, category: Some(category.to_string()), created_at, sort_index, notes, meta: Some(meta), icon: None, icon_color: None, in_failover_queue: false, })) } }