From 134c439f1e5ba2641ec0135a08affc2e1dbc7f00 Mon Sep 17 00:00:00 2001 From: Arata Date: Wed, 1 May 2024 20:56:26 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E7=94=B3=E8=AB=8B=E3=81=AE?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-domain/src/entity/form.rs | 201 ++++++++++++++++-- .../src/service/verify_form_answer.rs | 10 +- crates/sos24-domain/src/test/fixture/form.rs | 3 +- .../sos24-infrastructure/src/mongodb/form.rs | 42 ++-- .../src/error/convert_error.rs | 25 +++ crates/sos24-presentation/src/model/form.rs | 10 +- crates/sos24-use-case/src/form/dto.rs | 64 +++--- .../src/form/interactor/create.rs | 6 +- .../src/form/interactor/update.rs | 6 +- schema.yml | 5 + 10 files changed, 294 insertions(+), 78 deletions(-) diff --git a/crates/sos24-domain/src/entity/form.rs b/crates/sos24-domain/src/entity/form.rs index 756c862a..c2a790f4 100644 --- a/crates/sos24-domain/src/entity/form.rs +++ b/crates/sos24-domain/src/entity/form.rs @@ -15,6 +15,16 @@ use super::permission::{PermissionDeniedError, Permissions}; pub enum FormError { #[error("The end time is earlier than the start time")] EndTimeEarlierThanStartTime, + #[error("The minimum length is greater than the maximum length")] + MinLengthGreaterThanMaxLength, + #[error("The minimum value is greater than the maximum value")] + MinGreaterThanMax, + #[error("The options is empty")] + EmptyOptions, + #[error("The minimum selection is greater than the options")] + MinSelectionGreaterThanOptions, + #[error("The minimum selection is greater than the maximum selection")] + MinSelectionGreaterThanMaxSelection, } #[derive(Debug, Clone, PartialEq, Eq, Getters)] @@ -365,32 +375,64 @@ impl FormItemKind { min_length: Option, max_length: Option, allow_newline: FormItemAllowNewline, - ) -> Self { - Self::String(FormItemString { + ) -> Result { + if let (Some(min_length), Some(max_length)) = (min_length.clone(), max_length.clone()) { + if min_length.value() > max_length.value() { + return Err(FormError::MinLengthGreaterThanMaxLength); + } + } + + Ok(Self::String(FormItemString { min_length, max_length, allow_newline, - }) + })) } - pub fn new_int(min: Option, max: Option) -> Self { - Self::Int(FormItemInt { min, max }) + pub fn new_int(min: Option, max: Option) -> Result { + if let (Some(min), Some(max)) = (min.clone(), max.clone()) { + if min.value() > max.value() { + return Err(FormError::MinGreaterThanMax); + } + } + + Ok(Self::Int(FormItemInt { min, max })) } - pub fn new_choose_one(options: Vec) -> Self { - Self::ChooseOne(FormItemChooseOne { options }) + pub fn new_choose_one(options: Vec) -> Result { + if options.is_empty() { + return Err(FormError::EmptyOptions); + } + + Ok(Self::ChooseOne(FormItemChooseOne { options })) } pub fn new_choose_many( options: Vec, min_selection: Option, max_selection: Option, - ) -> Self { - Self::ChooseMany(FormItemChooseMany { + ) -> Result { + if options.is_empty() { + return Err(FormError::EmptyOptions); + } + if let Some(min_selection) = min_selection.clone() { + if min_selection.value() > options.len() as u32 { + return Err(FormError::MinSelectionGreaterThanOptions); + } + } + if let (Some(min_selection), Some(max_selection)) = + (min_selection.clone(), max_selection.clone()) + { + if min_selection.value() > max_selection.value() { + return Err(FormError::MinSelectionGreaterThanMaxSelection); + } + } + + Ok(Self::ChooseMany(FormItemChooseMany { options, min_selection, max_selection, - }) + })) } pub fn new_file( @@ -520,23 +562,45 @@ pub struct DestructedFormItemFile { pub limit: Option, } -impl_value_object!(FormItemMinLength(i32)); -impl_value_object!(FormItemMaxLength(i32)); +impl_value_object!(FormItemMinLength(u32)); +impl_value_object!(FormItemMaxLength(u32)); impl_value_object!(FormItemAllowNewline(bool)); impl_value_object!(FormItemMin(i32)); impl_value_object!(FormItemMax(i32)); impl_value_object!(FormItemOption(String)); -impl_value_object!(FormItemMinSelection(i32)); -impl_value_object!(FormItemMaxSelection(i32)); +impl_value_object!(FormItemMinSelection(u32)); +impl_value_object!(FormItemMaxSelection(u32)); impl_value_object!(FormItemExtension(String)); -impl_value_object!(FormItemLimit(i32)); +impl_value_object!(FormItemLimit(u32)); #[cfg(test)] mod tests { - use crate::{entity::form::FormError, test::fixture}; + use crate::{ + entity::form::{ + FormError, FormItemAllowNewline, FormItemKind, FormItemMax, FormItemMaxLength, + FormItemMaxSelection, FormItemMin, FormItemMinLength, FormItemMinSelection, + FormItemOption, + }, + test::fixture, + }; use super::Form; + #[test] + fn 申請の開始時間が終了時間より前ならばエラーを返さない() { + let form = Form::create( + fixture::form::title1(), + fixture::form::description1(), + fixture::form::starts_at1(), + fixture::form::ends_at1(), + fixture::form::categories1(), + fixture::form::attributes1(), + fixture::form::items1(), + fixture::form::attachments1(), + ); + assert!(form.is_ok()); + } + #[test] fn 申請の開始時間が終了時間より後ならばエラーを返す() { let form = Form::create( @@ -551,4 +615,109 @@ mod tests { ); assert!(matches!(form, Err(FormError::EndTimeEarlierThanStartTime))); } + + #[test] + fn 文字列項目の最小文字数が最大文字数以下ならばエラーを返さない() { + let item = FormItemKind::new_string( + Some(FormItemMinLength::new(1)), + Some(FormItemMaxLength::new(2)), + FormItemAllowNewline::new(false), + ); + assert!(item.is_ok()); + } + + #[test] + fn 文字列項目の最小文字数が最大文字数より大きいならばエラーを返す() { + let item = FormItemKind::new_string( + Some(FormItemMinLength::new(2)), + Some(FormItemMaxLength::new(1)), + FormItemAllowNewline::new(false), + ); + assert!(matches!( + item, + Err(FormError::MinLengthGreaterThanMaxLength) + )); + } + + #[test] + fn 数値項目の最小値が最大値以下ならばエラーを返さない() { + let item = FormItemKind::new_int(Some(FormItemMin::new(1)), Some(FormItemMax::new(2))); + assert!(item.is_ok()); + } + + #[test] + fn 数値項目の最小値が最大値より大きいならばエラーを返す() { + let item = FormItemKind::new_int(Some(FormItemMin::new(2)), Some(FormItemMax::new(1))); + assert!(matches!(item, Err(FormError::MinGreaterThanMax))); + } + + #[test] + fn 選択肢項目の選択肢が1個以上ならばエラーを返さない() { + let item = FormItemKind::new_choose_one(vec![FormItemOption::new("a".to_string())]); + assert!(item.is_ok()); + } + + #[test] + fn 選択肢項目の選択肢が0個ならばエラーを返す() { + let item = FormItemKind::new_choose_one(vec![]); + assert!(matches!(item, Err(FormError::EmptyOptions))); + } + + #[test] + fn 複数選択項目の最小選択数が選択肢数以下ならばエラーを返さない() { + let item = FormItemKind::new_choose_many( + vec![ + FormItemOption::new("a".to_string()), + FormItemOption::new("b".to_string()), + ], + Some(FormItemMinSelection::new(2)), + None, + ); + assert!(item.is_ok()); + } + + #[test] + fn 複数選択項目の最小選択数が選択肢数より大きいならばエラーを返す() { + let item = FormItemKind::new_choose_many( + vec![ + FormItemOption::new("a".to_string()), + FormItemOption::new("b".to_string()), + ], + Some(FormItemMinSelection::new(3)), + None, + ); + assert!(matches!( + item, + Err(FormError::MinSelectionGreaterThanOptions) + )); + } + + #[test] + fn 複数選択項目の最小選択数が最大選択数以下ならばエラーを返さない() { + let item = FormItemKind::new_choose_many( + vec![ + FormItemOption::new("a".to_string()), + FormItemOption::new("b".to_string()), + ], + Some(FormItemMinSelection::new(1)), + Some(FormItemMaxSelection::new(2)), + ); + assert!(item.is_ok()); + } + + #[test] + fn 複数選択項目の最小選択数が最大選択数より大きいならばエラーを返す() { + let item = FormItemKind::new_choose_many( + vec![ + FormItemOption::new("a".to_string()), + FormItemOption::new("b".to_string()), + ], + Some(FormItemMinSelection::new(2)), + Some(FormItemMaxSelection::new(1)), + ); + assert!(matches!( + item, + Err(FormError::MinSelectionGreaterThanMaxSelection) + )); + } } diff --git a/crates/sos24-domain/src/service/verify_form_answer.rs b/crates/sos24-domain/src/service/verify_form_answer.rs index 68c39a63..20e30412 100644 --- a/crates/sos24-domain/src/service/verify_form_answer.rs +++ b/crates/sos24-domain/src/service/verify_form_answer.rs @@ -19,9 +19,9 @@ pub enum VerifyFormAnswerError { #[error("Answer item {0:?} has invalid kind")] InvalidAnswerItemKind(FormItemId), #[error("String answer item {0:?} is too short (min: {1})")] - TooShortString(FormItemId, i32), + TooShortString(FormItemId, u32), #[error("String answer item {0:?} is too long (max: {1})")] - TooLongString(FormItemId, i32), + TooLongString(FormItemId, u32), #[error("String answer item {0:?} contains newline")] NewlineNotAllowed(FormItemId), #[error("Int answer item {0:?} is too small (min: {1})")] @@ -33,11 +33,11 @@ pub enum VerifyFormAnswerError { #[error("ChooseMany answer item {0:?} has invalid option: {1}")] InvalidChooseManyOption(FormItemId, String), #[error("ChooseOne answer item {0:?} has too few options (min: {1})")] - TooFewOptionsChooseMany(FormItemId, i32), + TooFewOptionsChooseMany(FormItemId, u32), #[error("ChooseOne answer item {0:?} has too many options (max: {1})")] - TooManyOptionsChooseMany(FormItemId, i32), + TooManyOptionsChooseMany(FormItemId, u32), #[error("File answer item {0:?} has too many files (max: {1})")] - TooManyFiles(FormItemId, i32), + TooManyFiles(FormItemId, u32), } pub fn verify(form: &Form, answer: &FormAnswer) -> Result<(), VerifyFormAnswerError> { diff --git a/crates/sos24-domain/src/test/fixture/form.rs b/crates/sos24-domain/src/test/fixture/form.rs index b5c6712f..17133d02 100644 --- a/crates/sos24-domain/src/test/fixture/form.rs +++ b/crates/sos24-domain/src/test/fixture/form.rs @@ -66,6 +66,7 @@ pub fn formitem_kind1() -> FormItemKind { Some(FormItemMaxLength::new(10)), FormItemAllowNewline::new(true), ) + .unwrap() } pub fn items1() -> Vec { @@ -148,7 +149,7 @@ pub fn formitem_required2() -> FormItemRequired { } pub fn formitem_kind2() -> FormItemKind { - FormItemKind::new_int(Some(FormItemMin::new(1)), Some(FormItemMax::new(2))) + FormItemKind::new_int(Some(FormItemMin::new(1)), Some(FormItemMax::new(2))).unwrap() } pub fn items2() -> Vec { diff --git a/crates/sos24-infrastructure/src/mongodb/form.rs b/crates/sos24-infrastructure/src/mongodb/form.rs index 2c453057..c8aaf277 100644 --- a/crates/sos24-infrastructure/src/mongodb/form.rs +++ b/crates/sos24-infrastructure/src/mongodb/form.rs @@ -124,7 +124,7 @@ impl TryFrom for FormItem { FormItemName::new(value.name), value.description.map(FormItemDescription::new), FormItemRequired::new(value.required), - FormItemKind::from(value.kind), + FormItemKind::try_from(value.kind)?, )) } } @@ -132,8 +132,8 @@ impl TryFrom for FormItem { #[derive(Debug, Serialize, Deserialize)] pub enum FormItemKindDoc { String { - min_length: Option, - max_length: Option, + min_length: Option, + max_length: Option, allow_newline: bool, }, Int { @@ -145,12 +145,12 @@ pub enum FormItemKindDoc { }, ChooseMany { options: Vec, - min_selection: Option, - max_selection: Option, + min_selection: Option, + max_selection: Option, }, File { extensions: Option>, - limit: Option, + limit: Option, }, } @@ -199,37 +199,39 @@ impl From for FormItemKindDoc { } } -impl From for FormItemKind { - fn from(value: FormItemKindDoc) -> Self { +impl TryFrom for FormItemKind { + type Error = anyhow::Error; + fn try_from(value: FormItemKindDoc) -> Result { match value { FormItemKindDoc::String { min_length, max_length, allow_newline, - } => FormItemKind::new_string( + } => Ok(FormItemKind::new_string( min_length.map(FormItemMinLength::new), max_length.map(FormItemMaxLength::new), FormItemAllowNewline::new(allow_newline), - ), - FormItemKindDoc::Int { min, max } => { - FormItemKind::new_int(min.map(FormItemMin::new), max.map(FormItemMax::new)) - } - FormItemKindDoc::ChooseOne { options } => { - FormItemKind::new_choose_one(options.into_iter().map(FormItemOption::new).collect()) - } + )?), + FormItemKindDoc::Int { min, max } => Ok(FormItemKind::new_int( + min.map(FormItemMin::new), + max.map(FormItemMax::new), + )?), + FormItemKindDoc::ChooseOne { options } => Ok(FormItemKind::new_choose_one( + options.into_iter().map(FormItemOption::new).collect(), + )?), FormItemKindDoc::ChooseMany { options, min_selection, max_selection, - } => FormItemKind::new_choose_many( + } => Ok(FormItemKind::new_choose_many( options.into_iter().map(FormItemOption::new).collect(), min_selection.map(FormItemMinSelection::new), max_selection.map(FormItemMaxSelection::new), - ), - FormItemKindDoc::File { extensions, limit } => FormItemKind::new_file( + )?), + FormItemKindDoc::File { extensions, limit } => Ok(FormItemKind::new_file( extensions.map(|it| it.into_iter().map(FormItemExtension::new).collect()), limit.map(FormItemLimit::new), - ), + )), } } } diff --git a/crates/sos24-presentation/src/error/convert_error.rs b/crates/sos24-presentation/src/error/convert_error.rs index 55c7745a..c30f1ef0 100644 --- a/crates/sos24-presentation/src/error/convert_error.rs +++ b/crates/sos24-presentation/src/error/convert_error.rs @@ -474,6 +474,31 @@ impl From for AppError { "form/end-time-earlier-than-start-time".to_string(), error.to_string(), ), + FormError::MinLengthGreaterThanMaxLength => AppError::new( + StatusCode::BAD_REQUEST, + "form/min-length-greater-than-max-length".to_string(), + error.to_string(), + ), + FormError::MinGreaterThanMax => AppError::new( + StatusCode::BAD_REQUEST, + "form/min-greater-than-max".to_string(), + error.to_string(), + ), + FormError::EmptyOptions => AppError::new( + StatusCode::BAD_REQUEST, + "form/empty-options".to_string(), + error.to_string(), + ), + FormError::MinSelectionGreaterThanOptions => AppError::new( + StatusCode::BAD_REQUEST, + "form/min-selection-greater-than-options".to_string(), + error.to_string(), + ), + FormError::MinSelectionGreaterThanMaxSelection => AppError::new( + StatusCode::BAD_REQUEST, + "form/min-selection-greater-than-max-selection".to_string(), + error.to_string(), + ), } } } diff --git a/crates/sos24-presentation/src/model/form.rs b/crates/sos24-presentation/src/model/form.rs index 88be9a38..d5c77a4e 100644 --- a/crates/sos24-presentation/src/model/form.rs +++ b/crates/sos24-presentation/src/model/form.rs @@ -221,8 +221,8 @@ impl From for FormItem { #[serde(tag = "type", rename_all = "snake_case")] pub enum FormItemKind { String { - min_length: Option, - max_length: Option, + min_length: Option, + max_length: Option, allow_newline: bool, }, Int { @@ -234,12 +234,12 @@ pub enum FormItemKind { }, ChooseMany { options: Vec, - min_selection: Option, - max_selection: Option, + min_selection: Option, + max_selection: Option, }, File { extensions: Option>, - limit: Option, + limit: Option, }, } diff --git a/crates/sos24-use-case/src/form/dto.rs b/crates/sos24-use-case/src/form/dto.rs index bad90d8a..ff845e23 100644 --- a/crates/sos24-use-case/src/form/dto.rs +++ b/crates/sos24-use-case/src/form/dto.rs @@ -8,6 +8,8 @@ use sos24_domain::entity::form_answer::FormAnswer; use crate::project::dto::{ProjectAttributesDto, ProjectCategoriesDto}; +use super::FormUseCaseError; + #[derive(Debug)] pub struct NewFormItemDto { name: String, @@ -32,14 +34,15 @@ impl NewFormItemDto { } } -impl From for FormItem { - fn from(value: NewFormItemDto) -> Self { - FormItem::create( +impl TryFrom for FormItem { + type Error = FormUseCaseError; + fn try_from(value: NewFormItemDto) -> Result { + Ok(FormItem::create( FormItemName::new(value.name), value.description.map(FormItemDescription::new), FormItemRequired::new(value.required), - FormItemKind::from(value.kind), - ) + FormItemKind::try_from(value.kind)?, + )) } } @@ -158,14 +161,15 @@ impl FormItemDto { } } -impl From for FormItem { - fn from(value: FormItemDto) -> Self { - FormItem::create( +impl TryFrom for FormItem { + type Error = FormUseCaseError; + fn try_from(value: FormItemDto) -> Result { + Ok(FormItem::create( FormItemName::new(value.name), value.description.map(FormItemDescription::new), FormItemRequired::new(value.required), - FormItemKind::from(value.kind), - ) + FormItemKind::try_from(value.kind)?, + )) } } @@ -185,8 +189,8 @@ impl From for FormItemDto { #[derive(Debug)] pub enum FormItemKindDto { String { - min_length: Option, - max_length: Option, + min_length: Option, + max_length: Option, allow_newline: bool, }, Int { @@ -198,46 +202,48 @@ pub enum FormItemKindDto { }, ChooseMany { options: Vec, - min_selection: Option, - max_selection: Option, + min_selection: Option, + max_selection: Option, }, File { extensions: Option>, - limit: Option, + limit: Option, }, } -impl From for FormItemKind { - fn from(value: FormItemKindDto) -> Self { +impl TryFrom for FormItemKind { + type Error = FormUseCaseError; + fn try_from(value: FormItemKindDto) -> Result { match value { FormItemKindDto::String { min_length, max_length, allow_newline, - } => FormItemKind::new_string( + } => Ok(FormItemKind::new_string( min_length.map(FormItemMinLength::new), max_length.map(FormItemMaxLength::new), FormItemAllowNewline::new(allow_newline), - ), - FormItemKindDto::Int { min, max } => { - FormItemKind::new_int(min.map(FormItemMin::new), max.map(FormItemMax::new)) - } - FormItemKindDto::ChooseOne { options } => { - FormItemKind::new_choose_one(options.into_iter().map(FormItemOption::new).collect()) - } + )?), + FormItemKindDto::Int { min, max } => Ok(FormItemKind::new_int( + min.map(FormItemMin::new), + max.map(FormItemMax::new), + )?), + FormItemKindDto::ChooseOne { options } => Ok(FormItemKind::new_choose_one( + options.into_iter().map(FormItemOption::new).collect(), + )?), FormItemKindDto::ChooseMany { options, min_selection, max_selection, - } => FormItemKind::new_choose_many( + } => Ok(FormItemKind::new_choose_many( options.into_iter().map(FormItemOption::new).collect(), min_selection.map(FormItemMinSelection::new), max_selection.map(FormItemMaxSelection::new), - ), - FormItemKindDto::File { extensions, limit } => FormItemKind::new_file( + )?), + FormItemKindDto::File { extensions, limit } => Ok(FormItemKind::new_file( extensions.map(|it| it.into_iter().map(FormItemExtension::new).collect()), limit.map(FormItemLimit::new), - ), + )), } } } diff --git a/crates/sos24-use-case/src/form/interactor/create.rs b/crates/sos24-use-case/src/form/interactor/create.rs index 2305cd03..8fcb84ce 100644 --- a/crates/sos24-use-case/src/form/interactor/create.rs +++ b/crates/sos24-use-case/src/form/interactor/create.rs @@ -44,7 +44,11 @@ impl FormUseCase { DateTime::try_from(raw_form.ends_at)?, ProjectCategories::from(raw_form.categories), ProjectAttributes::from(raw_form.attributes), - raw_form.items.into_iter().map(FormItem::from).collect(), + raw_form + .items + .into_iter() + .map(FormItem::try_from) + .collect::>()?, raw_form .attachments .into_iter() diff --git a/crates/sos24-use-case/src/form/interactor/update.rs b/crates/sos24-use-case/src/form/interactor/update.rs index f593c202..05603e97 100644 --- a/crates/sos24-use-case/src/form/interactor/update.rs +++ b/crates/sos24-use-case/src/form/interactor/update.rs @@ -63,7 +63,11 @@ impl FormUseCase { new_form.set_ends_at(&actor, DateTime::try_from(form_data.ends_at)?)?; new_form.set_categories(&actor, ProjectCategories::from(form_data.categories))?; new_form.set_attributes(&actor, ProjectAttributes::from(form_data.attributes))?; - let new_items = form_data.items.into_iter().map(FormItem::from).collect(); + let new_items = form_data + .items + .into_iter() + .map(FormItem::try_from) + .collect::>()?; new_form.set_items(&actor, new_items)?; let new_attachments = form_data .attachments diff --git a/schema.yml b/schema.yml index c8aafb57..3c73afa1 100644 --- a/schema.yml +++ b/schema.yml @@ -2253,10 +2253,12 @@ components: type: integer format: int32 nullable: true + minimum: 0 min_length: type: integer format: int32 nullable: true + minimum: 0 type: type: string enum: @@ -2299,10 +2301,12 @@ components: type: integer format: int32 nullable: true + minimum: 0 min_selection: type: integer format: int32 nullable: true + minimum: 0 options: type: array items: @@ -2324,6 +2328,7 @@ components: type: integer format: int32 nullable: true + minimum: 0 type: type: string enum: From 09ce327f13fc2852549faadec29b660444c4abe1 Mon Sep 17 00:00:00 2001 From: Arata Date: Wed, 1 May 2024 21:48:32 +0900 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20README=E3=82=92=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 29 ++++++++++++++++------------ README.md | 54 +++++++++++------------------------------------------ 2 files changed, 28 insertions(+), 55 deletions(-) diff --git a/.env.sample b/.env.sample index 2c0aa6ac..4c5371ee 100644 --- a/.env.sample +++ b/.env.sample @@ -1,32 +1,37 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=postgres -POSTGRES_HOSTNAME=postgres +POSTGRES_HOSTNAME=localhost +POSTGRES_PORT=5432 -POSTGRES_DB_URL=postgresql://postgres:postgres@postgres:5432/postgres +POSTGRES_DB_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}" # For sqlx::test -DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres +DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB}" -MONGO_INITDB_ROOT_USERNAME=mongo +MONGO_INITDB_ROOT_USERNAME=root MONGO_INITDB_ROOT_PASSWORD=password MONGO_DB=mongo -MONGO_DB_URL="mongodb://mongo:password:27017" +MONGO_DB_URL="mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@localhost:27017" +ME_CONFIG_MONGODB_ADMINUSERNAME=root +ME_CONFIG_MONGODB_ADMINPASSWORD=password +ME_CONFIG_MONGODB_SERVER=mongo + +PORT=8080 FIREBASE_PROJECT_ID="" FIREBASE_SERVICE_ACCOUNT_KEY="" -PROJECT_APPLICATION_START_AT="2024-03-16T15:00:00Z" -PROJECT_APPLICATION_END_AT="2024-03-17T15:00:00Z" -REQUIRE_EMAIL_VERIFICATION=false -S3_ENDPOINT=http://s3.isk01.sakurastorage.jp -S3_REGION=jp-north-1 +PROJECT_APPLICATION_START_AT="2024-03-15T22:19:08+09:00" +PROJECT_APPLICATION_END_AT="2024-04-10T22:19:08+09:00" +REQUIRE_EMAIL_VERIFICATION=true + +S3_ENDPOINT="" +S3_REGION="" S3_BUCKET="" S3_ACCESS_KEY_ID="" S3_SECRET_ACCESS_KEY="" -PORT=8080 - SEND_GRID_API_KEY="" EMAIL_SENDER_ADDRESS="" EMAIL_REPLY_TO_ADDRESS="" diff --git a/README.md b/README.md index 5de470c9..de7e5982 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,28 @@ # sos24-server + [![CD(prodution)](https://github.com/sohosai/sos24-server/actions/workflows/cd.yaml/badge.svg)](https://github.com/sohosai/sos24-server/actions/workflows/cd.yaml) [![CD(beta)](https://github.com/sohosai/sos24-server/actions/workflows/cd-beta.yaml/badge.svg)](https://github.com/sohosai/sos24-server/actions/workflows/cd-beta.yaml) [![CD(staging)](https://github.com/sohosai/sos24-server/actions/workflows/cd-staging.yaml/badge.svg)](https://github.com/sohosai/sos24-server/actions/workflows/cd-staging.yaml) +雙峰祭オンラインシステムのサーバーです。 > [!NOTE] > クエリを変更した場合はCIを通すために `cargo sqlx prepare --workspace` を実行してください。 -## sos24-presentation - -sos24-presentationクレートは、アプリケーションのHTTPサーバーとしての役割を果たしています。このクレートは、HTTPリクエストを適切なユースケースにルーティングし、その結果をHTTPレスポンスとしてクライアントに返します。 - -具体的には、create_app関数でHTTPルーターを作成し、それぞれのエンドポイント(例えば、ニュースやユーザーに関するエンドポイント)を適切なハンドラー関数にルーティングします。これらのハンドラー関数は、Modules構造体を通じてユースケースにアクセスします。 - -Modules構造体は、アプリケーションの設定と、ニュースとユーザーに関するユースケースを保持します。これらのユースケースは、sos24-use-caseクレートで定義されています。 - -また、ToStatusCodeトレイトは、エラーをHTTPステータスコードに変換するために使用されます。 - -## sos24-use-case - -sos24-use-caseクレートは、アプリケーションのビジネスロジックを実装しています。このクレートは、アプリケーションのユースケース(操作)を表現するためのインターフェースを提供します。 - -具体的には、UserUseCaseとNewsUseCaseの2つの主要なユースケースがあります。 - -UserUseCaseは、ユーザーに関連する操作を提供します。これには、ユーザーの作成(createメソッド)、特定のIDを持つユーザーの検索(find_by_idメソッド)などが含まれます。 - -一方、NewsUseCaseは、ニュースに関連する操作を提供します。これには、ニュースの作成(createメソッド)、ニュースのリスト取得(listメソッド)などが含まれます。 - -これらのユースケースは、Repositoriesトレイトを通じてデータストレージにアクセスします。このトレイトは、sos24-domainクレートで定義されたエンティティを操作するためのメソッドを提供します。 - -また、このクレートは、UserDtoやCreateUserDtoなどのデータ転送オブジェクト(DTO)を定義しています。これらのDTOは、ユースケースとプレゼンテーション層との間でデータをやり取りするために使用されます。 - -## sos24-domain - -sos24-domainクレートは、アプリケーションのドメインモデルを定義しています。このクレートは、アプリケーションのビジネスロジックの核心部分を表現するエンティティと、それらのエンティティを永続化するためのリポジトリインターフェースを提供します。 - -具体的には、entityモジュールとrepositoryモジュールが主要な部分です。 - -entityやNewsなどのエンティティが定義されています。これらのエンティティは、アプリケーションのビジネスルールをカプセル化し、アプリケーションの状態を表現します。 - -一方、repositoryモジュールでは、これらのエンティティを永続化するためのインターフェースが定義されています。これにより、ビジネスロジックは永続化の詳細から分離され、異なる永続化メカニズムを容易に切り替えることができます。 - -また、このクレートはtestモジュールも提供しています。これは、ドメインモデルの単体テストをサポートするためのユーティリティを提供します。 +## 環境構築 -このように、sos24-domainクレートは、アプリケーションのビジネスロジックを表現し、そのロジックをテストし、永続化するための基盤を提供します。 +### 環境変数 -## sos24-infrastructure +`.env.sample`を参考に`.env`ファイルを作成し、環境変数を設定してください。 -sos24-infrastructureクレートは、アプリケーションのインフラストラクチャ層を定義しています。このクレートは、アプリケーションのビジネスロジックをサポートするための具体的な永続化メカニズムと外部サービスの実装を提供します。 +### データベースのセットアップ -具体的には、firebaseモジュールとpostgresqlモジュールが主要な部分です。 +`cargo install sqlx-cli`で`sqlx-cli`をインストールします。その後`cargo sqlx database create`でデータベースを作成し、`cargo sqlx migrate run`でマイグレーションを実行します。 -firebaseモジュールでは、Firebaseを使用したユーザー認証の実装が提供されています。FirebaseUserRepositoryImplクラスは、Firebaseを使用してユーザー情報を取得するための具体的なメカニズムを提供します。 +### ビルド -一方、postgresqlモジュールでは、PostgreSQLを使用したデータの永続化の実装が提供されています。PgNewsRepositoryやPgUserRepositoryクラスは、それぞれニュースとユーザー情報をPostgreSQLデータベースに永続化するための具体的なメカニズムを提供します。 +`cargo run --bin sos24-presentation`でサーバーを起動します。 -また、DefaultRepositoriesクラスは、これらの具体的なリポジトリの実装をまとめ、sos24-domainクレートで定義されたリポジトリインターフェースを満たす形で提供します。 +### テスト -このように、sos24-infrastructureクレートは、アプリケーションのビジネスロジックをサポートするための具体的なインフラストラクチャを提供します。 +`cargo test`もしくは`cargo nextest run`でテストを実行します。 From 588124d30dcdd5b0fab53a6ef64e086777defbde Mon Sep 17 00:00:00 2001 From: Arata Date: Thu, 2 May 2024 04:21:58 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=E7=94=B3=E8=AB=8B=E3=82=A8?= =?UTF-8?q?=E3=82=AF=E3=82=B9=E3=83=9D=E3=83=BC=E3=83=88=E6=99=82=E3=81=AB?= =?UTF-8?q?=E3=80=81=E5=AF=BE=E8=B1=A1=E3=81=A8=E3=81=AA=E3=82=8B=E5=85=A8?= =?UTF-8?q?=E3=81=A6=E3=81=AE=E4=BC=81=E7=94=BB=E3=82=92=E5=90=AB=E3=82=81?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/route/form_answer.rs | 2 +- crates/sos24-use-case/src/form_answer/dto.rs | 2 +- .../interactor/export_by_form_id.rs | 68 ++++++++++--------- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/crates/sos24-presentation/src/route/form_answer.rs b/crates/sos24-presentation/src/route/form_answer.rs index ec389cc3..ba5eab73 100644 --- a/crates/sos24-presentation/src/route/form_answer.rs +++ b/crates/sos24-presentation/src/route/form_answer.rs @@ -169,7 +169,7 @@ pub async fn handle_export( form_answer.project_index.to_string(), form_answer.project_title, form_answer.project_group_name, - form_answer.created_at, + form_answer.created_at.unwrap_or_default(), ] .into_iter() .chain( diff --git a/crates/sos24-use-case/src/form_answer/dto.rs b/crates/sos24-use-case/src/form_answer/dto.rs index 42d5d9e0..da19b67c 100644 --- a/crates/sos24-use-case/src/form_answer/dto.rs +++ b/crates/sos24-use-case/src/form_answer/dto.rs @@ -148,5 +148,5 @@ pub struct FormAnswerToBeExportedDto { pub project_title: String, pub project_group_name: String, pub form_answer_item_values: Vec>, - pub created_at: String, + pub created_at: Option, } diff --git a/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs b/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs index 77f257c7..fb3ddd59 100644 --- a/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs +++ b/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs @@ -1,7 +1,7 @@ use chrono_tz::Asia::Tokyo; use sos24_domain::entity::form::FormId; -use sos24_domain::entity::form_answer::{FormAnswer, FormAnswerItem, FormAnswerItemKind}; +use sos24_domain::entity::form_answer::{FormAnswerItem, FormAnswerItemKind}; use sos24_domain::repository::form::FormRepository; use sos24_domain::repository::project::ProjectRepository; use sos24_domain::{ @@ -31,6 +31,12 @@ impl FormAnswerUseCase { .await? .ok_or(FormAnswerUseCaseError::FormNotFound(form_id.clone()))?; + let project_list = self.repositories.project_repository().list().await?; + let target_project_list: Vec<_> = project_list + .into_iter() + .filter(|project_with_owners| form.is_sent_to(&project_with_owners.project)) + .collect(); + let form = form.destruct(); let form_title = form.title.value(); let (form_item_ids, form_item_names): (Vec<_>, Vec<_>) = form @@ -39,47 +45,45 @@ impl FormAnswerUseCase { .map(|item| (item.id().clone(), item.name().clone().value())) .unzip(); - let form_answer_list = self - .repositories - .form_answer_repository() - .find_by_form_id(form_id) - .await? - .into_iter() - .map(FormAnswer::destruct) - .collect::>(); - let mut form_answers = Vec::new(); - for form_answer in form_answer_list { - let project_id = form_answer.project_id; - let project_with_owners = self + for project_with_owner in target_project_list { + let project_id = project_with_owner.project.id().clone(); + let form_answer = self .repositories - .project_repository() - .find_by_id(project_id.clone()) - .await? - .ok_or(FormAnswerUseCaseError::ProjectNotFound(project_id.clone()))?; - let project = project_with_owners.project.destruct(); + .form_answer_repository() + .find_by_project_id_and_form_id(project_id, form_id.clone()) + .await?; - let form_answer_item_values = form_item_ids - .iter() - .map(|item_id| { - form_answer - .items + let (form_answer_item_values, created_at) = match form_answer { + Some(form_answer) => { + let form_answer = form_answer.destruct(); + let values = form_item_ids .iter() - .find(|item| item.item_id() == item_id) - .map(convert_answer_item_to_string) - }) - .collect(); + .map(|item_id| { + form_answer + .items + .iter() + .find(|item| item.item_id() == item_id) + .map(convert_answer_item_to_string) + }) + .collect(); + let created_at = form_answer + .created_at + .value() + .with_timezone(&Tokyo) + .to_rfc3339(); + (values, Some(created_at)) + } + None => (form_item_ids.iter().map(|_| None).collect(), None), + }; + let project = project_with_owner.project.destruct(); form_answers.push(FormAnswerToBeExportedDto { project_index: project.index.value(), project_title: project.title.value().to_string(), project_group_name: project.group_name.value().to_string(), form_answer_item_values, - created_at: form_answer - .created_at - .value() - .with_timezone(&Tokyo) - .to_rfc3339(), + created_at, }); } From 653388c17e349d36c2d2ce20ecaa73aba80c65d5 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 15:15:36 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refactor:=20infrastructure=E3=81=AE?= =?UTF-8?q?=E3=83=A2=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/{sendgrid => }/email.rs | 2 +- .../src/{postgresql => }/file_data.rs | 2 +- .../src/{s3 => }/file_object.rs | 2 +- .../src/{firebase => }/firebase_user.rs | 2 +- .../src/{mongodb => }/form.rs | 2 +- .../src/{mongodb => }/form_answer.rs | 2 +- .../src/{postgresql => }/invitation.rs | 2 +- crates/sos24-infrastructure/src/lib.rs | 47 ++++++++++--------- .../src/{postgresql => }/news.rs | 2 +- .../src/{postgresql => }/project.rs | 4 +- crates/sos24-infrastructure/src/shared.rs | 5 ++ .../src/{ => shared}/firebase.rs | 2 - .../src/{ => shared}/mongodb.rs | 3 -- .../src/{ => shared}/postgresql.rs | 6 --- .../src/{ => shared}/s3.rs | 2 - .../src/{ => shared}/sendgrid.rs | 2 - .../src/{postgresql => }/user.rs | 2 +- crates/sos24-presentation/src/module.rs | 5 +- 18 files changed, 45 insertions(+), 49 deletions(-) rename crates/sos24-infrastructure/src/{sendgrid => }/email.rs (98%) rename crates/sos24-infrastructure/src/{postgresql => }/file_data.rs (99%) rename crates/sos24-infrastructure/src/{s3 => }/file_object.rs (99%) rename crates/sos24-infrastructure/src/{firebase => }/firebase_user.rs (98%) rename crates/sos24-infrastructure/src/{mongodb => }/form.rs (99%) rename crates/sos24-infrastructure/src/{mongodb => }/form_answer.rs (99%) rename crates/sos24-infrastructure/src/{postgresql => }/invitation.rs (99%) rename crates/sos24-infrastructure/src/{postgresql => }/news.rs (99%) rename crates/sos24-infrastructure/src/{postgresql => }/project.rs (99%) create mode 100644 crates/sos24-infrastructure/src/shared.rs rename crates/sos24-infrastructure/src/{ => shared}/firebase.rs (97%) rename crates/sos24-infrastructure/src/{ => shared}/mongodb.rs (94%) rename crates/sos24-infrastructure/src/{ => shared}/postgresql.rs (89%) rename crates/sos24-infrastructure/src/{ => shared}/s3.rs (97%) rename crates/sos24-infrastructure/src/{ => shared}/sendgrid.rs (95%) rename crates/sos24-infrastructure/src/{postgresql => }/user.rs (99%) diff --git a/crates/sos24-infrastructure/src/sendgrid/email.rs b/crates/sos24-infrastructure/src/email.rs similarity index 98% rename from crates/sos24-infrastructure/src/sendgrid/email.rs rename to crates/sos24-infrastructure/src/email.rs index 7998dbae..53a2696a 100644 --- a/crates/sos24-infrastructure/src/sendgrid/email.rs +++ b/crates/sos24-infrastructure/src/email.rs @@ -1,7 +1,7 @@ use sendgrid::v3::{Content, Email, Message, Personalization}; use sos24_use_case::shared::adapter::email::{self, EmailSender, SendEmailCommand}; -use super::SendGrid; +use crate::shared::sendgrid::SendGrid; pub struct SendGridEmailSender { sender: SendGrid, diff --git a/crates/sos24-infrastructure/src/postgresql/file_data.rs b/crates/sos24-infrastructure/src/file_data.rs similarity index 99% rename from crates/sos24-infrastructure/src/postgresql/file_data.rs rename to crates/sos24-infrastructure/src/file_data.rs index e101813f..2c78dfb5 100644 --- a/crates/sos24-infrastructure/src/postgresql/file_data.rs +++ b/crates/sos24-infrastructure/src/file_data.rs @@ -12,7 +12,7 @@ use sos24_domain::{ repository::file_data::{FileDataRepository, FileDataRepositoryError}, }; -use crate::postgresql::Postgresql; +use crate::shared::postgresql::Postgresql; #[derive(FromRow)] pub struct FileDataRow { diff --git a/crates/sos24-infrastructure/src/s3/file_object.rs b/crates/sos24-infrastructure/src/file_object.rs similarity index 99% rename from crates/sos24-infrastructure/src/s3/file_object.rs rename to crates/sos24-infrastructure/src/file_object.rs index 91bb13aa..c830af7b 100644 --- a/crates/sos24-infrastructure/src/s3/file_object.rs +++ b/crates/sos24-infrastructure/src/file_object.rs @@ -13,7 +13,7 @@ use sos24_domain::{ repository::file_object::{FileObjectRepository, FileObjectRepositoryError}, }; -use super::S3; +use crate::shared::s3::S3; pub struct S3FileObjectRepository { s3: S3, diff --git a/crates/sos24-infrastructure/src/firebase/firebase_user.rs b/crates/sos24-infrastructure/src/firebase_user.rs similarity index 98% rename from crates/sos24-infrastructure/src/firebase/firebase_user.rs rename to crates/sos24-infrastructure/src/firebase_user.rs index 76db83af..b6dd39de 100644 --- a/crates/sos24-infrastructure/src/firebase/firebase_user.rs +++ b/crates/sos24-infrastructure/src/firebase_user.rs @@ -10,7 +10,7 @@ use sos24_domain::{ repository::firebase_user::{FirebaseUserRepository, FirebaseUserRepositoryError}, }; -use super::FirebaseAuth; +use crate::shared::firebase::FirebaseAuth; pub struct FirebaseUserRepositoryImpl { auth: FirebaseAuth, diff --git a/crates/sos24-infrastructure/src/mongodb/form.rs b/crates/sos24-infrastructure/src/form.rs similarity index 99% rename from crates/sos24-infrastructure/src/mongodb/form.rs rename to crates/sos24-infrastructure/src/form.rs index c8aaf277..90ec0450 100644 --- a/crates/sos24-infrastructure/src/mongodb/form.rs +++ b/crates/sos24-infrastructure/src/form.rs @@ -22,7 +22,7 @@ use sos24_domain::{ repository::form::{FormRepository, FormRepositoryError}, }; -use super::MongoDb; +use crate::shared::mongodb::MongoDb; #[derive(Debug, Serialize, Deserialize)] pub struct FormDoc { diff --git a/crates/sos24-infrastructure/src/mongodb/form_answer.rs b/crates/sos24-infrastructure/src/form_answer.rs similarity index 99% rename from crates/sos24-infrastructure/src/mongodb/form_answer.rs rename to crates/sos24-infrastructure/src/form_answer.rs index f768ea85..5f1d6caf 100644 --- a/crates/sos24-infrastructure/src/mongodb/form_answer.rs +++ b/crates/sos24-infrastructure/src/form_answer.rs @@ -20,7 +20,7 @@ use sos24_domain::{ repository::form_answer::{FormAnswerRepository, FormAnswerRepositoryError}, }; -use super::MongoDb; +use crate::shared::mongodb::MongoDb; #[derive(Debug, Serialize, Deserialize)] pub struct FormAnswerDoc { diff --git a/crates/sos24-infrastructure/src/postgresql/invitation.rs b/crates/sos24-infrastructure/src/invitation.rs similarity index 99% rename from crates/sos24-infrastructure/src/postgresql/invitation.rs rename to crates/sos24-infrastructure/src/invitation.rs index 73159606..7d6a7f9d 100644 --- a/crates/sos24-infrastructure/src/postgresql/invitation.rs +++ b/crates/sos24-infrastructure/src/invitation.rs @@ -12,7 +12,7 @@ use sos24_domain::{ repository::invitation::{InvitationRepository, InvitationRepositoryError}, }; -use super::Postgresql; +use crate::shared::postgresql::Postgresql; #[derive(FromRow)] pub struct InvitationRow { diff --git a/crates/sos24-infrastructure/src/lib.rs b/crates/sos24-infrastructure/src/lib.rs index ae5b8903..250ff39d 100644 --- a/crates/sos24-infrastructure/src/lib.rs +++ b/crates/sos24-infrastructure/src/lib.rs @@ -1,27 +1,30 @@ -use firebase::firebase_user::FirebaseUserRepositoryImpl; -use firebase::FirebaseAuth; -use mongodb::form::MongoFormRepository; -use mongodb::form_answer::MongoFormAnswerRepository; -use mongodb::MongoDb; -use postgresql::file_data::PgFileDataRepository; -use postgresql::invitation::PgInvitationRepository; -use postgresql::project::PgProjectRepository; -use postgresql::user::PgUserRepository; -use s3::file_object::S3FileObjectRepository; -use s3::S3; -use sendgrid::email::SendGridEmailSender; -use sendgrid::SendGrid; +use email::SendGridEmailSender; +use file_data::PgFileDataRepository; +use file_object::S3FileObjectRepository; +use firebase_user::FirebaseUserRepositoryImpl; +use form::MongoFormRepository; +use form_answer::MongoFormAnswerRepository; +use invitation::PgInvitationRepository; +use news::PgNewsRepository; +use project::PgProjectRepository; +use shared::{ + firebase::FirebaseAuth, mongodb::MongoDb, postgresql::Postgresql, s3::S3, sendgrid::SendGrid, +}; use sos24_domain::repository::Repositories; use sos24_use_case::shared::adapter::Adapters; - -use crate::postgresql::news::PgNewsRepository; -use crate::postgresql::Postgresql; - -pub mod firebase; -pub mod mongodb; -pub mod postgresql; -pub mod s3; -pub mod sendgrid; +use user::PgUserRepository; + +pub mod email; +pub mod file_data; +pub mod file_object; +pub mod firebase_user; +pub mod form; +pub mod form_answer; +pub mod invitation; +pub mod news; +pub mod project; +pub mod shared; +pub mod user; pub struct DefaultRepositories { firebase_user_repository: FirebaseUserRepositoryImpl, diff --git a/crates/sos24-infrastructure/src/postgresql/news.rs b/crates/sos24-infrastructure/src/news.rs similarity index 99% rename from crates/sos24-infrastructure/src/postgresql/news.rs rename to crates/sos24-infrastructure/src/news.rs index 98ee7bbb..c56b051c 100644 --- a/crates/sos24-infrastructure/src/postgresql/news.rs +++ b/crates/sos24-infrastructure/src/news.rs @@ -8,7 +8,7 @@ use sos24_domain::entity::news::{News, NewsBody, NewsId, NewsTitle}; use sos24_domain::entity::project::{ProjectAttributes, ProjectCategories}; use sos24_domain::repository::news::{NewsRepository, NewsRepositoryError}; -use crate::postgresql::Postgresql; +use crate::shared::postgresql::Postgresql; #[derive(FromRow)] pub struct NewsRow { diff --git a/crates/sos24-infrastructure/src/postgresql/project.rs b/crates/sos24-infrastructure/src/project.rs similarity index 99% rename from crates/sos24-infrastructure/src/postgresql/project.rs rename to crates/sos24-infrastructure/src/project.rs index 58023373..1c150d10 100644 --- a/crates/sos24-infrastructure/src/postgresql/project.rs +++ b/crates/sos24-infrastructure/src/project.rs @@ -14,7 +14,9 @@ use sos24_domain::{ repository::project::{ProjectRepository, ProjectRepositoryError, ProjectWithOwners}, }; -use super::{user::UserRoleRow, Postgresql}; +use crate::shared::postgresql::Postgresql; + +use super::user::UserRoleRow; #[derive(FromRow)] pub struct ProjectWithOwnersRow { diff --git a/crates/sos24-infrastructure/src/shared.rs b/crates/sos24-infrastructure/src/shared.rs new file mode 100644 index 00000000..b15f31ad --- /dev/null +++ b/crates/sos24-infrastructure/src/shared.rs @@ -0,0 +1,5 @@ +pub mod firebase; +pub mod mongodb; +pub mod postgresql; +pub mod s3; +pub mod sendgrid; diff --git a/crates/sos24-infrastructure/src/firebase.rs b/crates/sos24-infrastructure/src/shared/firebase.rs similarity index 97% rename from crates/sos24-infrastructure/src/firebase.rs rename to crates/sos24-infrastructure/src/shared/firebase.rs index 35e2e6b1..bff94ba4 100644 --- a/crates/sos24-infrastructure/src/firebase.rs +++ b/crates/sos24-infrastructure/src/shared/firebase.rs @@ -2,8 +2,6 @@ use std::ops::Deref; use rs_firebase_admin_sdk::{App, CustomServiceAccount, LiveAuthAdmin}; -pub mod firebase_user; - pub struct FirebaseAuth(LiveAuthAdmin); impl FirebaseAuth { diff --git a/crates/sos24-infrastructure/src/mongodb.rs b/crates/sos24-infrastructure/src/shared/mongodb.rs similarity index 94% rename from crates/sos24-infrastructure/src/mongodb.rs rename to crates/sos24-infrastructure/src/shared/mongodb.rs index 3cc48c0b..3fc1569b 100644 --- a/crates/sos24-infrastructure/src/mongodb.rs +++ b/crates/sos24-infrastructure/src/shared/mongodb.rs @@ -2,9 +2,6 @@ use std::ops::Deref; use mongodb::{options::ClientOptions, Client}; -pub mod form; -pub mod form_answer; - #[derive(Clone)] pub struct MongoDb(mongodb::Database); diff --git a/crates/sos24-infrastructure/src/postgresql.rs b/crates/sos24-infrastructure/src/shared/postgresql.rs similarity index 89% rename from crates/sos24-infrastructure/src/postgresql.rs rename to crates/sos24-infrastructure/src/shared/postgresql.rs index fbcdd719..c9465d78 100644 --- a/crates/sos24-infrastructure/src/postgresql.rs +++ b/crates/sos24-infrastructure/src/shared/postgresql.rs @@ -3,12 +3,6 @@ use std::ops::Deref; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; -pub mod file_data; -pub mod invitation; -pub mod news; -pub mod project; -pub mod user; - #[derive(Clone)] pub struct Postgresql(pub(crate) PgPool); diff --git a/crates/sos24-infrastructure/src/s3.rs b/crates/sos24-infrastructure/src/shared/s3.rs similarity index 97% rename from crates/sos24-infrastructure/src/s3.rs rename to crates/sos24-infrastructure/src/shared/s3.rs index fb3aca6c..d56a2c22 100644 --- a/crates/sos24-infrastructure/src/s3.rs +++ b/crates/sos24-infrastructure/src/shared/s3.rs @@ -5,8 +5,6 @@ use aws_sdk_s3::{ Client, }; -pub mod file_object; - #[derive(Clone)] pub struct S3(Client); diff --git a/crates/sos24-infrastructure/src/sendgrid.rs b/crates/sos24-infrastructure/src/shared/sendgrid.rs similarity index 95% rename from crates/sos24-infrastructure/src/sendgrid.rs rename to crates/sos24-infrastructure/src/shared/sendgrid.rs index 93f04312..3ec74dca 100644 --- a/crates/sos24-infrastructure/src/sendgrid.rs +++ b/crates/sos24-infrastructure/src/shared/sendgrid.rs @@ -2,8 +2,6 @@ use std::ops::Deref; use sendgrid::v3::Sender; -pub mod email; - pub struct SendGrid(Sender); impl SendGrid { diff --git a/crates/sos24-infrastructure/src/postgresql/user.rs b/crates/sos24-infrastructure/src/user.rs similarity index 99% rename from crates/sos24-infrastructure/src/postgresql/user.rs rename to crates/sos24-infrastructure/src/user.rs index fc33570a..e0deac3e 100644 --- a/crates/sos24-infrastructure/src/postgresql/user.rs +++ b/crates/sos24-infrastructure/src/user.rs @@ -10,7 +10,7 @@ use sos24_domain::{ repository::user::{UserRepository, UserRepositoryError}, }; -use super::Postgresql; +use crate::shared::postgresql::Postgresql; #[derive(FromRow)] pub struct UserRow { diff --git a/crates/sos24-presentation/src/module.rs b/crates/sos24-presentation/src/module.rs index 4c832c5c..7932da1d 100644 --- a/crates/sos24-presentation/src/module.rs +++ b/crates/sos24-presentation/src/module.rs @@ -69,12 +69,13 @@ impl Modules { #[cfg(not(test))] pub async fn new(config: Config) -> anyhow::Result { - use crate::env; - use sos24_infrastructure::{ + use sos24_infrastructure::shared::{ firebase::FirebaseAuth, mongodb::MongoDb, postgresql::Postgresql, s3::S3, sendgrid::SendGrid, }; + use crate::env; + let db = Postgresql::new(&env::postgres_db_url()).await?; let mongo_db = MongoDb::new(&env::mongodb_db_url(), &env::mongodb_db_name()).await?; let auth = FirebaseAuth::new(&env::firebase_service_account_key()).await?; From 27e1bd3acc32446ce2d301c0c7f3f00f50fe804c Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 16:07:34 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20Slack=E9=80=9A=E7=9F=A5=E3=81=AEa?= =?UTF-8?q?dapter=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-use-case/src/project.rs | 13 ++++++++++--- .../src/project/interactor/create.rs | 13 ++++++++++++- .../src/project/interactor/delete_by_id.rs | 8 +++++++- .../src/project/interactor/find_by_id.rs | 10 +++++++++- .../src/project/interactor/find_owned.rs | 4 ++-- .../interactor/get_project_application_period.rs | 7 +++++-- .../sos24-use-case/src/project/interactor/list.rs | 8 +++++++- .../src/project/interactor/update.rs | 14 +++++++++++++- crates/sos24-use-case/src/shared/adapter.rs | 15 ++++++++++++++- .../src/shared/adapter/notification.rs | 7 +++++++ 10 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 crates/sos24-use-case/src/shared/adapter/notification.rs diff --git a/crates/sos24-use-case/src/project.rs b/crates/sos24-use-case/src/project.rs index f3b85b6d..575be545 100644 --- a/crates/sos24-use-case/src/project.rs +++ b/crates/sos24-use-case/src/project.rs @@ -17,6 +17,7 @@ use sos24_domain::{ repository::{project::ProjectRepositoryError, Repositories}, }; +use crate::shared::adapter::Adapters; use crate::shared::context::ContextError; pub mod dto; @@ -57,16 +58,22 @@ pub enum ProjectUseCaseError { InternalError(#[from] anyhow::Error), } -pub struct ProjectUseCase { +pub struct ProjectUseCase { repositories: Arc, + adapters: Arc, project_application_period: ProjectApplicationPeriod, // TODO creation_lock: tokio::sync::Mutex<()>, // FIXME } -impl ProjectUseCase { - pub fn new(repositories: Arc, project_application_period: ProjectApplicationPeriod) -> Self { +impl ProjectUseCase { + pub fn new( + repositories: Arc, + adapters: Arc, + project_application_period: ProjectApplicationPeriod, + ) -> Self { Self { repositories, + adapters, project_application_period, creation_lock: tokio::sync::Mutex::new(()), } diff --git a/crates/sos24-use-case/src/project/interactor/create.rs b/crates/sos24-use-case/src/project/interactor/create.rs index 5b45f0a0..ea3b4156 100644 --- a/crates/sos24-use-case/src/project/interactor/create.rs +++ b/crates/sos24-use-case/src/project/interactor/create.rs @@ -9,6 +9,8 @@ use sos24_domain::repository::Repositories; use sos24_domain::{ensure, entity::project::Project}; use crate::project::dto::ProjectAttributesDto; +use crate::shared::adapter::notification::Notifier; +use crate::shared::adapter::Adapters; use crate::{ project::{dto::ProjectCategoryDto, ProjectUseCase, ProjectUseCaseError}, shared::context::ContextProvider, @@ -25,7 +27,7 @@ pub struct CreateProjectCommand { pub owner_id: String, } -impl ProjectUseCase { +impl ProjectUseCase { pub async fn create( &self, ctx: &impl ContextProvider, @@ -86,6 +88,7 @@ mod tests { use crate::project::dto::{ProjectAttributesDto, ProjectCategoryDto}; use crate::project::interactor::create::CreateProjectCommand; use crate::project::{ProjectUseCase, ProjectUseCaseError}; + use crate::shared::adapter::MockAdapters; use crate::shared::context::TestContext; #[tokio::test] @@ -103,8 +106,10 @@ mod tests { .project_repository_mut() .expect_find_by_sub_owner_id() .returning(|_| Ok(None)); + let mut adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -145,8 +150,10 @@ mod tests { .project_repository_mut() .expect_find_by_sub_owner_id() .returning(|_| Ok(None)); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -186,8 +193,10 @@ mod tests { .project_repository_mut() .expect_find_by_sub_owner_id() .returning(|_| Ok(None)); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::not_applicable_period(), ); @@ -227,8 +236,10 @@ mod tests { .project_repository_mut() .expect_find_by_sub_owner_id() .returning(|_| Ok(None)); + let mut adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::not_applicable_period(), ); diff --git a/crates/sos24-use-case/src/project/interactor/delete_by_id.rs b/crates/sos24-use-case/src/project/interactor/delete_by_id.rs index d27ef449..161f9079 100644 --- a/crates/sos24-use-case/src/project/interactor/delete_by_id.rs +++ b/crates/sos24-use-case/src/project/interactor/delete_by_id.rs @@ -8,9 +8,10 @@ use sos24_domain::repository::project::ProjectRepository; use sos24_domain::repository::Repositories; use crate::project::{ProjectUseCase, ProjectUseCaseError}; +use crate::shared::adapter::Adapters; use crate::shared::context::ContextProvider; -impl ProjectUseCase { +impl ProjectUseCase { pub async fn delete_by_id( &self, ctx: &impl ContextProvider, @@ -60,6 +61,7 @@ mod tests { use sos24_domain::test::repository::MockRepositories; use crate::project::{ProjectUseCase, ProjectUseCaseError}; + use crate::shared::adapter::MockAdapters; use crate::shared::context::TestContext; #[tokio::test] @@ -73,8 +75,10 @@ mod tests { fixture::user::user1(UserRole::Committee), ))) }); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -117,8 +121,10 @@ mod tests { .file_data_repository_mut() .expect_delete_by_owner_project() .returning(|_| Ok(())); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); diff --git a/crates/sos24-use-case/src/project/interactor/find_by_id.rs b/crates/sos24-use-case/src/project/interactor/find_by_id.rs index 2f7827d2..7462225b 100644 --- a/crates/sos24-use-case/src/project/interactor/find_by_id.rs +++ b/crates/sos24-use-case/src/project/interactor/find_by_id.rs @@ -6,9 +6,10 @@ use sos24_domain::repository::Repositories; use crate::project::dto::ProjectDto; use crate::project::{ProjectUseCase, ProjectUseCaseError}; +use crate::shared::adapter::Adapters; use crate::shared::context::ContextProvider; -impl ProjectUseCase { +impl ProjectUseCase { pub async fn find_by_id( &self, ctx: &impl ContextProvider, @@ -44,6 +45,7 @@ mod tests { use sos24_domain::test::repository::MockRepositories; use crate::project::{ProjectUseCase, ProjectUseCaseError}; + use crate::shared::adapter::MockAdapters; use crate::shared::context::TestContext; #[tokio::test] @@ -57,8 +59,10 @@ mod tests { fixture::user::user1(UserRole::General), ))) }); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -80,8 +84,10 @@ mod tests { fixture::user::user2(UserRole::General), ))) }); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -108,8 +114,10 @@ mod tests { fixture::user::user2(UserRole::General), ))) }); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); diff --git a/crates/sos24-use-case/src/project/interactor/find_owned.rs b/crates/sos24-use-case/src/project/interactor/find_owned.rs index 323ef388..0444d24a 100644 --- a/crates/sos24-use-case/src/project/interactor/find_owned.rs +++ b/crates/sos24-use-case/src/project/interactor/find_owned.rs @@ -4,10 +4,10 @@ use sos24_domain::repository::Repositories; use crate::{ project::{dto::ProjectDto, ProjectUseCase, ProjectUseCaseError}, - shared::context::ContextProvider, + shared::{adapter::Adapters, context::ContextProvider}, }; -impl ProjectUseCase { +impl ProjectUseCase { pub async fn find_owned( &self, ctx: &impl ContextProvider, diff --git a/crates/sos24-use-case/src/project/interactor/get_project_application_period.rs b/crates/sos24-use-case/src/project/interactor/get_project_application_period.rs index fa1ae499..71aa7850 100644 --- a/crates/sos24-use-case/src/project/interactor/get_project_application_period.rs +++ b/crates/sos24-use-case/src/project/interactor/get_project_application_period.rs @@ -1,8 +1,11 @@ use sos24_domain::repository::Repositories; -use crate::project::{dto::ProjectApplicationPeriodDto, ProjectUseCase, ProjectUseCaseError}; +use crate::{ + project::{dto::ProjectApplicationPeriodDto, ProjectUseCase, ProjectUseCaseError}, + shared::adapter::Adapters, +}; -impl ProjectUseCase { +impl ProjectUseCase { pub async fn get_project_application_period( &self, ) -> Result { diff --git a/crates/sos24-use-case/src/project/interactor/list.rs b/crates/sos24-use-case/src/project/interactor/list.rs index d634fe3a..142ec531 100644 --- a/crates/sos24-use-case/src/project/interactor/list.rs +++ b/crates/sos24-use-case/src/project/interactor/list.rs @@ -5,9 +5,10 @@ use sos24_domain::repository::Repositories; use crate::project::dto::ProjectDto; use crate::project::{ProjectUseCase, ProjectUseCaseError}; +use crate::shared::adapter::Adapters; use crate::shared::context::ContextProvider; -impl ProjectUseCase { +impl ProjectUseCase { pub async fn list( &self, ctx: &impl ContextProvider, @@ -30,13 +31,16 @@ mod tests { use sos24_domain::test::repository::MockRepositories; use crate::project::{ProjectUseCase, ProjectUseCaseError}; + use crate::shared::adapter::MockAdapters; use crate::shared::context::TestContext; #[tokio::test] async fn 一般ユーザーは企画一覧を取得できない() { let repositories = MockRepositories::default(); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -57,8 +61,10 @@ mod tests { .project_repository_mut() .expect_list() .returning(|| Ok(vec![])); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); diff --git a/crates/sos24-use-case/src/project/interactor/update.rs b/crates/sos24-use-case/src/project/interactor/update.rs index 49101620..e4183328 100644 --- a/crates/sos24-use-case/src/project/interactor/update.rs +++ b/crates/sos24-use-case/src/project/interactor/update.rs @@ -9,6 +9,7 @@ use sos24_domain::repository::Repositories; use crate::project::dto::{ProjectAttributesDto, ProjectCategoryDto}; use crate::project::{ProjectUseCase, ProjectUseCaseError}; +use crate::shared::adapter::Adapters; use crate::shared::context::ContextProvider; #[derive(Debug)] @@ -23,7 +24,7 @@ pub struct UpdateProjectCommand { pub remarks: Option, } -impl ProjectUseCase { +impl ProjectUseCase { pub async fn update( &self, ctx: &impl ContextProvider, @@ -92,6 +93,7 @@ mod tests { use crate::project::dto::{ProjectAttributesDto, ProjectCategoryDto}; use crate::project::interactor::update::UpdateProjectCommand; use crate::project::{ProjectUseCase, ProjectUseCaseError}; + use crate::shared::adapter::MockAdapters; use crate::shared::context::TestContext; #[tokio::test] @@ -109,8 +111,10 @@ mod tests { .project_repository_mut() .expect_update() .returning(|_| Ok(())); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -148,8 +152,10 @@ mod tests { .project_repository_mut() .expect_update() .returning(|_| Ok(())); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::not_applicable_period(), ); @@ -190,8 +196,10 @@ mod tests { .project_repository_mut() .expect_update() .returning(|_| Ok(())); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -234,8 +242,10 @@ mod tests { .project_repository_mut() .expect_update() .returning(|_| Ok(())); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::applicable_period(), ); @@ -273,8 +283,10 @@ mod tests { .project_repository_mut() .expect_update() .returning(|_| Ok(())); + let adapters = MockAdapters::default(); let use_case = ProjectUseCase::new( Arc::new(repositories), + Arc::new(adapters), fixture::project_application_period::not_applicable_period(), ); diff --git a/crates/sos24-use-case/src/shared/adapter.rs b/crates/sos24-use-case/src/shared/adapter.rs index d52968d3..4ef6cd6e 100644 --- a/crates/sos24-use-case/src/shared/adapter.rs +++ b/crates/sos24-use-case/src/shared/adapter.rs @@ -1,28 +1,41 @@ -use self::email::MockEmailSender; +use self::{email::MockEmailSender, notification::MockNotifier}; pub mod email; +pub mod notification; pub trait Adapters: Send + Sync + 'static { type EmailSenderImpl: email::EmailSender; + type NotifierImpl: notification::Notifier; fn email_sender(&self) -> &Self::EmailSenderImpl; + fn notifier(&self) -> &Self::NotifierImpl; } #[derive(Default)] pub struct MockAdapters { email_sender: MockEmailSender, + notifier: MockNotifier, } impl MockAdapters { pub fn email_sender_mut(&mut self) -> &mut MockEmailSender { &mut self.email_sender } + + pub fn notifier_mut(&mut self) -> &mut MockNotifier { + &mut self.notifier + } } impl Adapters for MockAdapters { type EmailSenderImpl = MockEmailSender; + type NotifierImpl = MockNotifier; fn email_sender(&self) -> &Self::EmailSenderImpl { &self.email_sender } + + fn notifier(&self) -> &Self::NotifierImpl { + &self.notifier + } } diff --git a/crates/sos24-use-case/src/shared/adapter/notification.rs b/crates/sos24-use-case/src/shared/adapter/notification.rs new file mode 100644 index 00000000..a46b935e --- /dev/null +++ b/crates/sos24-use-case/src/shared/adapter/notification.rs @@ -0,0 +1,7 @@ +use mockall::automock; + +#[automock] +#[allow(async_fn_in_trait)] +pub trait Notifier: Send + Sync + 'static { + async fn notify(&self, message: String) -> anyhow::Result<()>; +} From 43671457eab279660839a982906d775947578f96 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 16:17:32 +0900 Subject: [PATCH 06/12] =?UTF-8?q?refactor:=20URL=E3=81=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=87=A6=E7=90=86=E3=82=92=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interactor/check_form_and_send_notify.rs | 17 ++++++++--------- .../src/news/interactor/create.rs | 6 +----- crates/sos24-use-case/src/shared.rs | 1 + crates/sos24-use-case/src/shared/app_url.rs | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 crates/sos24-use-case/src/shared/app_url.rs diff --git a/crates/sos24-use-case/src/form/interactor/check_form_and_send_notify.rs b/crates/sos24-use-case/src/form/interactor/check_form_and_send_notify.rs index 75030c7f..4f7e2389 100644 --- a/crates/sos24-use-case/src/form/interactor/check_form_and_send_notify.rs +++ b/crates/sos24-use-case/src/form/interactor/check_form_and_send_notify.rs @@ -5,11 +5,14 @@ use sos24_domain::repository::{form::FormRepository, project::ProjectRepository, use crate::{ form::{FormUseCase, FormUseCaseError}, - shared::adapter::{ - email::{Email, EmailSender, SendEmailCommand}, - Adapters, + shared::{ + adapter::{ + email::{Email, EmailSender, SendEmailCommand}, + Adapters, + }, + app_url, + context::ContextProvider, }, - shared::context::ContextProvider, }; impl FormUseCase { @@ -81,11 +84,7 @@ Email : {email} .value() .with_timezone(&Tokyo) .format("%Y年%m月%d日 %H:%M"), - url = format!( - "{}/forms/{}", - ctx.config().app_url, - form.id().clone().value() - ), + url = app_url::form(ctx, form.id().clone()), email = ctx.config().email_reply_to_address.clone(), ), }; diff --git a/crates/sos24-use-case/src/news/interactor/create.rs b/crates/sos24-use-case/src/news/interactor/create.rs index b9158fe8..91737d58 100644 --- a/crates/sos24-use-case/src/news/interactor/create.rs +++ b/crates/sos24-use-case/src/news/interactor/create.rs @@ -117,11 +117,7 @@ Email : {email} 電話 : 029-853-2899"#, title = news.title().clone().value(), body = news.body().clone().value(), - url = format!( - "{}/news/{}", - ctx.config().app_url, - news.id().clone().value() - ), + url = app_url::news(ctx, news.id().clone()), email = ctx.config().email_reply_to_address.clone(), ), }; diff --git a/crates/sos24-use-case/src/shared.rs b/crates/sos24-use-case/src/shared.rs index 9239aab3..23c6c4be 100644 --- a/crates/sos24-use-case/src/shared.rs +++ b/crates/sos24-use-case/src/shared.rs @@ -1,2 +1,3 @@ pub mod adapter; +pub mod app_url; pub mod context; diff --git a/crates/sos24-use-case/src/shared/app_url.rs b/crates/sos24-use-case/src/shared/app_url.rs new file mode 100644 index 00000000..e4e6122d --- /dev/null +++ b/crates/sos24-use-case/src/shared/app_url.rs @@ -0,0 +1,15 @@ +use sos24_domain::entity::{form::FormId, news::NewsId, project::ProjectId}; + +use super::context::ContextProvider; + +pub fn form(ctx: &impl ContextProvider, form_id: FormId) -> String { + format!("{}/forms/{}", ctx.config().app_url, form_id.value()) +} + +pub fn news(ctx: &impl ContextProvider, news_id: NewsId) -> String { + format!("{}/news/{}", ctx.config().app_url, news_id.value()) +} + +pub fn project(ctx: &impl ContextProvider, project_id: ProjectId) -> String { + format!("{}/projects/{}", ctx.config().app_url, project_id.value()) +} From ff47d7b149d27061855bbbb308bd217ce12925f6 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 16:17:52 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=E7=94=B3=E8=AB=8B=E3=83=BB?= =?UTF-8?q?=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=83=BB=E4=BC=81=E7=94=BB?= =?UTF-8?q?=E4=BD=9C=E6=88=90=E3=81=98=E3=81=AB=E9=80=9A=E7=9F=A5=E3=82=92?= =?UTF-8?q?=E9=80=81=E4=BF=A1=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/form/interactor/create.rs | 23 +++++++++++++++++-- .../src/news/interactor/create.rs | 15 ++++++++++++ .../src/project/interactor/create.rs | 19 +++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/sos24-use-case/src/form/interactor/create.rs b/crates/sos24-use-case/src/form/interactor/create.rs index 8fcb84ce..ffff7333 100644 --- a/crates/sos24-use-case/src/form/interactor/create.rs +++ b/crates/sos24-use-case/src/form/interactor/create.rs @@ -13,7 +13,11 @@ use sos24_domain::{ use crate::{ form::{dto::NewFormItemDto, FormUseCase, FormUseCaseError}, project::dto::{ProjectAttributesDto, ProjectCategoriesDto}, - shared::{adapter::Adapters, context::ContextProvider}, + shared::{ + adapter::{notification::Notifier, Adapters}, + app_url, + context::ContextProvider, + }, }; #[derive(Debug)] @@ -57,7 +61,18 @@ impl FormUseCase { )?; let form_id = form.id().clone(); + let form_title = form.title().clone(); self.repositories.form_repository().create(form).await?; + + self.adapters + .notifier() + .notify(format!( + "申請「{}」が作成されました。\n{}", + form_title.value(), + app_url::form(ctx, form_id.clone()), + )) + .await?; + Ok(form_id.value().to_string()) } } @@ -130,7 +145,11 @@ mod tests { .form_repository_mut() .expect_create() .returning(|_| Ok(())); - let adapters = MockAdapters::default(); + let mut adapters = MockAdapters::default(); + adapters + .notifier_mut() + .expect_notify() + .returning(|_| Ok(())); let use_case = FormUseCase::new(Arc::new(repositories), Arc::new(adapters)); let ctx = TestContext::new(fixture::actor::actor1(UserRole::CommitteeOperator)); diff --git a/crates/sos24-use-case/src/news/interactor/create.rs b/crates/sos24-use-case/src/news/interactor/create.rs index 91737d58..b8a66ed4 100644 --- a/crates/sos24-use-case/src/news/interactor/create.rs +++ b/crates/sos24-use-case/src/news/interactor/create.rs @@ -20,8 +20,10 @@ use crate::{ shared::{ adapter::{ email::{Email, EmailSender, SendEmailCommand}, + notification::Notifier, Adapters, }, + app_url, context::ContextProvider, }, }; @@ -123,6 +125,15 @@ Email : {email} }; self.adapters.email_sender().send_email(command).await?; + self.adapters + .notifier() + .notify(format!( + "お知らせ「{}」が公開されました。\n{}", + news.title().clone().value(), + app_url::news(ctx, news_id.clone()), + )) + .await?; + Ok(news_id.value().to_string()) } } @@ -192,6 +203,10 @@ mod tests { .email_sender_mut() .expect_send_email() .returning(|_| Ok(())); + adapters + .notifier_mut() + .expect_notify() + .returning(|_| Ok(())); let use_case = NewsUseCase::new(Arc::new(repositories), Arc::new(adapters)); let ctx = TestContext::new(fixture::actor::actor1(UserRole::CommitteeOperator)); diff --git a/crates/sos24-use-case/src/project/interactor/create.rs b/crates/sos24-use-case/src/project/interactor/create.rs index ea3b4156..9978044a 100644 --- a/crates/sos24-use-case/src/project/interactor/create.rs +++ b/crates/sos24-use-case/src/project/interactor/create.rs @@ -11,6 +11,7 @@ use sos24_domain::{ensure, entity::project::Project}; use crate::project::dto::ProjectAttributesDto; use crate::shared::adapter::notification::Notifier; use crate::shared::adapter::Adapters; +use crate::shared::app_url; use crate::{ project::{dto::ProjectCategoryDto, ProjectUseCase, ProjectUseCaseError}, shared::context::ContextProvider, @@ -43,6 +44,7 @@ impl ProjectUseCase { return Err(ProjectUseCaseError::ApplicationsNotAccepted); } + let project_title = raw_project.title.clone(); let project_id = { let lock = self.creation_lock.lock().await; @@ -73,6 +75,15 @@ impl ProjectUseCase { project_id }; + self.adapters + .notifier() + .notify(format!( + "企画「{}」が登録されました。\n{}", + project_title, + app_url::project(ctx, project_id.clone()), + )) + .await?; + Ok(project_id.value().to_string()) } } @@ -107,6 +118,10 @@ mod tests { .expect_find_by_sub_owner_id() .returning(|_| Ok(None)); let mut adapters = MockAdapters::default(); + adapters + .notifier_mut() + .expect_notify() + .returning(|_| Ok(())); let use_case = ProjectUseCase::new( Arc::new(repositories), Arc::new(adapters), @@ -237,6 +252,10 @@ mod tests { .expect_find_by_sub_owner_id() .returning(|_| Ok(None)); let mut adapters = MockAdapters::default(); + adapters + .notifier_mut() + .expect_notify() + .returning(|_| Ok(())); let use_case = ProjectUseCase::new( Arc::new(repositories), Arc::new(adapters), From 4cc340363fdd1d976e25e124152d0cec649270d4 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 16:18:26 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20Slack=E9=80=9A=E7=9F=A5=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/sos24-infrastructure/Cargo.toml | 1 + crates/sos24-infrastructure/src/lib.rs | 11 ++++- .../sos24-infrastructure/src/notification.rs | 47 +++++++++++++++++++ crates/sos24-presentation/src/env.rs | 4 ++ crates/sos24-presentation/src/module.rs | 21 +++++++-- 6 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 crates/sos24-infrastructure/src/notification.rs diff --git a/Cargo.lock b/Cargo.lock index ef706ced..01f7646e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3228,6 +3228,7 @@ dependencies = [ "chrono", "futures-util", "mongodb", + "reqwest 0.11.27", "rs-firebase-admin-sdk", "sendgrid", "serde", diff --git a/crates/sos24-infrastructure/Cargo.toml b/crates/sos24-infrastructure/Cargo.toml index 858453b5..bb247438 100644 --- a/crates/sos24-infrastructure/Cargo.toml +++ b/crates/sos24-infrastructure/Cargo.toml @@ -12,6 +12,7 @@ aws-sdk-s3.workspace = true chrono.workspace = true futures-util.workspace = true mongodb.workspace = true +reqwest.workspace = true rs-firebase-admin-sdk.workspace = true sendgrid.workspace = true serde.workspace = true diff --git a/crates/sos24-infrastructure/src/lib.rs b/crates/sos24-infrastructure/src/lib.rs index 250ff39d..90e094cb 100644 --- a/crates/sos24-infrastructure/src/lib.rs +++ b/crates/sos24-infrastructure/src/lib.rs @@ -6,6 +6,7 @@ use form::MongoFormRepository; use form_answer::MongoFormAnswerRepository; use invitation::PgInvitationRepository; use news::PgNewsRepository; +use notification::SlackNotifier; use project::PgProjectRepository; use shared::{ firebase::FirebaseAuth, mongodb::MongoDb, postgresql::Postgresql, s3::S3, sendgrid::SendGrid, @@ -22,6 +23,7 @@ pub mod form; pub mod form_answer; pub mod invitation; pub mod news; +pub mod notification; pub mod project; pub mod shared; pub mod user; @@ -104,20 +106,27 @@ impl Repositories for DefaultRepositories { pub struct DefaultAdapters { email_sender: SendGridEmailSender, + notifier: SlackNotifier, } impl DefaultAdapters { - pub fn new(send_grid: SendGrid) -> Self { + pub fn new(send_grid: SendGrid, slack_webhook_url: Option) -> Self { Self { email_sender: SendGridEmailSender::new(send_grid), + notifier: SlackNotifier::new(slack_webhook_url), } } } impl Adapters for DefaultAdapters { type EmailSenderImpl = SendGridEmailSender; + type NotifierImpl = SlackNotifier; fn email_sender(&self) -> &Self::EmailSenderImpl { &self.email_sender } + + fn notifier(&self) -> &Self::NotifierImpl { + &self.notifier + } } diff --git a/crates/sos24-infrastructure/src/notification.rs b/crates/sos24-infrastructure/src/notification.rs new file mode 100644 index 00000000..f47bff10 --- /dev/null +++ b/crates/sos24-infrastructure/src/notification.rs @@ -0,0 +1,47 @@ +use std::time::Duration; + +use anyhow::Context; +use reqwest::ClientBuilder; +use serde_json::json; +use sos24_use_case::shared::adapter::notification::Notifier; + +pub struct SlackNotifier { + webhook_url: Option, +} + +impl SlackNotifier { + pub fn new(webhook_url: Option) -> Self { + let webhook_url = match webhook_url { + Some(url) => { + // 有効なURLかどうかをチェックする + let _ = reqwest::Url::parse(&url).expect("Invalid URL"); + Some(url) + } + None => None, + }; + + Self { webhook_url } + } +} + +impl Notifier for SlackNotifier { + async fn notify(&self, message: String) -> anyhow::Result<()> { + tracing::info!("Slack通知を送信します"); + + let Some(ref webhook_url) = self.webhook_url else { + tracing::warn!("SlackのWebhook URLが設定されていないため、通知を送信しませんでした"); + return Ok(()); + }; + + let client = ClientBuilder::new() + .timeout(Duration::from_secs(60)) + .build() + .context("Failed to create HTTP client")?; + + let body = json!({"text": message}); + client.post(webhook_url).json(&body).send().await?; + + tracing::info!("Slack通知を送信しました"); + Ok(()) + } +} diff --git a/crates/sos24-presentation/src/env.rs b/crates/sos24-presentation/src/env.rs index 18055c83..bba00672 100644 --- a/crates/sos24-presentation/src/env.rs +++ b/crates/sos24-presentation/src/env.rs @@ -97,3 +97,7 @@ pub fn email_reply_to_address() -> String { pub fn app_url() -> String { env::var("APP_URL").expect("Env `APP_URL` must be set") } + +pub fn slack_webhook_url() -> Option { + env::var("SLACK_WEBHOOK_URL").ok() +} diff --git a/crates/sos24-presentation/src/module.rs b/crates/sos24-presentation/src/module.rs index 7932da1d..0fb477c8 100644 --- a/crates/sos24-presentation/src/module.rs +++ b/crates/sos24-presentation/src/module.rs @@ -29,7 +29,7 @@ pub struct Modules { invitation_use_case: InvitationUseCase, news_use_case: NewsUseCase, file_use_case: FileUseCase, - project_use_case: ProjectUseCase, + project_use_case: ProjectUseCase, user_use_case: UserUseCase, } @@ -58,7 +58,7 @@ impl Modules { &self.file_use_case } - pub fn project_use_case(&self) -> &ProjectUseCase { + pub fn project_use_case(&self) -> &ProjectUseCase { &self.project_use_case } @@ -94,7 +94,10 @@ pub async fn new(config: Config) -> anyhow::Result { )); let send_grid = SendGrid::new(&env::send_grid_api_key()); - let adapters = Arc::new(sos24_infrastructure::DefaultAdapters::new(send_grid)); + let adapters = Arc::new(sos24_infrastructure::DefaultAdapters::new( + send_grid, + env::slack_webhook_url(), + )); let application_period = ProjectApplicationPeriod::new( config.project_application_start_at.clone(), @@ -111,7 +114,11 @@ pub async fn new(config: Config) -> anyhow::Result { ), news_use_case: NewsUseCase::new(Arc::clone(&repositories), Arc::clone(&adapters)), file_use_case: FileUseCase::new(Arc::clone(&repositories)), - project_use_case: ProjectUseCase::new(Arc::clone(&repositories), application_period), + project_use_case: ProjectUseCase::new( + Arc::clone(&repositories), + Arc::clone(&adapters), + application_period, + ), user_use_case: UserUseCase::new(Arc::clone(&repositories)), }) } @@ -136,7 +143,11 @@ pub async fn new_test( ), news_use_case: NewsUseCase::new(Arc::clone(&repositories), Arc::clone(&adapters)), file_use_case: FileUseCase::new(Arc::clone(&repositories)), - project_use_case: ProjectUseCase::new(Arc::clone(&repositories), application_period), + project_use_case: ProjectUseCase::new( + Arc::clone(&repositories), + Arc::clone(&adapters), + application_period, + ), user_use_case: UserUseCase::new(Arc::clone(&repositories)), }) } From 925534be53d6d701b055cf02b1630f2254442e63 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 16:46:25 +0900 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20=E3=82=A8=E3=82=AF=E3=82=B9?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=88=E6=99=82=E3=81=AE=E6=97=A5=E6=99=82?= =?UTF-8?q?=E3=81=AE=E5=BD=A2=E5=BC=8F=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/sos24-presentation/src/model/project.rs | 6 +++++- crates/sos24-presentation/src/model/user.rs | 6 +++++- .../src/form_answer/interactor/export_by_form_id.rs | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/sos24-presentation/src/model/project.rs b/crates/sos24-presentation/src/model/project.rs index 2ff3a2b1..7ac09c2a 100644 --- a/crates/sos24-presentation/src/model/project.rs +++ b/crates/sos24-presentation/src/model/project.rs @@ -181,7 +181,11 @@ impl From for ProjectToBeExported { .collect::>() .join(";"), remarks: project.remarks, - created_at: project.created_at.with_timezone(&Tokyo).to_rfc3339(), + created_at: project + .created_at + .with_timezone(&Tokyo) + .format("%Y-%m-%d %H:%M:%S") + .to_string(), } } } diff --git a/crates/sos24-presentation/src/model/user.rs b/crates/sos24-presentation/src/model/user.rs index 7ea28edc..edf6b6a8 100644 --- a/crates/sos24-presentation/src/model/user.rs +++ b/crates/sos24-presentation/src/model/user.rs @@ -138,7 +138,11 @@ impl From for UserTobeExported { kana_name: user.kana_name, email: user.email, role: user.role.to_string(), - created_at: user.created_at.with_timezone(&Tokyo).to_rfc3339(), + created_at: user + .created_at + .with_timezone(&Tokyo) + .format("%Y-%m-%d %H:%M:%S") + .to_string(), } } } diff --git a/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs b/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs index fb3ddd59..c965ebb0 100644 --- a/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs +++ b/crates/sos24-use-case/src/form_answer/interactor/export_by_form_id.rs @@ -71,7 +71,8 @@ impl FormAnswerUseCase { .created_at .value() .with_timezone(&Tokyo) - .to_rfc3339(); + .format("%Y-%m-%d %H:%M:%S") + .to_string(); (values, Some(created_at)) } None => (form_item_ids.iter().map(|_| None).collect(), None), From 6b0b9fa28c3cab46b0eedaad4c8fd1eded120463 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 16:47:50 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20docker-compose.yml=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-beta.yml | 11 ++++++----- docker-compose-prd.yml | 3 ++- docker-compose-stg.yml | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docker-compose-beta.yml b/docker-compose-beta.yml index bb14079e..ab103a83 100644 --- a/docker-compose-beta.yml +++ b/docker-compose-beta.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: app: @@ -24,6 +24,7 @@ services: - EMAIL_SENDER_ADDRESS=${EMAIL_SENDER_ADDRESS} - EMAIL_REPLY_TO_ADDRESS=${EMAIL_REPLY_TO_ADDRESS} - APP_URL=${APP_URL} + - SLACK_WEBHOOK_URL=${APP_URL} labels: traefik.enable: "true" traefik.docker.network: "portainer-traefik" @@ -63,8 +64,8 @@ volumes: networks: portainer-traefik: - name: portainer-traefik - external: true + name: portainer-traefik + external: true sos24-server-beta: - name: sos24-server-beta - internal: true + name: sos24-server-beta + internal: true diff --git a/docker-compose-prd.yml b/docker-compose-prd.yml index 94041a06..bbfa71d4 100644 --- a/docker-compose-prd.yml +++ b/docker-compose-prd.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: app: @@ -26,6 +26,7 @@ services: - EMAIL_SENDER_ADDRESS=${EMAIL_SENDER_ADDRESS} - EMAIL_REPLY_TO_ADDRESS=${EMAIL_REPLY_TO_ADDRESS} - APP_URL=${APP_URL} + - SLACK_WEBHOOK_URL=${APP_URL} labels: - traefik.enable=true - traefik.docker.network=sos24-traefik diff --git a/docker-compose-stg.yml b/docker-compose-stg.yml index d2b4f227..0d32ae4e 100644 --- a/docker-compose-stg.yml +++ b/docker-compose-stg.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: app: @@ -24,6 +24,7 @@ services: - EMAIL_SENDER_ADDRESS=${EMAIL_SENDER_ADDRESS} - EMAIL_REPLY_TO_ADDRESS=${EMAIL_REPLY_TO_ADDRESS} - APP_URL=${APP_URL} + - SLACK_WEBHOOK_URL=${APP_URL} labels: traefik.enable: "true" traefik.docker.network: "portainer-traefik" From 3dbd82833e9a65863e84c9b3da72ca6f646e55f4 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 17:23:40 +0900 Subject: [PATCH 11/12] =?UTF-8?q?chore:=20docker-compose.yml=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-beta.yml | 2 +- docker-compose-prd.yml | 2 +- docker-compose-stg.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose-beta.yml b/docker-compose-beta.yml index ab103a83..34122d06 100644 --- a/docker-compose-beta.yml +++ b/docker-compose-beta.yml @@ -24,7 +24,7 @@ services: - EMAIL_SENDER_ADDRESS=${EMAIL_SENDER_ADDRESS} - EMAIL_REPLY_TO_ADDRESS=${EMAIL_REPLY_TO_ADDRESS} - APP_URL=${APP_URL} - - SLACK_WEBHOOK_URL=${APP_URL} + - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL} labels: traefik.enable: "true" traefik.docker.network: "portainer-traefik" diff --git a/docker-compose-prd.yml b/docker-compose-prd.yml index bbfa71d4..0b9bef59 100644 --- a/docker-compose-prd.yml +++ b/docker-compose-prd.yml @@ -26,7 +26,7 @@ services: - EMAIL_SENDER_ADDRESS=${EMAIL_SENDER_ADDRESS} - EMAIL_REPLY_TO_ADDRESS=${EMAIL_REPLY_TO_ADDRESS} - APP_URL=${APP_URL} - - SLACK_WEBHOOK_URL=${APP_URL} + - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL} labels: - traefik.enable=true - traefik.docker.network=sos24-traefik diff --git a/docker-compose-stg.yml b/docker-compose-stg.yml index 0d32ae4e..7ab98ca2 100644 --- a/docker-compose-stg.yml +++ b/docker-compose-stg.yml @@ -24,7 +24,7 @@ services: - EMAIL_SENDER_ADDRESS=${EMAIL_SENDER_ADDRESS} - EMAIL_REPLY_TO_ADDRESS=${EMAIL_REPLY_TO_ADDRESS} - APP_URL=${APP_URL} - - SLACK_WEBHOOK_URL=${APP_URL} + - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL} labels: traefik.enable: "true" traefik.docker.network: "portainer-traefik" From d1944b7507eb6b9ec97dd5f5d25825f134128f13 Mon Sep 17 00:00:00 2001 From: Arata Date: Fri, 3 May 2024 21:07:09 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20Slack=E9=80=9A=E7=9F=A5=E3=81=AEUR?= =?UTF-8?q?L=E3=82=92=E5=AE=9F=E5=A7=94=E4=BA=BA=E5=90=91=E3=81=91?= =?UTF-8?q?=E3=81=AE=E3=82=82=E3=81=AE=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/form/interactor/create.rs | 2 +- .../src/news/interactor/create.rs | 2 +- .../src/project/interactor/create.rs | 2 +- crates/sos24-use-case/src/shared/app_url.rs | 24 +++++++++++++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/sos24-use-case/src/form/interactor/create.rs b/crates/sos24-use-case/src/form/interactor/create.rs index ffff7333..b2474c48 100644 --- a/crates/sos24-use-case/src/form/interactor/create.rs +++ b/crates/sos24-use-case/src/form/interactor/create.rs @@ -69,7 +69,7 @@ impl FormUseCase { .notify(format!( "申請「{}」が作成されました。\n{}", form_title.value(), - app_url::form(ctx, form_id.clone()), + app_url::committee_form(ctx, form_id.clone()), )) .await?; diff --git a/crates/sos24-use-case/src/news/interactor/create.rs b/crates/sos24-use-case/src/news/interactor/create.rs index b8a66ed4..ab54530e 100644 --- a/crates/sos24-use-case/src/news/interactor/create.rs +++ b/crates/sos24-use-case/src/news/interactor/create.rs @@ -130,7 +130,7 @@ Email : {email} .notify(format!( "お知らせ「{}」が公開されました。\n{}", news.title().clone().value(), - app_url::news(ctx, news_id.clone()), + app_url::committee_news(ctx, news_id.clone()), )) .await?; diff --git a/crates/sos24-use-case/src/project/interactor/create.rs b/crates/sos24-use-case/src/project/interactor/create.rs index 9978044a..83cdf6d6 100644 --- a/crates/sos24-use-case/src/project/interactor/create.rs +++ b/crates/sos24-use-case/src/project/interactor/create.rs @@ -80,7 +80,7 @@ impl ProjectUseCase { .notify(format!( "企画「{}」が登録されました。\n{}", project_title, - app_url::project(ctx, project_id.clone()), + app_url::committee_project(ctx, project_id.clone()), )) .await?; diff --git a/crates/sos24-use-case/src/shared/app_url.rs b/crates/sos24-use-case/src/shared/app_url.rs index e4e6122d..3c3aed92 100644 --- a/crates/sos24-use-case/src/shared/app_url.rs +++ b/crates/sos24-use-case/src/shared/app_url.rs @@ -10,6 +10,26 @@ pub fn news(ctx: &impl ContextProvider, news_id: NewsId) -> String { format!("{}/news/{}", ctx.config().app_url, news_id.value()) } -pub fn project(ctx: &impl ContextProvider, project_id: ProjectId) -> String { - format!("{}/projects/{}", ctx.config().app_url, project_id.value()) +pub fn committee_form(ctx: &impl ContextProvider, form_id: FormId) -> String { + format!( + "{}/committee/forms/{}", + ctx.config().app_url, + form_id.value() + ) +} + +pub fn committee_news(ctx: &impl ContextProvider, news_id: NewsId) -> String { + format!( + "{}/committee/news/{}", + ctx.config().app_url, + news_id.value() + ) +} + +pub fn committee_project(ctx: &impl ContextProvider, project_id: ProjectId) -> String { + format!( + "{}/committee/projects/{}", + ctx.config().app_url, + project_id.value() + ) }