From a0dc40d2ca7fc42dab0fa263584d86325182f697 Mon Sep 17 00:00:00 2001 From: Lumither Tao Date: Sun, 1 Dec 2024 02:35:13 -0800 Subject: [PATCH] feat(blog::api) fix apis, frontend --- backend/api_server/Cargo.toml | 2 +- .../api_server/src/blog/api/get_post_list.rs | 60 +++++- backend/api_server/src/blog/api/mod.rs | 1 - backend/api_server/src/blog/api/refresh.rs | 190 ------------------ backend/api_server/src/blog/mod.rs | 4 +- backend/api_server/src/init/database.rs | 5 +- frontend/app/blog/BlogPostCard.tsx | 6 +- frontend/app/blog/PostList.tsx | 4 +- frontend/app/blog/page.tsx | 12 +- frontend/app/blog/post/[id]/Headers.tsx | 8 +- frontend/app/blog/post/[id]/PostCard.tsx | 2 +- frontend/app/blog/post/[id]/page.tsx | 8 +- frontend/components/ThemeSwitcher.tsx | 4 +- frontend/package.json | 2 +- 14 files changed, 82 insertions(+), 226 deletions(-) delete mode 100644 backend/api_server/src/blog/api/refresh.rs diff --git a/backend/api_server/Cargo.toml b/backend/api_server/Cargo.toml index 6307f72..af17326 100644 --- a/backend/api_server/Cargo.toml +++ b/backend/api_server/Cargo.toml @@ -15,7 +15,7 @@ dotenv = "0.15.0" dirs = "5.0.1" tokio = { version = "1.40.0", features = ["full"] } sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono", "uuid"] } -axum = "0.7.5" +axum = { version = "0.7.5", features = ["macros"] } serde = { version = "1.0.199", features = ["derive"] } serde_json = "1.0.128" uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] } diff --git a/backend/api_server/src/blog/api/get_post_list.rs b/backend/api_server/src/blog/api/get_post_list.rs index 854f495..9db7c61 100644 --- a/backend/api_server/src/blog/api/get_post_list.rs +++ b/backend/api_server/src/blog/api/get_post_list.rs @@ -1,3 +1,5 @@ +use std::error::Error; + use constants::blog::PAGE_SIZE; use axum::{extract::Query, extract::State, http::StatusCode, Json}; @@ -14,6 +16,7 @@ pub struct Params { page: Option, } +#[axum::debug_handler] pub async fn get_post_list( State(db_connection): State, Query(params): Query, @@ -47,33 +50,72 @@ pub async fn get_post_list( } }; - let mut res = Map::new(); + let mut res = Vec::new(); let posts: Vec<_> = post_list .into_iter() - .map(|post| analyze_post(&db_connection, post)) + .map(|post| async { analyze_post(&db_connection, post).await }) .collect(); - let posts = join_all(posts).await; + for post in posts { + match post { + Ok(p) => res.push(p), + Err(e) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(json!(("error", e.to_string()))), + )) + } + } + } - res.insert("posts".to_string(), Value::from(posts)); + // res.insert("posts".to_string(), Value::from(posts)); Ok(Json::from(Value::from(res))) } -async fn analyze_post(db_connection: &PgPool, post: PgRow) -> Value { +async fn analyze_post(db_connection: &PgPool, post: PgRow) -> Result> { let post_id: Uuid = post.get("post_id"); let tags = fetch_post_tags(db_connection, post_id).await; + let meta = fetch_post_meta(db_connection, post_id).await.unwrap(); + let header_img = meta.get("header_img"); + let last_update = meta.get("last_update"); - json!({ + Ok(json!({ "post_id": post_id.to_string(), "title": post.get::("title"), "category": post.get::("category"), - "header_img": post.get::("header_img"), + "header_img": header_img, "sub_title": post.get::("sub_title"), "summary": post.get::("summary"), - "last_update": post.get::, _>("last_update"), + "last_update": last_update, "first_update": post.get::, _>("first_update"), "tags": tags, - }) + })) +} + +async fn fetch_post_meta(db_connection: &PgPool, post_id: Uuid) -> Option { + match query( + r#" +SELECT jsonb_strip_nulls(to_jsonb(t.*)) - 'post_id' AS meta +FROM meta t +WHERE post_id = $1; + "#, + ) + .bind(post_id) + .fetch_one(db_connection) + .await + { + Ok(value) => Some(value.get::("meta")), + Err(e) => { + error!( + "Database error on Reading `{}` from table `{}`: <{}>: {}", + "post meta data", + "meta", + post_id.to_string(), + e.to_string() + ); + None + } + } } async fn fetch_post_tags(db_connection: &PgPool, post_id: Uuid) -> Vec { diff --git a/backend/api_server/src/blog/api/mod.rs b/backend/api_server/src/blog/api/mod.rs index e946d12..5e23c3d 100644 --- a/backend/api_server/src/blog/api/mod.rs +++ b/backend/api_server/src/blog/api/mod.rs @@ -1,4 +1,3 @@ pub mod get_page_count; pub mod get_post; pub mod get_post_list; -pub mod refresh; diff --git a/backend/api_server/src/blog/api/refresh.rs b/backend/api_server/src/blog/api/refresh.rs deleted file mode 100644 index cc7b331..0000000 --- a/backend/api_server/src/blog/api/refresh.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::path::Path; -use std::str::FromStr; - -use axum::extract::{Query, State}; -use axum::http::StatusCode; -use axum::Json; -use serde::Deserialize; -use serde_json::{Map, Value}; -use sqlx::PgPool; - -// use crate::api::post::errors::refresh::PostIdxError; -// use crate::api::post::utils::{build_post_idx, post}; -// use crate::model::blog::{POST_BASE_MODEL, POST_HASH_MODEL, POST_META_MODEL, TAG_MODEL}; - -#[derive(Deserialize)] -pub struct Params { - /// refresh level - level: Option, -} - -pub async fn refresh( - State(db_connection): State, - Query(params): Query, -) -> Result)> { - // let is_force_refresh = params.level.unwrap_or("".to_string()) == "force"; - // - // // search post - // let data_repo_dir = match env::var("GIT_WORK_DIR") { - // Ok(value) => value, - // Err(_) => { - // return Err(( - // StatusCode::INTERNAL_SERVER_ERROR, - // Json::from(json!(( - // "error", - // "error at parsing environment variable: GIT_WORK_DIR" - // ))), - // )); - // } - // }; - // let post_dir = Path::new(&data_repo_dir).join("posts"); - // - // if is_force_refresh { - // if let Err(e) = query("DROP TABLE IF EXISTS hash, meta, post, tag;") - // .execute(&db_connection) - // .await - // { - // error!( - // "DB query error when deleting table: `hash`, `meta`, `post`, `tag`: {}", - // e.to_string() - // ); - // return Err(( - // StatusCode::INTERNAL_SERVER_ERROR, - // Json::from(json!(("error", e.to_string()))), - // )); - // } - // } - // - // // generate post idx - // let post_path = search_md(&post_dir); - // - // let post_list_with_hash: Vec<(Map<_, _>, Vec<_>)> = post_path - // .into_iter() - // .filter_map(|post_path| match post::from_path(&post_path) { - // Ok(post) => match fs::read(&post_path) { - // Ok(bytes) => { - // let mut hasher = Sha256::new(); - // hasher.update(bytes); - // let hash = hasher.finalize().to_vec(); - // Some((post, hash)) - // } - // Err(e) => { - // error!( - // "Failed to read file while hashing the post <{}>, skipping: {}", - // post_path, - // e.to_string() - // ); - // None - // } - // }, - // Err(e) => { - // error!( - // "Failed to parse post <{}>, skipping: {}", - // post_path, - // e.to_string() - // ); - // None - // } - // }) - // .collect(); - // - // // db create - // query(POST_BASE_MODEL) - // .execute(&db_connection) - // .await - // .unwrap(); - // query(POST_META_MODEL) - // .execute(&db_connection) - // .await - // .unwrap(); - // query(TAG_MODEL).execute(&db_connection).await.unwrap(); - // query(POST_HASH_MODEL) - // .execute(&db_connection) - // .await - // .unwrap(); - // - // let res: Vec<_> = stream::iter(post_list_with_hash) - // .map(|(post, hash)| { - // let db_clone = db_connection.clone(); - // async move { build_post_idx(&db_clone, post, hash).await } - // }) - // .buffer_unordered(usize::MAX) - // .collect() - // .await; - // - // let errs: Vec<_> = res - // .into_iter() - // .filter_map(|res| res.err()) - // .map(|e| e.to_string()) - // .collect(); - // - // if !errs.is_empty() { - // Err(( - // StatusCode::INTERNAL_SERVER_ERROR, - // Json::from(json!({"error": errs})), - // )) - // } else { - // Ok(StatusCode::OK) - // } - todo!() -} - -/// Search a dir, return a list of markdown files -/// -/// # Arguments -/// * `entry`: dir path -/// -/// # Returns -/// Vec -/// * `String`: path of a markdown file -/// -fn search_md(entry: &Path) -> Vec { - if entry.ends_with(".git") { - return vec![]; - } - if entry.is_dir() { - if let Ok(files) = entry.read_dir() { - files - .flat_map(|file| { - let file_path = file.unwrap().path(); - search_md(file_path.as_path()) - }) - .collect() - } else { - vec![] - } - } else if entry.extension().is_some_and(|ext| ext == "md") { - vec![entry.to_str().unwrap().to_string()] - } else { - vec![] - } -} - -async fn build_post_idx( - db_connection: &PgPool, - post: Map, - hash: Vec, - // ) -> Result<(), PostIdxError> { -) -> Result<(), ()> { - todo!() - // let meta = &post["meta"]; - // - // // extract post id - // let post_id = match Uuid::from_str(post["post_id"].as_str().unwrap()) { - // Ok(id) => id, - // Err(e) => { - // return Err(PostIdxError::InvalidUUID { - // id: post["post_id"].as_str().unwrap().to_string(), - // err_msg: e.to_string(), - // }) - // } - // }; - // - // if build_post_idx::hash(db_connection, post_id, hash).await? { - // build_post_idx::base(db_connection, &post, post_id, meta).await?; - // build_post_idx::tag(db_connection, post_id, meta).await?; - // build_post_idx::meta(db_connection, post_id, meta).await?; - // } - // - // Ok(()) -} diff --git a/backend/api_server/src/blog/mod.rs b/backend/api_server/src/blog/mod.rs index 8b61780..6d14b15 100644 --- a/backend/api_server/src/blog/mod.rs +++ b/backend/api_server/src/blog/mod.rs @@ -5,14 +5,12 @@ use crate::blog::api::{ get_page_count::get_page_count, get_post::get_post, get_post_list::get_post_list, }; -use crate::blog::api::refresh::refresh; -use axum::routing::{get, post}; +use axum::routing::get; use axum::Router; use sqlx::{Pool, Postgres}; pub fn get_router() -> Router> { Router::new() - .route("/refresh_posts", post(refresh)) .route("/get_post_list", get(get_post_list)) .route("/get_page_count", get(get_page_count)) .route("/get_post/:post_id", get(get_post)) diff --git a/backend/api_server/src/init/database.rs b/backend/api_server/src/init/database.rs index e403269..c2a8aad 100644 --- a/backend/api_server/src/init/database.rs +++ b/backend/api_server/src/init/database.rs @@ -11,7 +11,10 @@ pub async fn init() -> Result, Box> { let db_connect_option = PgConnectOptions::from_str(&parse_db_uri()) .unwrap() .disable_statement_logging(); - Ok(PgPoolOptions::new().connect_with(db_connect_option).await?) + Ok(PgPoolOptions::new() + .max_connections(1) // todo: temporal solution, to be fixed + .connect_with(db_connect_option) + .await?) } fn parse_db_uri() -> String { diff --git a/frontend/app/blog/BlogPostCard.tsx b/frontend/app/blog/BlogPostCard.tsx index 3829fd2..0ca61a8 100644 --- a/frontend/app/blog/BlogPostCard.tsx +++ b/frontend/app/blog/BlogPostCard.tsx @@ -18,7 +18,7 @@ function BlogPostCard({ post }: Props) { // let last_update = new Date((post as any)['last_update']); let first_update = new Date((post as any)['first_update']); let post_id = (post as any)['post_id']; - let header_img = (post as any)['header_img']; + let header_img = ((post as any)['header_img'] as string | null); return ( @@ -29,7 +29,7 @@ function BlogPostCard({ post }: Props) { isHoverable > - { header_img !== '' &&
+ { header_img &&
{
-
+
{ category }

{ title }

diff --git a/frontend/app/blog/PostList.tsx b/frontend/app/blog/PostList.tsx index 4eff3a8..591c6ea 100644 --- a/frontend/app/blog/PostList.tsx +++ b/frontend/app/blog/PostList.tsx @@ -8,8 +8,8 @@ import { Card, CardBody } from '@nextui-org/card'; const PostList = async ({ page }: { page: number }) => { try { - const res = await fetchNocturneApi(`/get_post_list?page=${ page }`); - let posts = (await res.json() as any)['posts']; + const res = await fetchNocturneApi(`/blog/get_post_list?page=${ page }`); + const posts = await res.json(); return ( // blog list
diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx index fb6d6cc..6690937 100644 --- a/frontend/app/blog/page.tsx +++ b/frontend/app/blog/page.tsx @@ -2,15 +2,15 @@ import PostList from '@/app/blog/PostList'; import Pagination from '@/app/blog/Pagination'; import { fetchNocturneApi } from '@/app/blog/api'; -async function Blog({ searchParams }: { - searchParams?: { - page?: number - }; +async function Blog({ + searchParams +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { - const parsePage = Number(searchParams?.page); + const parsePage = Number((await searchParams)?.page); const currPage = parsePage > 0 ? parsePage : 1; - const ret = await fetchNocturneApi(`/get_page_count`); + const ret = await fetchNocturneApi(`/blog/get_page_count`); const pageCount = (await ret.json() as any)['res'] as number; return ( diff --git a/frontend/app/blog/post/[id]/Headers.tsx b/frontend/app/blog/post/[id]/Headers.tsx index cb44322..62e9090 100644 --- a/frontend/app/blog/post/[id]/Headers.tsx +++ b/frontend/app/blog/post/[id]/Headers.tsx @@ -18,11 +18,11 @@ const Headers = (props: Props) => { // let last_update = new Date((post as any)['last_update']); let first_update = new Date(post['first_update']); // let post_id = (post as any)['post_id']; - let header_img = post['header_img']; + let header_img = post['meta']['header_img'] as string | null; return ( <> - { header_img !== '' &&
+ { header_img &&
{ {
}
-
+
{ category }

{ title }

diff --git a/frontend/app/blog/post/[id]/PostCard.tsx b/frontend/app/blog/post/[id]/PostCard.tsx index 056f3d1..958979f 100644 --- a/frontend/app/blog/post/[id]/PostCard.tsx +++ b/frontend/app/blog/post/[id]/PostCard.tsx @@ -13,7 +13,7 @@ async function PostCard(props: Props) { try { - const res = await fetchNocturneApi(`/get_post/${ props.id }`); + const res = await fetchNocturneApi(`/blog/get_post/${ props.id }`); const ret: any = await res.json(); return (
diff --git a/frontend/app/blog/post/[id]/page.tsx b/frontend/app/blog/post/[id]/page.tsx index 13ea3b2..c29364d 100644 --- a/frontend/app/blog/post/[id]/page.tsx +++ b/frontend/app/blog/post/[id]/page.tsx @@ -3,7 +3,11 @@ import PostCard from '@/app/blog/post/[id]/PostCard'; import { MotionDiv } from '@/app/public/MotionDiv'; import ButtonGoBack from '@/app/blog/post/[id]/ButtonGoBack'; -function BlogReader({ params }: { params: { id: string } }) { +async function BlogReader({ + params +}: { + params: Promise<{ id: string }> +}) { return (
- +
diff --git a/frontend/components/ThemeSwitcher.tsx b/frontend/components/ThemeSwitcher.tsx index 07f6f15..0dbc95f 100644 --- a/frontend/components/ThemeSwitcher.tsx +++ b/frontend/components/ThemeSwitcher.tsx @@ -49,9 +49,9 @@ function ThemeSwitcher() { fullWidth isIconOnly={ isMobile } > - { switcherElements.get(currTheme.values().next().value)?.icon } + { switcherElements.get(currTheme.values().next().value as string)?.icon }

- { switcherElements.get(currTheme.values().next().value)?.display_name } + { switcherElements.get(currTheme.values().next().value as string)?.display_name }

diff --git a/frontend/package.json b/frontend/package.json index ed666ec..92be224 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "@types/react-syntax-highlighter": "^15.5.13", "framer-motion": "^11.3.24", "lxgw-wenkai-webfont": "^1.7.0", - "next": "14.2.3", + "next": "^15.0.3", "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1",