Skip to main content

server/command/
config.rs

1use sqlx::Row;
2use sqlx::types::Uuid;
3use std::fmt::Debug;
4use supp_macro::command;
5
6use super::{CmdError, CmdResult};
7use crate::{
8    config::{ConfigError, ConfigOption},
9    db::get_connection,
10    user::User,
11};
12command! {
13    GetConfig {
14        #[required]
15        user_id: Uuid,
16        #[required]
17        name: String,
18    } => {
19        let user = User { id: user_id };
20        // An absent key is "not set" (Ok(None) → the native emits
21        // `(:config-value nil)`), not a hard error. This keeps absent
22        // (nil) distinct from a key holding the empty string ("") while
23        // letting genuine DB errors still propagate.
24        match user.config(&name).await {
25            Ok(opt) => Ok(opt.map(|v| match v {
26                ConfigOption::String(s) => CmdResult::String(s),
27                ConfigOption::Blob(b) => CmdResult::Data(b),
28            })),
29            Err(ConfigError::NoConfig(_)) => Ok(None),
30            Err(e) => Err(e.into()),
31        }
32    }
33}
34
35command! {
36    GetVersion {
37    } => {
38        const HASH: &str = env!("GIT_HASH");
39        Ok(Some(CmdResult::String(HASH.to_string())))
40    }
41}
42
43command! {
44    GetBuildDate {
45    } => {
46    const BUILD: &str = env!("BUILD_DATE");
47        Ok(Some(CmdResult::String(BUILD.to_string())))
48    }
49}
50
51command! {
52    SetConfig {
53        #[required]
54        user_id: Uuid,
55        #[required]
56        name: String,
57        #[required]
58        value: String,
59    } => {
60        let user = User { id: user_id };
61        user.set_config(&name, ConfigOption::String(value)).await?;
62        Ok(None)
63    }
64}
65
66command! {
67    SelectColumn {
68        #[required]
69        field: String,
70        #[required]
71        table: String,
72    } => {
73        let mut conn = get_connection().await.map_err(|err| {
74            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
75            ConfigError::DB
76        })?;
77
78        // `field`/`table` are SQL identifiers (not bindable as `$n`), so they
79        // are interpolated. `AssertSqlSafe` is sound ONLY because select-column
80        // is a trusted-operator escape hatch: it is reachable from the CLI
81        // `sql selcol` / direct dispatch, never registered as an RPC/script
82        // native (see rpc::natives::config). Do not expose it to untrusted
83        // callers without validating/quoting the identifiers first.
84        let values: Vec<String> = sqlx::query(sqlx::AssertSqlSafe(format!(
85            "SELECT {field}::text FROM {table}"
86        )))
87            .fetch_all(&mut *conn)
88            .await?
89            .into_iter()
90            .map(|row| row.get::<Option<String>, _>(0).unwrap_or_default())
91            .collect();
92
93        Ok(Some(CmdResult::Lines(values)))
94    }
95}
96
97#[cfg(test)]
98mod command_config_tests {
99    use super::*;
100    use crate::{db::DB_POOL, user::User};
101    use sqlx::{PgPool, types::Uuid};
102    use supp_macro::local_db_sqlx_test;
103    use tokio::sync::OnceCell;
104
105    /// Context for keeping environment intact
106    static CONTEXT: OnceCell<()> = OnceCell::const_new();
107    static USER: OnceCell<User> = OnceCell::const_new();
108
109    async fn setup() {
110        CONTEXT
111            .get_or_init(|| async {
112                #[cfg(feature = "testlog")]
113                let _ = env_logger::builder()
114                    .is_test(true)
115                    .filter_level(log::LevelFilter::Trace)
116                    .try_init();
117            })
118            .await;
119        USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
120            .await;
121    }
122
123    #[local_db_sqlx_test]
124    async fn set_then_get_config_round_trips_per_user(pool: PgPool) -> anyhow::Result<()> {
125        let user_id = Uuid::new_v4();
126        SetConfig::new()
127            .user_id(user_id)
128            .name("testfield".to_string())
129            .value("testval".to_string())
130            .run()
131            .await?;
132
133        let val = GetConfig::new()
134            .user_id(user_id)
135            .name("testfield".to_string())
136            .run()
137            .await?
138            .unwrap();
139        assert_eq!(val.to_string(), "testval");
140    }
141}