BLOG-104 Implement CRUD functionality for Posts #108
16
backend/.sqlx/query-0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7.json
generated
Normal file
16
backend/.sqlx/query-0c9effcc24f4319c47898e0ade4e5ccef3c47c014cfcb65805cbf1c625fef1e7.json
generated
Normal file
@ -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"
|
||||
}
|
32
backend/.sqlx/query-38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019.json
generated
Normal file
32
backend/.sqlx/query-38181c2e36077c546944fbfe124c623706f920fd6b1a9a1cd143ecee6c9d5019.json
generated
Normal file
@ -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"
|
||||
}
|
16
backend/.sqlx/query-5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json
generated
Normal file
16
backend/.sqlx/query-5189bdfd0aa6b4ac478cc48efde4cdbd9cc9605fe0f2c4dc4506827fa0fd2ad6.json
generated
Normal file
@ -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"
|
||||
}
|
34
backend/.sqlx/query-a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a.json
generated
Normal file
34
backend/.sqlx/query-a0e1ed95ce9d705653281455cc59e8ed130a496b09dccbf89e919f4c9798e91a.json
generated
Normal file
@ -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"
|
||||
}
|
14
backend/.sqlx/query-b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e.json
generated
Normal file
14
backend/.sqlx/query-b084aa65fa3cdb1abdd02fd9e2ade04a71dd98eef245780a6f34f0b72564f63e.json
generated
Normal file
@ -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"
|
||||
}
|
19
backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json
generated
Normal file
19
backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json
generated
Normal file
@ -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"
|
||||
}
|
26
backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json
generated
Normal file
26
backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json
generated
Normal file
@ -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"
|
||||
}
|
23
backend/.sqlx/query-f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3.json
generated
Normal file
23
backend/.sqlx/query-f4ef2b4e53389d2bf6a6299fc4e4ffd0df1393e1805ae1c37306b25c721de7e3.json
generated
Normal file
@ -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"
|
||||
}
|
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@ -3537,6 +3537,7 @@ dependencies = [
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,39 @@
|
||||
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 content: String,
|
||||
pub label_ids: Vec<i32>,
|
||||
|
||||
#[schema(format = Uri)]
|
||||
pub preview_image_url: String,
|
||||
|
||||
#[schema(required, format = DateTime)]
|
||||
pub published_time: Option<String>,
|
||||
}
|
||||
|
||||
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
|
||||
.and_then(|time_str| DateTime::parse_from_rfc3339(&time_str).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
},
|
||||
content: self.content,
|
||||
}
|
||||
}
|
||||
}
|
@ -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<PostResponseDto, PostError>;
|
||||
|
||||
async fn create_post(&self, post: CreatePostRequestDto) -> Result<PostResponseDto, PostError>;
|
||||
|
||||
async fn update_post(
|
||||
&self,
|
||||
id: i32,
|
||||
post: UpdatePostRequestDto,
|
||||
) -> Result<PostResponseDto, PostError>;
|
||||
|
||||
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<dyn GetAllPostInfoUseCase>,
|
||||
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
|
||||
create_post_use_case: Arc<dyn CreatePostUseCase>,
|
||||
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>,
|
||||
@ -58,6 +71,8 @@ impl PostControllerImpl {
|
||||
pub fn new(
|
||||
get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
|
||||
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
|
||||
create_post_use_case: Arc<dyn CreatePostUseCase>,
|
||||
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>,
|
||||
@ -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<PostResponseDto, PostError> {
|
||||
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<PostResponseDto, PostError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,13 @@ pub struct PostInfoResponseDto {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub preview_image_url: String,
|
||||
pub labels: Vec<LabelResponseDto>,
|
||||
pub published_time: Option<i64>,
|
||||
|
||||
#[schema(format = Uri)]
|
||||
pub preview_image_url: String,
|
||||
|
||||
#[schema(format = DateTime)]
|
||||
pub published_time: Option<String>,
|
||||
}
|
||||
|
||||
impl From<PostInfo> for PostInfoResponseDto {
|
||||
@ -27,9 +31,7 @@ impl From<PostInfo> 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
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 content: String,
|
||||
pub label_ids: Vec<i32>,
|
||||
|
||||
#[schema(format = Uri)]
|
||||
pub preview_image_url: String,
|
||||
|
||||
#[schema(required, format = DateTime)]
|
||||
pub published_time: Option<String>,
|
||||
}
|
||||
|
||||
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
|
||||
.and_then(|time_str| DateTime::parse_from_rfc3339(&time_str).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc)),
|
||||
},
|
||||
content: self.content,
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Vec<PostInfoMapper>, PostError>;
|
||||
async fn get_full_post(&self, id: i32) -> Result<PostMapper, PostError>;
|
||||
async fn get_post_by_id(&self, id: i32) -> Result<PostMapper, PostError>;
|
||||
async fn create_post(&self, post: PostMapper, label_ids: &[i32]) -> Result<i32, PostError>;
|
||||
async fn update_post(&self, post: PostMapper, label_ids: &[i32]) -> Result<(), PostError>;
|
||||
}
|
||||
|
@ -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<Post, PostError> {
|
||||
async fn get_post_by_id(&self, id: i32) -> Result<Post, PostError> {
|
||||
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<i32, 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
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
@ -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<Vec<PostInfo>, PostError>;
|
||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError>;
|
||||
async fn get_post_by_id(&self, id: i32) -> Result<Post, PostError>;
|
||||
async fn create_post(&self, post: Post, label_ids: &[i32]) -> Result<i32, PostError>;
|
||||
async fn update_post(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<i32, PostError>;
|
||||
}
|
||||
|
||||
pub struct CreatePostUseCaseImpl {
|
||||
post_repository: Arc<dyn PostRepository>,
|
||||
}
|
||||
|
||||
impl CreatePostUseCaseImpl {
|
||||
pub fn new(post_repository: Arc<dyn PostRepository>) -> Self {
|
||||
Self { post_repository }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CreatePostUseCase for CreatePostUseCaseImpl {
|
||||
async fn execute(&self, post: Post, label_ids: &[i32]) -> Result<i32, PostError> {
|
||||
self.post_repository
|
||||
.create_post(post, label_ids)
|
||||
.await
|
||||
}
|
||||
}
|
@ -25,6 +25,6 @@ impl GetFullPostUseCaseImpl {
|
||||
#[async_trait]
|
||||
impl GetFullPostUseCase for GetFullPostUseCaseImpl {
|
||||
async fn execute(&self, id: i32) -> Result<Post, PostError> {
|
||||
self.post_repository.get_full_post(id).await
|
||||
self.post_repository.get_post_by_id(id).await
|
||||
}
|
||||
}
|
||||
|
@ -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<dyn PostRepository>,
|
||||
}
|
||||
|
||||
impl UpdatePostUseCaseImpl {
|
||||
pub fn new(post_repository: Arc<dyn PostRepository>) -> 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
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
@ -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::<PostInfoWithLabelRecord>()
|
||||
@ -105,7 +105,7 @@ impl PostDbService for PostDbServiceImpl {
|
||||
Ok(ordered_posts)
|
||||
}
|
||||
|
||||
async fn get_full_post(&self, id: i32) -> Result<PostMapper, PostError> {
|
||||
async fn get_post_by_id(&self, id: i32) -> Result<PostMapper, PostError> {
|
||||
let mut query_builder = sqlx::QueryBuilder::new(
|
||||
r#"
|
||||
SELECT
|
||||
@ -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::<PostWithLabelRecord>()
|
||||
@ -182,4 +182,121 @@ impl PostDbService for PostDbServiceImpl {
|
||||
None => Err(PostError::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_post(&self, post: PostMapper, label_ids: &[i32]) -> Result<i32, PostError> {
|
||||
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 (order, &label_id) in label_ids.iter().enumerate() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO post_label (
|
||||
post_id, label_id, "order"
|
||||
) VALUES ($1, $2, $3)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
post_id,
|
||||
label_id,
|
||||
order as i32,
|
||||
)
|
||||
.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 (order, &label_id) in label_ids.iter().enumerate() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO post_label (
|
||||
post_id, label_id, "order"
|
||||
) VALUES ($1, $2, $3)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
post.id,
|
||||
label_id,
|
||||
order as i32,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<dyn PostController>,
|
||||
post_dto: web::Json<CreatePostRequestDto>,
|
||||
_: 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -0,0 +1,43 @@
|
||||
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),
|
||||
(status = 404, description = "Post not found"),
|
||||
),
|
||||
security(
|
||||
("oauth2" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn update_post_handler(
|
||||
post_controller: web::Data<dyn PostController>,
|
||||
path: web::Path<i32>,
|
||||
post_dto: web::Json<UpdatePostRequestDto>,
|
||||
_: 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
ALTER TABLE post_label ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;
|
@ -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,
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user