diff --git a/backend/Cargo.lock b/backend/Cargo.lock index b9189d6..68485b3 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1876,6 +1876,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "label" +version = "0.3.1" +dependencies = [ + "actix-web", + "anyhow", + "async-trait", + "auth", + "common", + "sentry", + "serde", + "sqlx", + "utoipa", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2407,6 +2422,7 @@ dependencies = [ "auth", "chrono", "common", + "label", "regex", "sentry", "serde", @@ -3179,6 +3195,7 @@ dependencies = [ "env_logger", "hex", "image", + "label", "openidconnect", "percent-encoding", "post", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d3092f2..cbbe29c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -4,6 +4,7 @@ members = [ "feature/auth", "feature/common", "feature/image", + "feature/label", "feature/post", ] resolver = "2" @@ -53,4 +54,5 @@ server.path = "server" auth.path = "feature/auth" common.path = "feature/common" image.path = "feature/image" +label.path = "feature/label" post.path = "feature/post" diff --git a/backend/feature/image/src/application/error/image_error.rs b/backend/feature/image/src/application/error/image_error.rs index 0d046b2..b787725 100644 --- a/backend/feature/image/src/application/error/image_error.rs +++ b/backend/feature/image/src/application/error/image_error.rs @@ -7,6 +7,12 @@ pub enum ImageError { Unexpected(anyhow::Error), } +impl Into for ImageError { + fn into(self) -> String { + format!("{}", self) + } +} + impl Display for ImageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/backend/feature/image/src/framework/web/get_image_by_id_handler.rs b/backend/feature/image/src/framework/web/get_image_by_id_handler.rs index e345c8c..593ab8d 100644 --- a/backend/feature/image/src/framework/web/get_image_by_id_handler.rs +++ b/backend/feature/image/src/framework/web/get_image_by_id_handler.rs @@ -15,7 +15,7 @@ use crate::{ summary = "Get image by ID", responses ( (status = 200, body = inline(ResponseBodySchema), content_type = "image/*"), - (status = 404, description = "Image not found") + (status = 404, description = ImageError::NotFound), ) )] pub async fn get_image_by_id_handler( @@ -31,12 +31,12 @@ pub async fn get_image_by_id_handler( .body(image_response.data), Err(e) => match e { ImageError::NotFound => HttpResponse::NotFound().finish(), - ImageError::Unexpected(e) => { - capture_anyhow(&e); + ImageError::UnsupportedMimeType(_) => { + capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } - _ => { - capture_anyhow(&anyhow!(e)); + ImageError::Unexpected(e) => { + capture_anyhow(&e); HttpResponse::InternalServerError().finish() } }, diff --git a/backend/feature/image/src/framework/web/upload_image_handler.rs b/backend/feature/image/src/framework/web/upload_image_handler.rs index 3964a8f..faaf2f1 100644 --- a/backend/feature/image/src/framework/web/upload_image_handler.rs +++ b/backend/feature/image/src/framework/web/upload_image_handler.rs @@ -25,7 +25,7 @@ use crate::{ ), responses ( (status = 201, body = ImageInfoResponseDto), - (status = 400, description = "Unsupported MIME type or file field not found"), + (status = 400, description = ImageError::UnsupportedMimeType("{MIME_TYPE}".to_string())), ), security( ("oauth2" = []) @@ -78,12 +78,12 @@ pub async fn upload_image_handler( ImageError::UnsupportedMimeType(mime_type) => { HttpResponse::BadRequest().body(format!("Unsupported MIME type: {}", mime_type)) } - ImageError::Unexpected(e) => { - capture_anyhow(&e); + ImageError::NotFound => { + capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } - _ => { - capture_anyhow(&anyhow!(e)); + ImageError::Unexpected(e) => { + capture_anyhow(&e); HttpResponse::InternalServerError().finish() } }, diff --git a/backend/feature/label/Cargo.toml b/backend/feature/label/Cargo.toml new file mode 100644 index 0000000..6579e51 --- /dev/null +++ b/backend/feature/label/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "label" +version.workspace = true +edition.workspace = true + +[dependencies] +actix-web.workspace = true +anyhow.workspace = true +async-trait.workspace = true +sentry.workspace = true +serde.workspace = true +sqlx.workspace = true +utoipa.workspace = true + +auth.workspace = true +common.workspace = true diff --git a/backend/feature/label/src/adapter.rs b/backend/feature/label/src/adapter.rs new file mode 100644 index 0000000..a5233c4 --- /dev/null +++ b/backend/feature/label/src/adapter.rs @@ -0,0 +1,2 @@ +pub mod delivery; +pub mod gateway; diff --git a/backend/feature/label/src/adapter/delivery.rs b/backend/feature/label/src/adapter/delivery.rs new file mode 100644 index 0000000..6c3370f --- /dev/null +++ b/backend/feature/label/src/adapter/delivery.rs @@ -0,0 +1,6 @@ +pub mod color_request_dto; +pub mod color_response_dto; +pub mod create_label_request_dto; +pub mod label_controller; +pub mod label_response_dto; +pub mod update_label_request_dto; diff --git a/backend/feature/post/src/adapter/delivery/color_request_dto.rs b/backend/feature/label/src/adapter/delivery/color_request_dto.rs similarity index 100% rename from backend/feature/post/src/adapter/delivery/color_request_dto.rs rename to backend/feature/label/src/adapter/delivery/color_request_dto.rs diff --git a/backend/feature/post/src/adapter/delivery/color_response_dto.rs b/backend/feature/label/src/adapter/delivery/color_response_dto.rs similarity index 100% rename from backend/feature/post/src/adapter/delivery/color_response_dto.rs rename to backend/feature/label/src/adapter/delivery/color_response_dto.rs diff --git a/backend/feature/post/src/adapter/delivery/create_label_request_dto.rs b/backend/feature/label/src/adapter/delivery/create_label_request_dto.rs similarity index 100% rename from backend/feature/post/src/adapter/delivery/create_label_request_dto.rs rename to backend/feature/label/src/adapter/delivery/create_label_request_dto.rs diff --git a/backend/feature/label/src/adapter/delivery/label_controller.rs b/backend/feature/label/src/adapter/delivery/label_controller.rs new file mode 100644 index 0000000..e931adb --- /dev/null +++ b/backend/feature/label/src/adapter/delivery/label_controller.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + adapter::delivery::{ + create_label_request_dto::CreateLabelRequestDto, label_response_dto::LabelResponseDto, + update_label_request_dto::UpdateLabelRequestDto, + }, + application::{ + error::label_error::LabelError, + use_case::{ + create_label_use_case::CreateLabelUseCase, + get_all_labels_use_case::GetAllLabelsUseCase, + update_label_use_case::UpdateLabelUseCase, + }, + }, +}; + +#[async_trait] +pub trait LabelController: Send + Sync { + async fn create_label( + &self, + label: CreateLabelRequestDto, + ) -> Result; + + async fn update_label( + &self, + id: i32, + label: UpdateLabelRequestDto, + ) -> Result; + + async fn get_all_labels(&self) -> Result, LabelError>; +} + +pub struct LabelControllerImpl { + create_label_use_case: Arc, + update_label_use_case: Arc, + get_all_labels_use_case: Arc, +} + +impl LabelControllerImpl { + pub fn new( + create_label_use_case: Arc, + update_label_use_case: Arc, + get_all_labels_use_case: Arc, + ) -> Self { + Self { + create_label_use_case, + update_label_use_case, + get_all_labels_use_case, + } + } +} + +#[async_trait] +impl LabelController for LabelControllerImpl { + async fn create_label( + &self, + label: CreateLabelRequestDto, + ) -> Result { + let mut label_entity = label.into_entity(); + let id = self + .create_label_use_case + .execute(label_entity.clone()) + .await?; + + label_entity.id = id; + Ok(LabelResponseDto::from(label_entity)) + } + + async fn update_label( + &self, + id: i32, + label: UpdateLabelRequestDto, + ) -> Result { + let label_entity = label.into_entity(id); + self.update_label_use_case + .execute(label_entity.clone()) + .await?; + + Ok(LabelResponseDto::from(label_entity)) + } + + async fn get_all_labels(&self) -> Result, LabelError> { + let result = self.get_all_labels_use_case.execute().await; + + result.map(|labels| { + labels + .into_iter() + .map(|label| LabelResponseDto::from(label)) + .collect() + }) + } +} diff --git a/backend/feature/post/src/adapter/delivery/label_response_dto.rs b/backend/feature/label/src/adapter/delivery/label_response_dto.rs similarity index 100% rename from backend/feature/post/src/adapter/delivery/label_response_dto.rs rename to backend/feature/label/src/adapter/delivery/label_response_dto.rs diff --git a/backend/feature/post/src/adapter/delivery/update_label_request_dto.rs b/backend/feature/label/src/adapter/delivery/update_label_request_dto.rs similarity index 100% rename from backend/feature/post/src/adapter/delivery/update_label_request_dto.rs rename to backend/feature/label/src/adapter/delivery/update_label_request_dto.rs diff --git a/backend/feature/label/src/adapter/gateway.rs b/backend/feature/label/src/adapter/gateway.rs new file mode 100644 index 0000000..7ea603d --- /dev/null +++ b/backend/feature/label/src/adapter/gateway.rs @@ -0,0 +1,4 @@ +pub mod color_db_mapper; +pub mod label_db_mapper; +pub mod label_db_service; +pub mod label_repository_impl; diff --git a/backend/feature/post/src/adapter/gateway/color_db_mapper.rs b/backend/feature/label/src/adapter/gateway/color_db_mapper.rs similarity index 100% rename from backend/feature/post/src/adapter/gateway/color_db_mapper.rs rename to backend/feature/label/src/adapter/gateway/color_db_mapper.rs diff --git a/backend/feature/post/src/adapter/gateway/label_db_mapper.rs b/backend/feature/label/src/adapter/gateway/label_db_mapper.rs similarity index 100% rename from backend/feature/post/src/adapter/gateway/label_db_mapper.rs rename to backend/feature/label/src/adapter/gateway/label_db_mapper.rs diff --git a/backend/feature/post/src/adapter/gateway/label_db_service.rs b/backend/feature/label/src/adapter/gateway/label_db_service.rs similarity index 69% rename from backend/feature/post/src/adapter/gateway/label_db_service.rs rename to backend/feature/label/src/adapter/gateway/label_db_service.rs index 64d88de..41bf2e4 100644 --- a/backend/feature/post/src/adapter/gateway/label_db_service.rs +++ b/backend/feature/label/src/adapter/gateway/label_db_service.rs @@ -1,13 +1,13 @@ use async_trait::async_trait; use crate::{ - adapter::gateway::label_db_mapper::LabelMapper, application::error::post_error::PostError, + adapter::gateway::label_db_mapper::LabelMapper, application::error::label_error::LabelError, }; #[async_trait] pub trait LabelDbService: Send + Sync { - async fn create_label(&self, label: LabelMapper) -> Result; - async fn update_label(&self, label: LabelMapper) -> Result<(), PostError>; - async fn get_label_by_id(&self, id: i32) -> Result; - async fn get_all_labels(&self) -> Result, PostError>; + async fn create_label(&self, label: LabelMapper) -> Result; + async fn update_label(&self, label: LabelMapper) -> Result<(), LabelError>; + async fn get_label_by_id(&self, id: i32) -> Result; + async fn get_all_labels(&self) -> Result, LabelError>; } diff --git a/backend/feature/post/src/adapter/gateway/label_repository_impl.rs b/backend/feature/label/src/adapter/gateway/label_repository_impl.rs similarity index 72% rename from backend/feature/post/src/adapter/gateway/label_repository_impl.rs rename to backend/feature/label/src/adapter/gateway/label_repository_impl.rs index 59e6949..69d268c 100644 --- a/backend/feature/post/src/adapter/gateway/label_repository_impl.rs +++ b/backend/feature/label/src/adapter/gateway/label_repository_impl.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use crate::{ adapter::gateway::{label_db_mapper::LabelMapper, label_db_service::LabelDbService}, - application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, + application::{error::label_error::LabelError, gateway::label_repository::LabelRepository}, domain::entity::label::Label, }; @@ -20,26 +20,26 @@ impl LabelRepositoryImpl { #[async_trait] impl LabelRepository for LabelRepositoryImpl { - async fn create_label(&self, label: Label) -> Result { + async fn create_label(&self, label: Label) -> Result { self.label_db_service .create_label(LabelMapper::from(label)) .await } - async fn update_label(&self, label: Label) -> Result<(), PostError> { + async fn update_label(&self, label: Label) -> Result<(), LabelError> { self.label_db_service .update_label(LabelMapper::from(label)) .await } - async fn get_label_by_id(&self, id: i32) -> Result { + async fn get_label_by_id(&self, id: i32) -> Result { self.label_db_service .get_label_by_id(id) .await .map(|mapper| mapper.into_entity()) } - async fn get_all_labels(&self) -> Result, PostError> { + async fn get_all_labels(&self) -> Result, LabelError> { self.label_db_service.get_all_labels().await.map(|mappers| { mappers .into_iter() diff --git a/backend/feature/label/src/application.rs b/backend/feature/label/src/application.rs new file mode 100644 index 0000000..f24625e --- /dev/null +++ b/backend/feature/label/src/application.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod gateway; +pub mod use_case; diff --git a/backend/feature/label/src/application/error.rs b/backend/feature/label/src/application/error.rs new file mode 100644 index 0000000..a373c4c --- /dev/null +++ b/backend/feature/label/src/application/error.rs @@ -0,0 +1 @@ +pub mod label_error; diff --git a/backend/feature/label/src/application/error/label_error.rs b/backend/feature/label/src/application/error/label_error.rs new file mode 100644 index 0000000..3ff2707 --- /dev/null +++ b/backend/feature/label/src/application/error/label_error.rs @@ -0,0 +1,26 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum LabelError { + NotFound, + Unauthorized, + DuplicatedLabelName, + Unexpected(anyhow::Error), +} + +impl Into for LabelError { + fn into(self) -> String { + format!("{}", self) + } +} + +impl Display for LabelError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LabelError::NotFound => write!(f, "Label not found"), + LabelError::Unauthorized => write!(f, "Unauthorized access"), + LabelError::DuplicatedLabelName => write!(f, "Label name already exists"), + LabelError::Unexpected(e) => write!(f, "Unexpected error: {}", e), + } + } +} diff --git a/backend/feature/label/src/application/gateway.rs b/backend/feature/label/src/application/gateway.rs new file mode 100644 index 0000000..3547f18 --- /dev/null +++ b/backend/feature/label/src/application/gateway.rs @@ -0,0 +1 @@ +pub mod label_repository; diff --git a/backend/feature/label/src/application/gateway/label_repository.rs b/backend/feature/label/src/application/gateway/label_repository.rs new file mode 100644 index 0000000..513ad93 --- /dev/null +++ b/backend/feature/label/src/application/gateway/label_repository.rs @@ -0,0 +1,11 @@ +use async_trait::async_trait; + +use crate::{application::error::label_error::LabelError, domain::entity::label::Label}; + +#[async_trait] +pub trait LabelRepository: Send + Sync { + async fn create_label(&self, label: Label) -> Result; + async fn update_label(&self, label: Label) -> Result<(), LabelError>; + async fn get_label_by_id(&self, id: i32) -> Result; + async fn get_all_labels(&self) -> Result, LabelError>; +} diff --git a/backend/feature/label/src/application/use_case.rs b/backend/feature/label/src/application/use_case.rs new file mode 100644 index 0000000..883d99d --- /dev/null +++ b/backend/feature/label/src/application/use_case.rs @@ -0,0 +1,3 @@ +pub mod create_label_use_case; +pub mod get_all_labels_use_case; +pub mod update_label_use_case; diff --git a/backend/feature/post/src/application/use_case/create_label_use_case.rs b/backend/feature/label/src/application/use_case/create_label_use_case.rs similarity index 69% rename from backend/feature/post/src/application/use_case/create_label_use_case.rs rename to backend/feature/label/src/application/use_case/create_label_use_case.rs index 8d6c64e..b47be5f 100644 --- a/backend/feature/post/src/application/use_case/create_label_use_case.rs +++ b/backend/feature/label/src/application/use_case/create_label_use_case.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use async_trait::async_trait; use crate::{ - application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, + application::{error::label_error::LabelError, gateway::label_repository::LabelRepository}, domain::entity::label::Label, }; #[async_trait] pub trait CreateLabelUseCase: Send + Sync { - async fn execute(&self, label: Label) -> Result; + async fn execute(&self, label: Label) -> Result; } pub struct CreateLabelUseCaseImpl { @@ -24,7 +24,7 @@ impl CreateLabelUseCaseImpl { #[async_trait] impl CreateLabelUseCase for CreateLabelUseCaseImpl { - async fn execute(&self, label: Label) -> Result { + async fn execute(&self, label: Label) -> Result { self.label_repository.create_label(label).await } } diff --git a/backend/feature/post/src/application/use_case/get_all_labels_use_case.rs b/backend/feature/label/src/application/use_case/get_all_labels_use_case.rs similarity index 70% rename from backend/feature/post/src/application/use_case/get_all_labels_use_case.rs rename to backend/feature/label/src/application/use_case/get_all_labels_use_case.rs index 7a2df8e..0710ad9 100644 --- a/backend/feature/post/src/application/use_case/get_all_labels_use_case.rs +++ b/backend/feature/label/src/application/use_case/get_all_labels_use_case.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use async_trait::async_trait; use crate::{ - application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, + application::{error::label_error::LabelError, gateway::label_repository::LabelRepository}, domain::entity::label::Label, }; #[async_trait] pub trait GetAllLabelsUseCase: Send + Sync { - async fn execute(&self) -> Result, PostError>; + async fn execute(&self) -> Result, LabelError>; } pub struct GetAllLabelsUseCaseImpl { @@ -24,7 +24,7 @@ impl GetAllLabelsUseCaseImpl { #[async_trait] impl GetAllLabelsUseCase for GetAllLabelsUseCaseImpl { - async fn execute(&self) -> Result, PostError> { + async fn execute(&self) -> Result, LabelError> { self.label_repository.get_all_labels().await } } diff --git a/backend/feature/post/src/application/use_case/update_label_use_case.rs b/backend/feature/label/src/application/use_case/update_label_use_case.rs similarity index 69% rename from backend/feature/post/src/application/use_case/update_label_use_case.rs rename to backend/feature/label/src/application/use_case/update_label_use_case.rs index b2678aa..3973648 100644 --- a/backend/feature/post/src/application/use_case/update_label_use_case.rs +++ b/backend/feature/label/src/application/use_case/update_label_use_case.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use async_trait::async_trait; use crate::{ - application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, + application::{error::label_error::LabelError, gateway::label_repository::LabelRepository}, domain::entity::label::Label, }; #[async_trait] pub trait UpdateLabelUseCase: Send + Sync { - async fn execute(&self, label: Label) -> Result<(), PostError>; + async fn execute(&self, label: Label) -> Result<(), LabelError>; } pub struct UpdateLabelUseCaseImpl { @@ -24,7 +24,7 @@ impl UpdateLabelUseCaseImpl { #[async_trait] impl UpdateLabelUseCase for UpdateLabelUseCaseImpl { - async fn execute(&self, label: Label) -> Result<(), PostError> { + async fn execute(&self, label: Label) -> Result<(), LabelError> { self.label_repository.update_label(label).await } } diff --git a/backend/feature/label/src/domain.rs b/backend/feature/label/src/domain.rs new file mode 100644 index 0000000..e8c3d6a --- /dev/null +++ b/backend/feature/label/src/domain.rs @@ -0,0 +1 @@ +pub mod entity; diff --git a/backend/feature/label/src/domain/entity.rs b/backend/feature/label/src/domain/entity.rs new file mode 100644 index 0000000..917a727 --- /dev/null +++ b/backend/feature/label/src/domain/entity.rs @@ -0,0 +1,2 @@ +pub mod color; +pub mod label; diff --git a/backend/feature/post/src/domain/entity/color.rs b/backend/feature/label/src/domain/entity/color.rs similarity index 100% rename from backend/feature/post/src/domain/entity/color.rs rename to backend/feature/label/src/domain/entity/color.rs diff --git a/backend/feature/post/src/domain/entity/label.rs b/backend/feature/label/src/domain/entity/label.rs similarity index 100% rename from backend/feature/post/src/domain/entity/label.rs rename to backend/feature/label/src/domain/entity/label.rs diff --git a/backend/feature/label/src/framework.rs b/backend/feature/label/src/framework.rs new file mode 100644 index 0000000..1f54796 --- /dev/null +++ b/backend/feature/label/src/framework.rs @@ -0,0 +1,2 @@ +pub mod db; +pub mod web; diff --git a/backend/feature/label/src/framework/db.rs b/backend/feature/label/src/framework/db.rs new file mode 100644 index 0000000..ae57f9e --- /dev/null +++ b/backend/feature/label/src/framework/db.rs @@ -0,0 +1,3 @@ +pub mod label_db_service_impl; + +mod label_record; diff --git a/backend/feature/post/src/framework/db/label_db_service_impl.rs b/backend/feature/label/src/framework/db/label_db_service_impl.rs similarity index 79% rename from backend/feature/post/src/framework/db/label_db_service_impl.rs rename to backend/feature/label/src/framework/db/label_db_service_impl.rs index 9a0d775..3e4815d 100644 --- a/backend/feature/post/src/framework/db/label_db_service_impl.rs +++ b/backend/feature/label/src/framework/db/label_db_service_impl.rs @@ -4,7 +4,7 @@ use sqlx::{Pool, Postgres}; use crate::{ adapter::gateway::{label_db_mapper::LabelMapper, label_db_service::LabelDbService}, - application::error::post_error::PostError, + application::error::label_error::LabelError, framework::db::label_record::LabelRecord, }; @@ -20,7 +20,7 @@ impl LabelDbServiceImpl { #[async_trait] impl LabelDbService for LabelDbServiceImpl { - async fn create_label(&self, label: LabelMapper) -> Result { + async fn create_label(&self, label: LabelMapper) -> Result { let id = sqlx::query_scalar!( r#" INSERT INTO label (name, color) @@ -35,16 +35,16 @@ impl LabelDbService for LabelDbServiceImpl { .map_err(|e| { if let sqlx::Error::Database(db_err) = &e { if db_err.constraint() == Some("idx_label_name") { - return PostError::DuplicatedLabelName; + return LabelError::DuplicatedLabelName; } } - PostError::Unexpected(DatabaseError(e).into()) + LabelError::Unexpected(DatabaseError(e).into()) })?; Ok(id) } - async fn update_label(&self, label: LabelMapper) -> Result<(), PostError> { + async fn update_label(&self, label: LabelMapper) -> Result<(), LabelError> { let affected_rows = sqlx::query!( r#" UPDATE label @@ -60,21 +60,21 @@ impl LabelDbService for LabelDbServiceImpl { .map_err(|e| { if let sqlx::Error::Database(db_err) = &e { if db_err.constraint() == Some("idx_label_name") { - return PostError::DuplicatedLabelName; + return LabelError::DuplicatedLabelName; } } - PostError::Unexpected(DatabaseError(e).into()) + LabelError::Unexpected(DatabaseError(e).into()) })? .rows_affected(); if affected_rows == 0 { - return Err(PostError::NotFound); + return Err(LabelError::NotFound); } Ok(()) } - async fn get_label_by_id(&self, id: i32) -> Result { + async fn get_label_by_id(&self, id: i32) -> Result { let record = sqlx::query_as!( LabelRecord, r#" @@ -86,15 +86,15 @@ impl LabelDbService for LabelDbServiceImpl { ) .fetch_optional(&self.db_pool) .await - .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?; + .map_err(|e| LabelError::Unexpected(DatabaseError(e).into()))?; match record { Some(record) => Ok(record.into_mapper()), - None => Err(PostError::NotFound), + None => Err(LabelError::NotFound), } } - async fn get_all_labels(&self) -> Result, PostError> { + async fn get_all_labels(&self) -> Result, LabelError> { let records = sqlx::query_as!( LabelRecord, r#" @@ -106,7 +106,7 @@ impl LabelDbService for LabelDbServiceImpl { ) .fetch_all(&self.db_pool) .await - .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?; + .map_err(|e| LabelError::Unexpected(DatabaseError(e).into()))?; let mappers = records .into_iter() diff --git a/backend/feature/post/src/framework/db/label_record.rs b/backend/feature/label/src/framework/db/label_record.rs similarity index 100% rename from backend/feature/post/src/framework/db/label_record.rs rename to backend/feature/label/src/framework/db/label_record.rs diff --git a/backend/feature/label/src/framework/web.rs b/backend/feature/label/src/framework/web.rs new file mode 100644 index 0000000..d9c80eb --- /dev/null +++ b/backend/feature/label/src/framework/web.rs @@ -0,0 +1,5 @@ +pub mod create_label_handler; +pub mod get_all_labels_handler; +pub mod label_api_doc; +pub mod label_web_routes; +pub mod update_label_handler; diff --git a/backend/feature/post/src/framework/web/create_label_handler.rs b/backend/feature/label/src/framework/web/create_label_handler.rs similarity index 55% rename from backend/feature/post/src/framework/web/create_label_handler.rs rename to backend/feature/label/src/framework/web/create_label_handler.rs index ca1fda6..fe36657 100644 --- a/backend/feature/post/src/framework/web/create_label_handler.rs +++ b/backend/feature/label/src/framework/web/create_label_handler.rs @@ -5,43 +5,43 @@ use sentry::integrations::anyhow::capture_anyhow; use crate::{ adapter::delivery::{ - create_label_request_dto::CreateLabelRequestDto, label_response_dto::LabelResponseDto, - post_controller::PostController, + create_label_request_dto::CreateLabelRequestDto, label_controller::LabelController, + label_response_dto::LabelResponseDto, }, - application::error::post_error::PostError, + application::error::label_error::LabelError, }; #[utoipa::path( post, path = "/label", - tag = "post", + tag = "label", summary = "Create a new label", responses( (status = 201, body = LabelResponseDto), + (status = 401, description = LabelError::Unauthorized), + (status = 409, description = LabelError::DuplicatedLabelName), ), security( ("oauth2" = []) ) )] pub async fn create_label_handler( - post_controller: web::Data, + label_controller: web::Data, label_dto: web::Json, _: UserId, ) -> impl Responder { - let result = post_controller.create_label(label_dto.into_inner()).await; + let result = label_controller.create_label(label_dto.into_inner()).await; match result { Ok(label) => HttpResponse::Created().json(label), Err(e) => match e { - PostError::Unauthorized => HttpResponse::Unauthorized().finish(), - PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(), - PostError::NotFound - | PostError::InvalidSemanticId - | PostError::DuplicatedSemanticId => { + LabelError::Unauthorized => HttpResponse::Unauthorized().finish(), + LabelError::DuplicatedLabelName => HttpResponse::Conflict().finish(), + LabelError::NotFound => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } - PostError::Unexpected(e) => { + LabelError::Unexpected(e) => { capture_anyhow(&e); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/get_all_labels_handler.rs b/backend/feature/label/src/framework/web/get_all_labels_handler.rs similarity index 57% rename from backend/feature/post/src/framework/web/get_all_labels_handler.rs rename to backend/feature/label/src/framework/web/get_all_labels_handler.rs index 7ad0cda..455f708 100644 --- a/backend/feature/post/src/framework/web/get_all_labels_handler.rs +++ b/backend/feature/label/src/framework/web/get_all_labels_handler.rs @@ -3,36 +3,32 @@ use anyhow::anyhow; use sentry::integrations::anyhow::capture_anyhow; use crate::{ - adapter::delivery::{label_response_dto::LabelResponseDto, post_controller::PostController}, - application::error::post_error::PostError, + adapter::delivery::{label_controller::LabelController, label_response_dto::LabelResponseDto}, + application::error::label_error::LabelError, }; #[utoipa::path( get, path = "/label", - tag = "post", + tag = "label", summary = "Get all labels", responses( (status = 200, body = Vec) ) )] pub async fn get_all_labels_handler( - post_controller: web::Data, + label_controller: web::Data, ) -> impl Responder { - let result = post_controller.get_all_labels().await; + let result = label_controller.get_all_labels().await; match result { Ok(labels) => HttpResponse::Ok().json(labels), Err(e) => match e { - PostError::NotFound - | PostError::Unauthorized - | PostError::InvalidSemanticId - | PostError::DuplicatedSemanticId - | PostError::DuplicatedLabelName => { + LabelError::NotFound | LabelError::Unauthorized | LabelError::DuplicatedLabelName => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } - PostError::Unexpected(e) => { + LabelError::Unexpected(e) => { capture_anyhow(&e); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/label/src/framework/web/label_api_doc.rs b/backend/feature/label/src/framework/web/label_api_doc.rs new file mode 100644 index 0000000..5e44d95 --- /dev/null +++ b/backend/feature/label/src/framework/web/label_api_doc.rs @@ -0,0 +1,14 @@ +use crate::framework::web::{create_label_handler, get_all_labels_handler, update_label_handler}; +use utoipa::{OpenApi, openapi}; + +#[derive(OpenApi)] +#[openapi(paths( + create_label_handler::create_label_handler, + update_label_handler::update_label_handler, + get_all_labels_handler::get_all_labels_handler +))] +struct ApiDoc; + +pub fn openapi() -> openapi::OpenApi { + ApiDoc::openapi() +} diff --git a/backend/feature/label/src/framework/web/label_web_routes.rs b/backend/feature/label/src/framework/web/label_web_routes.rs new file mode 100644 index 0000000..f3d2701 --- /dev/null +++ b/backend/feature/label/src/framework/web/label_web_routes.rs @@ -0,0 +1,15 @@ +use actix_web::web; + +use crate::framework::web::{ + create_label_handler::create_label_handler, get_all_labels_handler::get_all_labels_handler, + update_label_handler::update_label_handler, +}; + +pub fn configure_label_routes(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/label") + .route("", web::get().to(get_all_labels_handler)) + .route("", web::post().to(create_label_handler)) + .route("/{id}", web::put().to(update_label_handler)), + ); +} diff --git a/backend/feature/post/src/framework/web/update_label_handler.rs b/backend/feature/label/src/framework/web/update_label_handler.rs similarity index 54% rename from backend/feature/post/src/framework/web/update_label_handler.rs rename to backend/feature/label/src/framework/web/update_label_handler.rs index f72352f..5271bb0 100644 --- a/backend/feature/post/src/framework/web/update_label_handler.rs +++ b/backend/feature/label/src/framework/web/update_label_handler.rs @@ -1,51 +1,48 @@ use actix_web::{HttpResponse, Responder, web}; -use anyhow::anyhow; use auth::framework::web::auth_middleware::UserId; use sentry::integrations::anyhow::capture_anyhow; use crate::{ adapter::delivery::{ - label_response_dto::LabelResponseDto, post_controller::PostController, + label_controller::LabelController, label_response_dto::LabelResponseDto, update_label_request_dto::UpdateLabelRequestDto, }, - application::error::post_error::PostError, + application::error::label_error::LabelError, }; #[utoipa::path( put, path = "/label/{id}", - tag = "post", + tag = "label", summary = "Update a label by ID", responses( (status = 200, body = LabelResponseDto), - (status = 404, description = "Label not found"), + (status = 401, description = LabelError::Unauthorized), + (status = 404, description = LabelError::NotFound), + (status = 409, description = LabelError::DuplicatedLabelName), ), security( ("oauth2" = []) ) )] pub async fn update_label_handler( - post_controller: web::Data, + label_controller: web::Data, label_dto: web::Json, path: web::Path, _: UserId, ) -> impl Responder { let id = path.into_inner(); - let result = post_controller + let result = label_controller .update_label(id, label_dto.into_inner()) .await; match result { Ok(label) => HttpResponse::Ok().json(label), Err(e) => match e { - PostError::NotFound => HttpResponse::NotFound().finish(), - PostError::Unauthorized => HttpResponse::Unauthorized().finish(), - PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(), - PostError::InvalidSemanticId | PostError::DuplicatedSemanticId => { - capture_anyhow(&anyhow!(e)); - HttpResponse::InternalServerError().finish() - } - PostError::Unexpected(e) => { + LabelError::NotFound => HttpResponse::NotFound().finish(), + LabelError::Unauthorized => HttpResponse::Unauthorized().finish(), + LabelError::DuplicatedLabelName => HttpResponse::Conflict().finish(), + LabelError::Unexpected(e) => { capture_anyhow(&e); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/label/src/lib.rs b/backend/feature/label/src/lib.rs new file mode 100644 index 0000000..062f800 --- /dev/null +++ b/backend/feature/label/src/lib.rs @@ -0,0 +1,4 @@ +pub mod adapter; +pub mod application; +pub mod domain; +pub mod framework; diff --git a/backend/feature/post/Cargo.toml b/backend/feature/post/Cargo.toml index aecb5fd..aa7e125 100644 --- a/backend/feature/post/Cargo.toml +++ b/backend/feature/post/Cargo.toml @@ -16,3 +16,4 @@ utoipa.workspace = true auth.workspace = true common.workspace = true +label.workspace = true diff --git a/backend/feature/post/src/adapter/delivery.rs b/backend/feature/post/src/adapter/delivery.rs index 67542af..a40703b 100644 --- a/backend/feature/post/src/adapter/delivery.rs +++ b/backend/feature/post/src/adapter/delivery.rs @@ -1,11 +1,6 @@ -pub mod color_request_dto; -pub mod color_response_dto; -pub mod create_label_request_dto; pub mod create_post_request_dto; -pub mod label_response_dto; pub mod post_controller; pub mod post_info_query_dto; pub mod post_info_response_dto; pub mod post_response_dto; -pub mod update_label_request_dto; pub mod update_post_request_dto; diff --git a/backend/feature/post/src/adapter/delivery/post_controller.rs b/backend/feature/post/src/adapter/delivery/post_controller.rs index 56c9034..7941d79 100644 --- a/backend/feature/post/src/adapter/delivery/post_controller.rs +++ b/backend/feature/post/src/adapter/delivery/post_controller.rs @@ -4,28 +4,18 @@ use async_trait::async_trait; use crate::{ adapter::delivery::{ - create_label_request_dto::CreateLabelRequestDto, create_post_request_dto::CreatePostRequestDto, post_info_query_dto::PostQueryDto, - update_label_request_dto::UpdateLabelRequestDto, update_post_request_dto::UpdatePostRequestDto, }, application::{ error::post_error::PostError, use_case::{ - create_label_use_case::CreateLabelUseCase, create_post_use_case::CreatePostUseCase, - get_all_labels_use_case::GetAllLabelsUseCase, - get_all_post_info_use_case::GetAllPostInfoUseCase, - get_post_by_id_use_case::GetPostByIdUseCase, - get_post_by_semantic_id_use_case::GetPostBySemanticIdUseCase, - update_label_use_case::UpdateLabelUseCase, update_post_use_case::UpdatePostUseCase, + create_post_use_case::CreatePostUseCase, get_all_post_info_use_case::GetAllPostInfoUseCase, get_post_by_id_use_case::GetPostByIdUseCase, get_post_by_semantic_id_use_case::GetPostBySemanticIdUseCase, update_post_use_case::UpdatePostUseCase }, }, }; -use super::{ - label_response_dto::LabelResponseDto, post_info_response_dto::PostInfoResponseDto, - post_response_dto::PostResponseDto, -}; +use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::PostResponseDto}; #[async_trait] pub trait PostController: Send + Sync { @@ -53,19 +43,6 @@ pub trait PostController: Send + Sync { post: UpdatePostRequestDto, user_id: i32, ) -> Result; - - async fn create_label( - &self, - label: CreateLabelRequestDto, - ) -> Result; - - async fn update_label( - &self, - id: i32, - label: UpdateLabelRequestDto, - ) -> Result; - - async fn get_all_labels(&self) -> Result, PostError>; } pub struct PostControllerImpl { @@ -74,9 +51,6 @@ pub struct PostControllerImpl { get_post_by_semantic_id_use_case: Arc, create_post_use_case: Arc, update_post_use_case: Arc, - create_label_use_case: Arc, - update_label_use_case: Arc, - get_all_labels_use_case: Arc, } impl PostControllerImpl { @@ -86,9 +60,6 @@ impl PostControllerImpl { get_post_by_semantic_id_use_case: Arc, create_post_use_case: Arc, update_post_use_case: Arc, - create_label_use_case: Arc, - update_label_use_case: Arc, - get_all_labels_use_case: Arc, ) -> Self { Self { get_all_post_info_use_case, @@ -96,9 +67,6 @@ impl PostControllerImpl { get_post_by_semantic_id_use_case, create_post_use_case, update_post_use_case, - create_label_use_case, - update_label_use_case, - get_all_labels_use_case, } } @@ -161,44 +129,6 @@ impl PostController for PostControllerImpl { } } - async fn create_label( - &self, - label: CreateLabelRequestDto, - ) -> Result { - let mut label_entity = label.into_entity(); - let id = self - .create_label_use_case - .execute(label_entity.clone()) - .await?; - - label_entity.id = id; - Ok(LabelResponseDto::from(label_entity)) - } - - async fn update_label( - &self, - id: i32, - label: UpdateLabelRequestDto, - ) -> Result { - let label_entity = label.into_entity(id); - self.update_label_use_case - .execute(label_entity.clone()) - .await?; - - Ok(LabelResponseDto::from(label_entity)) - } - - async fn get_all_labels(&self) -> Result, PostError> { - let result = self.get_all_labels_use_case.execute().await; - - result.map(|labels| { - labels - .into_iter() - .map(|label| LabelResponseDto::from(label)) - .collect() - }) - } - async fn create_post( &self, post: CreatePostRequestDto, diff --git a/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs b/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs index 7553423..a535ef7 100644 --- a/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs @@ -1,10 +1,9 @@ +use label::adapter::delivery::label_response_dto::LabelResponseDto; use serde::Serialize; use utoipa::ToSchema; use crate::domain::entity::post_info::PostInfo; -use super::label_response_dto::LabelResponseDto; - #[derive(Serialize, ToSchema)] pub struct PostInfoResponseDto { pub id: i32, diff --git a/backend/feature/post/src/adapter/gateway.rs b/backend/feature/post/src/adapter/gateway.rs index aa4960d..009b434 100644 --- a/backend/feature/post/src/adapter/gateway.rs +++ b/backend/feature/post/src/adapter/gateway.rs @@ -1,7 +1,3 @@ -pub mod color_db_mapper; -pub mod label_db_mapper; -pub mod label_db_service; -pub mod label_repository_impl; pub mod post_db_mapper; pub mod post_db_service; pub mod post_info_db_mapper; diff --git a/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs b/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs index 9478738..796cf5a 100644 --- a/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs +++ b/backend/feature/post/src/adapter/gateway/post_info_db_mapper.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; +use label::adapter::gateway::label_db_mapper::LabelMapper; -use crate::{adapter::gateway::label_db_mapper::LabelMapper, domain::entity::post_info::PostInfo}; +use crate::domain::entity::post_info::PostInfo; pub struct PostInfoMapper { pub id: i32, diff --git a/backend/feature/post/src/application/error/post_error.rs b/backend/feature/post/src/application/error/post_error.rs index 275efcc..e213e89 100644 --- a/backend/feature/post/src/application/error/post_error.rs +++ b/backend/feature/post/src/application/error/post_error.rs @@ -6,21 +6,25 @@ pub enum PostError { Unauthorized, InvalidSemanticId, DuplicatedSemanticId, - DuplicatedLabelName, Unexpected(anyhow::Error), } +impl Into for PostError { + fn into(self) -> String { + format!("{}", self) + } +} + impl Display for PostError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PostError::NotFound => write!(f, "Post not found"), - PostError::Unauthorized => write!(f, "Unauthorized access to post"), + PostError::Unauthorized => write!(f, "Unauthorized access"), PostError::InvalidSemanticId => write!( f, - "Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_\\-]+$`" + "Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_-]+$`" ), PostError::DuplicatedSemanticId => write!(f, "Semantic ID already exists"), - PostError::DuplicatedLabelName => write!(f, "Label name already exists"), PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e), } } diff --git a/backend/feature/post/src/application/gateway.rs b/backend/feature/post/src/application/gateway.rs index 885ce2c..23c53d2 100644 --- a/backend/feature/post/src/application/gateway.rs +++ b/backend/feature/post/src/application/gateway.rs @@ -1,2 +1 @@ -pub mod label_repository; pub mod post_repository; diff --git a/backend/feature/post/src/application/gateway/label_repository.rs b/backend/feature/post/src/application/gateway/label_repository.rs deleted file mode 100644 index dcc866a..0000000 --- a/backend/feature/post/src/application/gateway/label_repository.rs +++ /dev/null @@ -1,11 +0,0 @@ -use async_trait::async_trait; - -use crate::{application::error::post_error::PostError, domain::entity::label::Label}; - -#[async_trait] -pub trait LabelRepository: Send + Sync { - async fn create_label(&self, label: Label) -> Result; - async fn update_label(&self, label: Label) -> Result<(), PostError>; - async fn get_label_by_id(&self, id: i32) -> Result; - async fn get_all_labels(&self) -> Result, PostError>; -} diff --git a/backend/feature/post/src/application/use_case.rs b/backend/feature/post/src/application/use_case.rs index 3afc291..2d3c8ca 100644 --- a/backend/feature/post/src/application/use_case.rs +++ b/backend/feature/post/src/application/use_case.rs @@ -1,8 +1,5 @@ -pub mod create_label_use_case; pub mod create_post_use_case; -pub mod get_all_labels_use_case; pub mod get_all_post_info_use_case; pub mod get_post_by_id_use_case; pub mod get_post_by_semantic_id_use_case; -pub mod update_label_use_case; pub mod update_post_use_case; diff --git a/backend/feature/post/src/domain/entity.rs b/backend/feature/post/src/domain/entity.rs index ed8d91b..32aa001 100644 --- a/backend/feature/post/src/domain/entity.rs +++ b/backend/feature/post/src/domain/entity.rs @@ -1,4 +1,2 @@ -pub mod color; -pub mod label; pub mod post_info; pub mod post; diff --git a/backend/feature/post/src/domain/entity/post_info.rs b/backend/feature/post/src/domain/entity/post_info.rs index 3aebd5a..95b6e38 100644 --- a/backend/feature/post/src/domain/entity/post_info.rs +++ b/backend/feature/post/src/domain/entity/post_info.rs @@ -1,10 +1,9 @@ use chrono::{DateTime, Utc}; +use label::domain::entity::label::Label; use regex::Regex; use crate::application::error::post_error::PostError; -use super::label::Label; - pub struct PostInfo { pub id: i32, pub semantic_id: String, @@ -21,7 +20,7 @@ impl PostInfo { return Err(PostError::InvalidSemanticId); } - let re = Regex::new(r"^[0-9a-zA-Z_\-]+$").unwrap(); + let re = Regex::new(r"^[0-9a-zA-Z_-]+$").unwrap(); if !re.is_match(&self.semantic_id) { return Err(PostError::InvalidSemanticId); } diff --git a/backend/feature/post/src/framework/db.rs b/backend/feature/post/src/framework/db.rs index 9aee388..3c0dcdf 100644 --- a/backend/feature/post/src/framework/db.rs +++ b/backend/feature/post/src/framework/db.rs @@ -1,6 +1,4 @@ -pub mod label_db_service_impl; pub mod post_db_service_impl; -mod label_record; mod post_info_with_label_record; mod post_with_label_record; diff --git a/backend/feature/post/src/framework/db/post_db_service_impl.rs b/backend/feature/post/src/framework/db/post_db_service_impl.rs index ac68214..db08ab1 100644 --- a/backend/feature/post/src/framework/db/post_db_service_impl.rs +++ b/backend/feature/post/src/framework/db/post_db_service_impl.rs @@ -2,12 +2,13 @@ use std::collections::HashMap; use async_trait::async_trait; use common::framework::error::DatabaseError; +use label::adapter::gateway::{color_db_mapper::ColorMapper, label_db_mapper::LabelMapper}; use sqlx::{Pool, Postgres}; use crate::{ adapter::gateway::{ - color_db_mapper::ColorMapper, label_db_mapper::LabelMapper, post_db_mapper::PostMapper, - post_db_service::PostDbService, post_info_db_mapper::PostInfoMapper, + post_db_mapper::PostMapper, post_db_service::PostDbService, + post_info_db_mapper::PostInfoMapper, }, application::error::post_error::PostError, }; diff --git a/backend/feature/post/src/framework/web.rs b/backend/feature/post/src/framework/web.rs index f066568..23b6ca0 100644 --- a/backend/feature/post/src/framework/web.rs +++ b/backend/feature/post/src/framework/web.rs @@ -1,10 +1,7 @@ pub mod post_api_doc; pub mod post_web_routes; -mod create_label_handler; mod create_post_handler; -mod get_all_labels_handler; mod get_all_post_info_handler; mod get_post_by_id_handler; -mod update_label_handler; mod update_post_handler; diff --git a/backend/feature/post/src/framework/web/create_post_handler.rs b/backend/feature/post/src/framework/web/create_post_handler.rs index e884022..15af5ed 100644 --- a/backend/feature/post/src/framework/web/create_post_handler.rs +++ b/backend/feature/post/src/framework/web/create_post_handler.rs @@ -18,6 +18,9 @@ use crate::{ summary = "Create a new post", responses( (status = 201, body = PostResponseDto), + (status = 400, description = PostError::InvalidSemanticId), + (status = 401, description = PostError::Unauthorized), + (status = 409, description = PostError::DuplicatedSemanticId), ), security( ("oauth2" = []) @@ -38,7 +41,7 @@ pub async fn create_post_handler( PostError::Unauthorized => HttpResponse::Unauthorized().finish(), PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(), PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(), - PostError::NotFound | PostError::DuplicatedLabelName => { + PostError::NotFound => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/get_all_post_info_handler.rs b/backend/feature/post/src/framework/web/get_all_post_info_handler.rs index f3f084d..0e838da 100644 --- a/backend/feature/post/src/framework/web/get_all_post_info_handler.rs +++ b/backend/feature/post/src/framework/web/get_all_post_info_handler.rs @@ -39,8 +39,7 @@ pub async fn get_all_post_info_handler( PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId - | PostError::DuplicatedSemanticId - | PostError::DuplicatedLabelName => { + | PostError::DuplicatedSemanticId => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs index 9705eb2..da4227b 100644 --- a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs +++ b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs @@ -16,7 +16,8 @@ use crate::{ description = "Only authenticated users can access unpublished posts. Accepts either numeric ID or semantic ID.", responses ( (status = 200, body = PostResponseDto), - (status = 404, description = "Post not found") + (status = 401, description = PostError::Unauthorized), + (status = 404, description = PostError::NotFound), ) )] pub async fn get_post_by_id_handler( @@ -34,9 +35,7 @@ pub async fn get_post_by_id_handler( Err(e) => match e { PostError::NotFound => HttpResponse::NotFound().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(), - PostError::InvalidSemanticId - | PostError::DuplicatedSemanticId - | PostError::DuplicatedLabelName => { + PostError::InvalidSemanticId | PostError::DuplicatedSemanticId => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/post_api_doc.rs b/backend/feature/post/src/framework/web/post_api_doc.rs index 9353710..2bde3c4 100644 --- a/backend/feature/post/src/framework/web/post_api_doc.rs +++ b/backend/feature/post/src/framework/web/post_api_doc.rs @@ -1,6 +1,5 @@ use crate::framework::web::{ - create_label_handler, create_post_handler, get_all_labels_handler, get_all_post_info_handler, - get_post_by_id_handler, update_label_handler, update_post_handler, + create_post_handler, get_all_post_info_handler, get_post_by_id_handler, update_post_handler, }; use utoipa::{OpenApi, openapi}; @@ -10,9 +9,6 @@ use utoipa::{OpenApi, openapi}; get_post_by_id_handler::get_post_by_id_handler, create_post_handler::create_post_handler, update_post_handler::update_post_handler, - create_label_handler::create_label_handler, - update_label_handler::update_label_handler, - get_all_labels_handler::get_all_labels_handler ))] struct ApiDoc; diff --git a/backend/feature/post/src/framework/web/post_web_routes.rs b/backend/feature/post/src/framework/web/post_web_routes.rs index 220b1ee..27bef97 100644 --- a/backend/feature/post/src/framework/web/post_web_routes.rs +++ b/backend/feature/post/src/framework/web/post_web_routes.rs @@ -1,11 +1,8 @@ use actix_web::web; use crate::framework::web::{ - create_label_handler::create_label_handler, create_post_handler::create_post_handler, - get_all_labels_handler::get_all_labels_handler, - get_all_post_info_handler::get_all_post_info_handler, - get_post_by_id_handler::get_post_by_id_handler, update_label_handler::update_label_handler, - update_post_handler::update_post_handler, + create_post_handler::create_post_handler, get_all_post_info_handler::get_all_post_info_handler, + get_post_by_id_handler::get_post_by_id_handler, update_post_handler::update_post_handler, }; pub fn configure_post_routes(cfg: &mut web::ServiceConfig) { @@ -16,11 +13,4 @@ pub fn configure_post_routes(cfg: &mut web::ServiceConfig) { .route("/{id}", web::get().to(get_post_by_id_handler)) .route("/{id}", web::put().to(update_post_handler)), ); - - cfg.service( - web::scope("/label") - .route("", web::get().to(get_all_labels_handler)) - .route("", web::post().to(create_label_handler)) - .route("/{id}", web::put().to(update_label_handler)), - ); } diff --git a/backend/feature/post/src/framework/web/update_post_handler.rs b/backend/feature/post/src/framework/web/update_post_handler.rs index ef256b3..28cab9a 100644 --- a/backend/feature/post/src/framework/web/update_post_handler.rs +++ b/backend/feature/post/src/framework/web/update_post_handler.rs @@ -1,5 +1,4 @@ use actix_web::{HttpResponse, Responder, web}; -use anyhow::anyhow; use auth::framework::web::auth_middleware::UserId; use sentry::integrations::anyhow::capture_anyhow; @@ -18,7 +17,10 @@ use crate::{ summary = "Update a post by ID", responses( (status = 200, body = PostResponseDto), - (status = 404, description = "Post not found"), + (status = 400, description = PostError::InvalidSemanticId), + (status = 401, description = PostError::Unauthorized), + (status = 404, description = PostError::NotFound), + (status = 409, description = PostError::DuplicatedSemanticId), ), security( ("oauth2" = []) @@ -42,10 +44,6 @@ pub async fn update_post_handler( PostError::Unauthorized => HttpResponse::Unauthorized().finish(), PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(), PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(), - PostError::DuplicatedLabelName => { - capture_anyhow(&anyhow!(e)); - HttpResponse::InternalServerError().finish() - } PostError::Unexpected(e) => { capture_anyhow(&e); HttpResponse::InternalServerError().finish() diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 03ed4c1..c0a265f 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -18,4 +18,5 @@ utoipa-redoc.workspace = true auth.workspace = true image.workspace = true +label.workspace = true post.workspace = true diff --git a/backend/server/src/api_doc.rs b/backend/server/src/api_doc.rs index e4d2e2a..0f6c619 100644 --- a/backend/server/src/api_doc.rs +++ b/backend/server/src/api_doc.rs @@ -1,6 +1,7 @@ use actix_web::web; use auth::framework::web::auth_api_doc; use image::framework::web::image_api_doc; +use label::framework::web::label_api_doc; use post::framework::web::post_api_doc; use utoipa::{ OpenApi, @@ -40,6 +41,7 @@ pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) { let openapi = ApiDoc::openapi() .merge_from(auth_api_doc::openapi()) .merge_from(image_api_doc::openapi()) + .merge_from(label_api_doc::openapi()) .merge_from(post_api_doc::openapi()); cfg.service(Redoc::with_url("/redoc", openapi)); diff --git a/backend/server/src/container.rs b/backend/server/src/container.rs index e2dd8b6..287ee33 100644 --- a/backend/server/src/container.rs +++ b/backend/server/src/container.rs @@ -27,25 +27,32 @@ use image::{ storage::image_storage_impl::ImageStorageImpl, }, }; +use label::{ + adapter::{ + delivery::label_controller::{LabelController, LabelControllerImpl}, + gateway::label_repository_impl::LabelRepositoryImpl, + }, + application::use_case::{ + create_label_use_case::CreateLabelUseCaseImpl, + get_all_labels_use_case::GetAllLabelsUseCaseImpl, + update_label_use_case::UpdateLabelUseCaseImpl, + }, + framework::db::label_db_service_impl::LabelDbServiceImpl, +}; use openidconnect::reqwest; use post::{ adapter::{ delivery::post_controller::{PostController, PostControllerImpl}, - gateway::{ - label_repository_impl::LabelRepositoryImpl, post_repository_impl::PostRepositoryImpl, - }, + gateway::post_repository_impl::PostRepositoryImpl, }, application::use_case::{ - create_label_use_case::CreateLabelUseCaseImpl, create_post_use_case::CreatePostUseCaseImpl, - get_all_labels_use_case::GetAllLabelsUseCaseImpl, + create_post_use_case::CreatePostUseCaseImpl, get_all_post_info_use_case::GetAllPostInfoUseCaseImpl, get_post_by_id_use_case::GetFullPostUseCaseImpl, get_post_by_semantic_id_use_case::GetPostBySemanticIdUseCaseImpl, - update_label_use_case::UpdateLabelUseCaseImpl, update_post_use_case::UpdatePostUseCaseImpl, - }, - framework::db::{ - label_db_service_impl::LabelDbServiceImpl, post_db_service_impl::PostDbServiceImpl, + update_post_use_case::UpdatePostUseCaseImpl, }, + framework::db::post_db_service_impl::PostDbServiceImpl, }; use sqlx::{Pool, Postgres}; @@ -54,6 +61,7 @@ use crate::configuration::Configuration; pub struct Container { pub auth_controller: Arc, pub image_controller: Arc, + pub label_controller: Arc, pub post_controller: Arc, } @@ -87,13 +95,27 @@ impl Container { get_user_use_case, )); - // Post - let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone())); + // Label let label_db_service = Arc::new(LabelDbServiceImpl::new(db_pool.clone())); - let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone())); let label_repository = Arc::new(LabelRepositoryImpl::new(label_db_service.clone())); + let create_label_use_case = Arc::new(CreateLabelUseCaseImpl::new(label_repository.clone())); + let update_label_use_case = Arc::new(UpdateLabelUseCaseImpl::new(label_repository.clone())); + let get_all_labels_use_case = + Arc::new(GetAllLabelsUseCaseImpl::new(label_repository.clone())); + + let label_controller = Arc::new(LabelControllerImpl::new( + create_label_use_case, + update_label_use_case, + get_all_labels_use_case, + )); + + // Post + let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone())); + + let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone())); + let get_all_post_info_use_case = Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone())); let get_post_by_id_use_case = @@ -104,10 +126,6 @@ impl Container { )); let create_post_use_case = Arc::new(CreatePostUseCaseImpl::new(post_repository.clone())); let update_post_use_case = Arc::new(UpdatePostUseCaseImpl::new(post_repository.clone())); - let create_label_use_case = Arc::new(CreateLabelUseCaseImpl::new(label_repository.clone())); - let update_label_use_case = Arc::new(UpdateLabelUseCaseImpl::new(label_repository.clone())); - let get_all_labels_use_case = - Arc::new(GetAllLabelsUseCaseImpl::new(label_repository.clone())); let post_controller = Arc::new(PostControllerImpl::new( get_all_post_info_use_case, @@ -115,9 +133,6 @@ impl Container { get_post_by_semantic_id_use_case, create_post_use_case, update_post_use_case, - create_label_use_case, - update_label_use_case, - get_all_labels_use_case, )); // Image @@ -141,6 +156,7 @@ impl Container { Self { auth_controller, image_controller, + label_controller, post_controller, } } diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 0fd269e..c0e2cdb 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -10,6 +10,7 @@ use actix_web::{ }; use auth::framework::web::auth_web_routes::configure_auth_routes; use image::framework::web::image_web_routes::configure_image_routes; +use label::framework::web::label_web_routes::configure_label_routes; use openidconnect::reqwest; use post::framework::web::post_web_routes::configure_post_routes; use server::{ @@ -84,9 +85,11 @@ fn create_app( .wrap(session_middleware_builder.build()) .app_data(web::Data::from(container.auth_controller)) .app_data(web::Data::from(container.image_controller)) + .app_data(web::Data::from(container.label_controller)) .app_data(web::Data::from(container.post_controller)) .configure(configure_api_doc_routes) .configure(configure_auth_routes) .configure(configure_image_routes) + .configure(configure_label_routes) .configure(configure_post_routes) } diff --git a/frontend/src/lib/post/adapter/gateway/colorResponseDto.ts b/frontend/src/lib/label/adapter/gateway/colorResponseDto.ts similarity index 94% rename from frontend/src/lib/post/adapter/gateway/colorResponseDto.ts rename to frontend/src/lib/label/adapter/gateway/colorResponseDto.ts index c886e3d..9d2f75f 100644 --- a/frontend/src/lib/post/adapter/gateway/colorResponseDto.ts +++ b/frontend/src/lib/label/adapter/gateway/colorResponseDto.ts @@ -1,4 +1,4 @@ -import { Color } from '$lib/post/domain/entity/color'; +import { Color } from '$lib/label/domain/entity/color'; import z from 'zod'; export const ColorResponseSchema = z.object({ diff --git a/frontend/src/lib/post/adapter/gateway/labelResponseDto.ts b/frontend/src/lib/label/adapter/gateway/labelResponseDto.ts similarity index 83% rename from frontend/src/lib/post/adapter/gateway/labelResponseDto.ts rename to frontend/src/lib/label/adapter/gateway/labelResponseDto.ts index be070c6..81535c4 100644 --- a/frontend/src/lib/post/adapter/gateway/labelResponseDto.ts +++ b/frontend/src/lib/label/adapter/gateway/labelResponseDto.ts @@ -1,5 +1,5 @@ -import { ColorResponseDto, ColorResponseSchema } from '$lib/post/adapter/gateway/colorResponseDto'; -import { Label } from '$lib/post/domain/entity/label'; +import { ColorResponseDto, ColorResponseSchema } from '$lib/label/adapter/gateway/colorResponseDto'; +import { Label } from '$lib/label/domain/entity/label'; import { z } from 'zod'; export const LabelResponseSchema = z.object({ diff --git a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts b/frontend/src/lib/label/adapter/presenter/colorViewModel.ts similarity index 97% rename from frontend/src/lib/post/adapter/presenter/colorViewModel.ts rename to frontend/src/lib/label/adapter/presenter/colorViewModel.ts index 202f6ad..9687b9e 100644 --- a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts +++ b/frontend/src/lib/label/adapter/presenter/colorViewModel.ts @@ -1,4 +1,4 @@ -import type { Color } from '$lib/post/domain/entity/color'; +import type { Color } from '$lib/label/domain/entity/color'; export class ColorViewModel { readonly red: number; diff --git a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts b/frontend/src/lib/label/adapter/presenter/labelViewModel.ts similarity index 88% rename from frontend/src/lib/post/adapter/presenter/labelViewModel.ts rename to frontend/src/lib/label/adapter/presenter/labelViewModel.ts index ce83376..8c06dbf 100644 --- a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts +++ b/frontend/src/lib/label/adapter/presenter/labelViewModel.ts @@ -1,8 +1,8 @@ import { ColorViewModel, type DehydratedColorProps, -} from '$lib/post/adapter/presenter/colorViewModel'; -import type { Label } from '$lib/post/domain/entity/label'; +} from '$lib/label/adapter/presenter/colorViewModel'; +import type { Label } from '$lib/label/domain/entity/label'; export class LabelViewModel { readonly id: number; diff --git a/frontend/src/lib/post/domain/entity/color.ts b/frontend/src/lib/label/domain/entity/color.ts similarity index 100% rename from frontend/src/lib/post/domain/entity/color.ts rename to frontend/src/lib/label/domain/entity/color.ts diff --git a/frontend/src/lib/post/domain/entity/label.ts b/frontend/src/lib/label/domain/entity/label.ts similarity index 79% rename from frontend/src/lib/post/domain/entity/label.ts rename to frontend/src/lib/label/domain/entity/label.ts index c264ec6..52e7eb7 100644 --- a/frontend/src/lib/post/domain/entity/label.ts +++ b/frontend/src/lib/label/domain/entity/label.ts @@ -1,4 +1,4 @@ -import type { Color } from '$lib/post/domain/entity/color'; +import type { Color } from '$lib/label/domain/entity/color'; export class Label { readonly id: number; diff --git a/frontend/src/lib/post/framework/ui/PostLabel.svelte b/frontend/src/lib/label/framework/ui/PostLabel.svelte similarity index 80% rename from frontend/src/lib/post/framework/ui/PostLabel.svelte rename to frontend/src/lib/label/framework/ui/PostLabel.svelte index 1962dd8..662c7c3 100644 --- a/frontend/src/lib/post/framework/ui/PostLabel.svelte +++ b/frontend/src/lib/label/framework/ui/PostLabel.svelte @@ -1,5 +1,5 @@ diff --git a/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts b/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts index 70b6b18..c4ef593 100644 --- a/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts +++ b/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts @@ -1,4 +1,4 @@ -import { LabelResponseDto, LabelResponseSchema } from '$lib/post/adapter/gateway/labelResponseDto'; +import { LabelResponseDto, LabelResponseSchema } from '$lib/label/adapter/gateway/labelResponseDto'; import { PostInfo } from '$lib/post/domain/entity/postInfo'; import z from 'zod'; diff --git a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts index e67bd9a..e130d6d 100644 --- a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts @@ -1,7 +1,7 @@ import { LabelViewModel, type DehydratedLabelProps, -} from '$lib/post/adapter/presenter/labelViewModel'; +} from '$lib/label/adapter/presenter/labelViewModel'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; export class PostInfoViewModel { diff --git a/frontend/src/lib/post/domain/entity/postInfo.ts b/frontend/src/lib/post/domain/entity/postInfo.ts index 35542d8..af987cb 100644 --- a/frontend/src/lib/post/domain/entity/postInfo.ts +++ b/frontend/src/lib/post/domain/entity/postInfo.ts @@ -1,4 +1,4 @@ -import type { Label } from '$lib/post/domain/entity/label'; +import type { Label } from '$lib/label/domain/entity/label'; export class PostInfo { readonly id: number; diff --git a/frontend/src/lib/post/framework/ui/PostContentHeader.svelte b/frontend/src/lib/post/framework/ui/PostContentHeader.svelte index d4b2b95..910a9e6 100644 --- a/frontend/src/lib/post/framework/ui/PostContentHeader.svelte +++ b/frontend/src/lib/post/framework/ui/PostContentHeader.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/lib/post/framework/ui/PostManagementPage.svelte b/frontend/src/lib/post/framework/ui/PostManagementPage.svelte index dfc98a2..01ac636 100644 --- a/frontend/src/lib/post/framework/ui/PostManagementPage.svelte +++ b/frontend/src/lib/post/framework/ui/PostManagementPage.svelte @@ -10,7 +10,7 @@ import CreatePostDialog, { type CreatePostDialogFormParams, } from '$lib/post/framework/ui/CreatePostDialog.svelte'; - import PostLabel from '$lib/post/framework/ui/PostLabel.svelte'; + import PostLabel from '$lib/label/framework/ui/PostLabel.svelte'; import { getContext, onMount } from 'svelte'; import { toast } from 'svelte-sonner'; diff --git a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte index 413b496..5e91d3d 100644 --- a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte +++ b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte @@ -1,6 +1,6 @@