Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

通知機能の実装 #604

Merged
merged 14 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
c5a9397
feat: 通知を送信する機能の実装
rito528 Nov 14, 2024
d774ad2
feat: リクエスト送信者の通知を取得するエンドポイントの実装
rito528 Nov 16, 2024
6c310a2
chore: 通知を取得するハンドラからエラーレスポンスを返せるようにする
rito528 Nov 16, 2024
0c48684
docs: Notification::new 関数のドキュメントを書く
rito528 Nov 16, 2024
a59a50d
chore: Notification::from_raw_parts を unsafe 関数にする
rito528 Nov 16, 2024
4c3c055
feat: メッセージの既読状態を変更する API の実装
rito528 Nov 20, 2024
b05fa26
fix: notification テーブルの外部キー制約をかけるカラム名の指定ミスを修正
rito528 Nov 21, 2024
1bda156
fix: 既読状態の更新に失敗する不具合を修正
rito528 Nov 21, 2024
a295f41
fix: 通知受信者から通知を引く関数で返す Notification を AuthorizationGuard で包む
rito528 Nov 23, 2024
574d313
refactor: NotificationRepository の map をネストするのを解消
rito528 Nov 23, 2024
50b2951
refactor: 既読状態の更新をするエンドポイントのレスポンス構成で and_then を使う
rito528 Nov 23, 2024
3c1686f
refactor: リクエストしたユーザーから Notification を取得するエンドポイントのレスポンス構成で and_then を使う
rito528 Nov 23, 2024
09e3481
chore: リクエストしたユーザーの通知を取得するエンドポイントを定義する関数名を変更
rito528 Nov 23, 2024
2f5332e
refactor: Notification から NotificationResponse に変換する関数を定義
rito528 Nov 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions server/Cargo.lock

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

pub async fn query_all(
Expand Down
Loading