sova_notifications/
list.rs1use 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}