BLOG-43 feat: using join table & is published query
This commit is contained in:
parent
3b374b2d75
commit
c73ce31e01
@ -1,4 +1,5 @@
|
|||||||
pub mod label_response_dto;
|
pub mod label_response_dto;
|
||||||
pub mod post_controller;
|
pub mod post_controller;
|
||||||
|
pub mod post_info_query_dto;
|
||||||
pub mod post_info_response_dto;
|
pub mod post_info_response_dto;
|
||||||
pub mod post_response_dto;
|
pub mod post_response_dto;
|
||||||
|
@ -14,7 +14,11 @@ use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::Post
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait PostController: Send + Sync {
|
pub trait PostController: Send + Sync {
|
||||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
async fn get_all_post_info(
|
||||||
|
&self,
|
||||||
|
is_published_only: bool,
|
||||||
|
) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
||||||
|
|
||||||
async fn get_full_post(&self, id: i32) -> Result<PostResponseDto, PostError>;
|
async fn get_full_post(&self, id: i32) -> Result<PostResponseDto, PostError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,8 +41,11 @@ impl PostControllerImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PostController for PostControllerImpl {
|
impl PostController for PostControllerImpl {
|
||||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfoResponseDto>, PostError> {
|
async fn get_all_post_info(
|
||||||
let result = self.get_all_post_info_use_case.execute().await;
|
&self,
|
||||||
|
is_published_only: bool,
|
||||||
|
) -> Result<Vec<PostInfoResponseDto>, PostError> {
|
||||||
|
let result = self.get_all_post_info_use_case.execute(is_published_only).await;
|
||||||
|
|
||||||
result.map(|post_info_list| {
|
result.map(|post_info_list| {
|
||||||
let post_info_response_dto_list: Vec<PostInfoResponseDto> = post_info_list
|
let post_info_response_dto_list: Vec<PostInfoResponseDto> = post_info_list
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PostQueryDto {
|
||||||
|
pub is_published_only: Option<bool>,
|
||||||
|
}
|
@ -7,6 +7,6 @@ use crate::{
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait PostDbService: Send + Sync {
|
pub trait PostDbService: Send + Sync {
|
||||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError>;
|
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_full_post(&self, id: i32) -> Result<Post, PostError>;
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,8 @@ impl PostRepositoryImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PostRepository for PostRepositoryImpl {
|
impl PostRepository for PostRepositoryImpl {
|
||||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError> {
|
async fn get_all_post_info(&self, is_published_only: bool) -> Result<Vec<PostInfo>, PostError> {
|
||||||
self.post_db_service.get_all_post_info().await
|
self.post_db_service.get_all_post_info(is_published_only).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError> {
|
async fn get_full_post(&self, id: i32) -> Result<Post, PostError> {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum PostError {
|
pub enum PostError {
|
||||||
DatabaseError(String),
|
DatabaseError(String),
|
||||||
NotFound,
|
NotFound,
|
||||||
|
@ -7,6 +7,6 @@ use crate::{
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait PostRepository: Send + Sync {
|
pub trait PostRepository: Send + Sync {
|
||||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError>;
|
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_full_post(&self, id: i32) -> Result<Post, PostError>;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use crate::{
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait GetAllPostInfoUseCase: Send + Sync {
|
pub trait GetAllPostInfoUseCase: Send + Sync {
|
||||||
async fn execute(&self) -> Result<Vec<PostInfo>, PostError>;
|
async fn execute(&self, is_published_only: bool) -> Result<Vec<PostInfo>, PostError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetAllPostInfoUseCaseImpl {
|
pub struct GetAllPostInfoUseCaseImpl {
|
||||||
@ -24,7 +24,7 @@ impl GetAllPostInfoUseCaseImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl GetAllPostInfoUseCase for GetAllPostInfoUseCaseImpl {
|
impl GetAllPostInfoUseCase for GetAllPostInfoUseCaseImpl {
|
||||||
async fn execute(&self) -> Result<Vec<PostInfo>, PostError> {
|
async fn execute(&self, is_published_only: bool) -> Result<Vec<PostInfo>, PostError> {
|
||||||
self.post_repository.get_all_post_info().await
|
self.post_repository.get_all_post_info(is_published_only).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,3 @@ pub struct PostInfo {
|
|||||||
pub labels: Vec<Label>,
|
pub labels: Vec<Label>,
|
||||||
pub published_time: Option<DateTime<Utc>>,
|
pub published_time: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,4 @@
|
|||||||
pub mod post_db_service_impl;
|
pub mod post_db_service_impl;
|
||||||
|
|
||||||
|
mod post_info_with_label_record;
|
||||||
|
mod post_with_label_record;
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
use std::{any::Any, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::{Error, Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
adapter::gateway::post_db_service::PostDbService,
|
adapter::gateway::post_db_service::PostDbService,
|
||||||
application::error::post_error::PostError,
|
application::error::post_error::PostError,
|
||||||
domain::entity::{post::Post, post_info::PostInfo},
|
domain::entity::{label::Label, post::Post, post_info::PostInfo},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
post_info_with_label_record::PostInfoWithLabelRecord,
|
||||||
|
post_with_label_record::PostWithLabelRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct PostDbServiceImpl {
|
pub struct PostDbServiceImpl {
|
||||||
@ -22,135 +27,142 @@ impl PostDbServiceImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PostDbService for PostDbServiceImpl {
|
impl PostDbService for PostDbServiceImpl {
|
||||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError> {
|
async fn get_all_post_info(&self, is_published_only: bool) -> Result<Vec<PostInfo>, PostError> {
|
||||||
let post_records = sqlx::query!(
|
let mut query_builder = sqlx::QueryBuilder::new(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
|
||||||
p.id,
|
|
||||||
p.title,
|
|
||||||
p.description,
|
|
||||||
p.preview_image_url,
|
|
||||||
p.published_time
|
|
||||||
FROM post p
|
|
||||||
WHERE p.deleted_time IS NULL
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.fetch_all(&*self.db_pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
|
||||||
|
|
||||||
let mut post_infos = Vec::<PostInfo>::new();
|
|
||||||
|
|
||||||
for post_record in post_records {
|
|
||||||
let label_records = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT
|
SELECT
|
||||||
l.id,
|
p.id AS post_id,
|
||||||
l.name,
|
p.title,
|
||||||
l.color
|
p.description,
|
||||||
FROM label l
|
p.preview_image_url,
|
||||||
JOIN post_label pl ON l.id = pl.label_id
|
p.published_time,
|
||||||
WHERE pl.post_id = $1
|
l.id AS label_id,
|
||||||
AND l.deleted_time IS NULL
|
l.name AS label_name,
|
||||||
"#,
|
l.color AS label_color
|
||||||
post_record.id
|
FROM
|
||||||
)
|
post p
|
||||||
|
LEFT JOIN
|
||||||
|
post_label pl ON p.id = pl.post_id
|
||||||
|
LEFT JOIN
|
||||||
|
label l ON pl.label_id = l.id AND l.deleted_time IS NULL
|
||||||
|
WHERE
|
||||||
|
p.deleted_time IS NULL
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_published_only {
|
||||||
|
query_builder.push(r#" AND p.published_time IS NOT NULL"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
query_builder.push(r#" ORDER BY p.id"#);
|
||||||
|
|
||||||
|
let records = query_builder
|
||||||
|
.build_query_as::<PostInfoWithLabelRecord>()
|
||||||
.fetch_all(&*self.db_pool)
|
.fetch_all(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||||
|
|
||||||
let labels = label_records
|
let mut post_info_map = HashMap::<i32, PostInfo>::new();
|
||||||
.into_iter()
|
|
||||||
.map(|label| crate::domain::entity::label::Label {
|
|
||||||
id: label.id,
|
|
||||||
name: label.name,
|
|
||||||
color: label.color as u32,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
post_infos.push(PostInfo {
|
for record in records {
|
||||||
id: post_record.id,
|
let post_info = post_info_map
|
||||||
title: post_record.title,
|
.entry(record.post_id)
|
||||||
description: post_record.description,
|
.or_insert_with(|| PostInfo {
|
||||||
preview_image_url: post_record.preview_image_url,
|
id: record.post_id,
|
||||||
labels: labels,
|
title: record.title,
|
||||||
published_time: post_record
|
description: record.description,
|
||||||
.published_time
|
preview_image_url: record.preview_image_url,
|
||||||
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
labels: Vec::new(),
|
||||||
});
|
published_time: record
|
||||||
|
.published_time
|
||||||
|
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let (Some(label_id), Some(label_name), Some(label_color)) =
|
||||||
|
(record.label_id, record.label_name, record.label_color)
|
||||||
|
{
|
||||||
|
post_info.labels.push(Label {
|
||||||
|
id: label_id,
|
||||||
|
name: label_name,
|
||||||
|
color: label_color as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(post_infos)
|
Ok(post_info_map.into_values().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError> {
|
async fn get_full_post(&self, id: i32) -> Result<Post, PostError> {
|
||||||
let post_record = sqlx::query!(
|
let mut query_builder = sqlx::QueryBuilder::new(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
p.id,
|
p.id AS post_id,
|
||||||
p.title,
|
p.title,
|
||||||
p.description,
|
p.description,
|
||||||
p.preview_image_url,
|
p.preview_image_url,
|
||||||
p.content,
|
p.content,
|
||||||
p.published_time
|
p.published_time,
|
||||||
FROM post p
|
l.id AS label_id,
|
||||||
WHERE p.id = $1
|
l.name AS label_name,
|
||||||
AND p.deleted_time IS NULL
|
l.color AS label_color
|
||||||
|
FROM
|
||||||
|
post p
|
||||||
|
LEFT JOIN
|
||||||
|
post_label pl ON p.id = pl.post_id
|
||||||
|
LEFT JOIN
|
||||||
|
label l ON pl.label_id = l.id AND l.deleted_time IS NULL
|
||||||
|
WHERE
|
||||||
|
p.deleted_time IS NULL AND p.id =
|
||||||
"#,
|
"#,
|
||||||
id
|
);
|
||||||
)
|
|
||||||
.fetch_one(&*self.db_pool)
|
query_builder.push_bind(id);
|
||||||
.await
|
query_builder.push(r#" ORDER BY p.id"#);
|
||||||
.map_err(|err| {
|
|
||||||
if err.type_id() == Error::RowNotFound.type_id() {
|
let records = query_builder
|
||||||
PostError::NotFound
|
.build_query_as::<PostWithLabelRecord>()
|
||||||
} else {
|
.fetch_all(&*self.db_pool)
|
||||||
PostError::DatabaseError(err.to_string())
|
.await
|
||||||
|
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||||
|
|
||||||
|
if records.is_empty() {
|
||||||
|
return Err(PostError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut post_map = HashMap::<i32, Post>::new();
|
||||||
|
|
||||||
|
for record in records {
|
||||||
|
let post = post_map.entry(record.post_id).or_insert_with(|| Post {
|
||||||
|
id: record.post_id,
|
||||||
|
info: PostInfo {
|
||||||
|
id: record.post_id,
|
||||||
|
title: record.title,
|
||||||
|
description: record.description,
|
||||||
|
preview_image_url: record.preview_image_url,
|
||||||
|
labels: Vec::new(),
|
||||||
|
published_time: record
|
||||||
|
.published_time
|
||||||
|
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
||||||
|
},
|
||||||
|
content: record.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let (Some(label_id), Some(label_name), Some(label_color)) =
|
||||||
|
(record.label_id, record.label_name, record.label_color)
|
||||||
|
{
|
||||||
|
post.info.labels.push(Label {
|
||||||
|
id: label_id,
|
||||||
|
name: label_name,
|
||||||
|
color: label_color as u32,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})?;
|
}
|
||||||
|
|
||||||
// TODO: extract label selecting process
|
let post = post_map.into_values().next();
|
||||||
let label_records = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
l.id,
|
|
||||||
l.name,
|
|
||||||
l.color
|
|
||||||
FROM label l
|
|
||||||
JOIN post_label pl ON l.id = pl.label_id
|
|
||||||
WHERE pl.post_id = $1
|
|
||||||
AND l.deleted_time IS NULL
|
|
||||||
"#,
|
|
||||||
post_record.id
|
|
||||||
)
|
|
||||||
.fetch_all(&*self.db_pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
|
||||||
|
|
||||||
let labels = label_records
|
match post {
|
||||||
.into_iter()
|
Some(v) => Ok(v),
|
||||||
.map(|label| crate::domain::entity::label::Label {
|
None => Err(PostError::NotFound),
|
||||||
id: label.id,
|
}
|
||||||
name: label.name,
|
|
||||||
color: label.color as u32,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let post = Post {
|
|
||||||
id: post_record.id,
|
|
||||||
content: post_record.content,
|
|
||||||
info: PostInfo {
|
|
||||||
id: post_record.id,
|
|
||||||
title: post_record.title,
|
|
||||||
description: post_record.description,
|
|
||||||
preview_image_url: post_record.preview_image_url,
|
|
||||||
labels: labels,
|
|
||||||
published_time: post_record
|
|
||||||
.published_time
|
|
||||||
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(post)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct PostInfoWithLabelRecord {
|
||||||
|
pub post_id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub preview_image_url: String,
|
||||||
|
pub published_time: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
pub label_id: Option<i32>,
|
||||||
|
pub label_name: Option<String>,
|
||||||
|
pub label_color: Option<i64>,
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, Debug)]
|
||||||
|
pub struct PostWithLabelRecord {
|
||||||
|
pub post_id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub preview_image_url: String,
|
||||||
|
pub content: String,
|
||||||
|
pub published_time: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
pub label_id: Option<i32>,
|
||||||
|
pub label_name: Option<String>,
|
||||||
|
pub label_color: Option<i64>,
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
use std::{any::Any, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
adapter::delivery::post_controller::PostController, application::error::post_error::PostError,
|
adapter::delivery::{post_controller::PostController, post_info_query_dto::PostQueryDto},
|
||||||
|
application::error::post_error::PostError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
||||||
@ -11,8 +12,12 @@ pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
|||||||
cfg.service(web::resource("/post/{id}").route(web::get().to(get_full_post)));
|
cfg.service(web::resource("/post/{id}").route(web::get().to(get_full_post)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_post_info(post_controller: web::Data<Arc<dyn PostController>>) -> impl Responder {
|
async fn get_all_post_info(
|
||||||
let result = post_controller.get_all_post_info().await;
|
post_controller: web::Data<Arc<dyn PostController>>,
|
||||||
|
query: web::Query<PostQueryDto>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let is_published_only = query.is_published_only.unwrap_or_else(|| true);
|
||||||
|
let result = post_controller.get_all_post_info(is_published_only).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
||||||
@ -33,7 +38,7 @@ async fn get_full_post(
|
|||||||
match result {
|
match result {
|
||||||
Ok(post) => HttpResponse::Ok().json(post),
|
Ok(post) => HttpResponse::Ok().json(post),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.type_id() == PostError::NotFound.type_id() {
|
if e == PostError::NotFound {
|
||||||
HttpResponse::NotFound().finish()
|
HttpResponse::NotFound().finish()
|
||||||
} else {
|
} else {
|
||||||
log::error!("{e:?}");
|
log::error!("{e:?}");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user