Compare commits
3 Commits
bb9a6bbb12
...
8f91d815bc
Author | SHA1 | Date | |
---|---|---|---|
8f91d815bc | |||
43582af2a5 | |||
6efd941395 |
9
backend/Cargo.lock
generated
9
backend/Cargo.lock
generated
@ -2407,6 +2407,7 @@ dependencies = [
|
||||
"auth",
|
||||
"chrono",
|
||||
"common",
|
||||
"regex",
|
||||
"sentry",
|
||||
"serde",
|
||||
"sqlx",
|
||||
@ -2638,9 +2639,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -2650,9 +2651,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -5,7 +5,6 @@ members = [
|
||||
"feature/common",
|
||||
"feature/image",
|
||||
"feature/post",
|
||||
"feature/common",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@ -33,6 +32,7 @@ openidconnect = { version = "4.0.1", features = [
|
||||
"reqwest-blocking",
|
||||
] }
|
||||
percent-encoding = "2.3.1"
|
||||
regex = "1.12.1"
|
||||
sentry = { version = "0.42.0", features = ["actix", "anyhow"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sqlx = { version = "0.8.5", features = [
|
||||
|
@ -8,6 +8,7 @@ actix-web.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
regex.workspace = true
|
||||
sentry.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
|
@ -6,6 +6,7 @@ use crate::domain::entity::{post::Post, post_info::PostInfo};
|
||||
|
||||
#[derive(Deserialize, ToSchema, Clone)]
|
||||
pub struct CreatePostRequestDto {
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub content: String,
|
||||
@ -24,6 +25,7 @@ impl CreatePostRequestDto {
|
||||
id: -1,
|
||||
info: PostInfo {
|
||||
id: -1,
|
||||
semantic_id: self.semantic_id,
|
||||
title: self.title,
|
||||
description: self.description,
|
||||
preview_image_url: self.preview_image_url,
|
||||
|
@ -15,8 +15,9 @@ use crate::{
|
||||
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,
|
||||
get_post_by_id_use_case::GetPostByIdUseCase,
|
||||
get_post_by_sementic_id_use_case::GetPostBySemanticIdUseCase,
|
||||
update_label_use_case::UpdateLabelUseCase, update_post_use_case::UpdatePostUseCase,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -34,9 +35,9 @@ pub trait PostController: Send + Sync {
|
||||
user_id: Option<i32>,
|
||||
) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
||||
|
||||
async fn get_post_by_id(
|
||||
async fn get_post_by_id_or_semantic_id(
|
||||
&self,
|
||||
id: i32,
|
||||
id_or_semantic_id: &str,
|
||||
user_id: Option<i32>,
|
||||
) -> Result<PostResponseDto, PostError>;
|
||||
|
||||
@ -69,7 +70,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>,
|
||||
get_post_by_id_use_case: Arc<dyn GetPostByIdUseCase>,
|
||||
get_post_by_semantic_id_use_case: Arc<dyn GetPostBySemanticIdUseCase>,
|
||||
create_post_use_case: Arc<dyn CreatePostUseCase>,
|
||||
update_post_use_case: Arc<dyn UpdatePostUseCase>,
|
||||
create_label_use_case: Arc<dyn CreateLabelUseCase>,
|
||||
@ -80,7 +82,8 @@ pub struct PostControllerImpl {
|
||||
impl PostControllerImpl {
|
||||
pub fn new(
|
||||
get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
|
||||
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
|
||||
get_post_by_id_use_case: Arc<dyn GetPostByIdUseCase>,
|
||||
get_post_by_semantic_id_use_case: Arc<dyn GetPostBySemanticIdUseCase>,
|
||||
create_post_use_case: Arc<dyn CreatePostUseCase>,
|
||||
update_post_use_case: Arc<dyn UpdatePostUseCase>,
|
||||
create_label_use_case: Arc<dyn CreateLabelUseCase>,
|
||||
@ -89,7 +92,8 @@ impl PostControllerImpl {
|
||||
) -> Self {
|
||||
Self {
|
||||
get_all_post_info_use_case,
|
||||
get_full_post_use_case,
|
||||
get_post_by_id_use_case,
|
||||
get_post_by_semantic_id_use_case,
|
||||
create_post_use_case,
|
||||
update_post_use_case,
|
||||
create_label_use_case,
|
||||
@ -97,6 +101,29 @@ impl PostControllerImpl {
|
||||
get_all_labels_use_case,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_post_by_id(
|
||||
&self,
|
||||
id: i32,
|
||||
user_id: Option<i32>,
|
||||
) -> Result<PostResponseDto, PostError> {
|
||||
let result = self.get_post_by_id_use_case.execute(id, user_id).await;
|
||||
|
||||
result.map(PostResponseDto::from)
|
||||
}
|
||||
|
||||
async fn get_post_by_semantic_id(
|
||||
&self,
|
||||
semantic_id: &str,
|
||||
user_id: Option<i32>,
|
||||
) -> Result<PostResponseDto, PostError> {
|
||||
let result = self
|
||||
.get_post_by_semantic_id_use_case
|
||||
.execute(semantic_id, user_id)
|
||||
.await;
|
||||
|
||||
result.map(PostResponseDto::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -121,14 +148,17 @@ impl PostController for PostControllerImpl {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_post_by_id(
|
||||
async fn get_post_by_id_or_semantic_id(
|
||||
&self,
|
||||
id: i32,
|
||||
id_or_semantic_id: &str,
|
||||
user_id: Option<i32>,
|
||||
) -> Result<PostResponseDto, PostError> {
|
||||
let result = self.get_full_post_use_case.execute(id, user_id).await;
|
||||
|
||||
result.map(PostResponseDto::from)
|
||||
if let Ok(id) = id_or_semantic_id.parse::<i32>() {
|
||||
self.get_post_by_id(id, user_id).await
|
||||
} else {
|
||||
let semantic_id = id_or_semantic_id;
|
||||
self.get_post_by_semantic_id(semantic_id, user_id).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_label(
|
||||
|
@ -8,6 +8,7 @@ use super::label_response_dto::LabelResponseDto;
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct PostInfoResponseDto {
|
||||
pub id: i32,
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub labels: Vec<LabelResponseDto>,
|
||||
@ -23,6 +24,7 @@ impl From<PostInfo> for PostInfoResponseDto {
|
||||
fn from(entity: PostInfo) -> Self {
|
||||
Self {
|
||||
id: entity.id,
|
||||
semantic_id: entity.semantic_id,
|
||||
title: entity.title,
|
||||
description: entity.description,
|
||||
preview_image_url: entity.preview_image_url,
|
||||
|
@ -6,6 +6,7 @@ use crate::domain::entity::{post::Post, post_info::PostInfo};
|
||||
|
||||
#[derive(Deserialize, ToSchema, Clone)]
|
||||
pub struct UpdatePostRequestDto {
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub content: String,
|
||||
@ -24,6 +25,7 @@ impl UpdatePostRequestDto {
|
||||
id,
|
||||
info: PostInfo {
|
||||
id,
|
||||
semantic_id: self.semantic_id,
|
||||
title: self.title,
|
||||
description: self.description,
|
||||
preview_image_url: self.preview_image_url,
|
||||
|
@ -14,4 +14,5 @@ pub trait PostDbService: Send + Sync {
|
||||
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>;
|
||||
async fn get_id_by_semantic_id(&self, semantic_id: &str) -> Result<i32, PostError>;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use crate::{adapter::gateway::label_db_mapper::LabelMapper, domain::entity::post
|
||||
|
||||
pub struct PostInfoMapper {
|
||||
pub id: i32,
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub preview_image_url: String,
|
||||
@ -15,13 +16,18 @@ impl PostInfoMapper {
|
||||
pub fn into_entity(self) -> PostInfo {
|
||||
PostInfo {
|
||||
id: self.id,
|
||||
semantic_id: self.semantic_id,
|
||||
title: self.title.clone(),
|
||||
description: self.description.clone(),
|
||||
preview_image_url: self.preview_image_url.clone(),
|
||||
published_time: self
|
||||
.published_time
|
||||
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
||||
labels: self.labels.into_iter().map(LabelMapper::into_entity).collect(),
|
||||
labels: self
|
||||
.labels
|
||||
.into_iter()
|
||||
.map(LabelMapper::into_entity)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ impl PostRepository for PostRepositoryImpl {
|
||||
async fn create_post(&self, post: Post, label_ids: &[i32]) -> Result<i32, PostError> {
|
||||
let info_mapper = PostInfoMapper {
|
||||
id: post.info.id,
|
||||
semantic_id: post.info.semantic_id,
|
||||
title: post.info.title,
|
||||
description: post.info.description,
|
||||
preview_image_url: post.info.preview_image_url,
|
||||
@ -65,6 +66,7 @@ impl PostRepository for PostRepositoryImpl {
|
||||
async fn update_post(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError> {
|
||||
let info_mapper = PostInfoMapper {
|
||||
id: post.info.id,
|
||||
semantic_id: post.info.semantic_id,
|
||||
title: post.info.title,
|
||||
description: post.info.description,
|
||||
preview_image_url: post.info.preview_image_url,
|
||||
@ -82,4 +84,8 @@ impl PostRepository for PostRepositoryImpl {
|
||||
.update_post(post_mapper, label_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_id_by_semantic_id(&self, semantic_id: &str) -> Result<i32, PostError> {
|
||||
self.post_db_service.get_id_by_semantic_id(semantic_id).await
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use std::fmt::Display;
|
||||
pub enum PostError {
|
||||
NotFound,
|
||||
Unauthorized,
|
||||
InvalidSemanticId,
|
||||
Unexpected(anyhow::Error),
|
||||
}
|
||||
|
||||
@ -12,6 +13,10 @@ impl Display for PostError {
|
||||
match self {
|
||||
PostError::NotFound => write!(f, "Post not found"),
|
||||
PostError::Unauthorized => write!(f, "Unauthorized access to post"),
|
||||
PostError::InvalidSemanticId => write!(
|
||||
f,
|
||||
"Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_\\-]+$`"
|
||||
),
|
||||
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
|
@ -11,4 +11,5 @@ pub trait PostRepository: Send + Sync {
|
||||
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>;
|
||||
async fn get_id_by_semantic_id(&self, semantic_id: &str) -> Result<i32, PostError>;
|
||||
}
|
||||
|
@ -2,6 +2,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 get_post_by_id_use_case;
|
||||
pub mod get_post_by_sementic_id_use_case;
|
||||
pub mod update_label_use_case;
|
||||
pub mod update_post_use_case;
|
||||
|
@ -25,8 +25,7 @@ impl CreatePostUseCaseImpl {
|
||||
#[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
|
||||
post.validate()?;
|
||||
self.post_repository.create_post(post, label_ids).await
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait GetFullPostUseCase: Send + Sync {
|
||||
pub trait GetPostByIdUseCase: Send + Sync {
|
||||
async fn execute(&self, id: i32, user_id: Option<i32>) -> Result<Post, PostError>;
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ impl GetFullPostUseCaseImpl {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetFullPostUseCase for GetFullPostUseCaseImpl {
|
||||
impl GetPostByIdUseCase for GetFullPostUseCaseImpl {
|
||||
async fn execute(&self, id: i32, user_id: Option<i32>) -> Result<Post, PostError> {
|
||||
let post = self.post_repository.get_post_by_id(id).await?;
|
||||
|
@ -0,0 +1,45 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
application::{
|
||||
error::post_error::PostError, gateway::post_repository::PostRepository,
|
||||
use_case::get_post_by_id_use_case::GetPostByIdUseCase,
|
||||
},
|
||||
domain::entity::post::Post,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait GetPostBySemanticIdUseCase: Send + Sync {
|
||||
async fn execute(&self, semantic_id: &str, user_id: Option<i32>) -> Result<Post, PostError>;
|
||||
}
|
||||
|
||||
pub struct GetPostIdBySemanticIdUseCaseImpl {
|
||||
post_repository: Arc<dyn PostRepository>,
|
||||
get_post_by_id_use_case: Arc<dyn GetPostByIdUseCase>,
|
||||
}
|
||||
|
||||
impl GetPostIdBySemanticIdUseCaseImpl {
|
||||
pub fn new(
|
||||
post_repository: Arc<dyn PostRepository>,
|
||||
get_post_by_id_use_case: Arc<dyn GetPostByIdUseCase>,
|
||||
) -> Self {
|
||||
Self {
|
||||
post_repository,
|
||||
get_post_by_id_use_case,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetPostBySemanticIdUseCase for GetPostIdBySemanticIdUseCaseImpl {
|
||||
async fn execute(&self, semantic_id: &str, user_id: Option<i32>) -> Result<Post, PostError> {
|
||||
let id = self
|
||||
.post_repository
|
||||
.get_id_by_semantic_id(semantic_id)
|
||||
.await?;
|
||||
|
||||
self.get_post_by_id_use_case.execute(id, user_id).await
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ impl UpdatePostUseCaseImpl {
|
||||
#[async_trait]
|
||||
impl UpdatePostUseCase for UpdatePostUseCaseImpl {
|
||||
async fn execute(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError> {
|
||||
post.validate()?;
|
||||
self.post_repository.update_post(post, label_ids).await
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use crate::application::error::post_error::PostError;
|
||||
|
||||
use super::post_info::PostInfo;
|
||||
|
||||
pub struct Post {
|
||||
@ -5,3 +7,10 @@ pub struct Post {
|
||||
pub info: PostInfo,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
pub fn validate(&self) -> Result<(), PostError> {
|
||||
self.info.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,31 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::application::error::post_error::PostError;
|
||||
|
||||
use super::label::Label;
|
||||
|
||||
pub struct PostInfo {
|
||||
pub id: i32,
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub preview_image_url: String,
|
||||
pub labels: Vec<Label>,
|
||||
pub published_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl PostInfo {
|
||||
pub fn validate(&self) -> Result<(), PostError> {
|
||||
if self.semantic_id.parse::<i32>().is_ok() {
|
||||
return Err(PostError::InvalidSemanticId);
|
||||
}
|
||||
|
||||
let re = Regex::new(r"^[0-9a-zA-Z_\-]+$").unwrap();
|
||||
if !re.is_match(&self.semantic_id) {
|
||||
return Err(PostError::InvalidSemanticId);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ impl PostDbService for PostDbServiceImpl {
|
||||
r#"
|
||||
SELECT
|
||||
p.id AS post_id,
|
||||
p.semantic_id,
|
||||
p.title,
|
||||
p.description,
|
||||
p.preview_image_url,
|
||||
@ -74,6 +75,7 @@ impl PostDbService for PostDbServiceImpl {
|
||||
.entry(record.post_id)
|
||||
.or_insert_with(|| PostInfoMapper {
|
||||
id: record.post_id,
|
||||
semantic_id: record.semantic_id.clone(),
|
||||
title: record.title.clone(),
|
||||
description: record.description.clone(),
|
||||
preview_image_url: record.preview_image_url.clone(),
|
||||
@ -111,6 +113,7 @@ impl PostDbService for PostDbServiceImpl {
|
||||
r#"
|
||||
SELECT
|
||||
p.id AS post_id,
|
||||
p.semantic_id,
|
||||
p.title,
|
||||
p.description,
|
||||
p.preview_image_url,
|
||||
@ -151,6 +154,7 @@ impl PostDbService for PostDbServiceImpl {
|
||||
.or_insert_with(|| PostMapper {
|
||||
id: record.post_id,
|
||||
info: PostInfoMapper {
|
||||
semantic_id: record.semantic_id.clone(),
|
||||
id: record.post_id,
|
||||
title: record.title.clone(),
|
||||
description: record.description.clone(),
|
||||
@ -194,10 +198,11 @@ impl PostDbService for PostDbServiceImpl {
|
||||
let post_id = sqlx::query_scalar!(
|
||||
r#"
|
||||
INSERT INTO post (
|
||||
title, description, preview_image_url, content, published_time
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
semantic_id, title, description, preview_image_url, content, published_time
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
"#,
|
||||
post.info.semantic_id,
|
||||
post.info.title,
|
||||
post.info.description,
|
||||
post.info.preview_image_url,
|
||||
@ -243,13 +248,15 @@ impl PostDbService for PostDbServiceImpl {
|
||||
r#"
|
||||
UPDATE post
|
||||
SET
|
||||
title = $1,
|
||||
description = $2,
|
||||
preview_image_url = $3,
|
||||
content = $4,
|
||||
published_time = $5
|
||||
WHERE id = $6
|
||||
semantic_id = $1,
|
||||
title = $2,
|
||||
description = $3,
|
||||
preview_image_url = $4,
|
||||
content = $5,
|
||||
published_time = $6
|
||||
WHERE id = $7
|
||||
"#,
|
||||
post.info.semantic_id,
|
||||
post.info.title,
|
||||
post.info.description,
|
||||
post.info.preview_image_url,
|
||||
@ -300,4 +307,23 @@ impl PostDbService for PostDbServiceImpl {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_id_by_semantic_id(&self, semantic_id: &str) -> Result<i32, PostError> {
|
||||
let id = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT id
|
||||
FROM post
|
||||
WHERE semantic_id = $1 AND deleted_time IS NULL
|
||||
"#,
|
||||
semantic_id,
|
||||
)
|
||||
.fetch_optional(&self.db_pool)
|
||||
.await
|
||||
.map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
|
||||
|
||||
match id {
|
||||
Some(id) => Ok(id),
|
||||
None => Err(PostError::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use chrono::NaiveDateTime;
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct PostInfoWithLabelRecord {
|
||||
pub post_id: i32,
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub preview_image_url: String,
|
||||
|
@ -3,6 +3,7 @@ use chrono::NaiveDateTime;
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct PostWithLabelRecord {
|
||||
pub post_id: i32,
|
||||
pub semantic_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub preview_image_url: String,
|
||||
|
@ -32,12 +32,16 @@ pub async fn create_label_handler(
|
||||
|
||||
match result {
|
||||
Ok(label) => HttpResponse::Created().json(label),
|
||||
Err(e) => {
|
||||
match e {
|
||||
PostError::Unexpected(e) => capture_anyhow(&e),
|
||||
_ => capture_anyhow(&anyhow!(e)),
|
||||
};
|
||||
Err(e) => match e {
|
||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||
PostError::NotFound | PostError::InvalidSemanticId => {
|
||||
capture_anyhow(&anyhow!(e));
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -34,12 +34,17 @@ pub async fn create_post_handler(
|
||||
|
||||
match result {
|
||||
Ok(post) => HttpResponse::Created().json(post),
|
||||
Err(e) => {
|
||||
match e {
|
||||
PostError::Unexpected(e) => capture_anyhow(&e),
|
||||
_ => capture_anyhow(&anyhow!(e)),
|
||||
};
|
||||
Err(e) => match e {
|
||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
|
||||
PostError::NotFound => {
|
||||
capture_anyhow(&anyhow!(e));
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,15 @@ pub async fn get_all_labels_handler(
|
||||
|
||||
match result {
|
||||
Ok(labels) => HttpResponse::Ok().json(labels),
|
||||
Err(e) => {
|
||||
match e {
|
||||
PostError::Unexpected(e) => capture_anyhow(&e),
|
||||
_ => capture_anyhow(&anyhow!(e)),
|
||||
};
|
||||
Err(e) => match e {
|
||||
PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => {
|
||||
capture_anyhow(&anyhow!(e));
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -35,12 +35,15 @@ pub async fn get_all_post_info_handler(
|
||||
|
||||
match result {
|
||||
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
||||
Err(e) => {
|
||||
match e {
|
||||
PostError::Unexpected(e) => capture_anyhow(&e),
|
||||
_ => capture_anyhow(&anyhow!(e)),
|
||||
};
|
||||
Err(e) => match e {
|
||||
PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => {
|
||||
capture_anyhow(&anyhow!(e));
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use anyhow::anyhow;
|
||||
use auth::framework::web::auth_middleware::UserId;
|
||||
use sentry::integrations::anyhow::capture_anyhow;
|
||||
|
||||
@ -11,8 +12,8 @@ use crate::{
|
||||
get,
|
||||
path = "/post/{id}",
|
||||
tag = "post",
|
||||
summary = "Get post by ID",
|
||||
description = "Only authenticated users can access unpublished posts.",
|
||||
summary = "Get post by ID or semantic ID",
|
||||
description = "Only authenticated users can access unpublished posts. Accepts either numeric ID or semantic ID.",
|
||||
responses (
|
||||
(status = 200, body = PostResponseDto),
|
||||
(status = 404, description = "Post not found")
|
||||
@ -20,12 +21,12 @@ use crate::{
|
||||
)]
|
||||
pub async fn get_post_by_id_handler(
|
||||
post_controller: web::Data<dyn PostController>,
|
||||
path: web::Path<i32>,
|
||||
path: web::Path<String>,
|
||||
user_id: Option<UserId>,
|
||||
) -> impl Responder {
|
||||
let id = path.into_inner();
|
||||
let id_or_semantic_id = path.into_inner();
|
||||
let result = post_controller
|
||||
.get_post_by_id(id, user_id.map(|id| id.get()))
|
||||
.get_post_by_id_or_semantic_id(&id_or_semantic_id, user_id.map(|id| id.get()))
|
||||
.await;
|
||||
|
||||
match result {
|
||||
@ -33,6 +34,10 @@ pub async fn get_post_by_id_handler(
|
||||
Err(e) => match e {
|
||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||
PostError::InvalidSemanticId => {
|
||||
capture_anyhow(&anyhow!(e));
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
|
@ -1,4 +1,5 @@
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use anyhow::anyhow;
|
||||
use auth::framework::web::auth_middleware::UserId;
|
||||
use sentry::integrations::anyhow::capture_anyhow;
|
||||
|
||||
@ -39,6 +40,10 @@ pub async fn update_label_handler(
|
||||
Err(e) => match e {
|
||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||
PostError::InvalidSemanticId => {
|
||||
capture_anyhow(&anyhow!(e));
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
|
@ -39,6 +39,7 @@ pub async fn update_post_handler(
|
||||
Err(e) => match e {
|
||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
|
||||
PostError::Unexpected(e) => {
|
||||
capture_anyhow(&e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
|
@ -0,0 +1,5 @@
|
||||
-- Drop the unique index on semantic_id
|
||||
DROP INDEX IF EXISTS idx_post_semantic_id;
|
||||
|
||||
-- Remove the semantic_id column from post table
|
||||
ALTER TABLE post DROP COLUMN IF EXISTS "semantic_id";
|
@ -0,0 +1,7 @@
|
||||
ALTER TABLE post ADD COLUMN "semantic_id" VARCHAR(100) NOT NULL DEFAULT '';
|
||||
|
||||
-- Update existing records to use their id as semantic_id
|
||||
UPDATE post SET semantic_id = id::VARCHAR WHERE semantic_id = '';
|
||||
|
||||
-- Create unique index on semantic_id
|
||||
CREATE UNIQUE INDEX idx_post_semantic_id ON post (semantic_id);
|
@ -36,13 +36,12 @@ use post::{
|
||||
},
|
||||
},
|
||||
application::use_case::{
|
||||
create_label_use_case::CreateLabelUseCaseImpl,
|
||||
create_post_use_case::CreatePostUseCaseImpl,
|
||||
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,
|
||||
get_post_by_id_use_case::GetFullPostUseCaseImpl,
|
||||
get_post_by_sementic_id_use_case::GetPostIdBySemanticIdUseCaseImpl,
|
||||
update_label_use_case::UpdateLabelUseCaseImpl, update_post_use_case::UpdatePostUseCaseImpl,
|
||||
},
|
||||
framework::db::{
|
||||
label_db_service_impl::LabelDbServiceImpl, post_db_service_impl::PostDbServiceImpl,
|
||||
@ -97,7 +96,12 @@ 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 get_post_by_id_use_case =
|
||||
Arc::new(GetFullPostUseCaseImpl::new(post_repository.clone()));
|
||||
let get_post_by_semantic_id_use_case = Arc::new(GetPostIdBySemanticIdUseCaseImpl::new(
|
||||
post_repository.clone(),
|
||||
get_post_by_id_use_case.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()));
|
||||
@ -107,7 +111,8 @@ impl Container {
|
||||
|
||||
let post_controller = Arc::new(PostControllerImpl::new(
|
||||
get_all_post_info_use_case,
|
||||
get_full_post_use_case,
|
||||
get_post_by_id_use_case,
|
||||
get_post_by_semantic_id_use_case,
|
||||
create_post_use_case,
|
||||
update_post_use_case,
|
||||
create_label_use_case,
|
||||
|
Loading…
x
Reference in New Issue
Block a user