-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add notification inbox (#3541)
* feat(db): add notification inbox tables - Added `notifications` table to store notification messages. - Added `readed_notifications` table to track which notifications have been read by users. * update * update * update * update * update * add notifications dao * update * add NotificationKind * [autofix.ci] apply automated fixes * update * feat(graphQL): add notifications api Signed-off-by: Wei Zhang <[email protected]> * feat(graphQL): add list notifications and mark read * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) --------- Signed-off-by: Wei Zhang <[email protected]> Co-authored-by: Meng Zhang <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
- Loading branch information
1 parent
b855821
commit 24792ad
Showing
13 changed files
with
1,212 additions
and
618 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
DROP TABLE notifications; | ||
DROP TABLE read_notifications; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
CREATE TABLE notifications ( | ||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||
|
||
created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')), | ||
updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')), | ||
|
||
-- enum of admin, all_user | ||
recipient VARCHAR(255) NOT NULL DEFAULT 'admin', | ||
|
||
-- content of notification, in markdown format. | ||
content TEXT NOT NULL | ||
); | ||
|
||
CREATE TABLE read_notifications ( | ||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||
user_id INTEGER NOT NULL, | ||
notification_id INTEGER NOT NULL, | ||
|
||
created_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')), | ||
updated_at TIMESTAMP NOT NULL DEFAULT(DATETIME('now')), | ||
|
||
CONSTRAINT idx_unique_user_id_notification_id UNIQUE (user_id, notification_id), | ||
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, | ||
FOREIGN KEY (notification_id) REFERENCES notifications(id) ON DELETE CASCADE | ||
) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
use anyhow::{Context, Result}; | ||
use chrono::{DateTime, Duration, Utc}; | ||
use sqlx::{prelude::*, query, query_as}; | ||
|
||
use crate::DbConn; | ||
|
||
pub const NOTIFICATION_RECIPIENT_ALL_USER: &str = "all_user"; | ||
pub const NOTIFICATION_RECIPIENT_ADMIN: &str = "admin"; | ||
|
||
#[derive(FromRow)] | ||
pub struct NotificationDAO { | ||
pub id: i64, | ||
|
||
pub recipient: String, | ||
pub content: String, | ||
pub read: bool, | ||
pub created_at: DateTime<Utc>, | ||
pub updated_at: DateTime<Utc>, | ||
} | ||
|
||
impl DbConn { | ||
pub async fn create_notification(&self, recipient: &str, content: &str) -> Result<i64> { | ||
let res = query!( | ||
"INSERT INTO notifications (recipient, content) VALUES (?, ?)", | ||
recipient, | ||
content | ||
) | ||
.execute(&self.pool) | ||
.await?; | ||
|
||
Ok(res.last_insert_rowid()) | ||
} | ||
|
||
pub async fn mark_notification_read(&self, id: i64, user_id: i64) -> Result<()> { | ||
query!( | ||
"INSERT INTO read_notifications (notification_id, user_id) VALUES (?, ?)", | ||
id, | ||
user_id | ||
) | ||
.execute(&self.pool) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
pub async fn mark_all_notifications_read_by_user(&self, user_id: i64) -> Result<()> { | ||
let user = self | ||
.get_user(user_id) | ||
.await? | ||
.context("User doesn't exist")?; | ||
let recipient_clause = if user.is_admin { | ||
format!( | ||
"recipient = '{}' OR recipient = '{}'", | ||
NOTIFICATION_RECIPIENT_ALL_USER, NOTIFICATION_RECIPIENT_ADMIN | ||
) | ||
} else { | ||
format!("recipient = '{}'", NOTIFICATION_RECIPIENT_ALL_USER) | ||
}; | ||
|
||
let query = format!( | ||
r#" | ||
INSERT INTO read_notifications (notification_id, user_id) | ||
SELECT | ||
notifications.id, | ||
? | ||
FROM | ||
notifications | ||
LEFT JOIN | ||
read_notifications | ||
ON | ||
notifications.id = read_notifications.notification_id | ||
AND read_notifications.user_id = ? | ||
WHERE | ||
{} | ||
AND read_notifications.notification_id IS NULL; | ||
"#, | ||
recipient_clause | ||
); | ||
|
||
sqlx::query(&query) | ||
.bind(user_id) | ||
.bind(user_id) | ||
.execute(&self.pool) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
pub async fn list_notifications_within_7days( | ||
&self, | ||
user_id: i64, | ||
) -> Result<Vec<NotificationDAO>> { | ||
let user = self | ||
.get_user(user_id) | ||
.await? | ||
.context("User doesn't exist")?; | ||
let recipient_clause = if user.is_admin { | ||
format!( | ||
"recipient = '{}' OR recipient = '{}'", | ||
NOTIFICATION_RECIPIENT_ALL_USER, NOTIFICATION_RECIPIENT_ADMIN | ||
) | ||
} else { | ||
format!("recipient = '{}'", NOTIFICATION_RECIPIENT_ALL_USER) | ||
}; | ||
let date_7days_ago = Utc::now() - Duration::days(7); | ||
let sql = format!( | ||
r#" | ||
SELECT | ||
notifications.id, | ||
notifications.created_at, | ||
notifications.updated_at, | ||
recipient, | ||
content, | ||
CASE | ||
WHEN read_notifications.user_id IS NOT NULL THEN 1 | ||
ELSE 0 | ||
END AS read | ||
FROM | ||
notifications | ||
LEFT JOIN | ||
read_notifications | ||
ON | ||
notifications.id = read_notifications.notification_id | ||
WHERE | ||
({recipient_clause}) | ||
AND notifications.created_at > '{date_7days_ago}' | ||
"# | ||
); | ||
let notifications = query_as(&sql).fetch_all(&self.pool).await?; | ||
Ok(notifications) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::testutils; | ||
|
||
/// Smoke test to ensure sql query is valid, actual functionality test shall happens at service level. | ||
#[tokio::test] | ||
async fn smoketest_list_notifications() { | ||
let db = DbConn::new_in_memory().await.unwrap(); | ||
let user1 = testutils::create_user(&db).await; | ||
let notifications = db.list_notifications_within_7days(user1).await.unwrap(); | ||
assert!(notifications.is_empty()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
use async_trait::async_trait; | ||
use chrono::{DateTime, Utc}; | ||
use juniper::{GraphQLEnum, GraphQLObject, ID}; | ||
|
||
use crate::Result; | ||
|
||
#[derive(GraphQLEnum, Clone, Debug)] | ||
pub enum NotificationRecipient { | ||
Admin, | ||
AllUser, | ||
} | ||
|
||
#[derive(GraphQLObject)] | ||
pub struct Notification { | ||
pub id: ID, | ||
pub content: String, | ||
pub read: bool, | ||
pub created_at: DateTime<Utc>, | ||
pub updated_at: DateTime<Utc>, | ||
} | ||
|
||
#[async_trait] | ||
pub trait NotificationService: Send + Sync { | ||
async fn list(&self, user_id: &ID) -> Result<Vec<Notification>>; | ||
|
||
async fn mark_read(&self, user_id: &ID, id: Option<ID>) -> Result<()>; | ||
} |
Oops, something went wrong.