//! Database queries for the `settings` table. //! //! Uses an upsert pattern: settings are created with defaults on first access //! and updated in place thereafter. use sqlx::PgPool; use uuid::Uuid; use crate::errors::AppError; use crate::models::settings::{UpdateSettingsRequest, UserSettings}; /// Row type for settings queries. #[derive(Debug, sqlx::FromRow)] struct SettingsRow { user_id: Uuid, theme: String, max_age_days: i32, categories: serde_json::Value, max_items_per_category: i32, search_agent_behavior: String, ai_provider: String, ai_model: String, ai_model_writing: String, rate_limit_max_requests: Option, rate_limit_time_window_seconds: Option, updated_at: chrono::DateTime, } impl TryFrom for UserSettings { type Error = AppError; fn try_from(row: SettingsRow) -> Result { let categories: Vec = serde_json::from_value(row.categories).map_err(|e| { AppError::Internal(anyhow::anyhow!("Failed to parse categories JSON: {}", e)) })?; Ok(Self { user_id: row.user_id, theme: row.theme, max_age_days: row.max_age_days, categories, max_items_per_category: row.max_items_per_category, search_agent_behavior: row.search_agent_behavior, ai_provider: row.ai_provider, ai_model: row.ai_model, ai_model_writing: row.ai_model_writing, rate_limit_max_requests: row.rate_limit_max_requests, rate_limit_time_window_seconds: row.rate_limit_time_window_seconds, updated_at: row.updated_at, }) } } /// Get user settings, creating default settings if none exist. /// /// This ensures that `GET /api/v1/settings` always returns valid settings /// even for newly created users. pub async fn get_or_create_default( pool: &PgPool, user_id: Uuid, ) -> Result { let defaults = UserSettings::default(); let categories_json = serde_json::to_value(&defaults.categories).map_err(|e| { AppError::Internal(anyhow::anyhow!("Failed to serialize default categories: {}", e)) })?; let row = sqlx::query_as::<_, SettingsRow>( r#" INSERT INTO settings (user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (user_id) DO UPDATE SET user_id = settings.user_id RETURNING user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, updated_at "#, ) .bind(user_id) .bind(&defaults.theme) .bind(defaults.max_age_days) .bind(&categories_json) .bind(defaults.max_items_per_category) .bind(&defaults.search_agent_behavior) .bind(&defaults.ai_provider) .bind(&defaults.ai_model) .bind(&defaults.ai_model_writing) .bind(defaults.rate_limit_max_requests) .bind(defaults.rate_limit_time_window_seconds) .fetch_one(pool) .await?; UserSettings::try_from(row) } /// Update user settings (upsert). pub async fn upsert( pool: &PgPool, user_id: Uuid, req: &UpdateSettingsRequest, ) -> Result { let categories_json = serde_json::to_value(&req.categories).map_err(|e| { AppError::Internal(anyhow::anyhow!("Failed to serialize categories: {}", e)) })?; let row = sqlx::query_as::<_, SettingsRow>( r#" INSERT INTO settings (user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme, max_age_days = EXCLUDED.max_age_days, categories = EXCLUDED.categories, max_items_per_category = EXCLUDED.max_items_per_category, search_agent_behavior = EXCLUDED.search_agent_behavior, ai_provider = EXCLUDED.ai_provider, ai_model = EXCLUDED.ai_model, ai_model_writing = EXCLUDED.ai_model_writing, rate_limit_max_requests = EXCLUDED.rate_limit_max_requests, rate_limit_time_window_seconds = EXCLUDED.rate_limit_time_window_seconds, updated_at = now() RETURNING user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, updated_at "#, ) .bind(user_id) .bind(&req.theme) .bind(req.max_age_days) .bind(&categories_json) .bind(req.max_items_per_category) .bind(&req.search_agent_behavior) .bind(&req.ai_provider) .bind(&req.ai_model) .bind(&req.ai_model_writing) .bind(req.rate_limit_max_requests) .bind(req.rate_limit_time_window_seconds) .fetch_one(pool) .await?; UserSettings::try_from(row) }