Skip to main content

sova_notifications/
list.rs

1//! Query helpers for notifications inbox.
2
3use crate::entity;
4use chrono::{DateTime, Utc};
5use sova_core::Result;
6use sova_db::{DbError, DbHandle};
7use sea_orm::{
8    ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
9    QuerySelect, Set,
10};
11use serde::Serialize;
12
13#[derive(Debug, Clone, Default)]
14pub struct NotificationFilter {
15    pub channel: Option<String>,
16    pub unread_only: bool,
17    pub limit: u64,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct NotificationRow {
22    pub id: i64,
23    pub user_id: i64,
24    pub channel: String,
25    pub event: String,
26    pub title: String,
27    pub body: Option<String>,
28    pub data: serde_json::Value,
29    pub read_at: Option<DateTime<Utc>>,
30    pub created_at: DateTime<Utc>,
31}
32
33impl From<entity::Model> for NotificationRow {
34    fn from(m: entity::Model) -> Self {
35        let data = serde_json::from_str(&m.data).unwrap_or(serde_json::json!({}));
36        Self {
37            id: m.id,
38            user_id: m.user_id,
39            channel: m.channel,
40            event: m.event,
41            title: m.title,
42            body: m.body,
43            data,
44            read_at: m.read_at,
45            created_at: m.created_at,
46        }
47    }
48}
49
50pub async fn list_notifications(
51    db: &DbHandle,
52    user_id: i64,
53    filter: NotificationFilter,
54) -> Result<Vec<NotificationRow>> {
55    let limit = filter.limit.clamp(1, 200);
56    let mut q = entity::Entity::find().filter(entity::Column::UserId.eq(user_id));
57    if let Some(ref ch) = filter.channel {
58        q = q.filter(entity::Column::Channel.eq(ch.clone()));
59    }
60    if filter.unread_only {
61        q = q.filter(entity::Column::ReadAt.is_null());
62    }
63    let rows = q
64        .order_by_desc(entity::Column::CreatedAt)
65        .limit(limit)
66        .all(db)
67        .await
68        .map_err(|e| sova_core::Error::from(DbError::from(e)))?;
69    Ok(rows.into_iter().map(NotificationRow::from).collect())
70}
71
72pub async fn unread_count(db: &DbHandle, user_id: i64, channel: Option<&str>) -> Result<u64> {
73    let mut q = entity::Entity::find()
74        .filter(entity::Column::UserId.eq(user_id))
75        .filter(entity::Column::ReadAt.is_null());
76    if let Some(ch) = channel {
77        q = q.filter(entity::Column::Channel.eq(ch));
78    }
79    q.count(db)
80        .await
81        .map_err(|e| sova_core::Error::from(DbError::from(e)))
82}
83
84pub async fn mark_read(db: &DbHandle, user_id: i64, id: i64) -> Result<bool> {
85    let Some(row) = entity::Entity::find_by_id(id)
86        .one(db)
87        .await
88        .map_err(|e| sova_core::Error::from(DbError::from(e)))?
89    else {
90        return Ok(false);
91    };
92    if row.user_id != user_id {
93        return Ok(false);
94    }
95    if row.read_at.is_some() {
96        return Ok(true);
97    }
98    let mut am: entity::ActiveModel = row.into();
99    am.read_at = Set(Some(Utc::now()));
100    am.update(db)
101        .await
102        .map_err(|e| sova_core::Error::from(DbError::from(e)))?;
103    Ok(true)
104}
105
106pub async fn mark_all_read(db: &DbHandle, user_id: i64, channel: Option<&str>) -> Result<u64> {
107    let mut q = entity::Entity::find()
108        .filter(entity::Column::UserId.eq(user_id))
109        .filter(entity::Column::ReadAt.is_null());
110    if let Some(ch) = channel {
111        q = q.filter(entity::Column::Channel.eq(ch));
112    }
113    let rows = q
114        .all(db)
115        .await
116        .map_err(|e| sova_core::Error::from(DbError::from(e)))?;
117    let n = rows.len() as u64;
118    let now = Utc::now();
119    for row in rows {
120        let mut am: entity::ActiveModel = row.into();
121        am.read_at = Set(Some(now));
122        am.update(db)
123            .await
124            .map_err(|e| sova_core::Error::from(DbError::from(e)))?;
125    }
126    Ok(n)
127}