Compare commits
2 Commits
e72f5a5a8e
...
a68275e215
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a68275e215 | ||
![]() |
2922d52416 |
14
backend/Cargo.lock
generated
14
backend/Cargo.lock
generated
@ -1489,9 +1489,9 @@ dependencies = [
|
||||
"actix-web",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"log",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2230,21 +2230,9 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.17"
|
||||
|
@ -13,6 +13,7 @@ chrono = "0.4.41"
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.11.8"
|
||||
futures = "0.3.31"
|
||||
log = "0.4.27"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sqlx = { version = "0.8.5", features = [
|
||||
"chrono",
|
||||
|
42
backend/README.md
Normal file
42
backend/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Backend
|
||||
|
||||
## Development
|
||||
|
||||
### SQL Migration
|
||||
|
||||
1. Install sqlx
|
||||
|
||||
```bash
|
||||
cargo install sqlx-cli
|
||||
```
|
||||
|
||||
2. Run migration
|
||||
|
||||
```bash
|
||||
sqlx migrate run
|
||||
```
|
||||
|
||||
### Run Project
|
||||
|
||||
1. Prepare for sql schema setup
|
||||
|
||||
```bash
|
||||
cargo sqlx prepare --workspace
|
||||
```
|
||||
|
||||
2. Run the server
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug cargo run
|
||||
```
|
||||
|
||||
3. (Optional) Hot restart
|
||||
|
||||
1. Install `watchexec`
|
||||
|
||||
2. Run the server with `watchexec`
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug watchexec -e rs -r 'cargo run'
|
||||
```
|
||||
|
@ -7,6 +7,6 @@ edition.workspace = true
|
||||
actix-web.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
tokio.workspace = true
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod label_response_dto;
|
||||
pub mod post_controller;
|
||||
pub mod post_info_response_dto;
|
||||
pub mod post_response_dto;
|
||||
|
@ -3,24 +3,34 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::{
|
||||
error::post_error::PostError, use_case::get_all_post_info_use_case::GetAllPostInfoUseCase,
|
||||
error::post_error::PostError,
|
||||
use_case::{
|
||||
get_all_post_info_use_case::GetAllPostInfoUseCase,
|
||||
get_full_post_use_case::GetFullPostUseCase,
|
||||
},
|
||||
};
|
||||
|
||||
use super::post_info_response_dto::PostInfoResponseDto;
|
||||
use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::PostResponseDto};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PostController: Send + Sync {
|
||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
||||
async fn get_full_post(&self, id: i32) -> Result<PostResponseDto, PostError>;
|
||||
}
|
||||
|
||||
pub struct PostControllerImpl {
|
||||
get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
|
||||
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
|
||||
}
|
||||
|
||||
impl PostControllerImpl {
|
||||
pub fn new(get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>) -> Self {
|
||||
pub fn new(
|
||||
get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
|
||||
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
|
||||
) -> Self {
|
||||
Self {
|
||||
get_all_post_info_use_case,
|
||||
get_full_post_use_case,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,4 +49,10 @@ impl PostController for PostControllerImpl {
|
||||
post_info_response_dto_list
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_full_post(&self, id: i32) -> Result<PostResponseDto, PostError> {
|
||||
let result = self.get_full_post_use_case.execute(id).await;
|
||||
|
||||
result.map(PostResponseDto::from)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::domain::entity::post::Post;
|
||||
|
||||
use super::post_info_response_dto::PostInfoResponseDto;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PostResponseDto {
|
||||
pub id: i32,
|
||||
pub info: PostInfoResponseDto,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl From<Post> for PostResponseDto {
|
||||
fn from(entity: Post) -> Self {
|
||||
Self {
|
||||
id: entity.id,
|
||||
info: PostInfoResponseDto::from(entity.info),
|
||||
content: entity.content,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,12 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{application::error::post_error::PostError, domain::entity::post_info::PostInfo};
|
||||
use crate::{
|
||||
application::error::post_error::PostError,
|
||||
domain::entity::{post::Post, post_info::PostInfo},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PostDbService: Send + Sync {
|
||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError>;
|
||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError>;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
application::{error::post_error::PostError, gateway::post_repository::PostRepository},
|
||||
domain::entity::post_info::PostInfo,
|
||||
domain::entity::{post::Post, post_info::PostInfo},
|
||||
};
|
||||
|
||||
use super::post_db_service::PostDbService;
|
||||
@ -22,8 +22,10 @@ impl PostRepositoryImpl {
|
||||
#[async_trait]
|
||||
impl PostRepository for PostRepositoryImpl {
|
||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError> {
|
||||
self.post_db_service
|
||||
.get_all_post_info()
|
||||
.await
|
||||
self.post_db_service.get_all_post_info().await
|
||||
}
|
||||
|
||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError> {
|
||||
self.post_db_service.get_full_post(id).await
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
#[derive(Debug)]
|
||||
pub enum PostError {
|
||||
DatabaseError(String),
|
||||
NotFound,
|
||||
|
@ -1,8 +1,12 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{application::error::post_error::PostError, domain::entity::post_info::PostInfo};
|
||||
use crate::{
|
||||
application::error::post_error::PostError,
|
||||
domain::entity::{post::Post, post_info::PostInfo},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PostRepository: Send + Sync {
|
||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError>;
|
||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError>;
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
pub mod get_all_post_info_use_case;
|
||||
pub mod get_full_post_use_case;
|
||||
|
@ -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 GetFullPostUseCase: Send + Sync {
|
||||
async fn execute(&self, id: i32) -> Result<Post, PostError>;
|
||||
}
|
||||
|
||||
pub struct GetFullPostUseCaseImpl {
|
||||
post_repository: Arc<dyn PostRepository>,
|
||||
}
|
||||
|
||||
impl GetFullPostUseCaseImpl {
|
||||
pub fn new(post_repository: Arc<dyn PostRepository>) -> Self {
|
||||
Self { post_repository }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetFullPostUseCase for GetFullPostUseCaseImpl {
|
||||
async fn execute(&self, id: i32) -> Result<Post, PostError> {
|
||||
self.post_repository.get_full_post(id).await
|
||||
}
|
||||
}
|
@ -10,3 +10,4 @@ pub struct PostInfo {
|
||||
pub labels: Vec<Label>,
|
||||
pub published_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use sqlx::{Error, Pool, Postgres};
|
||||
|
||||
use crate::{
|
||||
adapter::gateway::post_db_service::PostDbService, application::error::post_error::PostError,
|
||||
domain::entity::post_info::PostInfo,
|
||||
adapter::gateway::post_db_service::PostDbService,
|
||||
application::error::post_error::PostError,
|
||||
domain::entity::{post::Post, post_info::PostInfo},
|
||||
};
|
||||
|
||||
pub struct PostDbServiceImpl {
|
||||
@ -22,7 +23,7 @@ impl PostDbServiceImpl {
|
||||
#[async_trait]
|
||||
impl PostDbService for PostDbServiceImpl {
|
||||
async fn get_all_post_info(&self) -> Result<Vec<PostInfo>, PostError> {
|
||||
let posts = sqlx::query!(
|
||||
let post_records = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
p.id,
|
||||
@ -38,10 +39,10 @@ impl PostDbService for PostDbServiceImpl {
|
||||
.await
|
||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||
|
||||
let mut post_infos = Vec::new();
|
||||
let mut post_infos = Vec::<PostInfo>::new();
|
||||
|
||||
for post in posts {
|
||||
let labels = sqlx::query!(
|
||||
for post_record in post_records {
|
||||
let label_records = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
l.id,
|
||||
@ -52,13 +53,13 @@ impl PostDbService for PostDbServiceImpl {
|
||||
WHERE pl.post_id = $1
|
||||
AND l.deleted_time IS NULL
|
||||
"#,
|
||||
post.id
|
||||
post_record.id
|
||||
)
|
||||
.fetch_all(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||
|
||||
let domain_labels = labels
|
||||
let labels = label_records
|
||||
.into_iter()
|
||||
.map(|label| crate::domain::entity::label::Label {
|
||||
id: label.id,
|
||||
@ -68,12 +69,12 @@ impl PostDbService for PostDbServiceImpl {
|
||||
.collect();
|
||||
|
||||
post_infos.push(PostInfo {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
preview_image_url: post.preview_image_url,
|
||||
labels: domain_labels,
|
||||
published_time: post
|
||||
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)),
|
||||
});
|
||||
@ -81,4 +82,75 @@ impl PostDbService for PostDbServiceImpl {
|
||||
|
||||
Ok(post_infos)
|
||||
}
|
||||
|
||||
async fn get_full_post(&self, id: i32) -> Result<Post, PostError> {
|
||||
let post_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
p.id,
|
||||
p.title,
|
||||
p.description,
|
||||
p.preview_image_url,
|
||||
p.content,
|
||||
p.published_time
|
||||
FROM post p
|
||||
WHERE p.id = $1
|
||||
AND p.deleted_time IS NULL
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_one(&*self.db_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.type_id() == Error::RowNotFound.type_id() {
|
||||
PostError::NotFound
|
||||
} else {
|
||||
PostError::DatabaseError(err.to_string())
|
||||
}
|
||||
})?;
|
||||
|
||||
// TODO: extract label selecting process
|
||||
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
|
||||
.into_iter()
|
||||
.map(|label| crate::domain::entity::label::Label {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,44 @@
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
|
||||
use crate::{
|
||||
adapter::delivery::post_controller::{PostController, PostControllerImpl},
|
||||
application::use_case::get_all_post_info_use_case::GetAllPostInfoUseCase,
|
||||
adapter::delivery::post_controller::PostController, application::error::post_error::PostError,
|
||||
};
|
||||
|
||||
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("/post_info").route(web::get().to(get_all_post_info)));
|
||||
cfg.service(web::resource("/post/{id}").route(web::get().to(get_full_post)));
|
||||
}
|
||||
|
||||
async fn get_all_post_info(
|
||||
get_all_post_info_use_case: web::Data<Arc<dyn GetAllPostInfoUseCase>>,
|
||||
) -> impl Responder {
|
||||
let post_controller = PostControllerImpl::new(get_all_post_info_use_case.get_ref().clone());
|
||||
async fn get_all_post_info(post_controller: web::Data<Arc<dyn PostController>>) -> impl Responder {
|
||||
let result = post_controller.get_all_post_info().await;
|
||||
|
||||
match result {
|
||||
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
||||
Err(_) => HttpResponse::InternalServerError().finish(),
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_full_post(
|
||||
post_controller: web::Data<Arc<dyn PostController>>,
|
||||
path: web::Path<i32>,
|
||||
) -> impl Responder {
|
||||
let id = path.into_inner();
|
||||
let result = post_controller.get_full_post(id).await;
|
||||
|
||||
match result {
|
||||
Ok(post) => HttpResponse::Ok().json(post),
|
||||
Err(e) => {
|
||||
if e.type_id() == PostError::NotFound.type_id() {
|
||||
HttpResponse::NotFound().finish()
|
||||
} else {
|
||||
log::error!("{e:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
backend/server/src/container.rs
Normal file
37
backend/server/src/container.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use post::{
|
||||
adapter::{
|
||||
delivery::post_controller::{PostController, PostControllerImpl},
|
||||
gateway::post_repository_impl::PostRepositoryImpl,
|
||||
},
|
||||
application::use_case::{
|
||||
get_all_post_info_use_case::GetAllPostInfoUseCaseImpl,
|
||||
get_full_post_use_case::GetFullPostUseCaseImpl,
|
||||
},
|
||||
framework::db::post_db_service_impl::PostDbServiceImpl,
|
||||
};
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
pub struct Container {
|
||||
pub post_controller: Arc<dyn PostController>,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn new(db_pool: Arc<Pool<Postgres>>) -> Self {
|
||||
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool));
|
||||
|
||||
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone()));
|
||||
|
||||
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 post_controller = Arc::new(PostControllerImpl::new(
|
||||
get_all_post_info_use_case,
|
||||
get_full_post_use_case,
|
||||
));
|
||||
|
||||
Self { post_controller }
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
pub mod use_cases;
|
||||
pub mod container;
|
||||
|
@ -5,7 +5,7 @@ use actix_web::{
|
||||
web,
|
||||
};
|
||||
use post::framework::web::post_web_routes::configure_post_routes;
|
||||
use server::use_cases::UseCases;
|
||||
use server::container::Container;
|
||||
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
||||
use std::{env, sync::Arc};
|
||||
|
||||
@ -51,10 +51,10 @@ fn create_app(
|
||||
Error = Error,
|
||||
>,
|
||||
> {
|
||||
let use_cases = UseCases::new(db_pool.clone());
|
||||
let container = Container::new(db_pool.clone());
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::new(db_pool))
|
||||
.app_data(web::Data::new(use_cases.get_all_post_info_use_case))
|
||||
.app_data(web::Data::new(container.post_controller))
|
||||
.configure(configure_post_routes)
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use post::{
|
||||
adapter::gateway::post_repository_impl::PostRepositoryImpl,
|
||||
application::use_case::get_all_post_info_use_case::{
|
||||
GetAllPostInfoUseCase, GetAllPostInfoUseCaseImpl,
|
||||
},
|
||||
framework::db::post_db_service_impl::PostDbServiceImpl,
|
||||
};
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
pub struct UseCases {
|
||||
pub get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
|
||||
}
|
||||
|
||||
impl UseCases {
|
||||
pub fn new(db_pool: Arc<Pool<Postgres>>) -> Self {
|
||||
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool));
|
||||
|
||||
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service));
|
||||
|
||||
let get_all_post_info_use_case = Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository));
|
||||
|
||||
Self {
|
||||
get_all_post_info_use_case,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user