1
use sqlx::Row;
2
use sqlx::types::Uuid;
3
use std::fmt::Debug;
4
use supp_macro::command;
5

            
6
use super::{CmdError, CmdResult};
7
use crate::{
8
    config::{ConfigError, ConfigOption},
9
    db::get_connection,
10
    user::User,
11
};
12
command! {
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
51
            Ok(opt) => Ok(opt.map(|v| match v {
26
51
                ConfigOption::String(s) => CmdResult::String(s),
27
                ConfigOption::Blob(b) => CmdResult::Data(b),
28
51
            })),
29
            Err(ConfigError::NoConfig(_)) => Ok(None),
30
            Err(e) => Err(e.into()),
31
        }
32
    }
33
349
}
34

            
35
command! {
36
    GetVersion {
37
    } => {
38
        const HASH: &str = env!("GIT_HASH");
39
        Ok(Some(CmdResult::String(HASH.to_string())))
40
    }
41
196
}
42

            
43
command! {
44
    GetBuildDate {
45
    } => {
46
    const BUILD: &str = env!("BUILD_DATE");
47
        Ok(Some(CmdResult::String(BUILD.to_string())))
48
    }
49
141
}
50

            
51
command! {
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
150
}
65

            
66
command! {
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)]
98
mod 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
1
    async fn setup() {
110
1
        CONTEXT
111
1
            .get_or_init(|| async {
112
                #[cfg(feature = "testlog")]
113
1
                let _ = env_logger::builder()
114
1
                    .is_test(true)
115
1
                    .filter_level(log::LevelFilter::Trace)
116
1
                    .try_init();
117
2
            })
118
1
            .await;
119
2
        USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
120
1
            .await;
121
1
    }
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
}