BLOG-142 Move label to a new feature (#143)
All checks were successful
Frontend CI / build (push) Successful in 1m35s

### Description

- As the title

### Package Changes

_No response_

### Screenshots

_No response_

### Reference

Resolves #142.

### Checklist

- [x] A milestone is set
- [x] The related issuse has been linked to this branch

Reviewed-on: #143
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
SquidSpirit 2025-10-15 06:23:58 +08:00 committed by squid
parent 24a98f8f70
commit a577f94acd
81 changed files with 402 additions and 259 deletions

17
backend/Cargo.lock generated
View File

@ -1876,6 +1876,21 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "label"
version = "0.3.1"
dependencies = [
"actix-web",
"anyhow",
"async-trait",
"auth",
"common",
"sentry",
"serde",
"sqlx",
"utoipa",
]
[[package]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@ -2407,6 +2422,7 @@ dependencies = [
"auth", "auth",
"chrono", "chrono",
"common", "common",
"label",
"regex", "regex",
"sentry", "sentry",
"serde", "serde",
@ -3179,6 +3195,7 @@ dependencies = [
"env_logger", "env_logger",
"hex", "hex",
"image", "image",
"label",
"openidconnect", "openidconnect",
"percent-encoding", "percent-encoding",
"post", "post",

View File

@ -4,6 +4,7 @@ members = [
"feature/auth", "feature/auth",
"feature/common", "feature/common",
"feature/image", "feature/image",
"feature/label",
"feature/post", "feature/post",
] ]
resolver = "2" resolver = "2"
@ -53,4 +54,5 @@ server.path = "server"
auth.path = "feature/auth" auth.path = "feature/auth"
common.path = "feature/common" common.path = "feature/common"
image.path = "feature/image" image.path = "feature/image"
label.path = "feature/label"
post.path = "feature/post" post.path = "feature/post"

View File

@ -7,6 +7,12 @@ pub enum ImageError {
Unexpected(anyhow::Error), Unexpected(anyhow::Error),
} }
impl Into<String> for ImageError {
fn into(self) -> String {
format!("{}", self)
}
}
impl Display for ImageError { impl Display for ImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {

View File

@ -15,7 +15,7 @@ use crate::{
summary = "Get image by ID", summary = "Get image by ID",
responses ( responses (
(status = 200, body = inline(ResponseBodySchema), content_type = "image/*"), (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( pub async fn get_image_by_id_handler(
@ -31,12 +31,12 @@ pub async fn get_image_by_id_handler(
.body(image_response.data), .body(image_response.data),
Err(e) => match e { Err(e) => match e {
ImageError::NotFound => HttpResponse::NotFound().finish(), ImageError::NotFound => HttpResponse::NotFound().finish(),
ImageError::Unexpected(e) => { ImageError::UnsupportedMimeType(_) => {
capture_anyhow(&e); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
_ => { ImageError::Unexpected(e) => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
}, },

View File

@ -25,7 +25,7 @@ use crate::{
), ),
responses ( responses (
(status = 201, body = ImageInfoResponseDto), (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( security(
("oauth2" = []) ("oauth2" = [])
@ -78,12 +78,12 @@ pub async fn upload_image_handler(
ImageError::UnsupportedMimeType(mime_type) => { ImageError::UnsupportedMimeType(mime_type) => {
HttpResponse::BadRequest().body(format!("Unsupported MIME type: {}", mime_type)) HttpResponse::BadRequest().body(format!("Unsupported MIME type: {}", mime_type))
} }
ImageError::Unexpected(e) => { ImageError::NotFound => {
capture_anyhow(&e); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
_ => { ImageError::Unexpected(e) => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
}, },

View File

@ -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

View File

@ -0,0 +1,2 @@
pub mod delivery;
pub mod gateway;

View File

@ -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;

View File

@ -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<LabelResponseDto, LabelError>;
async fn update_label(
&self,
id: i32,
label: UpdateLabelRequestDto,
) -> Result<LabelResponseDto, LabelError>;
async fn get_all_labels(&self) -> Result<Vec<LabelResponseDto>, LabelError>;
}
pub struct LabelControllerImpl {
create_label_use_case: Arc<dyn CreateLabelUseCase>,
update_label_use_case: Arc<dyn UpdateLabelUseCase>,
get_all_labels_use_case: Arc<dyn GetAllLabelsUseCase>,
}
impl LabelControllerImpl {
pub fn new(
create_label_use_case: Arc<dyn CreateLabelUseCase>,
update_label_use_case: Arc<dyn UpdateLabelUseCase>,
get_all_labels_use_case: Arc<dyn GetAllLabelsUseCase>,
) -> 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<LabelResponseDto, LabelError> {
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<LabelResponseDto, LabelError> {
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<Vec<LabelResponseDto>, LabelError> {
let result = self.get_all_labels_use_case.execute().await;
result.map(|labels| {
labels
.into_iter()
.map(|label| LabelResponseDto::from(label))
.collect()
})
}
}

View File

@ -0,0 +1,4 @@
pub mod color_db_mapper;
pub mod label_db_mapper;
pub mod label_db_service;
pub mod label_repository_impl;

View File

@ -1,13 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::{ 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] #[async_trait]
pub trait LabelDbService: Send + Sync { pub trait LabelDbService: Send + Sync {
async fn create_label(&self, label: LabelMapper) -> Result<i32, PostError>; async fn create_label(&self, label: LabelMapper) -> Result<i32, LabelError>;
async fn update_label(&self, label: LabelMapper) -> Result<(), PostError>; async fn update_label(&self, label: LabelMapper) -> Result<(), LabelError>;
async fn get_label_by_id(&self, id: i32) -> Result<LabelMapper, PostError>; async fn get_label_by_id(&self, id: i32) -> Result<LabelMapper, LabelError>;
async fn get_all_labels(&self) -> Result<Vec<LabelMapper>, PostError>; async fn get_all_labels(&self) -> Result<Vec<LabelMapper>, LabelError>;
} }

View File

@ -4,7 +4,7 @@ use async_trait::async_trait;
use crate::{ use crate::{
adapter::gateway::{label_db_mapper::LabelMapper, label_db_service::LabelDbService}, 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, domain::entity::label::Label,
}; };
@ -20,26 +20,26 @@ impl LabelRepositoryImpl {
#[async_trait] #[async_trait]
impl LabelRepository for LabelRepositoryImpl { impl LabelRepository for LabelRepositoryImpl {
async fn create_label(&self, label: Label) -> Result<i32, PostError> { async fn create_label(&self, label: Label) -> Result<i32, LabelError> {
self.label_db_service self.label_db_service
.create_label(LabelMapper::from(label)) .create_label(LabelMapper::from(label))
.await .await
} }
async fn update_label(&self, label: Label) -> Result<(), PostError> { async fn update_label(&self, label: Label) -> Result<(), LabelError> {
self.label_db_service self.label_db_service
.update_label(LabelMapper::from(label)) .update_label(LabelMapper::from(label))
.await .await
} }
async fn get_label_by_id(&self, id: i32) -> Result<Label, PostError> { async fn get_label_by_id(&self, id: i32) -> Result<Label, LabelError> {
self.label_db_service self.label_db_service
.get_label_by_id(id) .get_label_by_id(id)
.await .await
.map(|mapper| mapper.into_entity()) .map(|mapper| mapper.into_entity())
} }
async fn get_all_labels(&self) -> Result<Vec<Label>, PostError> { async fn get_all_labels(&self) -> Result<Vec<Label>, LabelError> {
self.label_db_service.get_all_labels().await.map(|mappers| { self.label_db_service.get_all_labels().await.map(|mappers| {
mappers mappers
.into_iter() .into_iter()

View File

@ -0,0 +1,3 @@
pub mod error;
pub mod gateway;
pub mod use_case;

View File

@ -0,0 +1 @@
pub mod label_error;

View File

@ -0,0 +1,26 @@
use std::fmt::Display;
#[derive(Debug)]
pub enum LabelError {
NotFound,
Unauthorized,
DuplicatedLabelName,
Unexpected(anyhow::Error),
}
impl Into<String> 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),
}
}
}

View File

@ -0,0 +1 @@
pub mod label_repository;

View File

@ -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<i32, LabelError>;
async fn update_label(&self, label: Label) -> Result<(), LabelError>;
async fn get_label_by_id(&self, id: i32) -> Result<Label, LabelError>;
async fn get_all_labels(&self) -> Result<Vec<Label>, LabelError>;
}

View File

@ -0,0 +1,3 @@
pub mod create_label_use_case;
pub mod get_all_labels_use_case;
pub mod update_label_use_case;

View File

@ -3,13 +3,13 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crate::{ use crate::{
application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, application::{error::label_error::LabelError, gateway::label_repository::LabelRepository},
domain::entity::label::Label, domain::entity::label::Label,
}; };
#[async_trait] #[async_trait]
pub trait CreateLabelUseCase: Send + Sync { pub trait CreateLabelUseCase: Send + Sync {
async fn execute(&self, label: Label) -> Result<i32, PostError>; async fn execute(&self, label: Label) -> Result<i32, LabelError>;
} }
pub struct CreateLabelUseCaseImpl { pub struct CreateLabelUseCaseImpl {
@ -24,7 +24,7 @@ impl CreateLabelUseCaseImpl {
#[async_trait] #[async_trait]
impl CreateLabelUseCase for CreateLabelUseCaseImpl { impl CreateLabelUseCase for CreateLabelUseCaseImpl {
async fn execute(&self, label: Label) -> Result<i32, PostError> { async fn execute(&self, label: Label) -> Result<i32, LabelError> {
self.label_repository.create_label(label).await self.label_repository.create_label(label).await
} }
} }

View File

@ -3,13 +3,13 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crate::{ use crate::{
application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, application::{error::label_error::LabelError, gateway::label_repository::LabelRepository},
domain::entity::label::Label, domain::entity::label::Label,
}; };
#[async_trait] #[async_trait]
pub trait GetAllLabelsUseCase: Send + Sync { pub trait GetAllLabelsUseCase: Send + Sync {
async fn execute(&self) -> Result<Vec<Label>, PostError>; async fn execute(&self) -> Result<Vec<Label>, LabelError>;
} }
pub struct GetAllLabelsUseCaseImpl { pub struct GetAllLabelsUseCaseImpl {
@ -24,7 +24,7 @@ impl GetAllLabelsUseCaseImpl {
#[async_trait] #[async_trait]
impl GetAllLabelsUseCase for GetAllLabelsUseCaseImpl { impl GetAllLabelsUseCase for GetAllLabelsUseCaseImpl {
async fn execute(&self) -> Result<Vec<Label>, PostError> { async fn execute(&self) -> Result<Vec<Label>, LabelError> {
self.label_repository.get_all_labels().await self.label_repository.get_all_labels().await
} }
} }

View File

@ -3,13 +3,13 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crate::{ use crate::{
application::{error::post_error::PostError, gateway::label_repository::LabelRepository}, application::{error::label_error::LabelError, gateway::label_repository::LabelRepository},
domain::entity::label::Label, domain::entity::label::Label,
}; };
#[async_trait] #[async_trait]
pub trait UpdateLabelUseCase: Send + Sync { pub trait UpdateLabelUseCase: Send + Sync {
async fn execute(&self, label: Label) -> Result<(), PostError>; async fn execute(&self, label: Label) -> Result<(), LabelError>;
} }
pub struct UpdateLabelUseCaseImpl { pub struct UpdateLabelUseCaseImpl {
@ -24,7 +24,7 @@ impl UpdateLabelUseCaseImpl {
#[async_trait] #[async_trait]
impl UpdateLabelUseCase for UpdateLabelUseCaseImpl { 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 self.label_repository.update_label(label).await
} }
} }

View File

@ -0,0 +1 @@
pub mod entity;

View File

@ -0,0 +1,2 @@
pub mod color;
pub mod label;

View File

@ -0,0 +1,2 @@
pub mod db;
pub mod web;

View File

@ -0,0 +1,3 @@
pub mod label_db_service_impl;
mod label_record;

View File

@ -4,7 +4,7 @@ use sqlx::{Pool, Postgres};
use crate::{ use crate::{
adapter::gateway::{label_db_mapper::LabelMapper, label_db_service::LabelDbService}, 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, framework::db::label_record::LabelRecord,
}; };
@ -20,7 +20,7 @@ impl LabelDbServiceImpl {
#[async_trait] #[async_trait]
impl LabelDbService for LabelDbServiceImpl { impl LabelDbService for LabelDbServiceImpl {
async fn create_label(&self, label: LabelMapper) -> Result<i32, PostError> { async fn create_label(&self, label: LabelMapper) -> Result<i32, LabelError> {
let id = sqlx::query_scalar!( let id = sqlx::query_scalar!(
r#" r#"
INSERT INTO label (name, color) INSERT INTO label (name, color)
@ -35,16 +35,16 @@ impl LabelDbService for LabelDbServiceImpl {
.map_err(|e| { .map_err(|e| {
if let sqlx::Error::Database(db_err) = &e { if let sqlx::Error::Database(db_err) = &e {
if db_err.constraint() == Some("idx_label_name") { 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) 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!( let affected_rows = sqlx::query!(
r#" r#"
UPDATE label UPDATE label
@ -60,21 +60,21 @@ impl LabelDbService for LabelDbServiceImpl {
.map_err(|e| { .map_err(|e| {
if let sqlx::Error::Database(db_err) = &e { if let sqlx::Error::Database(db_err) = &e {
if db_err.constraint() == Some("idx_label_name") { 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(); .rows_affected();
if affected_rows == 0 { if affected_rows == 0 {
return Err(PostError::NotFound); return Err(LabelError::NotFound);
} }
Ok(()) Ok(())
} }
async fn get_label_by_id(&self, id: i32) -> Result<LabelMapper, PostError> { async fn get_label_by_id(&self, id: i32) -> Result<LabelMapper, LabelError> {
let record = sqlx::query_as!( let record = sqlx::query_as!(
LabelRecord, LabelRecord,
r#" r#"
@ -86,15 +86,15 @@ impl LabelDbService for LabelDbServiceImpl {
) )
.fetch_optional(&self.db_pool) .fetch_optional(&self.db_pool)
.await .await
.map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?; .map_err(|e| LabelError::Unexpected(DatabaseError(e).into()))?;
match record { match record {
Some(record) => Ok(record.into_mapper()), Some(record) => Ok(record.into_mapper()),
None => Err(PostError::NotFound), None => Err(LabelError::NotFound),
} }
} }
async fn get_all_labels(&self) -> Result<Vec<LabelMapper>, PostError> { async fn get_all_labels(&self) -> Result<Vec<LabelMapper>, LabelError> {
let records = sqlx::query_as!( let records = sqlx::query_as!(
LabelRecord, LabelRecord,
r#" r#"
@ -106,7 +106,7 @@ impl LabelDbService for LabelDbServiceImpl {
) )
.fetch_all(&self.db_pool) .fetch_all(&self.db_pool)
.await .await
.map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?; .map_err(|e| LabelError::Unexpected(DatabaseError(e).into()))?;
let mappers = records let mappers = records
.into_iter() .into_iter()

View File

@ -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;

View File

@ -5,43 +5,43 @@ use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{ adapter::delivery::{
create_label_request_dto::CreateLabelRequestDto, label_response_dto::LabelResponseDto, create_label_request_dto::CreateLabelRequestDto, label_controller::LabelController,
post_controller::PostController, label_response_dto::LabelResponseDto,
}, },
application::error::post_error::PostError, application::error::label_error::LabelError,
}; };
#[utoipa::path( #[utoipa::path(
post, post,
path = "/label", path = "/label",
tag = "post", tag = "label",
summary = "Create a new label", summary = "Create a new label",
responses( responses(
(status = 201, body = LabelResponseDto), (status = 201, body = LabelResponseDto),
(status = 401, description = LabelError::Unauthorized),
(status = 409, description = LabelError::DuplicatedLabelName),
), ),
security( security(
("oauth2" = []) ("oauth2" = [])
) )
)] )]
pub async fn create_label_handler( pub async fn create_label_handler(
post_controller: web::Data<dyn PostController>, label_controller: web::Data<dyn LabelController>,
label_dto: web::Json<CreateLabelRequestDto>, label_dto: web::Json<CreateLabelRequestDto>,
_: UserId, _: UserId,
) -> impl Responder { ) -> 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 { match result {
Ok(label) => HttpResponse::Created().json(label), Ok(label) => HttpResponse::Created().json(label),
Err(e) => match e { Err(e) => match e {
PostError::Unauthorized => HttpResponse::Unauthorized().finish(), LabelError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(), LabelError::DuplicatedLabelName => HttpResponse::Conflict().finish(),
PostError::NotFound LabelError::NotFound => {
| PostError::InvalidSemanticId
| PostError::DuplicatedSemanticId => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
PostError::Unexpected(e) => { LabelError::Unexpected(e) => {
capture_anyhow(&e); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }

View File

@ -3,36 +3,32 @@ use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow; use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{label_response_dto::LabelResponseDto, post_controller::PostController}, adapter::delivery::{label_controller::LabelController, label_response_dto::LabelResponseDto},
application::error::post_error::PostError, application::error::label_error::LabelError,
}; };
#[utoipa::path( #[utoipa::path(
get, get,
path = "/label", path = "/label",
tag = "post", tag = "label",
summary = "Get all labels", summary = "Get all labels",
responses( responses(
(status = 200, body = Vec<LabelResponseDto>) (status = 200, body = Vec<LabelResponseDto>)
) )
)] )]
pub async fn get_all_labels_handler( pub async fn get_all_labels_handler(
post_controller: web::Data<dyn PostController>, label_controller: web::Data<dyn LabelController>,
) -> impl Responder { ) -> impl Responder {
let result = post_controller.get_all_labels().await; let result = label_controller.get_all_labels().await;
match result { match result {
Ok(labels) => HttpResponse::Ok().json(labels), Ok(labels) => HttpResponse::Ok().json(labels),
Err(e) => match e { Err(e) => match e {
PostError::NotFound LabelError::NotFound | LabelError::Unauthorized | LabelError::DuplicatedLabelName => {
| PostError::Unauthorized
| PostError::InvalidSemanticId
| PostError::DuplicatedSemanticId
| PostError::DuplicatedLabelName => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
PostError::Unexpected(e) => { LabelError::Unexpected(e) => {
capture_anyhow(&e); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }

View File

@ -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()
}

View File

@ -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)),
);
}

View File

@ -1,51 +1,48 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow; use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{ adapter::delivery::{
label_response_dto::LabelResponseDto, post_controller::PostController, label_controller::LabelController, label_response_dto::LabelResponseDto,
update_label_request_dto::UpdateLabelRequestDto, update_label_request_dto::UpdateLabelRequestDto,
}, },
application::error::post_error::PostError, application::error::label_error::LabelError,
}; };
#[utoipa::path( #[utoipa::path(
put, put,
path = "/label/{id}", path = "/label/{id}",
tag = "post", tag = "label",
summary = "Update a label by ID", summary = "Update a label by ID",
responses( responses(
(status = 200, body = LabelResponseDto), (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( security(
("oauth2" = []) ("oauth2" = [])
) )
)] )]
pub async fn update_label_handler( pub async fn update_label_handler(
post_controller: web::Data<dyn PostController>, label_controller: web::Data<dyn LabelController>,
label_dto: web::Json<UpdateLabelRequestDto>, label_dto: web::Json<UpdateLabelRequestDto>,
path: web::Path<i32>, path: web::Path<i32>,
_: UserId, _: UserId,
) -> impl Responder { ) -> impl Responder {
let id = path.into_inner(); let id = path.into_inner();
let result = post_controller let result = label_controller
.update_label(id, label_dto.into_inner()) .update_label(id, label_dto.into_inner())
.await; .await;
match result { match result {
Ok(label) => HttpResponse::Ok().json(label), Ok(label) => HttpResponse::Ok().json(label),
Err(e) => match e { Err(e) => match e {
PostError::NotFound => HttpResponse::NotFound().finish(), LabelError::NotFound => HttpResponse::NotFound().finish(),
PostError::Unauthorized => HttpResponse::Unauthorized().finish(), LabelError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(), LabelError::DuplicatedLabelName => HttpResponse::Conflict().finish(),
PostError::InvalidSemanticId | PostError::DuplicatedSemanticId => { LabelError::Unexpected(e) => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }

View File

@ -0,0 +1,4 @@
pub mod adapter;
pub mod application;
pub mod domain;
pub mod framework;

View File

@ -16,3 +16,4 @@ utoipa.workspace = true
auth.workspace = true auth.workspace = true
common.workspace = true common.workspace = true
label.workspace = true

View File

@ -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 create_post_request_dto;
pub mod label_response_dto;
pub mod post_controller; pub mod post_controller;
pub mod post_info_query_dto; pub mod post_info_query_dto;
pub mod post_info_response_dto; pub mod post_info_response_dto;
pub mod post_response_dto; pub mod post_response_dto;
pub mod update_label_request_dto;
pub mod update_post_request_dto; pub mod update_post_request_dto;

View File

@ -4,28 +4,18 @@ use async_trait::async_trait;
use crate::{ use crate::{
adapter::delivery::{ adapter::delivery::{
create_label_request_dto::CreateLabelRequestDto,
create_post_request_dto::CreatePostRequestDto, post_info_query_dto::PostQueryDto, create_post_request_dto::CreatePostRequestDto, post_info_query_dto::PostQueryDto,
update_label_request_dto::UpdateLabelRequestDto,
update_post_request_dto::UpdatePostRequestDto, update_post_request_dto::UpdatePostRequestDto,
}, },
application::{ application::{
error::post_error::PostError, error::post_error::PostError,
use_case::{ use_case::{
create_label_use_case::CreateLabelUseCase, create_post_use_case::CreatePostUseCase, 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
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,
}, },
}, },
}; };
use super::{ use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::PostResponseDto};
label_response_dto::LabelResponseDto, post_info_response_dto::PostInfoResponseDto,
post_response_dto::PostResponseDto,
};
#[async_trait] #[async_trait]
pub trait PostController: Send + Sync { pub trait PostController: Send + Sync {
@ -53,19 +43,6 @@ pub trait PostController: Send + Sync {
post: UpdatePostRequestDto, post: UpdatePostRequestDto,
user_id: i32, user_id: i32,
) -> Result<PostResponseDto, PostError>; ) -> Result<PostResponseDto, PostError>;
async fn create_label(
&self,
label: CreateLabelRequestDto,
) -> Result<LabelResponseDto, PostError>;
async fn update_label(
&self,
id: i32,
label: UpdateLabelRequestDto,
) -> Result<LabelResponseDto, PostError>;
async fn get_all_labels(&self) -> Result<Vec<LabelResponseDto>, PostError>;
} }
pub struct PostControllerImpl { pub struct PostControllerImpl {
@ -74,9 +51,6 @@ pub struct PostControllerImpl {
get_post_by_semantic_id_use_case: Arc<dyn GetPostBySemanticIdUseCase>, get_post_by_semantic_id_use_case: Arc<dyn GetPostBySemanticIdUseCase>,
create_post_use_case: Arc<dyn CreatePostUseCase>, create_post_use_case: Arc<dyn CreatePostUseCase>,
update_post_use_case: Arc<dyn UpdatePostUseCase>, update_post_use_case: Arc<dyn UpdatePostUseCase>,
create_label_use_case: Arc<dyn CreateLabelUseCase>,
update_label_use_case: Arc<dyn UpdateLabelUseCase>,
get_all_labels_use_case: Arc<dyn GetAllLabelsUseCase>,
} }
impl PostControllerImpl { impl PostControllerImpl {
@ -86,9 +60,6 @@ impl PostControllerImpl {
get_post_by_semantic_id_use_case: Arc<dyn GetPostBySemanticIdUseCase>, get_post_by_semantic_id_use_case: Arc<dyn GetPostBySemanticIdUseCase>,
create_post_use_case: Arc<dyn CreatePostUseCase>, create_post_use_case: Arc<dyn CreatePostUseCase>,
update_post_use_case: Arc<dyn UpdatePostUseCase>, update_post_use_case: Arc<dyn UpdatePostUseCase>,
create_label_use_case: Arc<dyn CreateLabelUseCase>,
update_label_use_case: Arc<dyn UpdateLabelUseCase>,
get_all_labels_use_case: Arc<dyn GetAllLabelsUseCase>,
) -> Self { ) -> Self {
Self { Self {
get_all_post_info_use_case, get_all_post_info_use_case,
@ -96,9 +67,6 @@ impl PostControllerImpl {
get_post_by_semantic_id_use_case, get_post_by_semantic_id_use_case,
create_post_use_case, create_post_use_case,
update_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<LabelResponseDto, PostError> {
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<LabelResponseDto, PostError> {
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<Vec<LabelResponseDto>, 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( async fn create_post(
&self, &self,
post: CreatePostRequestDto, post: CreatePostRequestDto,

View File

@ -1,10 +1,9 @@
use label::adapter::delivery::label_response_dto::LabelResponseDto;
use serde::Serialize; use serde::Serialize;
use utoipa::ToSchema; use utoipa::ToSchema;
use crate::domain::entity::post_info::PostInfo; use crate::domain::entity::post_info::PostInfo;
use super::label_response_dto::LabelResponseDto;
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
pub struct PostInfoResponseDto { pub struct PostInfoResponseDto {
pub id: i32, pub id: i32,

View File

@ -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_mapper;
pub mod post_db_service; pub mod post_db_service;
pub mod post_info_db_mapper; pub mod post_info_db_mapper;

View File

@ -1,6 +1,7 @@
use chrono::{DateTime, NaiveDateTime, Utc}; 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 struct PostInfoMapper {
pub id: i32, pub id: i32,

View File

@ -6,21 +6,25 @@ pub enum PostError {
Unauthorized, Unauthorized,
InvalidSemanticId, InvalidSemanticId,
DuplicatedSemanticId, DuplicatedSemanticId,
DuplicatedLabelName,
Unexpected(anyhow::Error), Unexpected(anyhow::Error),
} }
impl Into<String> for PostError {
fn into(self) -> String {
format!("{}", self)
}
}
impl Display for PostError { impl Display for PostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
PostError::NotFound => write!(f, "Post not found"), PostError::NotFound => write!(f, "Post not found"),
PostError::Unauthorized => write!(f, "Unauthorized access to post"), PostError::Unauthorized => write!(f, "Unauthorized access"),
PostError::InvalidSemanticId => write!( PostError::InvalidSemanticId => write!(
f, 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::DuplicatedSemanticId => write!(f, "Semantic ID already exists"),
PostError::DuplicatedLabelName => write!(f, "Label name already exists"),
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e), PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
} }
} }

View File

@ -1,2 +1 @@
pub mod label_repository;
pub mod post_repository; pub mod post_repository;

View File

@ -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<i32, PostError>;
async fn update_label(&self, label: Label) -> Result<(), PostError>;
async fn get_label_by_id(&self, id: i32) -> Result<Label, PostError>;
async fn get_all_labels(&self) -> Result<Vec<Label>, PostError>;
}

View File

@ -1,8 +1,5 @@
pub mod create_label_use_case;
pub mod create_post_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_all_post_info_use_case;
pub mod get_post_by_id_use_case; pub mod get_post_by_id_use_case;
pub mod get_post_by_semantic_id_use_case; pub mod get_post_by_semantic_id_use_case;
pub mod update_label_use_case;
pub mod update_post_use_case; pub mod update_post_use_case;

View File

@ -1,4 +1,2 @@
pub mod color;
pub mod label;
pub mod post_info; pub mod post_info;
pub mod post; pub mod post;

View File

@ -1,10 +1,9 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use label::domain::entity::label::Label;
use regex::Regex; use regex::Regex;
use crate::application::error::post_error::PostError; use crate::application::error::post_error::PostError;
use super::label::Label;
pub struct PostInfo { pub struct PostInfo {
pub id: i32, pub id: i32,
pub semantic_id: String, pub semantic_id: String,
@ -21,7 +20,7 @@ impl PostInfo {
return Err(PostError::InvalidSemanticId); 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) { if !re.is_match(&self.semantic_id) {
return Err(PostError::InvalidSemanticId); return Err(PostError::InvalidSemanticId);
} }

View File

@ -1,6 +1,4 @@
pub mod label_db_service_impl;
pub mod post_db_service_impl; pub mod post_db_service_impl;
mod label_record;
mod post_info_with_label_record; mod post_info_with_label_record;
mod post_with_label_record; mod post_with_label_record;

View File

@ -2,12 +2,13 @@ use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use common::framework::error::DatabaseError; use common::framework::error::DatabaseError;
use label::adapter::gateway::{color_db_mapper::ColorMapper, label_db_mapper::LabelMapper};
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use crate::{ use crate::{
adapter::gateway::{ adapter::gateway::{
color_db_mapper::ColorMapper, label_db_mapper::LabelMapper, post_db_mapper::PostMapper, post_db_mapper::PostMapper, post_db_service::PostDbService,
post_db_service::PostDbService, post_info_db_mapper::PostInfoMapper, post_info_db_mapper::PostInfoMapper,
}, },
application::error::post_error::PostError, application::error::post_error::PostError,
}; };

View File

@ -1,10 +1,7 @@
pub mod post_api_doc; pub mod post_api_doc;
pub mod post_web_routes; pub mod post_web_routes;
mod create_label_handler;
mod create_post_handler; mod create_post_handler;
mod get_all_labels_handler;
mod get_all_post_info_handler; mod get_all_post_info_handler;
mod get_post_by_id_handler; mod get_post_by_id_handler;
mod update_label_handler;
mod update_post_handler; mod update_post_handler;

View File

@ -18,6 +18,9 @@ use crate::{
summary = "Create a new post", summary = "Create a new post",
responses( responses(
(status = 201, body = PostResponseDto), (status = 201, body = PostResponseDto),
(status = 400, description = PostError::InvalidSemanticId),
(status = 401, description = PostError::Unauthorized),
(status = 409, description = PostError::DuplicatedSemanticId),
), ),
security( security(
("oauth2" = []) ("oauth2" = [])
@ -38,7 +41,7 @@ pub async fn create_post_handler(
PostError::Unauthorized => HttpResponse::Unauthorized().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(), PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(), PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(),
PostError::NotFound | PostError::DuplicatedLabelName => { PostError::NotFound => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }

View File

@ -39,8 +39,7 @@ pub async fn get_all_post_info_handler(
PostError::NotFound PostError::NotFound
| PostError::Unauthorized | PostError::Unauthorized
| PostError::InvalidSemanticId | PostError::InvalidSemanticId
| PostError::DuplicatedSemanticId | PostError::DuplicatedSemanticId => {
| PostError::DuplicatedLabelName => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }

View File

@ -16,7 +16,8 @@ use crate::{
description = "Only authenticated users can access unpublished posts. Accepts either numeric ID or semantic ID.", description = "Only authenticated users can access unpublished posts. Accepts either numeric ID or semantic ID.",
responses ( responses (
(status = 200, body = PostResponseDto), (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( pub async fn get_post_by_id_handler(
@ -34,9 +35,7 @@ pub async fn get_post_by_id_handler(
Err(e) => match e { Err(e) => match e {
PostError::NotFound => HttpResponse::NotFound().finish(), PostError::NotFound => HttpResponse::NotFound().finish(),
PostError::Unauthorized => HttpResponse::Unauthorized().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::InvalidSemanticId PostError::InvalidSemanticId | PostError::DuplicatedSemanticId => {
| PostError::DuplicatedSemanticId
| PostError::DuplicatedLabelName => {
capture_anyhow(&anyhow!(e)); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }

View File

@ -1,6 +1,5 @@
use crate::framework::web::{ use crate::framework::web::{
create_label_handler, create_post_handler, get_all_labels_handler, get_all_post_info_handler, create_post_handler, get_all_post_info_handler, get_post_by_id_handler, update_post_handler,
get_post_by_id_handler, update_label_handler, update_post_handler,
}; };
use utoipa::{OpenApi, openapi}; use utoipa::{OpenApi, openapi};
@ -10,9 +9,6 @@ use utoipa::{OpenApi, openapi};
get_post_by_id_handler::get_post_by_id_handler, get_post_by_id_handler::get_post_by_id_handler,
create_post_handler::create_post_handler, create_post_handler::create_post_handler,
update_post_handler::update_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; struct ApiDoc;

View File

@ -1,11 +1,8 @@
use actix_web::web; use actix_web::web;
use crate::framework::web::{ use crate::framework::web::{
create_label_handler::create_label_handler, create_post_handler::create_post_handler, create_post_handler::create_post_handler, get_all_post_info_handler::get_all_post_info_handler,
get_all_labels_handler::get_all_labels_handler, get_post_by_id_handler::get_post_by_id_handler, update_post_handler::update_post_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,
}; };
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) { 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::get().to(get_post_by_id_handler))
.route("/{id}", web::put().to(update_post_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)),
);
} }

View File

@ -1,5 +1,4 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow; use sentry::integrations::anyhow::capture_anyhow;
@ -18,7 +17,10 @@ use crate::{
summary = "Update a post by ID", summary = "Update a post by ID",
responses( responses(
(status = 200, body = PostResponseDto), (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( security(
("oauth2" = []) ("oauth2" = [])
@ -42,10 +44,6 @@ pub async fn update_post_handler(
PostError::Unauthorized => HttpResponse::Unauthorized().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(), PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(),
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(), PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
PostError::DuplicatedLabelName => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => { PostError::Unexpected(e) => {
capture_anyhow(&e); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()

View File

@ -18,4 +18,5 @@ utoipa-redoc.workspace = true
auth.workspace = true auth.workspace = true
image.workspace = true image.workspace = true
label.workspace = true
post.workspace = true post.workspace = true

View File

@ -1,6 +1,7 @@
use actix_web::web; use actix_web::web;
use auth::framework::web::auth_api_doc; use auth::framework::web::auth_api_doc;
use image::framework::web::image_api_doc; use image::framework::web::image_api_doc;
use label::framework::web::label_api_doc;
use post::framework::web::post_api_doc; use post::framework::web::post_api_doc;
use utoipa::{ use utoipa::{
OpenApi, OpenApi,
@ -40,6 +41,7 @@ pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) {
let openapi = ApiDoc::openapi() let openapi = ApiDoc::openapi()
.merge_from(auth_api_doc::openapi()) .merge_from(auth_api_doc::openapi())
.merge_from(image_api_doc::openapi()) .merge_from(image_api_doc::openapi())
.merge_from(label_api_doc::openapi())
.merge_from(post_api_doc::openapi()); .merge_from(post_api_doc::openapi());
cfg.service(Redoc::with_url("/redoc", openapi)); cfg.service(Redoc::with_url("/redoc", openapi));

View File

@ -27,25 +27,32 @@ use image::{
storage::image_storage_impl::ImageStorageImpl, 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 openidconnect::reqwest;
use post::{ use post::{
adapter::{ adapter::{
delivery::post_controller::{PostController, PostControllerImpl}, delivery::post_controller::{PostController, PostControllerImpl},
gateway::{ gateway::post_repository_impl::PostRepositoryImpl,
label_repository_impl::LabelRepositoryImpl, post_repository_impl::PostRepositoryImpl,
},
}, },
application::use_case::{ application::use_case::{
create_label_use_case::CreateLabelUseCaseImpl, create_post_use_case::CreatePostUseCaseImpl, create_post_use_case::CreatePostUseCaseImpl,
get_all_labels_use_case::GetAllLabelsUseCaseImpl,
get_all_post_info_use_case::GetAllPostInfoUseCaseImpl, get_all_post_info_use_case::GetAllPostInfoUseCaseImpl,
get_post_by_id_use_case::GetFullPostUseCaseImpl, get_post_by_id_use_case::GetFullPostUseCaseImpl,
get_post_by_semantic_id_use_case::GetPostBySemanticIdUseCaseImpl, get_post_by_semantic_id_use_case::GetPostBySemanticIdUseCaseImpl,
update_label_use_case::UpdateLabelUseCaseImpl, update_post_use_case::UpdatePostUseCaseImpl, update_post_use_case::UpdatePostUseCaseImpl,
},
framework::db::{
label_db_service_impl::LabelDbServiceImpl, post_db_service_impl::PostDbServiceImpl,
}, },
framework::db::post_db_service_impl::PostDbServiceImpl,
}; };
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
@ -54,6 +61,7 @@ use crate::configuration::Configuration;
pub struct Container { pub struct Container {
pub auth_controller: Arc<dyn AuthController>, pub auth_controller: Arc<dyn AuthController>,
pub image_controller: Arc<dyn ImageController>, pub image_controller: Arc<dyn ImageController>,
pub label_controller: Arc<dyn LabelController>,
pub post_controller: Arc<dyn PostController>, pub post_controller: Arc<dyn PostController>,
} }
@ -87,13 +95,27 @@ impl Container {
get_user_use_case, get_user_use_case,
)); ));
// Post // Label
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone()));
let label_db_service = Arc::new(LabelDbServiceImpl::new(db_pool.clone())); 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 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 = let get_all_post_info_use_case =
Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone())); Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone()));
let get_post_by_id_use_case = 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 create_post_use_case = Arc::new(CreatePostUseCaseImpl::new(post_repository.clone()));
let update_post_use_case = Arc::new(UpdatePostUseCaseImpl::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( let post_controller = Arc::new(PostControllerImpl::new(
get_all_post_info_use_case, get_all_post_info_use_case,
@ -115,9 +133,6 @@ impl Container {
get_post_by_semantic_id_use_case, get_post_by_semantic_id_use_case,
create_post_use_case, create_post_use_case,
update_post_use_case, update_post_use_case,
create_label_use_case,
update_label_use_case,
get_all_labels_use_case,
)); ));
// Image // Image
@ -141,6 +156,7 @@ impl Container {
Self { Self {
auth_controller, auth_controller,
image_controller, image_controller,
label_controller,
post_controller, post_controller,
} }
} }

View File

@ -10,6 +10,7 @@ use actix_web::{
}; };
use auth::framework::web::auth_web_routes::configure_auth_routes; use auth::framework::web::auth_web_routes::configure_auth_routes;
use image::framework::web::image_web_routes::configure_image_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 openidconnect::reqwest;
use post::framework::web::post_web_routes::configure_post_routes; use post::framework::web::post_web_routes::configure_post_routes;
use server::{ use server::{
@ -84,9 +85,11 @@ fn create_app(
.wrap(session_middleware_builder.build()) .wrap(session_middleware_builder.build())
.app_data(web::Data::from(container.auth_controller)) .app_data(web::Data::from(container.auth_controller))
.app_data(web::Data::from(container.image_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)) .app_data(web::Data::from(container.post_controller))
.configure(configure_api_doc_routes) .configure(configure_api_doc_routes)
.configure(configure_auth_routes) .configure(configure_auth_routes)
.configure(configure_image_routes) .configure(configure_image_routes)
.configure(configure_label_routes)
.configure(configure_post_routes) .configure(configure_post_routes)
} }

View File

@ -1,4 +1,4 @@
import { Color } from '$lib/post/domain/entity/color'; import { Color } from '$lib/label/domain/entity/color';
import z from 'zod'; import z from 'zod';
export const ColorResponseSchema = z.object({ export const ColorResponseSchema = z.object({

View File

@ -1,5 +1,5 @@
import { ColorResponseDto, ColorResponseSchema } from '$lib/post/adapter/gateway/colorResponseDto'; import { ColorResponseDto, ColorResponseSchema } from '$lib/label/adapter/gateway/colorResponseDto';
import { Label } from '$lib/post/domain/entity/label'; import { Label } from '$lib/label/domain/entity/label';
import { z } from 'zod'; import { z } from 'zod';
export const LabelResponseSchema = z.object({ export const LabelResponseSchema = z.object({

View File

@ -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 { export class ColorViewModel {
readonly red: number; readonly red: number;

View File

@ -1,8 +1,8 @@
import { import {
ColorViewModel, ColorViewModel,
type DehydratedColorProps, type DehydratedColorProps,
} from '$lib/post/adapter/presenter/colorViewModel'; } from '$lib/label/adapter/presenter/colorViewModel';
import type { Label } from '$lib/post/domain/entity/label'; import type { Label } from '$lib/label/domain/entity/label';
export class LabelViewModel { export class LabelViewModel {
readonly id: number; readonly id: number;

View File

@ -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 { export class Label {
readonly id: number; readonly id: number;

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel'; import type { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
const { label }: { label: LabelViewModel } = $props(); const { label }: { label: LabelViewModel } = $props();
</script> </script>

View File

@ -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 { PostInfo } from '$lib/post/domain/entity/postInfo';
import z from 'zod'; import z from 'zod';

View File

@ -1,7 +1,7 @@
import { import {
LabelViewModel, LabelViewModel,
type DehydratedLabelProps, type DehydratedLabelProps,
} from '$lib/post/adapter/presenter/labelViewModel'; } from '$lib/label/adapter/presenter/labelViewModel';
import type { PostInfo } from '$lib/post/domain/entity/postInfo'; import type { PostInfo } from '$lib/post/domain/entity/postInfo';
export class PostInfoViewModel { export class PostInfoViewModel {

View File

@ -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 { export class PostInfo {
readonly id: number; readonly id: number;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import type { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
import PostLabel from '$lib/post/framework/ui/PostLabel.svelte'; import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
const { postInfo }: { postInfo: PostInfoViewModel } = $props(); const { postInfo }: { postInfo: PostInfoViewModel } = $props();
</script> </script>

View File

@ -10,7 +10,7 @@
import CreatePostDialog, { import CreatePostDialog, {
type CreatePostDialogFormParams, type CreatePostDialogFormParams,
} from '$lib/post/framework/ui/CreatePostDialog.svelte'; } 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 { getContext, onMount } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel'; import type { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
import PostLabel from '$lib/post/framework/ui/PostLabel.svelte'; import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
const { labels }: { labels: readonly LabelViewModel[] } = $props(); const { labels }: { labels: readonly LabelViewModel[] } = $props();
</script> </script>