scouter_sql/sql/
schema.rs

1use chrono::{DateTime, Utc};
2use scouter_types::{
3    alert::Alert,
4    custom::{BinnedCustomMetric, BinnedCustomMetricStats},
5    get_utc_datetime,
6    psi::{FeatureBinProportion, FeatureBinProportionResult},
7    RecordType,
8};
9use serde::{Deserialize, Serialize};
10use sqlx::{postgres::PgRow, Error, FromRow, Row};
11use std::collections::BTreeMap;
12use std::collections::HashMap;
13
14#[derive(Serialize, Deserialize, Debug, Clone)]
15pub struct DriftRecord {
16    pub created_at: DateTime<Utc>,
17    pub name: String,
18    pub space: String,
19    pub version: String,
20    pub feature: String,
21    pub value: f64,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct SpcFeatureResult {
26    pub feature: String,
27    pub created_at: Vec<DateTime<Utc>>,
28    pub values: Vec<f64>,
29}
30
31impl<'r> FromRow<'r, PgRow> for SpcFeatureResult {
32    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
33        Ok(SpcFeatureResult {
34            feature: row.try_get("feature")?,
35            created_at: row.try_get("created_at")?,
36            values: row.try_get("values")?,
37        })
38    }
39}
40
41pub struct FeatureBinProportionWrapper(pub FeatureBinProportion);
42
43impl<'r> FromRow<'r, PgRow> for FeatureBinProportionWrapper {
44    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
45        let value: serde_json::Value = row.try_get("bins")?;
46        let bins: BTreeMap<usize, f64> = serde_json::from_value(value).unwrap_or_default();
47
48        Ok(FeatureBinProportionWrapper(FeatureBinProportion {
49            feature: row.try_get("feature")?,
50            bins,
51        }))
52    }
53}
54
55pub struct BinnedCustomMetricWrapper(pub BinnedCustomMetric);
56
57impl<'r> FromRow<'r, PgRow> for BinnedCustomMetricWrapper {
58    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
59        let stats_json: Vec<serde_json::Value> = row.try_get("stats")?;
60
61        let stats: Vec<BinnedCustomMetricStats> = stats_json
62            .into_iter()
63            .map(|value| serde_json::from_value(value).unwrap_or_default())
64            .collect();
65
66        Ok(BinnedCustomMetricWrapper(BinnedCustomMetric {
67            metric: row.try_get("metric")?,
68            created_at: row.try_get("created_at")?,
69            stats,
70        }))
71    }
72}
73
74pub struct AlertWrapper(pub Alert);
75
76impl<'r> FromRow<'r, PgRow> for AlertWrapper {
77    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
78        let alert_value: serde_json::Value = row.try_get("alert")?;
79        let alert: BTreeMap<String, String> =
80            serde_json::from_value(alert_value).unwrap_or_default();
81
82        Ok(AlertWrapper(Alert {
83            created_at: row.try_get("created_at")?,
84            name: row.try_get("name")?,
85            space: row.try_get("space")?,
86            version: row.try_get("version")?,
87            alert,
88            entity_name: row.try_get("entity_name")?,
89            id: row.try_get("id")?,
90            drift_type: row.try_get("drift_type")?,
91            active: row.try_get("active")?,
92        }))
93    }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct TaskRequest {
98    pub name: String,
99    pub space: String,
100    pub version: String,
101    pub profile: String,
102    pub drift_type: String,
103    pub previous_run: DateTime<Utc>,
104    pub schedule: String,
105    pub uid: String,
106}
107
108impl<'r> FromRow<'r, PgRow> for TaskRequest {
109    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
110        let profile: serde_json::Value = row.try_get("profile")?;
111
112        Ok(TaskRequest {
113            name: row.try_get("name")?,
114            space: row.try_get("space")?,
115            version: row.try_get("version")?,
116            profile: profile.to_string(),
117            drift_type: row.try_get("drift_type")?,
118            previous_run: row.try_get("previous_run")?,
119            schedule: row.try_get("schedule")?,
120            uid: row.try_get("uid")?,
121        })
122    }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ObservabilityResult {
127    pub route_name: String,
128    pub created_at: Vec<DateTime<Utc>>,
129    pub p5: Vec<f64>,
130    pub p25: Vec<f64>,
131    pub p50: Vec<f64>,
132    pub p95: Vec<f64>,
133    pub p99: Vec<f64>,
134    pub total_request_count: Vec<i64>,
135    pub total_error_count: Vec<i64>,
136    pub error_latency: Vec<f64>,
137    pub status_counts: Vec<HashMap<String, i64>>,
138}
139
140impl<'r> FromRow<'r, PgRow> for ObservabilityResult {
141    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
142        // decode status counts to vec of jsonb
143        let status_counts: Vec<serde_json::Value> = row.try_get("status_counts")?;
144
145        // convert vec of jsonb to vec of hashmaps
146        let status_counts: Vec<HashMap<String, i64>> = status_counts
147            .into_iter()
148            .map(|value| serde_json::from_value(value).unwrap_or_default())
149            .collect();
150
151        Ok(ObservabilityResult {
152            route_name: row.try_get("route_name")?,
153            created_at: row.try_get("created_at")?,
154            p5: row.try_get("p5")?,
155            p25: row.try_get("p25")?,
156            p50: row.try_get("p50")?,
157            p95: row.try_get("p95")?,
158            p99: row.try_get("p99")?,
159            total_request_count: row.try_get("total_request_count")?,
160            total_error_count: row.try_get("total_error_count")?,
161            error_latency: row.try_get("error_latency")?,
162            status_counts,
163        })
164    }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct BinProportion {
169    pub bin_id: usize,
170    pub proportion: f64,
171}
172
173#[derive(Debug)]
174pub struct FeatureBinProportionResultWrapper(pub FeatureBinProportionResult);
175
176impl<'r> FromRow<'r, PgRow> for FeatureBinProportionResultWrapper {
177    fn from_row(row: &'r PgRow) -> Result<Self, Error> {
178        // Extract the bin_proportions as a Vec of tuples
179        let bin_proportions_json: Vec<serde_json::Value> = row.try_get("bin_proportions")?;
180
181        // Convert the Vec of tuples into a Vec of BinProportion structs
182        let bin_proportions: Vec<BTreeMap<usize, f64>> = bin_proportions_json
183            .into_iter()
184            .map(|json| serde_json::from_value(json).unwrap_or_default())
185            .collect();
186
187        let overall_proportions_json: serde_json::Value = row.try_get("overall_proportions")?;
188        let overall_proportions: BTreeMap<usize, f64> =
189            serde_json::from_value(overall_proportions_json).unwrap_or_default();
190
191        Ok(FeatureBinProportionResultWrapper(
192            FeatureBinProportionResult {
193                feature: row.try_get("feature")?,
194                created_at: row.try_get("created_at")?,
195                bin_proportions,
196                overall_proportions,
197            },
198        ))
199    }
200}
201#[derive(Debug, Clone, FromRow)]
202pub struct Entity {
203    pub space: String,
204    pub name: String,
205    pub version: String,
206    pub begin_timestamp: DateTime<Utc>,
207    pub end_timestamp: DateTime<Utc>,
208}
209
210impl Entity {
211    pub fn get_write_path(&self, record_type: &RecordType) -> String {
212        format!(
213            "{}/{}/{}/{}",
214            self.space, self.name, self.version, record_type
215        )
216    }
217}
218
219#[derive(Debug, Serialize, Deserialize, Clone)]
220pub struct User {
221    pub id: Option<i32>,
222    pub created_at: DateTime<Utc>,
223    pub active: bool,
224    pub username: String,
225    pub password_hash: String,
226    pub hashed_recovery_codes: Vec<String>,
227    pub permissions: Vec<String>,
228    pub group_permissions: Vec<String>,
229    pub role: String,
230    pub favorite_spaces: Vec<String>,
231    pub refresh_token: Option<String>,
232    pub email: String,
233    pub updated_at: DateTime<Utc>,
234}
235
236impl User {
237    #[allow(clippy::too_many_arguments)]
238    pub fn new(
239        username: String,
240        password_hash: String,
241        email: String,
242        hashed_recovery_codes: Vec<String>,
243        permissions: Option<Vec<String>>,
244        group_permissions: Option<Vec<String>>,
245        role: Option<String>,
246        favorite_spaces: Option<Vec<String>>,
247    ) -> Self {
248        let created_at = get_utc_datetime();
249
250        User {
251            id: None,
252            created_at,
253            active: true,
254            username,
255            password_hash,
256            hashed_recovery_codes,
257            permissions: permissions.unwrap_or(vec!["read:all".to_string()]),
258            group_permissions: group_permissions.unwrap_or(vec!["user".to_string()]),
259            favorite_spaces: favorite_spaces.unwrap_or_default(),
260            role: role.unwrap_or("user".to_string()),
261            refresh_token: None,
262            email,
263            updated_at: created_at,
264        }
265    }
266}
267
268impl FromRow<'_, PgRow> for User {
269    fn from_row(row: &PgRow) -> Result<Self, sqlx::Error> {
270        let id = row.try_get("id")?;
271        let created_at = row.try_get("created_at")?;
272        let updated_at = row.try_get("updated_at")?;
273        let active = row.try_get("active")?;
274        let username = row.try_get("username")?;
275        let password_hash = row.try_get("password_hash")?;
276        let email = row.try_get("email")?;
277        let role = row.try_get("role")?;
278        let refresh_token = row.try_get("refresh_token")?;
279
280        let group_permissions: Vec<String> =
281            serde_json::from_value(row.try_get("group_permissions")?).unwrap_or_default();
282
283        let permissions: Vec<String> =
284            serde_json::from_value(row.try_get("permissions")?).unwrap_or_default();
285
286        let hashed_recovery_codes: Vec<String> =
287            serde_json::from_value(row.try_get("hashed_recovery_codes")?).unwrap_or_default();
288
289        let favorite_spaces: Vec<String> =
290            serde_json::from_value(row.try_get("favorite_spaces")?).unwrap_or_default();
291
292        Ok(User {
293            id,
294            created_at,
295            updated_at,
296            active,
297            username,
298            password_hash,
299            email,
300            role,
301            refresh_token,
302            hashed_recovery_codes,
303            permissions,
304            group_permissions,
305            favorite_spaces,
306        })
307    }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
311pub struct UpdateAlertResult {
312    pub id: i32,
313    pub active: bool,
314    pub updated_at: DateTime<Utc>,
315}