bhtune_db/models/
settings.rs1use chrono::{DateTime, Utc};
2use sqlx::{Row, SqlitePool, sqlite::SqliteRow};
3
4use crate::error::{DbError, DbResult};
5
6#[derive(Debug, Clone, PartialEq)]
10pub struct SettingRow {
11 pub key: String,
12 pub value: serde_json::Value,
13 pub updated_at: DateTime<Utc>,
14}
15
16impl SettingRow {
17 pub async fn get(pool: &SqlitePool, key: &str) -> DbResult<Option<Self>> {
23 let row = sqlx::query("SELECT key, value, updated_at FROM settings WHERE key = ?")
24 .bind(key)
25 .fetch_optional(pool)
26 .await
27 .map_err(DbError::Query)?;
28
29 row.map(setting_from_row).transpose()
30 }
31
32 pub async fn upsert(
37 pool: &SqlitePool,
38 key: &str,
39 value: &serde_json::Value,
40 updated_at: DateTime<Utc>,
41 ) -> DbResult<Self> {
42 let value_json = value.to_string();
43 let row = sqlx::query(
44 "INSERT INTO settings (key, value, updated_at)
45 VALUES (?, ?, ?)
46 ON CONFLICT(key) DO UPDATE SET
47 value = excluded.value,
48 updated_at = excluded.updated_at
49 RETURNING key, value, updated_at",
50 )
51 .bind(key)
52 .bind(value_json)
53 .bind(updated_at)
54 .fetch_one(pool)
55 .await
56 .map_err(DbError::Query)?;
57
58 setting_from_row(row)
59 }
60}
61
62fn setting_from_row(row: SqliteRow) -> DbResult<SettingRow> {
63 let value_json: String = row.try_get("value").map_err(DbError::Query)?;
64 let value = serde_json::from_str(&value_json).map_err(|source| DbError::InvalidJsonShape {
65 column: "settings.value",
66 source,
67 })?;
68
69 Ok(SettingRow {
70 key: row.try_get("key").map_err(DbError::Query)?,
71 value,
72 updated_at: row.try_get("updated_at").map_err(DbError::Query)?,
73 })
74}
75#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[tokio::test]
82 async fn malformed_setting_json_is_reported_as_invalid_shape() {
83 let pool = crate::connect_in_memory().await.unwrap();
84 sqlx::query("CREATE TABLE malformed_settings (key TEXT, value TEXT, updated_at TEXT)")
85 .execute(&pool)
86 .await
87 .unwrap();
88 sqlx::query("INSERT INTO malformed_settings (key, value, updated_at) VALUES (?, ?, ?)")
89 .bind("draft")
90 .bind("{not-json")
91 .bind(chrono::Utc::now())
92 .execute(&pool)
93 .await
94 .unwrap();
95
96 let row = sqlx::query("SELECT key, value, updated_at FROM malformed_settings")
97 .fetch_one(&pool)
98 .await
99 .unwrap();
100 let err = setting_from_row(row).unwrap_err();
101 assert!(matches!(
102 err,
103 DbError::InvalidJsonShape {
104 column: "settings.value",
105 ..
106 }
107 ));
108 }
109}