//! App config (`user_config`) key/value commands. //! //! The SQLite side of settings that used to live in the frontend's //! `localStorage` (step 6 of the portable-config migration). Writes land in the //! `user_config` table, whose SyncStore-generated triggers replicate the keys //! the config spec ([`crate::config_key::CONFIG`]) marks `Synced`. Keys are //! validated against the spec on write, so the table stays the declared closed //! set rather than accumulating whatever the frontend sends. use std::collections::HashMap; use std::sync::Arc; use tauri::State; use tracing::instrument; use super::ApiError; use crate::config_key::CONFIG; use crate::state::AppState; #[allow( clippy::needless_pass_by_value, reason = "passed to .map_err(db_err), which supplies the error by value" )] fn db_err(e: sqlx::Error) -> ApiError { ApiError::database(e.to_string()) } /// Reject a key the spec does not declare, so `user_config` mirrors the spec's /// closed set and a typo cannot create a stray, never-synced row. fn ensure_known(key: &str) -> Result<(), ApiError> { if CONFIG.keys().any(|(known, _)| known == key) { Ok(()) } else { Err(ApiError::validation("key", "unknown config key")) } } /// One config value, or `None` when unset. #[tauri::command] #[instrument(skip_all)] pub async fn get_config( state: State<'_, Arc>, key: String, ) -> Result, ApiError> { let row: Option<(String,)> = sqlx::query_as("SELECT value FROM user_config WHERE key = ?1") .bind(&key) .fetch_optional(&state.pool) .await .map_err(db_err)?; Ok(row.map(|(v,)| v)) } /// Every config key and value, for the frontend to preload into its cache so its /// synchronous reads stay synchronous. #[tauri::command] #[instrument(skip_all)] pub async fn get_all_config( state: State<'_, Arc>, ) -> Result, ApiError> { let rows: Vec<(String, String)> = sqlx::query_as("SELECT key, value FROM user_config") .fetch_all(&state.pool) .await .map_err(db_err)?; Ok(rows.into_iter().collect()) } /// Set a config value (upsert). A synced key's write is captured by the engine's /// export trigger; a device-local key's is not. #[tauri::command] #[instrument(skip_all)] pub async fn set_config( state: State<'_, Arc>, key: String, value: String, ) -> Result<(), ApiError> { ensure_known(&key)?; sqlx::query( "INSERT INTO user_config (key, value) VALUES (?1, ?2) \ ON CONFLICT(key) DO UPDATE SET value = excluded.value", ) .bind(&key) .bind(&value) .execute(&state.pool) .await .map_err(db_err)?; Ok(()) } /// Remove a config key. Absent already is not an error. #[tauri::command] #[instrument(skip_all)] pub async fn delete_config(state: State<'_, Arc>, key: String) -> Result<(), ApiError> { sqlx::query("DELETE FROM user_config WHERE key = ?1") .bind(&key) .execute(&state.pool) .await .map_err(db_err)?; Ok(()) }