Skip to content

Commit

Permalink
Merge pull request #604 from GiganticMinecraft/feat/notifications
Browse files Browse the repository at this point in the history
通知機能の実装
  • Loading branch information
rito528 authored Nov 23, 2024
2 parents 8920757 + 2f5332e commit f41044f
Show file tree
Hide file tree
Showing 29 changed files with 862 additions and 92 deletions.
2 changes: 2 additions & 0 deletions server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/domain/src/form/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ pub struct Label {

pub type MessageId = types::Id<Message>;

#[derive(Getters, Debug)]
#[derive(Getters, PartialEq, Debug)]
pub struct Message {
id: MessageId,
related_answer: FormAnswer,
Expand Down
1 change: 1 addition & 0 deletions server/domain/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod form;
pub mod notification;
pub mod repository;
pub mod search;
pub mod types;
Expand Down
1 change: 1 addition & 0 deletions server/domain/src/notification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod models;
119 changes: 119 additions & 0 deletions server/domain/src/notification/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use derive_getters::Getters;
use serde::Deserialize;

use crate::{
form::models::MessageId,
types::authorization_guard::{AuthorizationGuard, AuthorizationGuardDefinitions, Create},
user::models::User,
};

#[derive(Deserialize, Debug)]
pub enum NotificationSource {
Message(MessageId),
}

pub type NotificationId = types::Id<Notification>;

#[derive(Deserialize, Getters, Debug)]
pub struct Notification {
id: NotificationId,
source: NotificationSource,
recipient: User,
is_read: bool,
}

impl Notification {
/// [`Notification`] を新しく作成します。
///
/// # Examples
/// ```
/// use domain::{
/// form::models::MessageId,
/// notification::models::{Notification, NotificationSource},
/// user::models::User,
/// };
///
/// let source = NotificationSource::Message(MessageId::new());
/// let recipient = User {
/// id: Default::default(),
/// name: "Alice".to_string(),
/// role: Default::default(),
/// };
/// let notification = Notification::new(source, recipient);
///
/// assert!(!notification.is_read());
/// ```
pub fn new(source: NotificationSource, recipient: User) -> Self {
Self {
id: NotificationId::new(),
source,
recipient,
is_read: false,
}
}

/// [`Notification`] の各フィールドを指定して新しく作成します。
///
/// # Examples
/// ```
/// use domain::{
/// form::models::MessageId,
/// notification::models::{Notification, NotificationId, NotificationSource},
/// user::models::User,
/// };
/// use uuid::Uuid;
///
/// let id = NotificationId::new();
///
/// let source = NotificationSource::Message(MessageId::new());
/// let recipient = User {
/// id: Uuid::new_v4(),
/// name: "Alice".to_string(),
/// role: Default::default(),
/// };
///
/// let notification = unsafe { Notification::from_raw_parts(id, source, recipient, false) };
/// ```
///
/// # Safety
/// この関数は [`Notification`] のバリデーションをスキップするため、
/// データベースからすでにバリデーションされているデータを読み出すときなど、
/// データの信頼性が保証されている場合にのみ使用してください。
pub unsafe fn from_raw_parts(
id: NotificationId,
source: NotificationSource,
recipient: User,
is_read: bool,
) -> Self {
Self {
id,
source,
recipient,
is_read,
}
}
}

impl AuthorizationGuardDefinitions<Notification> for Notification {
fn can_create(&self, actor: &User) -> bool {
self.recipient().id == actor.id
}

fn can_read(&self, actor: &User) -> bool {
self.recipient().id == actor.id
}

fn can_update(&self, actor: &User) -> bool {
self.recipient().id == actor.id
}

fn can_delete(&self, actor: &User) -> bool {
self.recipient().id == actor.id
}
}

impl From<Notification> for AuthorizationGuard<Notification, Create> {
fn from(value: Notification) -> Self {
AuthorizationGuard::new(value)
}
}
3 changes: 3 additions & 0 deletions server/domain/src/repository.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
pub mod form_repository;
pub mod notification_repository;
pub mod search_repository;
pub mod user_repository;

pub trait Repositories: Send + Sync {
type ConcreteFormRepository: form_repository::FormRepository;
type ConcreteUserRepository: user_repository::UserRepository;
type ConcreteSearchRepository: search_repository::SearchRepository;
type ConcreteNotificationRepository: notification_repository::NotificationRepository;

fn form_repository(&self) -> &Self::ConcreteFormRepository;
fn user_repository(&self) -> &Self::ConcreteUserRepository;
fn search_repository(&self) -> &Self::ConcreteSearchRepository;
fn notification_repository(&self) -> &Self::ConcreteNotificationRepository;
}
27 changes: 27 additions & 0 deletions server/domain/src/repository/notification_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use async_trait::async_trait;
use errors::Error;
use uuid::Uuid;

use crate::{
notification::models::{Notification, NotificationId},
types::authorization_guard::{AuthorizationGuard, Read, Update},
user::models::User,
};

#[async_trait]
pub trait NotificationRepository: Send + Sync + 'static {
async fn create(&self, notification: &Notification) -> Result<(), Error>;
async fn fetch_by_recipient_id(
&self,
recipient_id: Uuid,
) -> Result<Vec<AuthorizationGuard<Notification, Read>>, Error>;
async fn fetch_by_notification_ids(
&self,
notification_ids: Vec<NotificationId>,
) -> Result<Vec<AuthorizationGuard<Notification, Read>>, Error>;
async fn update_read_status(
&self,
actor: &User,
notifications: Vec<(AuthorizationGuard<Notification, Update>, bool)>,
) -> Result<(), Error>;
}
6 changes: 6 additions & 0 deletions server/entrypoint/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use presentation::{
update_form_handler, update_message_handler,
},
health_check_handler::health_check,
notification_handler::{fetch_by_request_user, update_read_state},
search_handler::cross_search,
user_handler::{end_session, get_my_user_info, patch_user_role, start_session, user_list},
};
Expand Down Expand Up @@ -153,6 +154,11 @@ async fn main() -> anyhow::Result<()> {
delete(delete_message_handler).patch(update_message_handler),
)
.with_state(shared_repository.to_owned())
.route(
"/notifications",
get(fetch_by_request_user).patch(update_read_state),
)
.with_state(shared_repository.to_owned())
.route("/health", get(health_check))
.route("/session", post(start_session).delete(end_session))
.with_state(shared_repository.to_owned())
Expand Down
8 changes: 5 additions & 3 deletions server/errors/src/usecase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ pub enum UseCaseError {
OutOfPeriod,
#[error("Do not have permission to post form comment.")]
DoNotHavePermissionToPostFormComment,
#[error("Answer Not found.")]
#[error("Answer not found.")]
AnswerNotFound,
#[error("Form Not found.")]
#[error("Form not found.")]
FormNotFound,
#[error("Message Not found.")]
#[error("Message not found.")]
MessageNotFound,
#[error("Notification not found.")]
NotificationNotFound,
}
2 changes: 2 additions & 0 deletions server/infra/resource/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ common = { path = "../../common" }
reqwest = { workspace = true }
serde_json = { workspace = true }
meilisearch-sdk = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
1 change: 1 addition & 0 deletions server/infra/resource/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ pub mod components;
pub mod config;
pub mod connection;
pub mod form;
pub mod notification;
pub mod search;
pub mod user;
23 changes: 22 additions & 1 deletion server/infra/resource/src/database/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use domain::{
FormDescription, FormId, FormTitle, Label, LabelId, Message, MessageId, OffsetAndLimit,
Question, ResponsePeriod, Visibility, WebhookUrl,
},
notification::models::{Notification, NotificationId},
user::models::{Role, User},
};
use errors::infra::InfraError;
Expand All @@ -13,20 +14,22 @@ use uuid::Uuid;

use crate::dto::{
AnswerLabelDto, CommentDto, FormAnswerContentDto, FormAnswerDto, FormDto, LabelDto, MessageDto,
QuestionDto, SimpleFormDto,
NotificationDto, QuestionDto, SimpleFormDto,
};

#[async_trait]
pub trait DatabaseComponents: Send + Sync {
type ConcreteFormDatabase: FormDatabase;
type ConcreteUserDatabase: UserDatabase;
type ConcreteNotificationDatabase: NotificationDatabase;
type ConcreteSearchDatabase: SearchDatabase;
type TransactionAcrossComponents: Send + Sync;

async fn begin_transaction(&self) -> anyhow::Result<Self::TransactionAcrossComponents>;
fn form(&self) -> &Self::ConcreteFormDatabase;
fn user(&self) -> &Self::ConcreteUserDatabase;
fn search(&self) -> &Self::ConcreteSearchDatabase;
fn notification(&self) -> &Self::ConcreteNotificationDatabase;
}

#[automock]
Expand Down Expand Up @@ -188,3 +191,21 @@ pub trait SearchDatabase: Send + Sync {
query: &str,
) -> Result<Vec<domain::search::models::Comment>, InfraError>;
}

#[automock]
#[async_trait]
pub trait NotificationDatabase: Send + Sync {
async fn create(&self, notification: &Notification) -> Result<(), InfraError>;
async fn fetch_by_recipient(
&self,
recipient_id: Uuid,
) -> Result<Vec<NotificationDto>, InfraError>;
async fn fetch_by_notification_ids(
&self,
notification_ids: Vec<NotificationId>,
) -> Result<Vec<NotificationDto>, InfraError>;
async fn update_read_status(
&self,
notification_id_with_is_read: Vec<(NotificationId, bool)>,
) -> Result<(), InfraError>;
}
5 changes: 5 additions & 0 deletions server/infra/resource/src/database/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl ConnectionPool {
#[async_trait]
impl DatabaseComponents for ConnectionPool {
type ConcreteFormDatabase = Self;
type ConcreteNotificationDatabase = Self;
type ConcreteSearchDatabase = Self;
type ConcreteUserDatabase = Self;
type TransactionAcrossComponents = DatabaseTransaction;
Expand All @@ -109,6 +110,10 @@ impl DatabaseComponents for ConnectionPool {
fn search(&self) -> &Self::ConcreteSearchDatabase {
self
}

fn notification(&self) -> &Self::ConcreteNotificationDatabase {
self
}
}

pub async fn query_all(
Expand Down
Loading

0 comments on commit f41044f

Please sign in to comment.