From c5a9397beb95b58d2f93b2bb69dc322c78a954a1 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:24:27 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E3=82=92?= =?UTF-8?q?=E9=80=81=E4=BF=A1=E3=81=99=E3=82=8B=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/domain/src/lib.rs | 1 + server/domain/src/notification.rs | 1 + server/domain/src/notification/models.rs | 30 ++++ server/domain/src/repository.rs | 3 + .../src/repository/notification_repository.rs | 9 + server/infra/resource/src/database.rs | 1 + .../infra/resource/src/database/components.rs | 9 + .../infra/resource/src/database/connection.rs | 5 + .../resource/src/database/notification.rs | 37 +++++ server/infra/resource/src/repository.rs | 6 + .../notification_repository_impl.rs | 21 +++ .../src/m20220101_000001_create_table.rs | 14 ++ server/presentation/src/form_handler.rs | 90 ++++++---- server/usecase/src/form.rs | 156 +++++++++++------- 14 files changed, 297 insertions(+), 86 deletions(-) create mode 100644 server/domain/src/notification.rs create mode 100644 server/domain/src/notification/models.rs create mode 100644 server/domain/src/repository/notification_repository.rs create mode 100644 server/infra/resource/src/database/notification.rs create mode 100644 server/infra/resource/src/repository/notification_repository_impl.rs diff --git a/server/domain/src/lib.rs b/server/domain/src/lib.rs index 84a80aad..38c28afd 100644 --- a/server/domain/src/lib.rs +++ b/server/domain/src/lib.rs @@ -1,4 +1,5 @@ pub mod form; +pub mod notification; pub mod repository; pub mod search; pub mod types; diff --git a/server/domain/src/notification.rs b/server/domain/src/notification.rs new file mode 100644 index 00000000..e072fd8b --- /dev/null +++ b/server/domain/src/notification.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/server/domain/src/notification/models.rs b/server/domain/src/notification/models.rs new file mode 100644 index 00000000..91f30677 --- /dev/null +++ b/server/domain/src/notification/models.rs @@ -0,0 +1,30 @@ +use derive_getters::Getters; +use serde::Deserialize; + +use crate::{form::models::MessageId, user::models::User}; + +#[derive(Deserialize, Debug)] +pub enum NotificationSource { + Message { message_id: MessageId }, +} + +pub type NotificationId = types::Id; + +#[derive(Deserialize, Getters, Debug)] +pub struct Notification { + id: NotificationId, + source: NotificationSource, + recipient: User, + is_read: bool, +} + +impl Notification { + pub fn new(source: NotificationSource, recipient: User) -> Self { + Self { + id: NotificationId::new(), + source, + recipient, + is_read: false, + } + } +} diff --git a/server/domain/src/repository.rs b/server/domain/src/repository.rs index 5bf910e0..20ecc9b7 100644 --- a/server/domain/src/repository.rs +++ b/server/domain/src/repository.rs @@ -1,4 +1,5 @@ pub mod form_repository; +pub mod notification_repository; pub mod search_repository; pub mod user_repository; @@ -6,8 +7,10 @@ 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; } diff --git a/server/domain/src/repository/notification_repository.rs b/server/domain/src/repository/notification_repository.rs new file mode 100644 index 00000000..6f6b0ad7 --- /dev/null +++ b/server/domain/src/repository/notification_repository.rs @@ -0,0 +1,9 @@ +use async_trait::async_trait; +use errors::Error; + +use crate::notification::models::Notification; + +#[async_trait] +pub trait NotificationRepository: Send + Sync + 'static { + async fn create(&self, notification: &Notification) -> Result<(), Error>; +} diff --git a/server/infra/resource/src/database.rs b/server/infra/resource/src/database.rs index 98e63bd1..4a28d0ae 100644 --- a/server/infra/resource/src/database.rs +++ b/server/infra/resource/src/database.rs @@ -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; diff --git a/server/infra/resource/src/database/components.rs b/server/infra/resource/src/database/components.rs index 03190008..9e5cce2f 100644 --- a/server/infra/resource/src/database/components.rs +++ b/server/infra/resource/src/database/components.rs @@ -5,6 +5,7 @@ use domain::{ FormDescription, FormId, FormTitle, Label, LabelId, Message, MessageId, OffsetAndLimit, Question, ResponsePeriod, Visibility, WebhookUrl, }, + notification::models::Notification, user::models::{Role, User}, }; use errors::infra::InfraError; @@ -20,6 +21,7 @@ use crate::dto::{ pub trait DatabaseComponents: Send + Sync { type ConcreteFormDatabase: FormDatabase; type ConcreteUserDatabase: UserDatabase; + type ConcreteNotificationDatabase: NotificationDatabase; type ConcreteSearchDatabase: SearchDatabase; type TransactionAcrossComponents: Send + Sync; @@ -27,6 +29,7 @@ pub trait DatabaseComponents: Send + Sync { fn form(&self) -> &Self::ConcreteFormDatabase; fn user(&self) -> &Self::ConcreteUserDatabase; fn search(&self) -> &Self::ConcreteSearchDatabase; + fn notification(&self) -> &Self::ConcreteNotificationDatabase; } #[automock] @@ -188,3 +191,9 @@ pub trait SearchDatabase: Send + Sync { query: &str, ) -> Result, InfraError>; } + +#[automock] +#[async_trait] +pub trait NotificationDatabase: Send + Sync { + async fn create(&self, notification: &Notification) -> Result<(), InfraError>; +} diff --git a/server/infra/resource/src/database/connection.rs b/server/infra/resource/src/database/connection.rs index 0e6e125c..27ef0bd2 100644 --- a/server/infra/resource/src/database/connection.rs +++ b/server/infra/resource/src/database/connection.rs @@ -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; @@ -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( diff --git a/server/infra/resource/src/database/notification.rs b/server/infra/resource/src/database/notification.rs new file mode 100644 index 00000000..65c1ad7d --- /dev/null +++ b/server/infra/resource/src/database/notification.rs @@ -0,0 +1,37 @@ +use async_trait::async_trait; +use domain::notification::models::{Notification, NotificationSource}; +use errors::infra::InfraError; + +use crate::database::{ + components::NotificationDatabase, + connection::{execute_and_values, ConnectionPool}, +}; + +#[async_trait] +impl NotificationDatabase for ConnectionPool { + async fn create(&self, notification: &Notification) -> Result<(), InfraError> { + let notification_source_with_id = match notification.source() { + NotificationSource::Message { message_id } => { + ("MESSAGE".to_owned(), message_id.to_string()) + } + }; + + let params = [ + notification.id().to_string().into(), + notification_source_with_id.0.into(), + notification_source_with_id.1.into(), + notification.recipient().id.to_string().into(), + notification.is_read().to_owned().into(), + ]; + + self.read_write_transaction(|txn| Box::pin(async move { + execute_and_values( + r"INSERT INTO notifications (id, source_type, source_id, recipient_id, is_read) VALUES (?, ?, ?, ?, ?)", + params, + txn + ).await?; + + Ok::<_, InfraError>(()) + })).await.map_err(Into::into) + } +} diff --git a/server/infra/resource/src/repository.rs b/server/infra/resource/src/repository.rs index 7d3996b0..4e3cecb0 100644 --- a/server/infra/resource/src/repository.rs +++ b/server/infra/resource/src/repository.rs @@ -1,4 +1,5 @@ pub mod form_repository_impl; +pub mod notification_repository_impl; pub mod search_repository_impl; pub mod user_repository_impl; @@ -29,6 +30,7 @@ impl Repository { impl Repositories for SharedRepository { type ConcreteFormRepository = Repository; + type ConcreteNotificationRepository = Repository; type ConcreteSearchRepository = Repository; type ConcreteUserRepository = Repository; @@ -36,6 +38,10 @@ impl Repositories for SharedRepository &Self::ConcreteNotificationRepository { + &self.0 + } + fn user_repository(&self) -> &Self::ConcreteUserRepository { &self.0 } diff --git a/server/infra/resource/src/repository/notification_repository_impl.rs b/server/infra/resource/src/repository/notification_repository_impl.rs new file mode 100644 index 00000000..1e5f417c --- /dev/null +++ b/server/infra/resource/src/repository/notification_repository_impl.rs @@ -0,0 +1,21 @@ +use async_trait::async_trait; +use domain::{ + notification::models::Notification, repository::notification_repository::NotificationRepository, +}; +use errors::Error; + +use crate::{ + database::components::{DatabaseComponents, NotificationDatabase}, + repository::Repository, +}; + +#[async_trait] +impl NotificationRepository for Repository { + async fn create(&self, notification: &Notification) -> Result<(), Error> { + self.client + .notification() + .create(notification) + .await + .map_err(Into::into) + } +} diff --git a/server/migration/src/m20220101_000001_create_table.rs b/server/migration/src/m20220101_000001_create_table.rs index 3c5c926e..218e7bd8 100644 --- a/server/migration/src/m20220101_000001_create_table.rs +++ b/server/migration/src/m20220101_000001_create_table.rs @@ -209,6 +209,20 @@ impl MigrationTrait for Migration { )) .await?; + connection + .execute(Statement::from_string( + DatabaseBackend::MySql, + r"CREATE TABLE IF NOT EXISTS notifications( + id UUID NOT NULL PRIMARY KEY, + source_type ENUM('MESSAGE') NOT NULL, + recipient CHAR(36) NOT NULL, + related_id UUID NOT NULL, + is_read BOOL DEFAULT FALSE NOT NULL, + FOREIGN KEY fk_notification_recipient(recipient) REFERENCES users(id) + )", + )) + .await?; + Ok(()) } diff --git a/server/presentation/src/form_handler.rs b/server/presentation/src/form_handler.rs index 250be5ab..82d07ea6 100644 --- a/server/presentation/src/form_handler.rs +++ b/server/presentation/src/form_handler.rs @@ -36,7 +36,8 @@ pub async fn create_form_handler( Json(form): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -61,7 +62,8 @@ pub async fn public_form_list_handler( Query(offset_and_limit): Query, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.public_form_list(offset_and_limit).await { @@ -75,7 +77,8 @@ pub async fn form_list_handler( Query(offset_and_limit): Query, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.form_list(offset_and_limit).await { @@ -89,7 +92,8 @@ pub async fn get_form_handler( Path(form_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.get_form(form_id).await { @@ -103,7 +107,8 @@ pub async fn delete_form_handler( Path(form_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.delete_form(form_id).await { @@ -118,7 +123,8 @@ pub async fn update_form_handler( Json(targets): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -145,7 +151,8 @@ pub async fn get_questions_handler( Path(form_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.get_questions(form_id).await { @@ -158,7 +165,8 @@ pub async fn get_all_answers( State(repository): State, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.get_all_answers().await { @@ -187,7 +195,8 @@ pub async fn get_answer_handler( Path(answer_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; let answer_dto = match form_use_case.get_answers(answer_id).await { @@ -231,7 +240,8 @@ pub async fn get_answer_by_form_id_handler( Path(form_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; if user.role == StandardUser { @@ -276,7 +286,8 @@ pub async fn post_answer_handler( Json(schema): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -294,7 +305,8 @@ pub async fn update_answer_handler( Json(schema): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -311,7 +323,8 @@ pub async fn create_question_handler( Json(questions): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -328,7 +341,8 @@ pub async fn put_question_handler( Json(questions): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -346,7 +360,8 @@ pub async fn post_form_comment( Json(comment_schema): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; let comment = Comment { @@ -372,7 +387,8 @@ pub async fn delete_form_comment_handler( Path(comment_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.delete_comment(comment_id).await { @@ -386,7 +402,8 @@ pub async fn create_label_for_answers( Json(label): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.create_label_for_answers(label.name).await { @@ -399,7 +416,8 @@ pub async fn get_labels_for_answers( State(repository): State, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.get_labels_for_answers().await { @@ -413,7 +431,8 @@ pub async fn delete_label_for_answers( Path(label_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.delete_label_for_answers(label_id).await { @@ -428,7 +447,8 @@ pub async fn edit_label_for_answers( Json(label): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -449,7 +469,8 @@ pub async fn replace_answer_labels( Json(label_ids): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -466,7 +487,8 @@ pub async fn create_label_for_forms( Json(label): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.create_label_for_forms(label.name).await { @@ -479,7 +501,8 @@ pub async fn get_labels_for_forms( State(repository): State, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.get_labels_for_forms().await { @@ -493,7 +516,8 @@ pub async fn delete_label_for_forms( Path(label_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.delete_label_for_forms(label_id).await { @@ -508,7 +532,8 @@ pub async fn edit_label_for_forms( Json(label): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -529,7 +554,8 @@ pub async fn replace_form_labels( Json(label_ids): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -548,7 +574,8 @@ pub async fn post_message_handler( Json(message): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -574,7 +601,8 @@ pub async fn update_message_handler( Json(body_schema): Json, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case @@ -592,7 +620,8 @@ pub async fn get_messages_handler( Path(answer_id): Path, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case.get_messages(answer_id).await { @@ -644,7 +673,8 @@ pub async fn delete_message_handler( Path((answer_id, message_id)): Path<(AnswerId, MessageId)>, ) -> impl IntoResponse { let form_use_case = FormUseCase { - repository: repository.form_repository(), + form_repository: repository.form_repository(), + notification_repository: repository.notification_repository(), }; match form_use_case diff --git a/server/usecase/src/form.rs b/server/usecase/src/form.rs index 1f076036..ada1e98a 100644 --- a/server/usecase/src/form.rs +++ b/server/usecase/src/form.rs @@ -5,7 +5,10 @@ use domain::{ FormId, FormTitle, Label, LabelId, Message, MessageId, OffsetAndLimit, Question, ResponsePeriod, SimpleForm, Visibility, Visibility::PUBLIC, WebhookUrl, }, - repository::form_repository::FormRepository, + notification::models::{Notification, NotificationSource}, + repository::{ + form_repository::FormRepository, notification_repository::NotificationRepository, + }, types::authorization_guard::{AuthorizationGuard, Read}, user::models::{ Role::{Administrator, StandardUser}, @@ -27,47 +30,48 @@ use types::Resolver; use crate::dto::AnswerDto; -pub struct FormUseCase<'a, FormRepo: FormRepository> { - pub repository: &'a FormRepo, +pub struct FormUseCase<'a, FormRepo: FormRepository, NotificationRepo: NotificationRepository> { + pub form_repository: &'a FormRepo, + pub notification_repository: &'a NotificationRepo, } -impl FormUseCase<'_, R> { +impl FormUseCase<'_, R1, R2> { pub async fn create_form( &self, title: FormTitle, description: FormDescription, user: User, ) -> Result { - self.repository.create(title, description, user).await + self.form_repository.create(title, description, user).await } pub async fn public_form_list( &self, offset_and_limit: OffsetAndLimit, ) -> Result, Error> { - self.repository.public_list(offset_and_limit).await + self.form_repository.public_list(offset_and_limit).await } pub async fn form_list( &self, offset_and_limit: OffsetAndLimit, ) -> Result, Error> { - self.repository.list(offset_and_limit).await + self.form_repository.list(offset_and_limit).await } pub async fn get_form(&self, form_id: FormId) -> Result { - self.repository + self.form_repository .get(form_id) .await? .ok_or(Error::from(FormNotFound)) } pub async fn delete_form(&self, form_id: FormId) -> Result<(), Error> { - self.repository.delete(form_id).await + self.form_repository.delete(form_id).await } pub async fn get_questions(&self, form_id: FormId) -> Result, Error> { - self.repository.get_questions(form_id).await + self.form_repository.get_questions(form_id).await } #[allow(clippy::too_many_arguments)] @@ -84,15 +88,18 @@ impl FormUseCase<'_, R> { answer_visibility: Option<&Visibility>, ) -> Result<(), Error> { let update_title: OptionFuture<_> = title - .map(|title| self.repository.update_title(form_id, title)) + .map(|title| self.form_repository.update_title(form_id, title)) .into(); let update_description: OptionFuture<_> = description - .map(|description| self.repository.update_description(form_id, description)) + .map(|description| { + self.form_repository + .update_description(form_id, description) + }) .into(); let update_response_period: OptionFuture<_> = if has_response_period.unwrap_or(false) { response_period .map(|response_period| { - self.repository + self.form_repository .update_response_period(form_id, response_period) }) .into() @@ -100,20 +107,20 @@ impl FormUseCase<'_, R> { None.into() }; let update_webhook: OptionFuture<_> = webhook - .map(|webhook| self.repository.update_webhook_url(form_id, webhook)) + .map(|webhook| self.form_repository.update_webhook_url(form_id, webhook)) .into(); let update_default_answer_title: OptionFuture<_> = default_answer_title .map(|default_answer_title| { - self.repository + self.form_repository .update_default_answer_title(form_id, default_answer_title) }) .into(); let update_visibility: OptionFuture<_> = visibility - .map(|visibility| self.repository.update_visibility(form_id, visibility)) + .map(|visibility| self.form_repository.update_visibility(form_id, visibility)) .into(); let update_answer_visibility: OptionFuture<_> = answer_visibility .map(|visibility| { - self.repository + self.form_repository .update_answer_visibility(form_id, visibility) }) .into(); @@ -140,7 +147,7 @@ impl FormUseCase<'_, R> { answers: Vec, ) -> Result<(), Error> { let is_within_period = form_id - .resolve(self.repository) + .resolve(self.form_repository) .await? .and_then(|form| { let response_period = form.settings.response_period; @@ -157,7 +164,7 @@ impl FormUseCase<'_, R> { .unwrap_or(true); if is_within_period { - self.repository + self.form_repository .post_answer(user, form_id, title, answers) .await } else { @@ -166,12 +173,12 @@ impl FormUseCase<'_, R> { } pub async fn get_answers(&self, answer_id: AnswerId) -> Result { - if let Some(form_answer) = self.repository.get_answers(answer_id).await? { - let fetch_contents = self.repository.get_answer_contents(answer_id); + if let Some(form_answer) = self.form_repository.get_answers(answer_id).await? { + let fetch_contents = self.form_repository.get_answer_contents(answer_id); let fetch_labels = self - .repository + .form_repository .get_labels_for_answers_by_answer_id(answer_id); - let fetch_comments = self.repository.get_comments(answer_id); + let fetch_comments = self.form_repository.get_comments(answer_id); let (contents, labels, comments) = try_join!(fetch_contents, fetch_labels, fetch_comments)?; @@ -188,13 +195,13 @@ impl FormUseCase<'_, R> { } pub async fn get_answers_by_form_id(&self, form_id: FormId) -> Result, Error> { - stream::iter(self.repository.get_answers_by_form_id(form_id).await?) + stream::iter(self.form_repository.get_answers_by_form_id(form_id).await?) .then(|form_answer| async { - let fetch_contents = self.repository.get_answer_contents(form_answer.id); + let fetch_contents = self.form_repository.get_answer_contents(form_answer.id); let fetch_labels = self - .repository + .form_repository .get_labels_for_answers_by_answer_id(form_answer.id); - let fetch_comments = self.repository.get_comments(form_answer.id); + let fetch_comments = self.form_repository.get_comments(form_answer.id); let (contents, labels, comments) = try_join!(fetch_contents, fetch_labels, fetch_comments)?; @@ -213,13 +220,13 @@ impl FormUseCase<'_, R> { } pub async fn get_all_answers(&self) -> Result, Error> { - stream::iter(self.repository.get_all_answers().await?) + stream::iter(self.form_repository.get_all_answers().await?) .then(|form_answer| async { - let fetch_contents = self.repository.get_answer_contents(form_answer.id); + let fetch_contents = self.form_repository.get_answer_contents(form_answer.id); let fetch_labels = self - .repository + .form_repository .get_labels_for_answers_by_answer_id(form_answer.id); - let fetch_comments = self.repository.get_comments(form_answer.id); + let fetch_comments = self.form_repository.get_comments(form_answer.id); let (contents, labels, comments) = try_join!(fetch_contents, fetch_labels, fetch_comments)?; @@ -242,7 +249,9 @@ impl FormUseCase<'_, R> { answer_id: AnswerId, title: Option, ) -> Result<(), Error> { - self.repository.update_answer_meta(answer_id, title).await + self.form_repository + .update_answer_meta(answer_id, title) + .await } pub async fn create_questions( @@ -250,7 +259,9 @@ impl FormUseCase<'_, R> { form_id: FormId, questions: Vec, ) -> Result<(), Error> { - self.repository.create_questions(form_id, questions).await + self.form_repository + .create_questions(form_id, questions) + .await } pub async fn put_questions( @@ -258,7 +269,7 @@ impl FormUseCase<'_, R> { form_id: FormId, questions: Vec, ) -> Result<(), Error> { - self.repository.put_questions(form_id, questions).await + self.form_repository.put_questions(form_id, questions).await } pub async fn post_comment(&self, comment: Comment, answer_id: AnswerId) -> Result<(), Error> { @@ -266,13 +277,13 @@ impl FormUseCase<'_, R> { Administrator => true, StandardUser => { let answer = answer_id - .resolve(self.repository) + .resolve(self.form_repository) .await? .ok_or(AnswerNotFound)?; let form = answer .form_id - .resolve(self.repository) + .resolve(self.form_repository) .await? .ok_or(FormNotFound)?; @@ -281,30 +292,36 @@ impl FormUseCase<'_, R> { }; if can_post_comment { - self.repository.post_comment(answer_id, &comment).await + self.form_repository.post_comment(answer_id, &comment).await } else { Err(Error::from(DoNotHavePermissionToPostFormComment)) } } pub async fn delete_comment(&self, comment_id: CommentId) -> Result<(), Error> { - self.repository.delete_comment(comment_id).await + self.form_repository.delete_comment(comment_id).await } pub async fn create_label_for_answers(&self, label_name: String) -> Result<(), Error> { - self.repository.create_label_for_answers(label_name).await + self.form_repository + .create_label_for_answers(label_name) + .await } pub async fn get_labels_for_answers(&self) -> Result, Error> { - self.repository.get_labels_for_answers().await + self.form_repository.get_labels_for_answers().await } pub async fn delete_label_for_answers(&self, label_id: LabelId) -> Result<(), Error> { - self.repository.delete_label_for_answers(label_id).await + self.form_repository + .delete_label_for_answers(label_id) + .await } pub async fn edit_label_for_answers(&self, label_schema: &Label) -> Result<(), Error> { - self.repository.edit_label_for_answers(label_schema).await + self.form_repository + .edit_label_for_answers(label_schema) + .await } pub async fn replace_answer_labels( @@ -312,25 +329,27 @@ impl FormUseCase<'_, R> { answer_id: AnswerId, label_ids: Vec, ) -> Result<(), Error> { - self.repository + self.form_repository .replace_answer_labels(answer_id, label_ids) .await } pub async fn create_label_for_forms(&self, label_name: String) -> Result<(), Error> { - self.repository.create_label_for_forms(label_name).await + self.form_repository + .create_label_for_forms(label_name) + .await } pub async fn get_labels_for_forms(&self) -> Result, Error> { - self.repository.get_labels_for_forms().await + self.form_repository.get_labels_for_forms().await } pub async fn delete_label_for_forms(&self, label_id: LabelId) -> Result<(), Error> { - self.repository.delete_label_for_forms(label_id).await + self.form_repository.delete_label_for_forms(label_id).await } pub async fn edit_label_for_forms(&self, label: &Label) -> Result<(), Error> { - self.repository.edit_label_for_forms(label).await + self.form_repository.edit_label_for_forms(label).await } pub async fn replace_form_labels( @@ -338,7 +357,7 @@ impl FormUseCase<'_, R> { form_id: FormId, label_ids: Vec, ) -> Result<(), Error> { - self.repository + self.form_repository .replace_form_labels(form_id, label_ids) .await } @@ -349,13 +368,36 @@ impl FormUseCase<'_, R> { message_body: String, answer_id: AnswerId, ) -> Result<(), Error> { - let form_answer = match self.repository.get_answers(answer_id).await? { + let form_answer = match self.form_repository.get_answers(answer_id).await? { Some(form_answer) => form_answer, None => return Err(Error::from(AnswerNotFound)), }; match Message::try_new(form_answer, actor.to_owned(), message_body) { - Ok(message) => self.repository.post_message(&actor, message.into()).await, + Ok(message) => { + let notification = Notification::new( + NotificationSource::Message { + message_id: message.id().to_owned(), + }, + message.related_answer().user.to_owned(), + ); + + let message_sender = message.sender().to_owned(); + + let post_message_result = self + .form_repository + .post_message(&actor, message.into()) + .await; + + match post_message_result { + Ok(_) if message_sender.id != notification.recipient().id => { + self.notification_repository.create(¬ification).await?; + Ok(()) + } + Err(error) => Err(error), + _ => Ok(()), + } + } Err(error) => Err(Error::from(error)), } } @@ -365,12 +407,14 @@ impl FormUseCase<'_, R> { answer_id: AnswerId, ) -> Result>, Error> { let answers = self - .repository + .form_repository .get_answers(answer_id) .await? .ok_or(AnswerNotFound)?; - self.repository.fetch_messages_by_answer(&answers).await + self.form_repository + .fetch_messages_by_answer(&answers) + .await } pub async fn update_message_body( @@ -381,7 +425,7 @@ impl FormUseCase<'_, R> { body: String, ) -> Result<(), Error> { let message = self - .repository + .form_repository .fetch_message(message_id) .await? .ok_or(MessageNotFound)?; @@ -390,7 +434,7 @@ impl FormUseCase<'_, R> { return Err(Error::from(MessageNotFound)); } - self.repository + self.form_repository .update_message_body(actor, message.into_update(), body) .await } @@ -402,7 +446,7 @@ impl FormUseCase<'_, R> { message_id: &MessageId, ) -> Result<(), Error> { let message = self - .repository + .form_repository .fetch_message(message_id) .await? .ok_or(MessageNotFound)?; @@ -411,7 +455,7 @@ impl FormUseCase<'_, R> { return Err(Error::from(MessageNotFound)); } - self.repository + self.form_repository .delete_message(actor, message.into_delete()) .await } From d774ad28df1ae46ba5a33b58c30e83a83346caff Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:11:07 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=E3=83=AA=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=82=B9=E3=83=88=E9=80=81=E4=BF=A1=E8=80=85=E3=81=AE=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E3=82=92=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Cargo.lock | 2 + server/domain/src/notification/models.rs | 16 ++++- .../src/repository/notification_repository.rs | 2 + server/entrypoint/src/main.rs | 3 + server/infra/resource/Cargo.toml | 2 + .../infra/resource/src/database/components.rs | 6 +- .../resource/src/database/notification.rs | 55 ++++++++++++++++-- server/infra/resource/src/dto.rs | 58 +++++++++++++++++++ .../notification_repository_impl.rs | 15 +++++ .../src/m20220101_000001_create_table.rs | 5 +- server/presentation/src/lib.rs | 1 + .../presentation/src/notification_handler.rs | 47 +++++++++++++++ server/presentation/src/schemas.rs | 1 + .../presentation/src/schemas/notification.rs | 1 + .../notification_response_schemas.rs | 10 ++++ server/usecase/src/form.rs | 4 +- server/usecase/src/lib.rs | 1 + server/usecase/src/notification.rs | 18 ++++++ 18 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 server/presentation/src/notification_handler.rs create mode 100644 server/presentation/src/schemas/notification.rs create mode 100644 server/presentation/src/schemas/notification/notification_response_schemas.rs create mode 100644 server/usecase/src/notification.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index 2ca7f834..ce1d903f 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2609,6 +2609,8 @@ dependencies = [ "serde", "serde_json", "sha256", + "strum", + "strum_macros", "tracing", "types", "uuid", diff --git a/server/domain/src/notification/models.rs b/server/domain/src/notification/models.rs index 91f30677..f8b64182 100644 --- a/server/domain/src/notification/models.rs +++ b/server/domain/src/notification/models.rs @@ -5,7 +5,7 @@ use crate::{form::models::MessageId, user::models::User}; #[derive(Deserialize, Debug)] pub enum NotificationSource { - Message { message_id: MessageId }, + Message(MessageId), } pub type NotificationId = types::Id; @@ -27,4 +27,18 @@ impl Notification { is_read: false, } } + + pub fn from_raw_parts( + id: NotificationId, + source: NotificationSource, + recipient: User, + is_read: bool, + ) -> Self { + Self { + id, + source, + recipient, + is_read, + } + } } diff --git a/server/domain/src/repository/notification_repository.rs b/server/domain/src/repository/notification_repository.rs index 6f6b0ad7..ff82fe75 100644 --- a/server/domain/src/repository/notification_repository.rs +++ b/server/domain/src/repository/notification_repository.rs @@ -1,9 +1,11 @@ use async_trait::async_trait; use errors::Error; +use uuid::Uuid; use crate::notification::models::Notification; #[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, Error>; } diff --git a/server/entrypoint/src/main.rs b/server/entrypoint/src/main.rs index e6019f44..6c52d80f 100644 --- a/server/entrypoint/src/main.rs +++ b/server/entrypoint/src/main.rs @@ -26,6 +26,7 @@ use presentation::{ update_form_handler, update_message_handler, }, health_check_handler::health_check, + notification_handler::fetch_by_recipient_id, search_handler::cross_search, user_handler::{end_session, get_my_user_info, patch_user_role, start_session, user_list}, }; @@ -153,6 +154,8 @@ async fn main() -> anyhow::Result<()> { delete(delete_message_handler).patch(update_message_handler), ) .with_state(shared_repository.to_owned()) + .route("/notifications", get(fetch_by_recipient_id)) + .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()) diff --git a/server/infra/resource/Cargo.toml b/server/infra/resource/Cargo.toml index 0dfbc9ca..09d7d285 100644 --- a/server/infra/resource/Cargo.toml +++ b/server/infra/resource/Cargo.toml @@ -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 } diff --git a/server/infra/resource/src/database/components.rs b/server/infra/resource/src/database/components.rs index 9e5cce2f..27991540 100644 --- a/server/infra/resource/src/database/components.rs +++ b/server/infra/resource/src/database/components.rs @@ -14,7 +14,7 @@ use uuid::Uuid; use crate::dto::{ AnswerLabelDto, CommentDto, FormAnswerContentDto, FormAnswerDto, FormDto, LabelDto, MessageDto, - QuestionDto, SimpleFormDto, + NotificationDto, QuestionDto, SimpleFormDto, }; #[async_trait] @@ -196,4 +196,8 @@ pub trait SearchDatabase: Send + Sync { #[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, InfraError>; } diff --git a/server/infra/resource/src/database/notification.rs b/server/infra/resource/src/database/notification.rs index 65c1ad7d..74732af3 100644 --- a/server/infra/resource/src/database/notification.rs +++ b/server/infra/resource/src/database/notification.rs @@ -1,17 +1,26 @@ +use std::str::FromStr; + use async_trait::async_trait; -use domain::notification::models::{Notification, NotificationSource}; +use domain::{ + notification::models::{Notification, NotificationSource}, + user::models::Role, +}; use errors::infra::InfraError; +use uuid::Uuid; -use crate::database::{ - components::NotificationDatabase, - connection::{execute_and_values, ConnectionPool}, +use crate::{ + database::{ + components::NotificationDatabase, + connection::{execute_and_values, query_all_and_values, ConnectionPool}, + }, + dto::{NotificationDto, NotificationSourceInformationDto, NotificationSourceTypeDto, UserDto}, }; #[async_trait] impl NotificationDatabase for ConnectionPool { async fn create(&self, notification: &Notification) -> Result<(), InfraError> { let notification_source_with_id = match notification.source() { - NotificationSource::Message { message_id } => { + NotificationSource::Message(message_id) => { ("MESSAGE".to_owned(), message_id.to_string()) } }; @@ -28,10 +37,44 @@ impl NotificationDatabase for ConnectionPool { execute_and_values( r"INSERT INTO notifications (id, source_type, source_id, recipient_id, is_read) VALUES (?, ?, ?, ?, ?)", params, - txn + txn, ).await?; Ok::<_, InfraError>(()) })).await.map_err(Into::into) } + + async fn fetch_by_recipient( + &self, + recipient_id: Uuid, + ) -> Result, InfraError> { + self.read_only_transaction(|txn| Box::pin(async move { + let rs = query_all_and_values( + r"SELECT notifications.id AS notification_id, source_type, source_id, is_read, recipient_id, name, role + FROM notifications + INNER JOIN users ON notifications.recipient_id = users.id + WHERE recipient_id = ?", + [recipient_id.into()], + txn, + ).await?; + + rs.into_iter() + .map(|rs| { + Ok::<_, InfraError>(NotificationDto { + id: uuid::Uuid::from_str(&rs.try_get::("", "notification_id")?)?, + source: NotificationSourceInformationDto { + source_type: NotificationSourceTypeDto::from_str(&rs.try_get::("", "source_type")?)?, + source_id: uuid::Uuid::from_str(&rs.try_get::("", "source_id")?)?, + }, + recipient: UserDto { + name: rs.try_get("", "name")?, + id: recipient_id, + role: Role::from_str(&rs.try_get::("", "role")?)?, + }, + is_read: rs.try_get("", "is_read")?, + }) + }) + .collect::, _>>() + })).await.map_err(Into::into) + } } diff --git a/server/infra/resource/src/dto.rs b/server/infra/resource/src/dto.rs index 090c0ef3..4de431ae 100644 --- a/server/infra/resource/src/dto.rs +++ b/server/infra/resource/src/dto.rs @@ -3,6 +3,7 @@ use domain::{ form::models::{FormSettings, ResponsePeriod}, user::models::{Role, User}, }; +use strum_macros::{Display, EnumString}; use uuid::Uuid; #[derive(Clone)] @@ -312,3 +313,60 @@ impl TryFrom for domain::form::models::Message { } } } + +#[derive(Debug, EnumString, Display)] +pub enum NotificationSourceTypeDto { + #[strum(serialize = "MESSAGE")] + Message, +} + +pub struct NotificationSourceInformationDto { + pub source_type: NotificationSourceTypeDto, + pub source_id: Uuid, +} + +impl TryFrom + for domain::notification::models::NotificationSource +{ + type Error = errors::domain::DomainError; + + fn try_from( + NotificationSourceInformationDto { + source_type, + source_id, + }: NotificationSourceInformationDto, + ) -> Result { + match source_type { + NotificationSourceTypeDto::Message => Ok( + domain::notification::models::NotificationSource::Message(source_id.into()), + ), + } + } +} + +pub struct NotificationDto { + pub id: Uuid, + pub source: NotificationSourceInformationDto, + pub recipient: UserDto, + pub is_read: bool, +} + +impl TryFrom for domain::notification::models::Notification { + type Error = errors::domain::DomainError; + + fn try_from( + NotificationDto { + id, + source, + recipient, + is_read, + }: NotificationDto, + ) -> Result { + Ok(domain::notification::models::Notification::from_raw_parts( + id.into(), + source.try_into()?, + recipient.try_into()?, + is_read, + )) + } +} diff --git a/server/infra/resource/src/repository/notification_repository_impl.rs b/server/infra/resource/src/repository/notification_repository_impl.rs index 1e5f417c..4dc4f58c 100644 --- a/server/infra/resource/src/repository/notification_repository_impl.rs +++ b/server/infra/resource/src/repository/notification_repository_impl.rs @@ -3,6 +3,7 @@ use domain::{ notification::models::Notification, repository::notification_repository::NotificationRepository, }; use errors::Error; +use uuid::Uuid; use crate::{ database::components::{DatabaseComponents, NotificationDatabase}, @@ -18,4 +19,18 @@ impl NotificationRepository for Repository .await .map_err(Into::into) } + + async fn fetch_by_recipient_id(&self, recipient_id: Uuid) -> Result, Error> { + self.client + .notification() + .fetch_by_recipient(recipient_id) + .await + .map(|notifications| { + notifications + .into_iter() + .map(TryInto::try_into) + .collect::, _>>() + })? + .map_err(Into::into) + } } diff --git a/server/migration/src/m20220101_000001_create_table.rs b/server/migration/src/m20220101_000001_create_table.rs index 218e7bd8..8092893d 100644 --- a/server/migration/src/m20220101_000001_create_table.rs +++ b/server/migration/src/m20220101_000001_create_table.rs @@ -215,7 +215,7 @@ impl MigrationTrait for Migration { r"CREATE TABLE IF NOT EXISTS notifications( id UUID NOT NULL PRIMARY KEY, source_type ENUM('MESSAGE') NOT NULL, - recipient CHAR(36) NOT NULL, + recipient_id CHAR(36) NOT NULL, related_id UUID NOT NULL, is_read BOOL DEFAULT FALSE NOT NULL, FOREIGN KEY fk_notification_recipient(recipient) REFERENCES users(id) @@ -245,7 +245,8 @@ impl MigrationTrait for Migration { default_answer_titles, form_answer_comments, form_answer_label_settings, - messages; + messages, + notifications; ", )) .await?; diff --git a/server/presentation/src/lib.rs b/server/presentation/src/lib.rs index 824f593b..81d8424d 100644 --- a/server/presentation/src/lib.rs +++ b/server/presentation/src/lib.rs @@ -2,6 +2,7 @@ pub mod auth; pub mod error_handler; pub mod form_handler; pub mod health_check_handler; +pub mod notification_handler; pub mod schemas; pub mod search_handler; pub mod search_schemas; diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs new file mode 100644 index 00000000..101dfe4c --- /dev/null +++ b/server/presentation/src/notification_handler.rs @@ -0,0 +1,47 @@ +use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; +use domain::{ + notification::models::NotificationSource, repository::Repositories, user::models::User, +}; +use itertools::Itertools; +use resource::repository::RealInfrastructureRepository; +use serde_json::json; +use usecase::notification::NotificationUseCase; + +use crate::schemas::notification::notification_response_schemas::NotificationResponse; + +pub async fn fetch_by_recipient_id( + Extension(user): Extension, + State(repository): State, +) -> impl IntoResponse { + let notification_usecase = NotificationUseCase { + repository: repository.notification_repository(), + }; + + match notification_usecase.fetch_notifications(user.id).await { + Ok(notifications) => { + let notification_response = notifications + .into_iter() + .map(|notification| { + let notification_source = match notification.source() { + NotificationSource::Message(message_id) => { + ("Message".to_string(), message_id.to_string()) + } + }; + + NotificationResponse { + id: notification.id().to_owned(), + source_type: notification_source.0, + source_id: notification_source.1, + is_read: notification.is_read().to_owned(), + } + }) + .collect_vec(); + + (StatusCode::OK, Json(json!(notification_response))) + } + Err(_err) => { + // TODO: 今のエラーハンドリングでは非効率すぎるので、解決してから書く + todo!() + } + } +} diff --git a/server/presentation/src/schemas.rs b/server/presentation/src/schemas.rs index bad540f3..e7d054e1 100644 --- a/server/presentation/src/schemas.rs +++ b/server/presentation/src/schemas.rs @@ -1 +1,2 @@ pub mod form; +pub mod notification; diff --git a/server/presentation/src/schemas/notification.rs b/server/presentation/src/schemas/notification.rs new file mode 100644 index 00000000..0bbd7d68 --- /dev/null +++ b/server/presentation/src/schemas/notification.rs @@ -0,0 +1 @@ +pub mod notification_response_schemas; diff --git a/server/presentation/src/schemas/notification/notification_response_schemas.rs b/server/presentation/src/schemas/notification/notification_response_schemas.rs new file mode 100644 index 00000000..9dac444f --- /dev/null +++ b/server/presentation/src/schemas/notification/notification_response_schemas.rs @@ -0,0 +1,10 @@ +use domain::notification::models::NotificationId; +use serde::Serialize; + +#[derive(Serialize, Debug)] +pub struct NotificationResponse { + pub id: NotificationId, + pub source_type: String, + pub source_id: String, + pub is_read: bool, +} diff --git a/server/usecase/src/form.rs b/server/usecase/src/form.rs index ada1e98a..888a6bb7 100644 --- a/server/usecase/src/form.rs +++ b/server/usecase/src/form.rs @@ -376,9 +376,7 @@ impl FormUseCase<'_, R1, R2> { match Message::try_new(form_answer, actor.to_owned(), message_body) { Ok(message) => { let notification = Notification::new( - NotificationSource::Message { - message_id: message.id().to_owned(), - }, + NotificationSource::Message(message.id().to_owned()), message.related_answer().user.to_owned(), ); diff --git a/server/usecase/src/lib.rs b/server/usecase/src/lib.rs index 1bdc3d45..4b00e3d9 100644 --- a/server/usecase/src/lib.rs +++ b/server/usecase/src/lib.rs @@ -1,4 +1,5 @@ pub mod dto; pub mod form; +pub mod notification; pub mod search; pub mod user; diff --git a/server/usecase/src/notification.rs b/server/usecase/src/notification.rs new file mode 100644 index 00000000..84f14539 --- /dev/null +++ b/server/usecase/src/notification.rs @@ -0,0 +1,18 @@ +use domain::{ + notification::models::Notification, repository::notification_repository::NotificationRepository, +}; +use errors::Error; +use uuid::Uuid; + +pub struct NotificationUseCase<'a, NotificationRepo: NotificationRepository> { + pub repository: &'a NotificationRepo, +} + +impl NotificationUseCase<'_, R> { + pub async fn fetch_notifications( + &self, + recipient_id: Uuid, + ) -> Result, Error> { + self.repository.fetch_by_recipient_id(recipient_id).await + } +} From 6c310a271094a74381b52443459a29319a4b4190 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 16 Nov 2024 16:59:21 +0900 Subject: [PATCH 03/14] =?UTF-8?q?chore:=20=E9=80=9A=E7=9F=A5=E3=82=92?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E3=83=8F=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=83=A9=E3=81=8B=E3=82=89=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=AC?= =?UTF-8?q?=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9=E3=82=92=E8=BF=94=E3=81=9B?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/presentation/src/notification_handler.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index 101dfe4c..b0630a4b 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -7,7 +7,10 @@ use resource::repository::RealInfrastructureRepository; use serde_json::json; use usecase::notification::NotificationUseCase; -use crate::schemas::notification::notification_response_schemas::NotificationResponse; +use crate::{ + error_handler::handle_error, + schemas::notification::notification_response_schemas::NotificationResponse, +}; pub async fn fetch_by_recipient_id( Extension(user): Extension, @@ -37,11 +40,8 @@ pub async fn fetch_by_recipient_id( }) .collect_vec(); - (StatusCode::OK, Json(json!(notification_response))) - } - Err(_err) => { - // TODO: 今のエラーハンドリングでは非効率すぎるので、解決してから書く - todo!() + (StatusCode::OK, Json(json!(notification_response))).into_response() } + Err(err) => handle_error(err).into_response(), } } From 0c4868464289c131553ab3d6d840dfae7d87a815 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:42:16 +0900 Subject: [PATCH 04/14] =?UTF-8?q?docs:=20Notification::new=20=E9=96=A2?= =?UTF-8?q?=E6=95=B0=E3=81=AE=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E6=9B=B8=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/domain/src/notification/models.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/domain/src/notification/models.rs b/server/domain/src/notification/models.rs index f8b64182..32252b84 100644 --- a/server/domain/src/notification/models.rs +++ b/server/domain/src/notification/models.rs @@ -19,6 +19,26 @@ pub struct Notification { } 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(), From a59a50dbb16b098ca1fec743fda8b70563e48a40 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:47:32 +0900 Subject: [PATCH 05/14] =?UTF-8?q?chore:=20Notification::from=5Fraw=5Fparts?= =?UTF-8?q?=20=E3=82=92=20unsafe=20=E9=96=A2=E6=95=B0=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/domain/src/notification/models.rs | 29 +++++++++++++++++++++++- server/infra/resource/src/dto.rs | 14 +++++++----- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/server/domain/src/notification/models.rs b/server/domain/src/notification/models.rs index 32252b84..149acec6 100644 --- a/server/domain/src/notification/models.rs +++ b/server/domain/src/notification/models.rs @@ -48,7 +48,34 @@ impl Notification { } } - pub fn from_raw_parts( + /// [`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, diff --git a/server/infra/resource/src/dto.rs b/server/infra/resource/src/dto.rs index 4de431ae..0f518a5c 100644 --- a/server/infra/resource/src/dto.rs +++ b/server/infra/resource/src/dto.rs @@ -362,11 +362,13 @@ impl TryFrom for domain::notification::models::Notification { is_read, }: NotificationDto, ) -> Result { - Ok(domain::notification::models::Notification::from_raw_parts( - id.into(), - source.try_into()?, - recipient.try_into()?, - is_read, - )) + unsafe { + Ok(domain::notification::models::Notification::from_raw_parts( + id.into(), + source.try_into()?, + recipient.try_into()?, + is_read, + )) + } } } From 4c3c055cf7a85b419e736fd310ee915d979cbef0 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:39:10 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AE=E6=97=A2=E8=AA=AD=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B=20API=20=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/domain/src/form/models.rs | 2 +- server/domain/src/notification/models.rs | 30 +++++++- .../src/repository/notification_repository.rs | 15 +++- server/entrypoint/src/main.rs | 7 +- server/errors/src/usecase.rs | 8 +- .../infra/resource/src/database/components.rs | 10 ++- .../resource/src/database/notification.rs | 76 ++++++++++++++++++- .../notification_repository_impl.rs | 46 ++++++++++- server/presentation/src/error_handler.rs | 8 ++ .../presentation/src/notification_handler.rs | 56 +++++++++++++- .../presentation/src/schemas/notification.rs | 1 + .../notification_request_schemas.rs | 8 ++ server/usecase/src/notification.rs | 68 ++++++++++++----- 13 files changed, 304 insertions(+), 31 deletions(-) create mode 100644 server/presentation/src/schemas/notification/notification_request_schemas.rs diff --git a/server/domain/src/form/models.rs b/server/domain/src/form/models.rs index ec0f65b5..9925d818 100644 --- a/server/domain/src/form/models.rs +++ b/server/domain/src/form/models.rs @@ -273,7 +273,7 @@ pub struct Label { pub type MessageId = types::Id; -#[derive(Getters, Debug)] +#[derive(Getters, PartialEq, Debug)] pub struct Message { id: MessageId, related_answer: FormAnswer, diff --git a/server/domain/src/notification/models.rs b/server/domain/src/notification/models.rs index 149acec6..021ff215 100644 --- a/server/domain/src/notification/models.rs +++ b/server/domain/src/notification/models.rs @@ -1,7 +1,11 @@ use derive_getters::Getters; use serde::Deserialize; -use crate::{form::models::MessageId, user::models::User}; +use crate::{ + form::models::MessageId, + types::authorization_guard::{AuthorizationGuard, AuthorizationGuardDefinitions, Create}, + user::models::User, +}; #[derive(Deserialize, Debug)] pub enum NotificationSource { @@ -89,3 +93,27 @@ impl Notification { } } } + +impl AuthorizationGuardDefinitions 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 for AuthorizationGuard { + fn from(value: Notification) -> Self { + AuthorizationGuard::new(value) + } +} diff --git a/server/domain/src/repository/notification_repository.rs b/server/domain/src/repository/notification_repository.rs index ff82fe75..2d76910c 100644 --- a/server/domain/src/repository/notification_repository.rs +++ b/server/domain/src/repository/notification_repository.rs @@ -2,10 +2,23 @@ use async_trait::async_trait; use errors::Error; use uuid::Uuid; -use crate::notification::models::Notification; +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, Error>; + async fn fetch_by_notification_ids( + &self, + notification_ids: Vec, + ) -> Result>, Error>; + async fn update_read_status( + &self, + actor: &User, + notifications: Vec<(AuthorizationGuard, bool)>, + ) -> Result<(), Error>; } diff --git a/server/entrypoint/src/main.rs b/server/entrypoint/src/main.rs index 6c52d80f..b9d8fcf9 100644 --- a/server/entrypoint/src/main.rs +++ b/server/entrypoint/src/main.rs @@ -26,7 +26,7 @@ use presentation::{ update_form_handler, update_message_handler, }, health_check_handler::health_check, - notification_handler::fetch_by_recipient_id, + notification_handler::{fetch_by_recipient_id, update_read_state}, search_handler::cross_search, user_handler::{end_session, get_my_user_info, patch_user_role, start_session, user_list}, }; @@ -154,7 +154,10 @@ async fn main() -> anyhow::Result<()> { delete(delete_message_handler).patch(update_message_handler), ) .with_state(shared_repository.to_owned()) - .route("/notifications", get(fetch_by_recipient_id)) + .route( + "/notifications", + get(fetch_by_recipient_id).patch(update_read_state), + ) .with_state(shared_repository.to_owned()) .route("/health", get(health_check)) .route("/session", post(start_session).delete(end_session)) diff --git a/server/errors/src/usecase.rs b/server/errors/src/usecase.rs index 450fea09..a2af1b28 100644 --- a/server/errors/src/usecase.rs +++ b/server/errors/src/usecase.rs @@ -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, } diff --git a/server/infra/resource/src/database/components.rs b/server/infra/resource/src/database/components.rs index 27991540..d3cb119f 100644 --- a/server/infra/resource/src/database/components.rs +++ b/server/infra/resource/src/database/components.rs @@ -5,7 +5,7 @@ use domain::{ FormDescription, FormId, FormTitle, Label, LabelId, Message, MessageId, OffsetAndLimit, Question, ResponsePeriod, Visibility, WebhookUrl, }, - notification::models::Notification, + notification::models::{Notification, NotificationId}, user::models::{Role, User}, }; use errors::infra::InfraError; @@ -200,4 +200,12 @@ pub trait NotificationDatabase: Send + Sync { &self, recipient_id: Uuid, ) -> Result, InfraError>; + async fn fetch_by_notification_ids( + &self, + notification_ids: Vec, + ) -> Result, InfraError>; + async fn update_read_status( + &self, + notification_id_with_is_read: Vec<(NotificationId, bool)>, + ) -> Result<(), InfraError>; } diff --git a/server/infra/resource/src/database/notification.rs b/server/infra/resource/src/database/notification.rs index 74732af3..9f97781f 100644 --- a/server/infra/resource/src/database/notification.rs +++ b/server/infra/resource/src/database/notification.rs @@ -2,16 +2,17 @@ use std::str::FromStr; use async_trait::async_trait; use domain::{ - notification::models::{Notification, NotificationSource}, + notification::models::{Notification, NotificationId, NotificationSource}, user::models::Role, }; use errors::infra::InfraError; +use types::Id; use uuid::Uuid; use crate::{ database::{ components::NotificationDatabase, - connection::{execute_and_values, query_all_and_values, ConnectionPool}, + connection::{batch_insert, execute_and_values, query_all_and_values, ConnectionPool}, }, dto::{NotificationDto, NotificationSourceInformationDto, NotificationSourceTypeDto, UserDto}, }; @@ -77,4 +78,75 @@ impl NotificationDatabase for ConnectionPool { .collect::, _>>() })).await.map_err(Into::into) } + + async fn fetch_by_notification_ids( + &self, + notification_ids: Vec, + ) -> Result, InfraError> { + self.read_only_transaction(|txn| { + Box::pin(async move { + let rs = query_all_and_values( + format!( + r"SELECT notifications.id AS notification_id, source_type, source_id, is_read, recipient_id, name, role + FROM notifications + INNER JOIN users ON notifications.recipient_id = users.id + WHERE notifications.id IN (?{})", + ", ?".repeat(notification_ids.len() - 1) + ) + .as_str(), + notification_ids + .into_iter() + .map(Id::into_inner) + .map(Into::into), + txn, + ) + .await?; + + rs.into_iter() + .map(|rs| { + Ok::<_, InfraError>(NotificationDto { + id: uuid::Uuid::from_str( + &rs.try_get::("", "notification_id")?, + )?, + source: NotificationSourceInformationDto { + source_type: NotificationSourceTypeDto::from_str( + &rs.try_get::("", "source_type")?, + )?, + source_id: uuid::Uuid::from_str( + &rs.try_get::("", "source_id")?, + )?, + }, + recipient: UserDto { + name: rs.try_get("", "name")?, + id: uuid::Uuid::from_str( + &rs.try_get::("", "recipient_id")?, + )?, + role: Role::from_str(&rs.try_get::("", "role")?)?, + }, + is_read: rs.try_get("", "is_read")?, + }) + }) + .collect::, _>>() + }) + }) + .await + .map_err(Into::into) + } + + async fn update_read_status( + &self, + notification_id_with_is_read: Vec<(NotificationId, bool)>, + ) -> Result<(), InfraError> { + self.read_write_transaction(|txn| { + Box::pin(async move { + batch_insert( + r"INSERT INTO notifications (id, is_read) VALUES (?, ?) ON DUPLICATE KEY UPDATE is_read = VALUES(is_read)", + notification_id_with_is_read.into_iter().flat_map(|(id, is_read)| [id.to_string().into(), is_read.into()]), + txn, + ).await?; + + Ok::<_, InfraError>(()) + }) + }).await.map_err(Into::into) + } } diff --git a/server/infra/resource/src/repository/notification_repository_impl.rs b/server/infra/resource/src/repository/notification_repository_impl.rs index 4dc4f58c..4bdec568 100644 --- a/server/infra/resource/src/repository/notification_repository_impl.rs +++ b/server/infra/resource/src/repository/notification_repository_impl.rs @@ -1,6 +1,9 @@ use async_trait::async_trait; use domain::{ - notification::models::Notification, repository::notification_repository::NotificationRepository, + notification::models::{Notification, NotificationId}, + repository::notification_repository::NotificationRepository, + types::authorization_guard::{AuthorizationGuard, Create, Read, Update}, + user::models::User, }; use errors::Error; use uuid::Uuid; @@ -33,4 +36,45 @@ impl NotificationRepository for Repository })? .map_err(Into::into) } + + async fn fetch_by_notification_ids( + &self, + notification_ids: Vec, + ) -> Result>, Error> { + self.client + .notification() + .fetch_by_notification_ids(notification_ids) + .await? + .into_iter() + .map(TryInto::::try_into) + .map(|notification| { + notification + .map(Into::into) + .map(AuthorizationGuard::<_, Create>::into_read) + }) + .collect::, _>>() + .map_err(Into::into) + } + + async fn update_read_status( + &self, + actor: &User, + notifications: Vec<(AuthorizationGuard, bool)>, + ) -> Result<(), Error> { + let update_targets = notifications + .into_iter() + .map(|(notification, is_read)| { + notification.try_update(actor, |notification| { + (notification.id().to_owned(), is_read) + }) + }) + .collect::, _>>() + .map_err(Into::::into)?; + + self.client + .notification() + .update_read_status(update_targets) + .await + .map_err(Into::into) + } } diff --git a/server/presentation/src/error_handler.rs b/server/presentation/src/error_handler.rs index 7d8191c8..5da52035 100644 --- a/server/presentation/src/error_handler.rs +++ b/server/presentation/src/error_handler.rs @@ -77,6 +77,14 @@ fn handle_usecase_error(err: UseCaseError) -> impl IntoResponse { })), ) .into_response(), + UseCaseError::NotificationNotFound => ( + StatusCode::NOT_FOUND, + Json(json!({ + "errorCode": "NOTIFICATION_NOT_FOUND", + "reason": "Notification not found" + })), + ) + .into_response(), } } diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index b0630a4b..c0cbd9bd 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -2,6 +2,7 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, use domain::{ notification::models::NotificationSource, repository::Repositories, user::models::User, }; +use errors::Error; use itertools::Itertools; use resource::repository::RealInfrastructureRepository; use serde_json::json; @@ -9,7 +10,10 @@ use usecase::notification::NotificationUseCase; use crate::{ error_handler::handle_error, - schemas::notification::notification_response_schemas::NotificationResponse, + schemas::notification::{ + notification_request_schemas::NotificationUpdateReadStateSchema, + notification_response_schemas::NotificationResponse, + }, }; pub async fn fetch_by_recipient_id( @@ -45,3 +49,53 @@ pub async fn fetch_by_recipient_id( Err(err) => handle_error(err).into_response(), } } + +pub async fn update_read_state( + Extension(user): Extension, + State(repository): State, + Json(update_targets): Json>, +) -> impl IntoResponse { + let notification_usecase = NotificationUseCase { + repository: repository.notification_repository(), + }; + + let update_targets = update_targets + .into_iter() + .map(|update_target| (update_target.notification_id, update_target.is_read)) + .collect_vec(); + + match notification_usecase + .update_notification_read_status(&user, update_targets) + .await + { + Ok(updated_notifications) => { + let notification_response = updated_notifications + .into_iter() + .map(|notification| { + notification.try_read(&user).map(|notification| { + let (source_type, source_id) = match notification.source() { + NotificationSource::Message(message_id) => { + ("MESSAGE".to_string(), message_id.to_string()) + } + }; + + NotificationResponse { + id: notification.id().to_owned(), + source_type, + source_id, + is_read: notification.is_read().to_owned(), + } + }) + }) + .collect::, _>>(); + + match notification_response { + Ok(notification_response) => { + (StatusCode::OK, Json(json!(notification_response))).into_response() + } + Err(err) => handle_error(Error::from(err)).into_response(), + } + } + Err(err) => handle_error(err).into_response(), + } +} diff --git a/server/presentation/src/schemas/notification.rs b/server/presentation/src/schemas/notification.rs index 0bbd7d68..ee2c0129 100644 --- a/server/presentation/src/schemas/notification.rs +++ b/server/presentation/src/schemas/notification.rs @@ -1 +1,2 @@ +pub mod notification_request_schemas; pub mod notification_response_schemas; diff --git a/server/presentation/src/schemas/notification/notification_request_schemas.rs b/server/presentation/src/schemas/notification/notification_request_schemas.rs new file mode 100644 index 00000000..5a49d01f --- /dev/null +++ b/server/presentation/src/schemas/notification/notification_request_schemas.rs @@ -0,0 +1,8 @@ +use domain::notification::models::NotificationId; +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct NotificationUpdateReadStateSchema { + pub notification_id: NotificationId, + pub is_read: bool, +} diff --git a/server/usecase/src/notification.rs b/server/usecase/src/notification.rs index 84f14539..67fca602 100644 --- a/server/usecase/src/notification.rs +++ b/server/usecase/src/notification.rs @@ -1,18 +1,50 @@ -use domain::{ - notification::models::Notification, repository::notification_repository::NotificationRepository, -}; -use errors::Error; -use uuid::Uuid; - -pub struct NotificationUseCase<'a, NotificationRepo: NotificationRepository> { - pub repository: &'a NotificationRepo, -} - -impl NotificationUseCase<'_, R> { - pub async fn fetch_notifications( - &self, - recipient_id: Uuid, - ) -> Result, Error> { - self.repository.fetch_by_recipient_id(recipient_id).await - } -} +use domain::{ + notification::models::{Notification, NotificationId}, + repository::notification_repository::NotificationRepository, + types::authorization_guard::{AuthorizationGuard, Read}, + user::models::User, +}; +use errors::Error; +use uuid::Uuid; + +pub struct NotificationUseCase<'a, NotificationRepo: NotificationRepository> { + pub repository: &'a NotificationRepo, +} + +impl NotificationUseCase<'_, R> { + pub async fn fetch_notifications( + &self, + recipient_id: Uuid, + ) -> Result, Error> { + self.repository.fetch_by_recipient_id(recipient_id).await + } + + pub async fn update_notification_read_status( + &self, + actor: &User, + notification_id_with_is_read: Vec<(NotificationId, bool)>, + ) -> Result>, Error> { + let (notification_id, is_read): (Vec, Vec) = + notification_id_with_is_read.into_iter().unzip(); + + let notifications = self + .repository + .fetch_by_notification_ids(notification_id.to_owned()) + .await?; + + self.repository + .update_read_status( + actor, + notifications + .into_iter() + .map(AuthorizationGuard::<_, Read>::into_update) + .zip(is_read.into_iter()) + .collect::>(), + ) + .await?; + + self.repository + .fetch_by_notification_ids(notification_id) + .await + } +} From b05fa2684b9cfe98897265e032d07c76674afa47 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:41:47 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20notification=20=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=83=96=E3=83=AB=E3=81=AE=E5=A4=96=E9=83=A8=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E5=88=B6=E7=B4=84=E3=82=92=E3=81=8B=E3=81=91=E3=82=8B=E3=82=AB?= =?UTF-8?q?=E3=83=A9=E3=83=A0=E5=90=8D=E3=81=AE=E6=8C=87=E5=AE=9A=E3=83=9F?= =?UTF-8?q?=E3=82=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/migration/src/m20220101_000001_create_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/migration/src/m20220101_000001_create_table.rs b/server/migration/src/m20220101_000001_create_table.rs index 8092893d..c1867507 100644 --- a/server/migration/src/m20220101_000001_create_table.rs +++ b/server/migration/src/m20220101_000001_create_table.rs @@ -218,7 +218,7 @@ impl MigrationTrait for Migration { recipient_id CHAR(36) NOT NULL, related_id UUID NOT NULL, is_read BOOL DEFAULT FALSE NOT NULL, - FOREIGN KEY fk_notification_recipient(recipient) REFERENCES users(id) + FOREIGN KEY fk_notification_recipient_id(recipient_id) REFERENCES users(id) )", )) .await?; From 1bda15623ef50d77d99667161ba0b928133ffe78 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:00:08 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20=E6=97=A2=E8=AA=AD=E7=8A=B6?= =?UTF-8?q?=E6=85=8B=E3=81=AE=E6=9B=B4=E6=96=B0=E3=81=AB=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E3=81=99=E3=82=8B=E4=B8=8D=E5=85=B7=E5=90=88=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource/src/database/notification.rs | 39 +++++++++++++------ .../src/m20220101_000001_create_table.rs | 2 +- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/server/infra/resource/src/database/notification.rs b/server/infra/resource/src/database/notification.rs index 9f97781f..3551e65b 100644 --- a/server/infra/resource/src/database/notification.rs +++ b/server/infra/resource/src/database/notification.rs @@ -6,13 +6,14 @@ use domain::{ user::models::Role, }; use errors::infra::InfraError; +use sea_orm::Value; use types::Id; use uuid::Uuid; use crate::{ database::{ components::NotificationDatabase, - connection::{batch_insert, execute_and_values, query_all_and_values, ConnectionPool}, + connection::{execute_and_values, query_all_and_values, ConnectionPool}, }, dto::{NotificationDto, NotificationSourceInformationDto, NotificationSourceTypeDto, UserDto}, }; @@ -55,7 +56,7 @@ impl NotificationDatabase for ConnectionPool { FROM notifications INNER JOIN users ON notifications.recipient_id = users.id WHERE recipient_id = ?", - [recipient_id.into()], + [recipient_id.to_string().into()], txn, ).await?; @@ -93,14 +94,14 @@ impl NotificationDatabase for ConnectionPool { WHERE notifications.id IN (?{})", ", ?".repeat(notification_ids.len() - 1) ) - .as_str(), + .as_str(), notification_ids .into_iter() .map(Id::into_inner) .map(Into::into), txn, ) - .await?; + .await?; rs.into_iter() .map(|rs| { @@ -129,8 +130,8 @@ impl NotificationDatabase for ConnectionPool { .collect::, _>>() }) }) - .await - .map_err(Into::into) + .await + .map_err(Into::into) } async fn update_read_status( @@ -139,14 +140,28 @@ impl NotificationDatabase for ConnectionPool { ) -> Result<(), InfraError> { self.read_write_transaction(|txn| { Box::pin(async move { - batch_insert( - r"INSERT INTO notifications (id, is_read) VALUES (?, ?) ON DUPLICATE KEY UPDATE is_read = VALUES(is_read)", - notification_id_with_is_read.into_iter().flat_map(|(id, is_read)| [id.to_string().into(), is_read.into()]), - txn, - ).await?; + let placeholders = vec!["?"; notification_id_with_is_read.len()].join(", "); + + let query = format!( + r"UPDATE notifications + SET is_read = ELT(FIELD(id, {placeholders}), {placeholders}) + WHERE id IN ({placeholders})" + ); + + let (notification_ids, is_reads): (Vec, Vec) = + notification_id_with_is_read + .into_iter() + .map(|(id, is_read)| (id.into_inner().to_string().into(), is_read.into())) + .unzip(); + + let params = [notification_ids.to_owned(), is_reads, notification_ids].concat(); + + execute_and_values(&query, params, txn).await?; Ok::<_, InfraError>(()) }) - }).await.map_err(Into::into) + }) + .await + .map_err(Into::into) } } diff --git a/server/migration/src/m20220101_000001_create_table.rs b/server/migration/src/m20220101_000001_create_table.rs index c1867507..f3edeaa6 100644 --- a/server/migration/src/m20220101_000001_create_table.rs +++ b/server/migration/src/m20220101_000001_create_table.rs @@ -215,8 +215,8 @@ impl MigrationTrait for Migration { r"CREATE TABLE IF NOT EXISTS notifications( id UUID NOT NULL PRIMARY KEY, source_type ENUM('MESSAGE') NOT NULL, + source_id UUID NOT NULL, recipient_id CHAR(36) NOT NULL, - related_id UUID NOT NULL, is_read BOOL DEFAULT FALSE NOT NULL, FOREIGN KEY fk_notification_recipient_id(recipient_id) REFERENCES users(id) )", From a295f414f8f778a80e9cd89f53f58d10cfd38cb6 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:16:36 +0900 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=E9=80=9A=E7=9F=A5=E5=8F=97?= =?UTF-8?q?=E4=BF=A1=E8=80=85=E3=81=8B=E3=82=89=E9=80=9A=E7=9F=A5=E3=82=92?= =?UTF-8?q?=E5=BC=95=E3=81=8F=E9=96=A2=E6=95=B0=E3=81=A7=E8=BF=94=E3=81=99?= =?UTF-8?q?=20Notification=20=E3=82=92=20AuthorizationGuard=20=E3=81=A7?= =?UTF-8?q?=E5=8C=85=E3=82=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/repository/notification_repository.rs | 5 ++++- .../repository/notification_repository_impl.rs | 14 ++++++++++---- server/presentation/src/notification_handler.rs | 17 ++++++++++++----- server/usecase/src/notification.rs | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/server/domain/src/repository/notification_repository.rs b/server/domain/src/repository/notification_repository.rs index 2d76910c..7e275903 100644 --- a/server/domain/src/repository/notification_repository.rs +++ b/server/domain/src/repository/notification_repository.rs @@ -11,7 +11,10 @@ use crate::{ #[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, Error>; + async fn fetch_by_recipient_id( + &self, + recipient_id: Uuid, + ) -> Result>, Error>; async fn fetch_by_notification_ids( &self, notification_ids: Vec, diff --git a/server/infra/resource/src/repository/notification_repository_impl.rs b/server/infra/resource/src/repository/notification_repository_impl.rs index 4bdec568..2a022ddd 100644 --- a/server/infra/resource/src/repository/notification_repository_impl.rs +++ b/server/infra/resource/src/repository/notification_repository_impl.rs @@ -6,6 +6,7 @@ use domain::{ user::models::User, }; use errors::Error; +use itertools::Itertools; use uuid::Uuid; use crate::{ @@ -23,7 +24,10 @@ impl NotificationRepository for Repository .map_err(Into::into) } - async fn fetch_by_recipient_id(&self, recipient_id: Uuid) -> Result, Error> { + async fn fetch_by_recipient_id( + &self, + recipient_id: Uuid, + ) -> Result>, Error> { self.client .notification() .fetch_by_recipient(recipient_id) @@ -31,9 +35,11 @@ impl NotificationRepository for Repository .map(|notifications| { notifications .into_iter() - .map(TryInto::try_into) - .collect::, _>>() - })? + .flat_map(TryInto::::try_into) + .map(Into::>::into) + .map(AuthorizationGuard::<_, Create>::into_read) + .collect_vec() + }) .map_err(Into::into) } diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index c0cbd9bd..d0e1b193 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -2,7 +2,7 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, use domain::{ notification::models::NotificationSource, repository::Repositories, user::models::User, }; -use errors::Error; +use errors::{domain::DomainError, Error}; use itertools::Itertools; use resource::repository::RealInfrastructureRepository; use serde_json::json; @@ -29,22 +29,29 @@ pub async fn fetch_by_recipient_id( let notification_response = notifications .into_iter() .map(|notification| { + let notification = notification.try_read(&user)?; + let notification_source = match notification.source() { NotificationSource::Message(message_id) => { ("Message".to_string(), message_id.to_string()) } }; - NotificationResponse { + Ok(NotificationResponse { id: notification.id().to_owned(), source_type: notification_source.0, source_id: notification_source.1, is_read: notification.is_read().to_owned(), - } + }) }) - .collect_vec(); + .collect::, DomainError>>(); - (StatusCode::OK, Json(json!(notification_response))).into_response() + match notification_response { + Ok(notification_responses) => { + (StatusCode::OK, Json(json!(notification_responses))).into_response() + } + Err(err) => handle_error(Error::from(err)).into_response(), + } } Err(err) => handle_error(err).into_response(), } diff --git a/server/usecase/src/notification.rs b/server/usecase/src/notification.rs index 67fca602..652350f7 100644 --- a/server/usecase/src/notification.rs +++ b/server/usecase/src/notification.rs @@ -15,7 +15,7 @@ impl NotificationUseCase<'_, R> { pub async fn fetch_notifications( &self, recipient_id: Uuid, - ) -> Result, Error> { + ) -> Result>, Error> { self.repository.fetch_by_recipient_id(recipient_id).await } From 574d31333e71d6d102a662c38c5c872cc9debf1e Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:26:46 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20NotificationRepository=20?= =?UTF-8?q?=E3=81=AE=20map=20=E3=82=92=E3=83=8D=E3=82=B9=E3=83=88=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=AE=E3=82=92=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification_repository_impl.rs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/server/infra/resource/src/repository/notification_repository_impl.rs b/server/infra/resource/src/repository/notification_repository_impl.rs index 2a022ddd..2bfb2309 100644 --- a/server/infra/resource/src/repository/notification_repository_impl.rs +++ b/server/infra/resource/src/repository/notification_repository_impl.rs @@ -28,38 +28,32 @@ impl NotificationRepository for Repository &self, recipient_id: Uuid, ) -> Result>, Error> { - self.client + Ok(self + .client .notification() .fetch_by_recipient(recipient_id) - .await - .map(|notifications| { - notifications - .into_iter() - .flat_map(TryInto::::try_into) - .map(Into::>::into) - .map(AuthorizationGuard::<_, Create>::into_read) - .collect_vec() - }) - .map_err(Into::into) + .await? + .into_iter() + .flat_map(TryInto::::try_into) + .map(Into::>::into) + .map(AuthorizationGuard::<_, Create>::into_read) + .collect_vec()) } async fn fetch_by_notification_ids( &self, notification_ids: Vec, ) -> Result>, Error> { - self.client + Ok(self + .client .notification() .fetch_by_notification_ids(notification_ids) .await? .into_iter() - .map(TryInto::::try_into) - .map(|notification| { - notification - .map(Into::into) - .map(AuthorizationGuard::<_, Create>::into_read) - }) - .collect::, _>>() - .map_err(Into::into) + .flat_map(TryInto::::try_into) + .map(Into::into) + .map(AuthorizationGuard::<_, Create>::into_read) + .collect_vec()) } async fn update_read_status( From 50b29517d55b7e3759489cfc4c67cb9222a3b143 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:39:18 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20=E6=97=A2=E8=AA=AD=E7=8A=B6?= =?UTF-8?q?=E6=85=8B=E3=81=AE=E6=9B=B4=E6=96=B0=E3=82=92=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9=E6=A7=8B?= =?UTF-8?q?=E6=88=90=E3=81=A7=20and=5Fthen=20=E3=82=92=E4=BD=BF=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/src/notification_handler.rs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index d0e1b193..ad3823f3 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -71,12 +71,11 @@ pub async fn update_read_state( .map(|update_target| (update_target.notification_id, update_target.is_read)) .collect_vec(); - match notification_usecase + let notification_response_or_error = notification_usecase .update_notification_read_status(&user, update_targets) .await - { - Ok(updated_notifications) => { - let notification_response = updated_notifications + .and_then(|updated_notifications| { + updated_notifications .into_iter() .map(|notification| { notification.try_read(&user).map(|notification| { @@ -94,14 +93,13 @@ pub async fn update_read_state( } }) }) - .collect::, _>>(); + .collect::, _>>() + .map_err(Into::into) + }); - match notification_response { - Ok(notification_response) => { - (StatusCode::OK, Json(json!(notification_response))).into_response() - } - Err(err) => handle_error(Error::from(err)).into_response(), - } + match notification_response_or_error { + Ok(notification_response) => { + (StatusCode::OK, Json(json!(notification_response))).into_response() } Err(err) => handle_error(err).into_response(), } From 3c1686f9edcae8070b782c9aa9a22bcf79da141e Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:53:37 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20=E3=83=AA=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=97=E3=81=9F=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=83=BC=E3=81=8B=E3=82=89=20Notification=20=E3=82=92=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=99=E3=82=8B=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=88=E3=81=AE=E3=83=AC=E3=82=B9=E3=83=9D?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E6=A7=8B=E6=88=90=E3=81=A7=20and=5Fthen=20?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/src/notification_handler.rs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index ad3823f3..34ee617b 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -2,7 +2,6 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, use domain::{ notification::models::NotificationSource, repository::Repositories, user::models::User, }; -use errors::{domain::DomainError, Error}; use itertools::Itertools; use resource::repository::RealInfrastructureRepository; use serde_json::json; @@ -24,34 +23,35 @@ pub async fn fetch_by_recipient_id( repository: repository.notification_repository(), }; - match notification_usecase.fetch_notifications(user.id).await { - Ok(notifications) => { - let notification_response = notifications + let notification_response_or_error = notification_usecase + .fetch_notifications(user.id) + .await + .and_then(|notifications| { + notifications .into_iter() .map(|notification| { - let notification = notification.try_read(&user)?; + notification.try_read(&user).map(|notification| { + let (source_type, source_id) = match notification.source() { + NotificationSource::Message(message_id) => { + ("MESSAGE".to_string(), message_id.to_string()) + } + }; - let notification_source = match notification.source() { - NotificationSource::Message(message_id) => { - ("Message".to_string(), message_id.to_string()) + NotificationResponse { + id: notification.id().to_owned(), + source_type, + source_id, + is_read: notification.is_read().to_owned(), } - }; - - Ok(NotificationResponse { - id: notification.id().to_owned(), - source_type: notification_source.0, - source_id: notification_source.1, - is_read: notification.is_read().to_owned(), }) }) - .collect::, DomainError>>(); + .collect::, _>>() + .map_err(Into::into) + }); - match notification_response { - Ok(notification_responses) => { - (StatusCode::OK, Json(json!(notification_responses))).into_response() - } - Err(err) => handle_error(Error::from(err)).into_response(), - } + match notification_response_or_error { + Ok(notification_response) => { + (StatusCode::OK, Json(json!(notification_response))).into_response() } Err(err) => handle_error(err).into_response(), } From 09e34810a25f46823408857400d1dfcd7e60470d Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:55:30 +0900 Subject: [PATCH 13/14] =?UTF-8?q?chore:=20=E3=83=AA=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=97=E3=81=9F=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E9=80=9A=E7=9F=A5=E3=82=92=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E5=AE=9A=E7=BE=A9=E3=81=99=E3=82=8B?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E5=90=8D=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/entrypoint/src/main.rs | 4 ++-- server/presentation/src/notification_handler.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/entrypoint/src/main.rs b/server/entrypoint/src/main.rs index b9d8fcf9..2244b3c1 100644 --- a/server/entrypoint/src/main.rs +++ b/server/entrypoint/src/main.rs @@ -26,7 +26,7 @@ use presentation::{ update_form_handler, update_message_handler, }, health_check_handler::health_check, - notification_handler::{fetch_by_recipient_id, update_read_state}, + 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}, }; @@ -156,7 +156,7 @@ async fn main() -> anyhow::Result<()> { .with_state(shared_repository.to_owned()) .route( "/notifications", - get(fetch_by_recipient_id).patch(update_read_state), + get(fetch_by_request_user).patch(update_read_state), ) .with_state(shared_repository.to_owned()) .route("/health", get(health_check)) diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index 34ee617b..7e2404be 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -15,7 +15,7 @@ use crate::{ }, }; -pub async fn fetch_by_recipient_id( +pub async fn fetch_by_request_user( Extension(user): Extension, State(repository): State, ) -> impl IntoResponse { From 2f5332eb28dd674d9fb4965d3cd4c40c209853b0 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:43:04 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20Notification=20=E3=81=8B?= =?UTF-8?q?=E3=82=89=20NotificationResponse=20=E3=81=AB=E5=A4=89=E6=8F=9B?= =?UTF-8?q?=E3=81=99=E3=82=8B=E9=96=A2=E6=95=B0=E3=82=92=E5=AE=9A=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/src/notification_handler.rs | 38 ++++--------------- .../notification_response_schemas.rs | 19 +++++++++- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/server/presentation/src/notification_handler.rs b/server/presentation/src/notification_handler.rs index 7e2404be..fc8d6b86 100644 --- a/server/presentation/src/notification_handler.rs +++ b/server/presentation/src/notification_handler.rs @@ -1,7 +1,5 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json}; -use domain::{ - notification::models::NotificationSource, repository::Repositories, user::models::User, -}; +use domain::{repository::Repositories, user::models::User}; use itertools::Itertools; use resource::repository::RealInfrastructureRepository; use serde_json::json; @@ -30,20 +28,9 @@ pub async fn fetch_by_request_user( notifications .into_iter() .map(|notification| { - notification.try_read(&user).map(|notification| { - let (source_type, source_id) = match notification.source() { - NotificationSource::Message(message_id) => { - ("MESSAGE".to_string(), message_id.to_string()) - } - }; - - NotificationResponse { - id: notification.id().to_owned(), - source_type, - source_id, - is_read: notification.is_read().to_owned(), - } - }) + notification + .try_read(&user) + .map(NotificationResponse::from_notification_ref) }) .collect::, _>>() .map_err(Into::into) @@ -78,20 +65,9 @@ pub async fn update_read_state( updated_notifications .into_iter() .map(|notification| { - notification.try_read(&user).map(|notification| { - let (source_type, source_id) = match notification.source() { - NotificationSource::Message(message_id) => { - ("MESSAGE".to_string(), message_id.to_string()) - } - }; - - NotificationResponse { - id: notification.id().to_owned(), - source_type, - source_id, - is_read: notification.is_read().to_owned(), - } - }) + notification + .try_read(&user) + .map(NotificationResponse::from_notification_ref) }) .collect::, _>>() .map_err(Into::into) diff --git a/server/presentation/src/schemas/notification/notification_response_schemas.rs b/server/presentation/src/schemas/notification/notification_response_schemas.rs index 9dac444f..fd5e81fc 100644 --- a/server/presentation/src/schemas/notification/notification_response_schemas.rs +++ b/server/presentation/src/schemas/notification/notification_response_schemas.rs @@ -1,4 +1,4 @@ -use domain::notification::models::NotificationId; +use domain::notification::models::{Notification, NotificationId, NotificationSource}; use serde::Serialize; #[derive(Serialize, Debug)] @@ -8,3 +8,20 @@ pub struct NotificationResponse { pub source_id: String, pub is_read: bool, } + +impl NotificationResponse { + pub fn from_notification_ref(notification: &Notification) -> Self { + let (source_type, source_id) = match notification.source() { + NotificationSource::Message(message_id) => { + ("MESSAGE".to_string(), message_id.to_string()) + } + }; + + Self { + id: notification.id().to_owned(), + source_type, + source_id, + is_read: notification.is_read().to_owned(), + } + } +}