BLOG-43 feat: connect to database
All checks were successful
Frontend CI / build (push) Successful in 1m29s
All checks were successful
Frontend CI / build (push) Successful in 1m29s
This commit is contained in:
parent
c08bc659b9
commit
e72f5a5a8e
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -2,7 +2,10 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"actix",
|
"actix",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dotenv",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sqlx",
|
||||||
"squidspirit"
|
"squidspirit"
|
||||||
],
|
],
|
||||||
"java.project.sourcePaths": [
|
"java.project.sourcePaths": [
|
||||||
|
2
backend/.gitignore
vendored
2
backend/.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
|
.env
|
||||||
|
/.sqlx
|
||||||
/target
|
/target
|
||||||
|
987
backend/Cargo.lock
generated
987
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,8 +10,17 @@ edition = "2024"
|
|||||||
actix-web = "4.10.2"
|
actix-web = "4.10.2"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
|
dotenv = "0.15.0"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
|
futures = "0.3.31"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.8.5", features = [
|
||||||
|
"chrono",
|
||||||
|
"macros",
|
||||||
|
"postgres",
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
] }
|
||||||
|
tokio = { version = "1.45.0", features = ["full"] }
|
||||||
|
|
||||||
server.path = "server"
|
server.path = "server"
|
||||||
post.path = "feature/post"
|
post.path = "feature/post"
|
||||||
|
5
backend/build.rs
Normal file
5
backend/build.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// generated by `sqlx migrate build-script`
|
||||||
|
fn main() {
|
||||||
|
// trigger recompilation when a new migration is added
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
}
|
@ -8,3 +8,5 @@ actix-web.workspace = true
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
pub mod controller;
|
pub mod delivery;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
|
@ -4,7 +4,7 @@ use crate::domain::entity::label::Label;
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct LabelResponseDto {
|
pub struct LabelResponseDto {
|
||||||
pub id: u32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub color: String,
|
pub color: String,
|
||||||
}
|
}
|
||||||
@ -14,8 +14,7 @@ impl From<Label> for LabelResponseDto {
|
|||||||
Self {
|
Self {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
color: entity.color,
|
color: format!("#{:08X}", entity.color),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -6,12 +6,12 @@ use super::label_response_dto::LabelResponseDto;
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct PostInfoResponseDto {
|
pub struct PostInfoResponseDto {
|
||||||
pub id: u32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: String,
|
||||||
pub labels: Vec<LabelResponseDto>,
|
pub labels: Vec<LabelResponseDto>,
|
||||||
pub published_time: i64,
|
pub published_time: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PostInfo> for PostInfoResponseDto {
|
impl From<PostInfo> for PostInfoResponseDto {
|
||||||
@ -26,7 +26,9 @@ impl From<PostInfo> for PostInfoResponseDto {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(LabelResponseDto::from)
|
.map(LabelResponseDto::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
published_time: entity.published_time.timestamp_micros(),
|
published_time: entity
|
||||||
|
.published_time
|
||||||
|
.map(|datetime| datetime.timestamp_micros()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1 +1,4 @@
|
|||||||
pub enum PostError {}
|
pub enum PostError {
|
||||||
|
DatabaseError(String),
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub struct Label {
|
pub struct Label {
|
||||||
pub id: u32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub color: String,
|
pub color: u32,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use super::post_info::PostInfo;
|
use super::post_info::PostInfo;
|
||||||
|
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: u32,
|
pub id: i32,
|
||||||
pub info: PostInfo,
|
pub info: PostInfo,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ use chrono::{DateTime, Utc};
|
|||||||
use super::label::Label;
|
use super::label::Label;
|
||||||
|
|
||||||
pub struct PostInfo {
|
pub struct PostInfo {
|
||||||
pub id: u32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: String,
|
||||||
pub labels: Vec<Label>,
|
pub labels: Vec<Label>,
|
||||||
pub published_time: DateTime<Utc>,
|
pub published_time: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,84 @@
|
|||||||
use std::str::FromStr;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::DateTime;
|
use chrono::{DateTime, Utc};
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
adapter::gateway::post_db_service::PostDbService, application::error::post_error::PostError,
|
adapter::gateway::post_db_service::PostDbService, application::error::post_error::PostError,
|
||||||
domain::entity::post_info::PostInfo,
|
domain::entity::post_info::PostInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct PostDbServiceImpl {}
|
pub struct PostDbServiceImpl {
|
||||||
|
db_pool: Arc<Pool<Postgres>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl PostDbServiceImpl {
|
impl PostDbServiceImpl {
|
||||||
pub fn new() -> Self {
|
pub fn new(db_pool: Arc<Pool<Postgres>>) -> Self {
|
||||||
Self {}
|
Self { db_pool }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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) -> Result<Vec<PostInfo>, PostError> {
|
||||||
Ok(vec![PostInfo {
|
let posts = sqlx::query!(
|
||||||
id: 1,
|
r#"
|
||||||
title: "Post 1".to_string(),
|
SELECT
|
||||||
description: "Description of post 1".to_string(),
|
p.id,
|
||||||
preview_image_url: "http://example.com/image1.jpg".to_string(),
|
p.title,
|
||||||
labels: vec![],
|
p.description,
|
||||||
published_time: DateTime::from_str("2023-10-01T12:00:00Z").unwrap(),
|
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::new();
|
||||||
|
|
||||||
|
for post in posts {
|
||||||
|
let labels = 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.id
|
||||||
|
)
|
||||||
|
.fetch_all(&*self.db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| PostError::DatabaseError(err.to_string()))?;
|
||||||
|
|
||||||
|
let domain_labels = labels
|
||||||
|
.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 {
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
description: post.description,
|
||||||
|
preview_image_url: post.preview_image_url,
|
||||||
|
labels: domain_labels,
|
||||||
|
published_time: post
|
||||||
|
.published_time
|
||||||
|
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(post_infos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,19 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
|
|
||||||
use crate::adapter::controller::post_controller::PostController;
|
use crate::{
|
||||||
|
adapter::delivery::post_controller::{PostController, PostControllerImpl},
|
||||||
|
application::use_case::get_all_post_info_use_case::GetAllPostInfoUseCase,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
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_info").route(web::get().to(get_all_post_info)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_post_info(post_controller: web::Data<Arc<dyn PostController>>) -> impl Responder {
|
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());
|
||||||
let result = post_controller.get_all_post_info().await;
|
let result = post_controller.get_all_post_info().await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
30
backend/migrations/20250505012740_v0.1.1.sql
Normal file
30
backend/migrations/20250505012740_v0.1.1.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE "post" (
|
||||||
|
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"preview_image_url" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"published_time" TIMESTAMP,
|
||||||
|
"deleted_time" TIMESTAMP,
|
||||||
|
"created_time" TIMESTAMP NOT NULL,
|
||||||
|
"updated_time" TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "label" (
|
||||||
|
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"color" BIGINT NOT NULL CHECK ("color" >= 0 AND "color" <= 4294967295),
|
||||||
|
"deleted_time" TIMESTAMP,
|
||||||
|
"created_time" TIMESTAMP NOT NULL,
|
||||||
|
"updated_time" TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "post_label" (
|
||||||
|
"post_id" INTEGER NOT NULL,
|
||||||
|
"label_id" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY ("post_id", "label_id"),
|
||||||
|
FOREIGN KEY ("post_id") REFERENCES "post" ("id") ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("label_id") REFERENCES "label" ("id") ON DELETE CASCADE
|
||||||
|
);
|
@ -5,6 +5,8 @@ edition.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
|
dotenv.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
|
|
||||||
post.workspace = true
|
post.workspace = true
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use post::{
|
|
||||||
adapter::{
|
|
||||||
controller::post_controller::{PostController, PostControllerImpl},
|
|
||||||
gateway::post_repository_impl::PostRepositoryImpl,
|
|
||||||
},
|
|
||||||
application::use_case::get_all_post_info_use_case::GetAllPostInfoUseCaseImpl,
|
|
||||||
framework::db::post_db_service_impl::PostDbServiceImpl,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Controllers {
|
|
||||||
pub post_controller: Arc<dyn PostController>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Controllers {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let post_db_service = Arc::new(PostDbServiceImpl::new());
|
|
||||||
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service));
|
|
||||||
let get_all_post_info_use_case = Arc::new(GetAllPostInfoUseCaseImpl::new(post_repository));
|
|
||||||
let post_controller = Arc::new(PostControllerImpl::new(get_all_post_info_use_case));
|
|
||||||
|
|
||||||
Self { post_controller }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +1 @@
|
|||||||
pub mod controllers;
|
pub mod use_cases;
|
||||||
|
@ -5,19 +5,44 @@ use actix_web::{
|
|||||||
web,
|
web,
|
||||||
};
|
};
|
||||||
use post::framework::web::post_web_routes::configure_post_routes;
|
use post::framework::web::post_web_routes::configure_post_routes;
|
||||||
use server::controllers::Controllers;
|
use server::use_cases::UseCases;
|
||||||
|
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
||||||
|
use std::{env, sync::Arc};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
HttpServer::new(create_app)
|
let db_pool = init_database().await;
|
||||||
|
|
||||||
|
HttpServer::new(move || create_app(db_pool.clone()))
|
||||||
.bind(("127.0.0.1", 8080))?
|
.bind(("127.0.0.1", 8080))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_app() -> App<
|
async fn init_database() -> Arc<Pool<Postgres>> {
|
||||||
|
let database_url = env::var("DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://postgres@localhost:5432/postgres".to_string());
|
||||||
|
|
||||||
|
let db_pool = PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create database connection pool");
|
||||||
|
|
||||||
|
sqlx::migrate!("../migrations")
|
||||||
|
.run(&db_pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run database migrations");
|
||||||
|
|
||||||
|
Arc::new(db_pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_app(
|
||||||
|
db_pool: Arc<Pool<Postgres>>,
|
||||||
|
) -> App<
|
||||||
impl ServiceFactory<
|
impl ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
Response = ServiceResponse<impl MessageBody>,
|
Response = ServiceResponse<impl MessageBody>,
|
||||||
@ -26,9 +51,10 @@ fn create_app() -> App<
|
|||||||
Error = Error,
|
Error = Error,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let controllers = Controllers::new();
|
let use_cases = UseCases::new(db_pool.clone());
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(controllers.post_controller))
|
.app_data(web::Data::new(db_pool))
|
||||||
|
.app_data(web::Data::new(use_cases.get_all_post_info_use_case))
|
||||||
.configure(configure_post_routes)
|
.configure(configure_post_routes)
|
||||||
}
|
}
|
||||||
|
28
backend/server/src/use_cases.rs
Normal file
28
backend/server/src/use_cases.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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