Skip to main content

ares/db/
alerts.rs

1use crate::types::{AppError, Result};
2use serde::{Deserialize, Serialize};
3use sqlx::{PgPool, Row};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6fn now_ts() -> i64 {
7    SystemTime::now()
8        .duration_since(UNIX_EPOCH)
9        .unwrap()
10        .as_secs() as i64
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Alert {
15    pub id: String,
16    pub severity: String,
17    pub source: String,
18    pub title: String,
19    pub message: String,
20    pub resolved: bool,
21    pub created_at: i64,
22    pub resolved_at: Option<i64>,
23    pub resolved_by: Option<String>,
24}
25
26pub async fn create_alert(
27    pool: &PgPool,
28    severity: &str,
29    source: &str,
30    title: &str,
31    message: &str,
32) -> Result<Alert> {
33    let id = uuid::Uuid::new_v4().to_string();
34    let now = now_ts();
35
36    sqlx::query(
37        "INSERT INTO alerts (id, severity, source, title, message, resolved, created_at)
38         VALUES ($1, $2, $3, $4, $5, FALSE, $6)",
39    )
40    .bind(&id)
41    .bind(severity)
42    .bind(source)
43    .bind(title)
44    .bind(message)
45    .bind(now)
46    .execute(pool)
47    .await
48    .map_err(|e| AppError::Database(e.to_string()))?;
49
50    Ok(Alert {
51        id,
52        severity: severity.to_string(),
53        source: source.to_string(),
54        title: title.to_string(),
55        message: message.to_string(),
56        resolved: false,
57        created_at: now,
58        resolved_at: None,
59        resolved_by: None,
60    })
61}
62
63pub async fn list_alerts(
64    pool: &PgPool,
65    severity_filter: Option<&str>,
66    resolved_filter: Option<bool>,
67    limit: i64,
68) -> Result<Vec<Alert>> {
69    // Build query dynamically based on filters
70    let mut query = String::from(
71        "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
72         FROM alerts WHERE 1=1"
73    );
74    let mut bind_idx = 1;
75
76    if severity_filter.is_some() {
77        query.push_str(&format!(" AND severity = ${}", bind_idx));
78        bind_idx += 1;
79    }
80    if resolved_filter.is_some() {
81        query.push_str(&format!(" AND resolved = ${}", bind_idx));
82        bind_idx += 1;
83    }
84    let _ = bind_idx;
85
86    query.push_str(&format!(" ORDER BY created_at DESC LIMIT {}", limit));
87
88    // Since sqlx doesn't support truly dynamic queries easily, use separate branches
89    let rows = match (severity_filter, resolved_filter) {
90        (Some(sev), Some(res)) => {
91            sqlx::query(
92                "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
93                 FROM alerts WHERE severity = $1 AND resolved = $2
94                 ORDER BY created_at DESC LIMIT $3"
95            )
96            .bind(sev)
97            .bind(res)
98            .bind(limit)
99            .fetch_all(pool)
100            .await
101        }
102        (Some(sev), None) => {
103            sqlx::query(
104                "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
105                 FROM alerts WHERE severity = $1
106                 ORDER BY created_at DESC LIMIT $2"
107            )
108            .bind(sev)
109            .bind(limit)
110            .fetch_all(pool)
111            .await
112        }
113        (None, Some(res)) => {
114            sqlx::query(
115                "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
116                 FROM alerts WHERE resolved = $1
117                 ORDER BY created_at DESC LIMIT $2"
118            )
119            .bind(res)
120            .bind(limit)
121            .fetch_all(pool)
122            .await
123        }
124        (None, None) => {
125            sqlx::query(
126                "SELECT id, severity, source, title, message, resolved, created_at, resolved_at, resolved_by
127                 FROM alerts ORDER BY created_at DESC LIMIT $1"
128            )
129            .bind(limit)
130            .fetch_all(pool)
131            .await
132        }
133    }
134    .map_err(|e| AppError::Database(e.to_string()))?;
135
136    rows.iter()
137        .map(|row| {
138            Ok(Alert {
139                id: row.get("id"),
140                severity: row.get("severity"),
141                source: row.get("source"),
142                title: row.get("title"),
143                message: row.get("message"),
144                resolved: row.get("resolved"),
145                created_at: row.get("created_at"),
146                resolved_at: row.get("resolved_at"),
147                resolved_by: row.get("resolved_by"),
148            })
149        })
150        .collect()
151}
152
153pub async fn resolve_alert(pool: &PgPool, alert_id: &str, resolved_by: Option<&str>) -> Result<()> {
154    let now = now_ts();
155
156    let result = sqlx::query(
157        "UPDATE alerts SET resolved = TRUE, resolved_at = $1, resolved_by = $2 WHERE id = $3 AND resolved = FALSE"
158    )
159    .bind(now)
160    .bind(resolved_by)
161    .bind(alert_id)
162    .execute(pool)
163    .await
164    .map_err(|e| AppError::Database(e.to_string()))?;
165
166    if result.rows_affected() == 0 {
167        return Err(AppError::NotFound(
168            "Alert not found or already resolved".to_string(),
169        ));
170    }
171
172    Ok(())
173}
174
175pub async fn get_active_alert_count(pool: &PgPool) -> Result<i64> {
176    let row = sqlx::query("SELECT COUNT(*) as cnt FROM alerts WHERE resolved = FALSE")
177        .fetch_one(pool)
178        .await
179        .map_err(|e| AppError::Database(e.to_string()))?;
180
181    Ok(row.get("cnt"))
182}