Skip to main content

max / goingson

3.0 KB · 100 lines History Blame Raw
1 //! App config (`user_config`) key/value commands.
2 //!
3 //! The SQLite side of settings that used to live in the frontend's
4 //! `localStorage` (step 6 of the portable-config migration). Writes land in the
5 //! `user_config` table, whose SyncStore-generated triggers replicate the keys
6 //! the config spec ([`crate::config_key::CONFIG`]) marks `Synced`. Keys are
7 //! validated against the spec on write, so the table stays the declared closed
8 //! set rather than accumulating whatever the frontend sends.
9
10 use std::collections::HashMap;
11 use std::sync::Arc;
12
13 use tauri::State;
14 use tracing::instrument;
15
16 use super::ApiError;
17 use crate::config_key::CONFIG;
18 use crate::state::AppState;
19
20 #[allow(
21 clippy::needless_pass_by_value,
22 reason = "passed to .map_err(db_err), which supplies the error by value"
23 )]
24 fn db_err(e: sqlx::Error) -> ApiError {
25 ApiError::database(e.to_string())
26 }
27
28 /// Reject a key the spec does not declare, so `user_config` mirrors the spec's
29 /// closed set and a typo cannot create a stray, never-synced row.
30 fn ensure_known(key: &str) -> Result<(), ApiError> {
31 if CONFIG.keys().any(|(known, _)| known == key) {
32 Ok(())
33 } else {
34 Err(ApiError::validation("key", "unknown config key"))
35 }
36 }
37
38 /// One config value, or `None` when unset.
39 #[tauri::command]
40 #[instrument(skip_all)]
41 pub async fn get_config(
42 state: State<'_, Arc<AppState>>,
43 key: String,
44 ) -> Result<Option<String>, ApiError> {
45 let row: Option<(String,)> = sqlx::query_as("SELECT value FROM user_config WHERE key = ?1")
46 .bind(&key)
47 .fetch_optional(&state.pool)
48 .await
49 .map_err(db_err)?;
50 Ok(row.map(|(v,)| v))
51 }
52
53 /// Every config key and value, for the frontend to preload into its cache so its
54 /// synchronous reads stay synchronous.
55 #[tauri::command]
56 #[instrument(skip_all)]
57 pub async fn get_all_config(
58 state: State<'_, Arc<AppState>>,
59 ) -> Result<HashMap<String, String>, ApiError> {
60 let rows: Vec<(String, String)> = sqlx::query_as("SELECT key, value FROM user_config")
61 .fetch_all(&state.pool)
62 .await
63 .map_err(db_err)?;
64 Ok(rows.into_iter().collect())
65 }
66
67 /// Set a config value (upsert). A synced key's write is captured by the engine's
68 /// export trigger; a device-local key's is not.
69 #[tauri::command]
70 #[instrument(skip_all)]
71 pub async fn set_config(
72 state: State<'_, Arc<AppState>>,
73 key: String,
74 value: String,
75 ) -> Result<(), ApiError> {
76 ensure_known(&key)?;
77 sqlx::query(
78 "INSERT INTO user_config (key, value) VALUES (?1, ?2) \
79 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
80 )
81 .bind(&key)
82 .bind(&value)
83 .execute(&state.pool)
84 .await
85 .map_err(db_err)?;
86 Ok(())
87 }
88
89 /// Remove a config key. Absent already is not an error.
90 #[tauri::command]
91 #[instrument(skip_all)]
92 pub async fn delete_config(state: State<'_, Arc<AppState>>, key: String) -> Result<(), ApiError> {
93 sqlx::query("DELETE FROM user_config WHERE key = ?1")
94 .bind(&key)
95 .execute(&state.pool)
96 .await
97 .map_err(db_err)?;
98 Ok(())
99 }
100