From 27c23367add594d7096c4792f74b0df9d7fa1a3f Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 13:31:23 +0800 Subject: [PATCH 1/5] BLOG-104 feat: create and update post functionality with corresponding DTOs and handlers --- backend/feature/post/src/adapter/delivery.rs | 2 + .../delivery/create_post_request_dto.rs | 37 ++++++ .../src/adapter/delivery/post_controller.rs | 48 ++++++- .../delivery/update_post_request_dto.rs | 37 ++++++ .../src/adapter/gateway/post_db_service.rs | 6 +- .../adapter/gateway/post_repository_impl.rs | 47 ++++++- .../application/gateway/post_repository.rs | 4 +- .../feature/post/src/application/use_case.rs | 2 + .../use_case/create_post_use_case.rs | 32 +++++ .../use_case/get_full_post_use_case.rs | 2 +- .../use_case/update_post_use_case.rs | 30 +++++ .../src/framework/db/post_db_service_impl.rs | 121 +++++++++++++++++- backend/feature/post/src/framework/web.rs | 2 + .../src/framework/web/create_post_handler.rs | 35 +++++ .../post/src/framework/web/post_api_doc.rs | 6 +- .../post/src/framework/web/post_web_routes.rs | 8 +- .../src/framework/web/update_post_handler.rs | 42 ++++++ backend/server/src/container.rs | 6 + 18 files changed, 452 insertions(+), 15 deletions(-) create mode 100644 backend/feature/post/src/adapter/delivery/create_post_request_dto.rs create mode 100644 backend/feature/post/src/adapter/delivery/update_post_request_dto.rs create mode 100644 backend/feature/post/src/application/use_case/create_post_use_case.rs create mode 100644 backend/feature/post/src/application/use_case/update_post_use_case.rs create mode 100644 backend/feature/post/src/framework/web/create_post_handler.rs create mode 100644 backend/feature/post/src/framework/web/update_post_handler.rs diff --git a/backend/feature/post/src/adapter/delivery.rs b/backend/feature/post/src/adapter/delivery.rs index c9d70c0..67542af 100644 --- a/backend/feature/post/src/adapter/delivery.rs +++ b/backend/feature/post/src/adapter/delivery.rs @@ -1,9 +1,11 @@ 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/create_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs new file mode 100644 index 0000000..9c273d2 --- /dev/null +++ b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs @@ -0,0 +1,37 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use utoipa::ToSchema; + +use crate::domain::entity::{post::Post, post_info::PostInfo}; + +#[derive(Deserialize, ToSchema, Clone)] +pub struct CreatePostRequestDto { + pub title: String, + pub description: String, + pub preview_image_url: String, + pub content: String, + pub label_ids: Vec, + + #[schema(required)] + pub published_time: Option, +} + +impl CreatePostRequestDto { + pub fn into_entity(self) -> Post { + Post { + id: -1, + info: PostInfo { + id: -1, + title: self.title, + description: self.description, + preview_image_url: self.preview_image_url, + labels: Vec::new(), + published_time: self + .published_time + .map(|micros| DateTime::::from_timestamp_micros(micros)) + .flatten(), + }, + content: self.content, + } + } +} diff --git a/backend/feature/post/src/adapter/delivery/post_controller.rs b/backend/feature/post/src/adapter/delivery/post_controller.rs index 1b94cb5..736d765 100644 --- a/backend/feature/post/src/adapter/delivery/post_controller.rs +++ b/backend/feature/post/src/adapter/delivery/post_controller.rs @@ -4,16 +4,19 @@ use async_trait::async_trait; use crate::{ adapter::delivery::{ - create_label_request_dto::CreateLabelRequestDto, post_info_query_dto::PostQueryDto, + 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_label_use_case::CreateLabelUseCase, create_post_use_case::CreatePostUseCase, get_all_labels_use_case::GetAllLabelsUseCase, get_all_post_info_use_case::GetAllPostInfoUseCase, get_full_post_use_case::GetFullPostUseCase, update_label_use_case::UpdateLabelUseCase, + update_post_use_case::UpdatePostUseCase, }, }, }; @@ -32,6 +35,14 @@ pub trait PostController: Send + Sync { async fn get_post_by_id(&self, id: i32) -> Result; + async fn create_post(&self, post: CreatePostRequestDto) -> Result; + + async fn update_post( + &self, + id: i32, + post: UpdatePostRequestDto, + ) -> Result; + async fn create_label( &self, label: CreateLabelRequestDto, @@ -49,6 +60,8 @@ pub trait PostController: Send + Sync { pub struct PostControllerImpl { get_all_post_info_use_case: Arc, get_full_post_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, @@ -58,6 +71,8 @@ impl PostControllerImpl { pub fn new( get_all_post_info_use_case: Arc, get_full_post_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, @@ -65,6 +80,8 @@ impl PostControllerImpl { Self { get_all_post_info_use_case, get_full_post_use_case, + create_post_use_case, + update_post_use_case, create_label_use_case, update_label_use_case, get_all_labels_use_case, @@ -136,4 +153,31 @@ impl PostController for PostControllerImpl { .collect() }) } + + async fn create_post(&self, post: CreatePostRequestDto) -> Result { + let label_ids = post.label_ids.clone(); + let post_entity = post.into_entity(); + + let id = self + .create_post_use_case + .execute(post_entity, &label_ids) + .await?; + + self.get_post_by_id(id).await + } + + async fn update_post( + &self, + id: i32, + post: UpdatePostRequestDto, + ) -> Result { + let label_ids = post.label_ids.clone(); + let post_entity = post.into_entity(id); + + self.update_post_use_case + .execute(post_entity, &label_ids) + .await?; + + self.get_post_by_id(id).await + } } diff --git a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs new file mode 100644 index 0000000..f7f3683 --- /dev/null +++ b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs @@ -0,0 +1,37 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use utoipa::ToSchema; + +use crate::domain::entity::{post::Post, post_info::PostInfo}; + +#[derive(Deserialize, ToSchema, Clone)] +pub struct UpdatePostRequestDto { + pub title: String, + pub description: String, + pub preview_image_url: String, + pub content: String, + pub label_ids: Vec, + + #[schema(required)] + pub published_time: Option, +} + +impl UpdatePostRequestDto { + pub fn into_entity(self, id: i32) -> Post { + Post { + id, + info: PostInfo { + id, + title: self.title, + description: self.description, + preview_image_url: self.preview_image_url, + labels: Vec::new(), + published_time: self + .published_time + .map(|micros| DateTime::::from_timestamp_micros(micros)) + .flatten(), + }, + content: self.content, + } + } +} diff --git a/backend/feature/post/src/adapter/gateway/post_db_service.rs b/backend/feature/post/src/adapter/gateway/post_db_service.rs index 489a0b2..ff330b8 100644 --- a/backend/feature/post/src/adapter/gateway/post_db_service.rs +++ b/backend/feature/post/src/adapter/gateway/post_db_service.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use crate::{ - adapter::gateway::{post_info_db_mapper::PostInfoMapper, post_db_mapper::PostMapper}, + adapter::gateway::{post_db_mapper::PostMapper, post_info_db_mapper::PostInfoMapper}, application::error::post_error::PostError, }; @@ -11,5 +11,7 @@ pub trait PostDbService: Send + Sync { &self, is_published_only: bool, ) -> Result, PostError>; - async fn get_full_post(&self, id: i32) -> Result; + async fn get_post_by_id(&self, id: i32) -> Result; + async fn create_post(&self, post: PostMapper, label_ids: &[i32]) -> Result; + async fn update_post(&self, post: PostMapper, label_ids: &[i32]) -> Result<(), PostError>; } diff --git a/backend/feature/post/src/adapter/gateway/post_repository_impl.rs b/backend/feature/post/src/adapter/gateway/post_repository_impl.rs index 6b9da9c..6fb0f97 100644 --- a/backend/feature/post/src/adapter/gateway/post_repository_impl.rs +++ b/backend/feature/post/src/adapter/gateway/post_repository_impl.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use crate::{ + adapter::gateway::{post_db_mapper::PostMapper, post_info_db_mapper::PostInfoMapper}, application::{error::post_error::PostError, gateway::post_repository::PostRepository}, domain::entity::{post::Post, post_info::PostInfo}, }; @@ -33,10 +34,52 @@ impl PostRepository for PostRepositoryImpl { }) } - async fn get_full_post(&self, id: i32) -> Result { + async fn get_post_by_id(&self, id: i32) -> Result { self.post_db_service - .get_full_post(id) + .get_post_by_id(id) .await .map(|mapper| mapper.into_entity()) } + + async fn create_post(&self, post: Post, label_ids: &[i32]) -> Result { + let info_mapper = PostInfoMapper { + id: post.info.id, + title: post.info.title, + description: post.info.description, + preview_image_url: post.info.preview_image_url, + labels: Vec::new(), + published_time: post.info.published_time.map(|dt| dt.naive_utc()), + }; + + let post_mapper = PostMapper { + id: post.id, + info: info_mapper, + content: post.content, + }; + + self.post_db_service + .create_post(post_mapper, label_ids) + .await + } + + async fn update_post(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError> { + let info_mapper = PostInfoMapper { + id: post.info.id, + title: post.info.title, + description: post.info.description, + preview_image_url: post.info.preview_image_url, + labels: Vec::new(), + published_time: post.info.published_time.map(|dt| dt.naive_utc()), + }; + + let post_mapper = PostMapper { + id: post.id, + info: info_mapper, + content: post.content, + }; + + self.post_db_service + .update_post(post_mapper, label_ids) + .await + } } diff --git a/backend/feature/post/src/application/gateway/post_repository.rs b/backend/feature/post/src/application/gateway/post_repository.rs index 4e6c9e5..36f910e 100644 --- a/backend/feature/post/src/application/gateway/post_repository.rs +++ b/backend/feature/post/src/application/gateway/post_repository.rs @@ -8,5 +8,7 @@ use crate::{ #[async_trait] pub trait PostRepository: Send + Sync { async fn get_all_post_info(&self, is_published_only: bool) -> Result, PostError>; - async fn get_full_post(&self, id: i32) -> Result; + async fn get_post_by_id(&self, id: i32) -> Result; + async fn create_post(&self, post: Post, label_ids: &[i32]) -> Result; + async fn update_post(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError>; } diff --git a/backend/feature/post/src/application/use_case.rs b/backend/feature/post/src/application/use_case.rs index c523e59..88d3d50 100644 --- a/backend/feature/post/src/application/use_case.rs +++ b/backend/feature/post/src/application/use_case.rs @@ -1,5 +1,7 @@ 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_full_post_use_case; pub mod update_label_use_case; +pub mod update_post_use_case; diff --git a/backend/feature/post/src/application/use_case/create_post_use_case.rs b/backend/feature/post/src/application/use_case/create_post_use_case.rs new file mode 100644 index 0000000..892f9a7 --- /dev/null +++ b/backend/feature/post/src/application/use_case/create_post_use_case.rs @@ -0,0 +1,32 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + application::{error::post_error::PostError, gateway::post_repository::PostRepository}, + domain::entity::post::Post, +}; + +#[async_trait] +pub trait CreatePostUseCase: Send + Sync { + async fn execute(&self, post: Post, label_ids: &[i32]) -> Result; +} + +pub struct CreatePostUseCaseImpl { + post_repository: Arc, +} + +impl CreatePostUseCaseImpl { + pub fn new(post_repository: Arc) -> Self { + Self { post_repository } + } +} + +#[async_trait] +impl CreatePostUseCase for CreatePostUseCaseImpl { + async fn execute(&self, post: Post, label_ids: &[i32]) -> Result { + self.post_repository + .create_post(post, label_ids) + .await + } +} diff --git a/backend/feature/post/src/application/use_case/get_full_post_use_case.rs b/backend/feature/post/src/application/use_case/get_full_post_use_case.rs index fe882b0..9fe345f 100644 --- a/backend/feature/post/src/application/use_case/get_full_post_use_case.rs +++ b/backend/feature/post/src/application/use_case/get_full_post_use_case.rs @@ -25,6 +25,6 @@ impl GetFullPostUseCaseImpl { #[async_trait] impl GetFullPostUseCase for GetFullPostUseCaseImpl { async fn execute(&self, id: i32) -> Result { - self.post_repository.get_full_post(id).await + self.post_repository.get_post_by_id(id).await } } diff --git a/backend/feature/post/src/application/use_case/update_post_use_case.rs b/backend/feature/post/src/application/use_case/update_post_use_case.rs new file mode 100644 index 0000000..295cdba --- /dev/null +++ b/backend/feature/post/src/application/use_case/update_post_use_case.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + application::{error::post_error::PostError, gateway::post_repository::PostRepository}, + domain::entity::post::Post, +}; + +#[async_trait] +pub trait UpdatePostUseCase: Send + Sync { + async fn execute(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError>; +} + +pub struct UpdatePostUseCaseImpl { + post_repository: Arc, +} + +impl UpdatePostUseCaseImpl { + pub fn new(post_repository: Arc) -> Self { + Self { post_repository } + } +} + +#[async_trait] +impl UpdatePostUseCase for UpdatePostUseCaseImpl { + async fn execute(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError> { + self.post_repository.update_post(post, label_ids).await + } +} 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 a6e3948..9d31551 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 @@ -5,8 +5,8 @@ use sqlx::{Pool, Postgres}; use crate::{ adapter::gateway::{ - color_db_mapper::ColorMapper, label_db_mapper::LabelMapper, post_db_service::PostDbService, - post_info_db_mapper::PostInfoMapper, post_db_mapper::PostMapper, + color_db_mapper::ColorMapper, label_db_mapper::LabelMapper, post_db_mapper::PostMapper, + post_db_service::PostDbService, post_info_db_mapper::PostInfoMapper, }, application::error::post_error::PostError, }; @@ -105,7 +105,7 @@ impl PostDbService for PostDbServiceImpl { Ok(ordered_posts) } - async fn get_full_post(&self, id: i32) -> Result { + async fn get_post_by_id(&self, id: i32) -> Result { let mut query_builder = sqlx::QueryBuilder::new( r#" SELECT @@ -182,4 +182,119 @@ impl PostDbService for PostDbServiceImpl { None => Err(PostError::NotFound), } } + + async fn create_post(&self, post: PostMapper, label_ids: &[i32]) -> Result { + let mut tx = self + .db_pool + .begin() + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + + let post_id = sqlx::query_scalar!( + r#" + INSERT INTO post ( + title, description, preview_image_url, content, published_time + ) VALUES ($1, $2, $3, $4, $5) + RETURNING id + "#, + post.info.title, + post.info.description, + post.info.preview_image_url, + post.content, + post.info.published_time, + ) + .fetch_one(&mut *tx) + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + + for label_id in label_ids { + sqlx::query!( + r#" + INSERT INTO post_label ( + post_id, label_id + ) VALUES ($1, $2) + ON CONFLICT DO NOTHING + "#, + post_id, + label_id, + ) + .execute(&mut *tx) + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + } + + tx.commit() + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + + Ok(post_id) + } + + async fn update_post(&self, post: PostMapper, label_ids: &[i32]) -> Result<(), PostError> { + let mut tx = self + .db_pool + .begin() + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + + let affected_rows = sqlx::query!( + r#" + UPDATE post + SET + title = $1, + description = $2, + preview_image_url = $3, + content = $4, + published_time = $5 + WHERE id = $6 + "#, + post.info.title, + post.info.description, + post.info.preview_image_url, + post.content, + post.info.published_time, + post.id, + ) + .execute(&mut *tx) + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))? + .rows_affected(); + + if affected_rows == 0 { + return Err(PostError::NotFound); + } + + sqlx::query!( + r#" + DELETE FROM post_label + WHERE post_id = $1 + "#, + post.id, + ) + .execute(&mut *tx) + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + + for label_id in label_ids { + sqlx::query!( + r#" + INSERT INTO post_label ( + post_id, label_id + ) VALUES ($1, $2) + ON CONFLICT DO NOTHING + "#, + post.id, + label_id, + ) + .execute(&mut *tx) + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + } + + tx.commit() + .await + .map_err(|err| PostError::DatabaseError(err.to_string()))?; + + Ok(()) + } } diff --git a/backend/feature/post/src/framework/web.rs b/backend/feature/post/src/framework/web.rs index b826afd..f066568 100644 --- a/backend/feature/post/src/framework/web.rs +++ b/backend/feature/post/src/framework/web.rs @@ -2,7 +2,9 @@ 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 new file mode 100644 index 0000000..c26a6b9 --- /dev/null +++ b/backend/feature/post/src/framework/web/create_post_handler.rs @@ -0,0 +1,35 @@ +use actix_web::{web, HttpResponse, Responder}; +use auth::framework::web::auth_middleware::UserId; + +use crate::adapter::delivery::{ + create_post_request_dto::CreatePostRequestDto, post_controller::PostController, + post_response_dto::PostResponseDto, +}; + +#[utoipa::path( + post, + path = "/post", + tag = "post", + summary = "Create a new post", + responses( + (status = 201, body = PostResponseDto), + ), + security( + ("oauth2" = []) + ) +)] +pub async fn create_post_handler( + post_controller: web::Data, + post_dto: web::Json, + _: UserId, +) -> impl Responder { + let result = post_controller.create_post(post_dto.into_inner()).await; + + match result { + Ok(post) => HttpResponse::Created().json(post), + Err(e) => { + log::error!("{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 42cf667..9353710 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,6 @@ use crate::framework::web::{ - create_label_handler, get_all_labels_handler, get_all_post_info_handler, - get_post_by_id_handler, update_label_handler, + 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, }; use utoipa::{OpenApi, openapi}; @@ -8,6 +8,8 @@ use utoipa::{OpenApi, openapi}; #[openapi(paths( get_all_post_info_handler::get_all_post_info_handler, 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 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 a42d4ac..220b1ee 100644 --- a/backend/feature/post/src/framework/web/post_web_routes.rs +++ b/backend/feature/post/src/framework/web/post_web_routes.rs @@ -1,16 +1,20 @@ use actix_web::web; use crate::framework::web::{ - create_label_handler::create_label_handler, get_all_labels_handler::get_all_labels_handler, + 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, }; pub fn configure_post_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/post") .route("", web::get().to(get_all_post_info_handler)) - .route("/{id}", web::get().to(get_post_by_id_handler)), + .route("", web::post().to(create_post_handler)) + .route("/{id}", web::get().to(get_post_by_id_handler)) + .route("/{id}", web::put().to(update_post_handler)), ); cfg.service( diff --git a/backend/feature/post/src/framework/web/update_post_handler.rs b/backend/feature/post/src/framework/web/update_post_handler.rs new file mode 100644 index 0000000..62a5857 --- /dev/null +++ b/backend/feature/post/src/framework/web/update_post_handler.rs @@ -0,0 +1,42 @@ +use actix_web::{HttpResponse, Responder, web}; +use auth::framework::web::auth_middleware::UserId; + +use crate::adapter::delivery::{ + post_controller::PostController, post_response_dto::PostResponseDto, + update_post_request_dto::UpdatePostRequestDto, +}; + +#[utoipa::path( + put, + path = "/post/{id}", + tag = "post", + summary = "Update a post by ID", + responses( + (status = 200, body = PostResponseDto), + ), + security( + ("oauth2" = []) + ) +)] +pub async fn update_post_handler( + post_controller: web::Data, + path: web::Path, + post_dto: web::Json, + _: UserId, +) -> impl Responder { + let id = path.into_inner(); + let result = post_controller.update_post(id, post_dto.into_inner()).await; + + match result { + Ok(post) => HttpResponse::Ok().json(post), + Err(e) => { + log::error!("{e:?}"); + match e { + crate::application::error::post_error::PostError::NotFound => { + HttpResponse::NotFound().finish() + } + _ => HttpResponse::InternalServerError().finish(), + } + } + } +} diff --git a/backend/server/src/container.rs b/backend/server/src/container.rs index b787292..d8685da 100644 --- a/backend/server/src/container.rs +++ b/backend/server/src/container.rs @@ -37,10 +37,12 @@ use post::{ }, application::use_case::{ create_label_use_case::CreateLabelUseCaseImpl, + create_post_use_case::CreatePostUseCaseImpl, get_all_labels_use_case::GetAllLabelsUseCaseImpl, get_all_post_info_use_case::GetAllPostInfoUseCaseImpl, get_full_post_use_case::GetFullPostUseCaseImpl, update_label_use_case::UpdateLabelUseCaseImpl, + update_post_use_case::UpdatePostUseCaseImpl, }, framework::db::{ label_db_service_impl::LabelDbServiceImpl, post_db_service_impl::PostDbServiceImpl, @@ -96,6 +98,8 @@ impl Container { let get_all_post_info_use_case = Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository.clone())); let get_full_post_use_case = Arc::new(GetFullPostUseCaseImpl::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 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 = @@ -104,6 +108,8 @@ impl Container { let post_controller = Arc::new(PostControllerImpl::new( get_all_post_info_use_case, get_full_post_use_case, + create_post_use_case, + update_post_use_case, create_label_use_case, update_label_use_case, get_all_labels_use_case, -- 2.47.2 From 30ec8651bb563f8e2c31e9df554eebede1e00cff Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 13:41:14 +0800 Subject: [PATCH 2/5] BLOG-104 feat: add order column to post_label and update related queries in PostDbServiceImpl --- ...e5ccef3c47c014cfcb65805cbf1c625fef1e7.json | 16 +++++++++ ...c623706f920fd6b1a9a1cd143ecee6c9d5019.json | 32 +++++++++++++++++ ...4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json | 16 +++++++++ ...9e8ed130a496b09dccbf89e919f4c9798e91a.json | 34 +++++++++++++++++++ ...de04a71dd98eef245780a6f34f0b72564f63e.json | 14 ++++++++ ...c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json | 19 +++++++++++ ...59d4e3829b9b2a6a496c9a429a05fbdb2e30a.json | 26 ++++++++++++++ ...4ffd0df1393e1805ae1c37306b25c721de7e3.json | 23 +++++++++++++ .../src/framework/db/post_db_service_impl.rs | 18 +++++----- ...20250802053708_add_order_to_post_label.sql | 1 + 10 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 backend/.sqlx/query-0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7.json create mode 100644 backend/.sqlx/query-38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019.json create mode 100644 backend/.sqlx/query-5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json create mode 100644 backend/.sqlx/query-a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a.json create mode 100644 backend/.sqlx/query-b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e.json create mode 100644 backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json create mode 100644 backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json create mode 100644 backend/.sqlx/query-f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3.json create mode 100644 backend/migrations/20250802053708_add_order_to_post_label.sql diff --git a/backend/.sqlx/query-0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7.json b/backend/.sqlx/query-0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7.json new file mode 100644 index 0000000..a48ace7 --- /dev/null +++ b/backend/.sqlx/query-0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO post_label (\n post_id, label_id, \"order\"\n ) VALUES ($1, $2, $3)\n ON CONFLICT DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7" +} diff --git a/backend/.sqlx/query-38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019.json b/backend/.sqlx/query-38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019.json new file mode 100644 index 0000000..fb39599 --- /dev/null +++ b/backend/.sqlx/query-38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, name, color\n FROM label\n WHERE deleted_time IS NULL\n ORDER BY id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "color", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019" +} diff --git a/backend/.sqlx/query-5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json b/backend/.sqlx/query-5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json new file mode 100644 index 0000000..787a9f7 --- /dev/null +++ b/backend/.sqlx/query-5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE label\n SET name = $1, color = $2\n WHERE id = $3 AND deleted_time IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6" +} diff --git a/backend/.sqlx/query-a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a.json b/backend/.sqlx/query-a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a.json new file mode 100644 index 0000000..46c8896 --- /dev/null +++ b/backend/.sqlx/query-a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, name, color\n FROM label\n WHERE id = $1 AND deleted_time IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "color", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a" +} diff --git a/backend/.sqlx/query-b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e.json b/backend/.sqlx/query-b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e.json new file mode 100644 index 0000000..fd83c51 --- /dev/null +++ b/backend/.sqlx/query-b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM post_label\n WHERE post_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + }, + "hash": "b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e" +} diff --git a/backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json b/backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json new file mode 100644 index 0000000..f194863 --- /dev/null +++ b/backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE post\n SET \n title = $1, \n description = $2, \n preview_image_url = $3, \n content = $4, \n published_time = $5\n WHERE id = $6\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Timestamp", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381" +} diff --git a/backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json b/backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json new file mode 100644 index 0000000..7d34ac9 --- /dev/null +++ b/backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO post (\n title, description, preview_image_url, content, published_time\n ) VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Timestamp" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a" +} diff --git a/backend/.sqlx/query-f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3.json b/backend/.sqlx/query-f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3.json new file mode 100644 index 0000000..0a36d1d --- /dev/null +++ b/backend/.sqlx/query-f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO label (name, color)\n VALUES ($1, $2)\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3" +} 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 9d31551..6fdcfba 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 @@ -58,7 +58,7 @@ impl PostDbService for PostDbServiceImpl { query_builder.push(r#" AND p.published_time IS NOT NULL"#); } - query_builder.push(r#" ORDER BY p.id"#); + query_builder.push(r#" ORDER BY p.id, pl."order""#); let records = query_builder .build_query_as::() @@ -130,7 +130,7 @@ impl PostDbService for PostDbServiceImpl { ); query_builder.push_bind(id); - query_builder.push(r#" ORDER BY p.id"#); + query_builder.push(r#" ORDER BY p.id, pl."order""#); let records = query_builder .build_query_as::() @@ -207,16 +207,17 @@ impl PostDbService for PostDbServiceImpl { .await .map_err(|err| PostError::DatabaseError(err.to_string()))?; - for label_id in label_ids { + for (order, &label_id) in label_ids.iter().enumerate() { sqlx::query!( r#" INSERT INTO post_label ( - post_id, label_id - ) VALUES ($1, $2) + post_id, label_id, "order" + ) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING "#, post_id, label_id, + order as i32, ) .execute(&mut *tx) .await @@ -275,16 +276,17 @@ impl PostDbService for PostDbServiceImpl { .await .map_err(|err| PostError::DatabaseError(err.to_string()))?; - for label_id in label_ids { + for (order, &label_id) in label_ids.iter().enumerate() { sqlx::query!( r#" INSERT INTO post_label ( - post_id, label_id - ) VALUES ($1, $2) + post_id, label_id, "order" + ) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING "#, post.id, label_id, + order as i32, ) .execute(&mut *tx) .await diff --git a/backend/migrations/20250802053708_add_order_to_post_label.sql b/backend/migrations/20250802053708_add_order_to_post_label.sql new file mode 100644 index 0000000..e7c16c6 --- /dev/null +++ b/backend/migrations/20250802053708_add_order_to_post_label.sql @@ -0,0 +1 @@ +ALTER TABLE post_label ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0; -- 2.47.2 From d69482116c3c5cfc1f3b9a7d16e51137bad23837 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 14:18:17 +0800 Subject: [PATCH 3/5] BLOG-104 refactor: use rfc3339 format datetime --- .../post/src/adapter/delivery/create_post_request_dto.rs | 8 ++++---- .../post/src/adapter/delivery/post_info_response_dto.rs | 8 ++++---- .../post/src/adapter/delivery/update_post_request_dto.rs | 8 ++++---- .../src/lib/post/adapter/gateway/postInfoResponseDto.ts | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs index 9c273d2..053b6a6 100644 --- a/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs @@ -12,8 +12,8 @@ pub struct CreatePostRequestDto { pub content: String, pub label_ids: Vec, - #[schema(required)] - pub published_time: Option, + #[schema(required, format = DateTime)] + pub published_time: Option, } impl CreatePostRequestDto { @@ -28,8 +28,8 @@ impl CreatePostRequestDto { labels: Vec::new(), published_time: self .published_time - .map(|micros| DateTime::::from_timestamp_micros(micros)) - .flatten(), + .and_then(|time_str| DateTime::parse_from_rfc3339(&time_str).ok()) + .map(|dt| dt.with_timezone(&Utc)), }, content: self.content, } 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 dd33091..176b9c3 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 @@ -12,7 +12,9 @@ pub struct PostInfoResponseDto { pub description: String, pub preview_image_url: String, pub labels: Vec, - pub published_time: Option, + + #[schema(format = DateTime)] + pub published_time: Option, } impl From for PostInfoResponseDto { @@ -27,9 +29,7 @@ impl From for PostInfoResponseDto { .into_iter() .map(LabelResponseDto::from) .collect(), - published_time: entity - .published_time - .map(|datetime| datetime.timestamp_micros()), + published_time: entity.published_time.map(|datetime| datetime.to_rfc3339()), } } } diff --git a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs index f7f3683..7ae2128 100644 --- a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs @@ -12,8 +12,8 @@ pub struct UpdatePostRequestDto { pub content: String, pub label_ids: Vec, - #[schema(required)] - pub published_time: Option, + #[schema(required, format = DateTime)] + pub published_time: Option, } impl UpdatePostRequestDto { @@ -28,8 +28,8 @@ impl UpdatePostRequestDto { labels: Vec::new(), published_time: self .published_time - .map(|micros| DateTime::::from_timestamp_micros(micros)) - .flatten(), + .and_then(|time_str| DateTime::parse_from_rfc3339(&time_str).ok()) + .map(|dt| dt.with_timezone(&Utc)), }, content: self.content, } diff --git a/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts b/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts index 1ada6b5..e6393b2 100644 --- a/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts +++ b/frontend/src/lib/post/adapter/gateway/postInfoResponseDto.ts @@ -8,7 +8,7 @@ export const PostInfoResponseSchema = z.object({ description: z.string(), preview_image_url: z.url(), labels: z.array(LabelResponseSchema), - published_time: z.number().int() + published_time: z.iso.datetime() }); export class PostInfoResponseDto { @@ -43,7 +43,7 @@ export class PostInfoResponseDto { description: parsedJson.description, previewImageUrl: new URL(parsedJson.preview_image_url), labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)), - publishedTime: new Date(parsedJson.published_time / 1000) + publishedTime: new Date(parsedJson.published_time) }); } -- 2.47.2 From 451951f22ab336dc2248d3d60f0a81931a9e74a7 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 14:28:39 +0800 Subject: [PATCH 4/5] BLOG-104 feat: add URI format to preview_image_url in request and response DTOs; remove maximum constraints from color DTOs --- backend/Cargo.lock | 1 + backend/Cargo.toml | 6 +++++- .../feature/auth/src/adapter/delivery/user_response_dto.rs | 2 ++ .../feature/post/src/adapter/delivery/color_request_dto.rs | 7 ------- .../post/src/adapter/delivery/color_response_dto.rs | 7 ------- .../post/src/adapter/delivery/create_post_request_dto.rs | 4 +++- .../post/src/adapter/delivery/post_info_response_dto.rs | 4 +++- .../post/src/adapter/delivery/update_post_request_dto.rs | 4 +++- 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 08f59a2..c367e74 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -3537,6 +3537,7 @@ dependencies = [ "quote", "regex", "syn", + "url", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 91ce1e9..e6cd1b2 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -30,7 +30,11 @@ sqlx = { version = "0.8.5", features = [ "runtime-tokio-rustls", ] } tokio = { version = "1.45.0", features = ["full"] } -utoipa = { version = "5.4.0", features = ["actix_extras"] } +utoipa = { version = "5.4.0", features = [ + "actix_extras", + "non_strict_integers", + "url", +] } utoipa-redoc = { version = "6.0.0", features = ["actix-web"] } server.path = "server" diff --git a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs index f040f2d..00e0b18 100644 --- a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs +++ b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs @@ -7,6 +7,8 @@ use crate::domain::entity::user::User; pub struct UserResponseDto { pub id: i32, pub displayed_name: String, + + #[schema(format = Email)] pub email: String, } diff --git a/backend/feature/post/src/adapter/delivery/color_request_dto.rs b/backend/feature/post/src/adapter/delivery/color_request_dto.rs index f6838a3..bd15888 100644 --- a/backend/feature/post/src/adapter/delivery/color_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/color_request_dto.rs @@ -5,16 +5,9 @@ use crate::domain::entity::color::Color; #[derive(Deserialize, ToSchema)] pub struct ColorRequestDto { - #[schema(maximum = 255)] pub red: u8, - - #[schema(maximum = 255)] pub green: u8, - - #[schema(maximum = 255)] pub blue: u8, - - #[schema(maximum = 255)] pub alpha: u8, } diff --git a/backend/feature/post/src/adapter/delivery/color_response_dto.rs b/backend/feature/post/src/adapter/delivery/color_response_dto.rs index 7e64681..274f95a 100644 --- a/backend/feature/post/src/adapter/delivery/color_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/color_response_dto.rs @@ -5,16 +5,9 @@ use crate::domain::entity::color::Color; #[derive(Serialize, ToSchema)] pub struct ColorResponseDto { - #[schema(maximum = 255)] pub red: u8, - - #[schema(maximum = 255)] pub green: u8, - - #[schema(maximum = 255)] pub blue: u8, - - #[schema(maximum = 255)] pub alpha: u8, } diff --git a/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs index 053b6a6..c242d0a 100644 --- a/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/create_post_request_dto.rs @@ -8,10 +8,12 @@ use crate::domain::entity::{post::Post, post_info::PostInfo}; pub struct CreatePostRequestDto { pub title: String, pub description: String, - pub preview_image_url: String, pub content: String, pub label_ids: Vec, + #[schema(format = Uri)] + pub preview_image_url: String, + #[schema(required, format = DateTime)] pub published_time: Option, } 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 176b9c3..1a5e94e 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 @@ -10,9 +10,11 @@ pub struct PostInfoResponseDto { pub id: i32, pub title: String, pub description: String, - pub preview_image_url: String, pub labels: Vec, + #[schema(format = Uri)] + pub preview_image_url: String, + #[schema(format = DateTime)] pub published_time: Option, } diff --git a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs index 7ae2128..29f96a7 100644 --- a/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs +++ b/backend/feature/post/src/adapter/delivery/update_post_request_dto.rs @@ -8,10 +8,12 @@ use crate::domain::entity::{post::Post, post_info::PostInfo}; pub struct UpdatePostRequestDto { pub title: String, pub description: String, - pub preview_image_url: String, pub content: String, pub label_ids: Vec, + #[schema(format = Uri)] + pub preview_image_url: String, + #[schema(required, format = DateTime)] pub published_time: Option, } -- 2.47.2 From d5b7e81b27edb1a35b42971314474c18272dafe6 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 14:29:50 +0800 Subject: [PATCH 5/5] BLOG-104 feat: add 404 response description for post update handler --- backend/feature/post/src/framework/web/update_post_handler.rs | 1 + 1 file changed, 1 insertion(+) 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 62a5857..d9cdb81 100644 --- a/backend/feature/post/src/framework/web/update_post_handler.rs +++ b/backend/feature/post/src/framework/web/update_post_handler.rs @@ -13,6 +13,7 @@ use crate::adapter::delivery::{ summary = "Update a post by ID", responses( (status = 200, body = PostResponseDto), + (status = 404, description = "Post not found"), ), security( ("oauth2" = []) -- 2.47.2