Skip to content

Commit

Permalink
feat(graphQL): add list notifications and mark read
Browse files Browse the repository at this point in the history
  • Loading branch information
zwpaper committed Dec 12, 2024
1 parent cda8e51 commit 83696ed
Show file tree
Hide file tree
Showing 11 changed files with 1,006 additions and 713 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
DROP TABLE notifications;
DROP TABLE readed_notifications;
DROP TABLE read_notifications;
2 changes: 1 addition & 1 deletion ee/tabby-db/migrations/0039_add-notification-inbox.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ CREATE TABLE notifications (
content TEXT NOT NULL
);

CREATE TABLE readed_notifications (
CREATE TABLE read_notifications (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
notification_id INTEGER NOT NULL,
Expand Down
Binary file modified ee/tabby-db/schema.sqlite
Binary file not shown.
1,294 changes: 647 additions & 647 deletions ee/tabby-db/schema/schema.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ee/tabby-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub use email_setting::EmailSettingDAO;
pub use integrations::IntegrationDAO;
pub use invitations::InvitationDAO;
pub use job_runs::JobRunDAO;
pub use notifications::NotificationDAO;
pub use oauth_credential::OAuthCredentialDAO;
pub use provided_repositories::ProvidedRepositoryDAO;
pub use repositories::RepositoryDAO;
Expand Down
80 changes: 73 additions & 7 deletions ee/tabby-db/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ 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>,
}
Expand All @@ -27,9 +31,9 @@ impl DbConn {
Ok(res.last_insert_rowid())
}

pub async fn mark_notification_readed(&self, id: i64, user_id: i64) -> Result<()> {
pub async fn mark_notification_read(&self, id: i64, user_id: i64) -> Result<()> {
query!(
"INSERT INTO readed_notifications (notification_id, user_id) VALUES (?, ?)",
"INSERT INTO read_notifications (notification_id, user_id) VALUES (?, ?)",
id,
user_id
)
Expand All @@ -39,6 +43,49 @@ impl DbConn {
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,
Expand All @@ -48,16 +95,35 @@ impl DbConn {
.await?
.context("User doesn't exist")?;
let recipient_clause = if user.is_admin {
"recipient = 'all_user' OR recipient = 'admin'"
format!(
"recipient = '{}' OR recipient = '{}'",
NOTIFICATION_RECIPIENT_ALL_USER, NOTIFICATION_RECIPIENT_ADMIN
)
} else {
"recipient = 'all_user'"
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
FROM notifications LEFT JOIN readed_notifications ON notifications.id = readed_notifications.notification_id
WHERE ({recipient_clause}) AND notifications.created_at > '{date_7days_ago}' AND readed_notifications.user_id IS NULL -- notification is not marked as readed
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?;
Expand Down
16 changes: 14 additions & 2 deletions ee/tabby-schema/src/dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::bail;
use hash_ids::HashIds;
use lazy_static::lazy_static;
use tabby_db::{
EmailSettingDAO, IntegrationDAO, InvitationDAO, JobRunDAO, OAuthCredentialDAO,
EmailSettingDAO, IntegrationDAO, InvitationDAO, JobRunDAO, NotificationDAO, OAuthCredentialDAO,
ServerSettingDAO, ThreadDAO, ThreadMessageAttachmentClientCode, ThreadMessageAttachmentCode,
ThreadMessageAttachmentDoc, ThreadMessageAttachmentIssueDoc, ThreadMessageAttachmentPullDoc,
ThreadMessageAttachmentWebDoc, UserEventDAO,
Expand All @@ -11,7 +11,7 @@ use tabby_db::{
use crate::{
integration::{Integration, IntegrationKind, IntegrationStatus},
interface::UserValue,
notification::NotificationRecipient,
notification::{Notification, NotificationRecipient},
repository::RepositoryKind,
schema::{
auth::{self, OAuthCredential, OAuthProvider},
Expand Down Expand Up @@ -186,6 +186,18 @@ impl TryFrom<UserEventDAO> for UserEvent {
}
}

impl From<NotificationDAO> for Notification {
fn from(value: NotificationDAO) -> Self {
Self {
id: value.id.as_id(),
content: value.content,
read: value.read,
created_at: value.created_at,
updated_at: value.updated_at,
}
}
}

impl From<ThreadMessageAttachmentCode> for thread::MessageAttachmentCode {
fn from(value: ThreadMessageAttachmentCode) -> Self {
Self {
Expand Down
66 changes: 11 additions & 55 deletions ee/tabby-schema/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use juniper::{
graphql_object, graphql_subscription, graphql_value, FieldError, GraphQLEnum, GraphQLObject,
IntoFieldError, Object, RootNode, ScalarValue, Value, ID,
};
use notification::NotificationService;
use repository::RepositoryGrepOutput;
use tabby_common::{
api::{code::CodeSearch, event::EventLogger},
Expand Down Expand Up @@ -104,6 +105,7 @@ pub trait ServiceLocator: Send + Sync {
fn context(&self) -> Arc<dyn ContextService>;
fn user_group(&self) -> Arc<dyn UserGroupService>;
fn access_policy(&self) -> Arc<dyn AccessPolicyService>;
fn notification(&self) -> Arc<dyn NotificationService>;
}

pub struct Context {
Expand Down Expand Up @@ -528,61 +530,9 @@ impl Query {
.await
}

async fn notifications(
ctx: &Context,
readed: Option<bool>,
) -> Result<Vec<notification::Notification>> {
async fn notifications(ctx: &Context) -> Result<Vec<notification::Notification>> {
let user = check_user(ctx).await?;
match readed {
Some(true) => Ok(vec![
notification::Notification {
id: "1".to_string().into(),
content: "Hello".into(),
read: true,
created_at: Utc::now(),
updated_at: Utc::now(),
},
notification::Notification {
id: "3".to_string().into(),
content: "Tabby".into(),
read: true,
created_at: Utc::now(),
updated_at: Utc::now(),
},
]),
Some(false) => Ok(vec![
notification::Notification {
id: "2".to_string().into(),
content: "World".into(),
read: false,
created_at: Utc::now(),
updated_at: Utc::now(),
},
notification::Notification {
id: "4".to_string().into(),
content: "Assistant".into(),
read: false,
created_at: Utc::now(),
updated_at: Utc::now(),
},
]),
None => Ok(vec![
notification::Notification {
id: "5".to_string().into(),
content: "World".into(),
read: false,
created_at: Utc::now(),
updated_at: Utc::now(),
},
notification::Notification {
id: "6".to_string().into(),
content: "Assistant".into(),
read: true,
created_at: Utc::now(),
updated_at: Utc::now(),
},
]),
}
ctx.locator.notification().list(&user.id).await
}

async fn disk_usage_stats(ctx: &Context) -> Result<DiskUsageStats> {
Expand Down Expand Up @@ -1046,7 +996,13 @@ impl Mutation {
Ok(true)
}

async fn mark_notifications_readed(ctx: &Context, notification_ids: Vec<ID>) -> Result<bool> {
async fn mark_notifications_read(ctx: &Context, notification_id: Option<ID>) -> Result<bool> {
let user = check_user(ctx).await?;

ctx.locator
.notification()
.mark_read(&user.id, notification_id)
.await?;
Ok(true)
}

Expand Down
10 changes: 10 additions & 0 deletions ee/tabby-schema/src/schema/notification.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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,
Expand All @@ -15,3 +18,10 @@ pub struct Notification {
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<()>;
}
9 changes: 9 additions & 0 deletions ee/tabby-webserver/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod event_logger;
pub mod integration;
pub mod job;
mod license;
mod notification;
mod preset_web_documents_data;
pub mod repository;
mod setting;
Expand Down Expand Up @@ -51,6 +52,7 @@ use tabby_schema::{
is_demo_mode,
job::JobService,
license::{IsLicenseValid, LicenseService},
notification::NotificationService,
policy,
repository::RepositoryService,
setting::SettingService,
Expand All @@ -72,6 +74,7 @@ struct ServerContext {
chat: Option<Arc<dyn ChatCompletionStream>>,
completion: Option<Arc<dyn CompletionStream>>,
auth: Arc<dyn AuthenticationService>,
notification: Arc<dyn NotificationService>,
license: Arc<dyn LicenseService>,
repository: Arc<dyn RepositoryService>,
integration: Arc<dyn IntegrationService>,
Expand Down Expand Up @@ -118,6 +121,7 @@ impl ServerContext {
));
let user_group = Arc::new(user_group::create(db_conn.clone()));
let access_policy = Arc::new(access_policy::create(db_conn.clone(), context.clone()));
let notification = Arc::new(notification::create(db_conn.clone()));

background_job::start(
db_conn.clone(),
Expand Down Expand Up @@ -150,6 +154,7 @@ impl ServerContext {
setting,
user_group,
access_policy,
notification,
db_conn,
user_rate_limiter: UserRateLimiter::default(),
}
Expand Down Expand Up @@ -290,6 +295,10 @@ impl ServiceLocator for ArcServerContext {
self.0.logger.clone()
}

fn notification(&self) -> Arc<dyn tabby_schema::notification::NotificationService> {
self.0.notification.clone()
}

fn job(&self) -> Arc<dyn JobService> {
self.0.job.clone()
}
Expand Down
Loading

0 comments on commit 83696ed

Please sign in to comment.