laterite_admin/settings/
store.rs1use chrono::{SecondsFormat, Utc};
10use laterite_core::strata::*;
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13use thiserror::Error;
14
15#[derive(Iden)]
16pub(crate) enum Settings {
17 Table,
18 Code,
19 Value,
20 UpdatedAt,
21}
22
23#[derive(Debug, Error)]
24pub enum SettingsError {
25 #[error("database error")]
26 Database(#[from] sqlx::Error),
27 #[error("settings serialization error")]
28 Serde(#[from] serde_json::Error),
29}
30
31pub trait SettingsModel: Serialize + DeserializeOwned + Default {
36 const CODE: &'static str;
38}
39
40pub async fn load<T: SettingsModel>(db: &Db) -> Result<T, SettingsError> {
42 match get(db, T::CODE).await? {
43 Some(value) => Ok(serde_json::from_value(value)?),
44 None => Ok(T::default()),
45 }
46}
47
48pub async fn save<T: SettingsModel>(db: &Db, model: &T) -> Result<(), SettingsError> {
50 set(db, T::CODE, &serde_json::to_value(model)?).await
51}
52
53pub async fn get(db: &Db, code: &str) -> Result<Option<serde_json::Value>, SettingsError> {
55 let (sql, values) = build(
56 db.backend,
57 Query::select()
58 .column(Settings::Value)
59 .from(Settings::Table)
60 .and_where(Expr::col(Settings::Code).eq(code))
61 .to_owned(),
62 );
63 let row = bind_values(sqlx::query(&sql), values)
64 .fetch_optional(&db.pool)
65 .await?;
66 match row {
67 Some(r) => {
68 let text = r.get_text("value")?;
69 Ok(Some(serde_json::from_str(&text)?))
70 }
71 None => Ok(None),
72 }
73}
74
75pub async fn set(db: &Db, code: &str, value: &serde_json::Value) -> Result<(), SettingsError> {
77 let json = serde_json::to_string(value)?;
78 let now = Utc::now().to_rfc3339_opts(SecondsFormat::Micros, true);
79 let (sql, values) = build(
80 db.backend,
81 Query::insert()
82 .into_table(Settings::Table)
83 .columns([Settings::Code, Settings::Value, Settings::UpdatedAt])
84 .values_panic([code.into(), json.into(), now.into()])
85 .on_conflict(
86 OnConflict::column(Settings::Code)
87 .update_columns([Settings::Value, Settings::UpdatedAt])
88 .to_owned(),
89 )
90 .to_owned(),
91 );
92 bind_values(sqlx::query(&sql), values)
93 .execute(&db.pool)
94 .await?;
95 Ok(())
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use serde::Deserialize;
102
103 async fn test_db() -> (Db, laterite_core::testing::TestGuard) {
106 laterite_core::testing::connect_test(&[super::super::migrations::migrations()]).await
107 }
108
109 #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
110 struct LogSettings {
111 #[serde(default)]
112 log_events: bool,
113 #[serde(default)]
114 log_requests: bool,
115 }
116
117 impl SettingsModel for LogSettings {
118 const CODE: &'static str = "test.log";
119 }
120
121 #[tokio::test]
122 async fn typed_round_trip_and_default() {
123 let (db, _guard) = test_db().await;
124
125 assert_eq!(
127 load::<LogSettings>(&db).await.unwrap(),
128 LogSettings::default()
129 );
130
131 let saved = LogSettings {
133 log_events: true,
134 log_requests: false,
135 };
136 save(&db, &saved).await.unwrap();
137 assert_eq!(load::<LogSettings>(&db).await.unwrap(), saved);
138
139 let raw = get(&db, LogSettings::CODE).await.unwrap().unwrap();
141 assert_eq!(raw["log_events"], serde_json::json!(true));
142 }
143
144 #[tokio::test]
145 async fn missing_field_uses_serde_default() {
146 let (db, _guard) = test_db().await;
147
148 set(
150 &db,
151 LogSettings::CODE,
152 &serde_json::json!({ "log_events": true }),
153 )
154 .await
155 .unwrap();
156 assert_eq!(
157 load::<LogSettings>(&db).await.unwrap(),
158 LogSettings {
159 log_events: true,
160 log_requests: false,
161 }
162 );
163 }
164}