You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
137 lines
5.2 KiB
Rust
137 lines
5.2 KiB
Rust
//! 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<i32>,
|
|
rate_limit_time_window_seconds: Option<i32>,
|
|
updated_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
impl TryFrom<SettingsRow> for UserSettings {
|
|
type Error = AppError;
|
|
|
|
fn try_from(row: SettingsRow) -> Result<Self, Self::Error> {
|
|
let categories: Vec<String> = 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<UserSettings, AppError> {
|
|
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<UserSettings, AppError> {
|
|
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)
|
|
}
|