From 80a164625b6765f5c22befe8ee7a1705a7625a40 Mon Sep 17 00:00:00 2001 From: Lumither Tao Date: Mon, 12 May 2025 01:07:23 +0800 Subject: [PATCH] feat(blog::api) pagination --- .gitignore | 2 +- .../modules/blog/api/get_page_count/mod.rs | 25 ---- .../api/get_page_count/post_page_count.sql | 2 - .../src/modules/blog/api/get_post_list.rs | 130 ------------------ backend/server/src/modules/blog/api/mod.rs | 5 +- .../api/{get_post/mod.rs => posts/get.rs} | 3 - .../get_post_by_str_id.sql | 0 .../{get_post => posts}/get_post_by_uuid.sql | 0 .../server/src/modules/blog/api/posts/mod.rs | 2 + .../modules/blog/api/posts/pagination/mod.rs | 42 ++++++ backend/server/src/modules/blog/mod.rs | 8 +- backend/server/src/utils/axum_response.rs | 31 +++++ backend/server/src/utils/mod.rs | 1 + 13 files changed, 83 insertions(+), 168 deletions(-) delete mode 100644 backend/server/src/modules/blog/api/get_page_count/mod.rs delete mode 100644 backend/server/src/modules/blog/api/get_page_count/post_page_count.sql delete mode 100644 backend/server/src/modules/blog/api/get_post_list.rs rename backend/server/src/modules/blog/api/{get_post/mod.rs => posts/get.rs} (83%) rename backend/server/src/modules/blog/api/{get_post => posts}/get_post_by_str_id.sql (100%) rename backend/server/src/modules/blog/api/{get_post => posts}/get_post_by_uuid.sql (100%) create mode 100644 backend/server/src/modules/blog/api/posts/mod.rs create mode 100644 backend/server/src/modules/blog/api/posts/pagination/mod.rs create mode 100644 backend/server/src/utils/axum_response.rs diff --git a/.gitignore b/.gitignore index 67b7a43..e333228 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ .idea .vscode -# exclude post repo +# exclude content repo /blog_posts_git # rustc ICE dump diff --git a/backend/server/src/modules/blog/api/get_page_count/mod.rs b/backend/server/src/modules/blog/api/get_page_count/mod.rs deleted file mode 100644 index ce20576..0000000 --- a/backend/server/src/modules/blog/api/get_page_count/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::constants::blog::PAGE_SIZE; - -use axum::{Json, extract::State}; -use serde_json::{Value, json}; -use sqlx::{PgPool, Row, query}; -use tracing::error; - -const GET_PAGE_COUNT_QUERY: &str = include_str!("post_page_count.sql"); - -pub async fn handler(State(db_connection): State) -> Result, Json> { - match query(GET_PAGE_COUNT_QUERY) - .bind(PAGE_SIZE as i64) - .fetch_one(&db_connection) - .await - { - Ok(res) => { - let res: i32 = res.get("count"); - Ok(Json::from(json!({"res": res}))) - } - Err(e) => { - error!("failed to get blog page count: {}", e.to_string()); - Err(Json::from(json!({"error": e.to_string()}))) - } - } -} diff --git a/backend/server/src/modules/blog/api/get_page_count/post_page_count.sql b/backend/server/src/modules/blog/api/get_page_count/post_page_count.sql deleted file mode 100644 index 5ccce6b..0000000 --- a/backend/server/src/modules/blog/api/get_page_count/post_page_count.sql +++ /dev/null @@ -1,2 +0,0 @@ -SELECT cast(ceil(count(*) / cast($1 AS FLOAT)) AS INTEGER) AS count -FROM posts; diff --git a/backend/server/src/modules/blog/api/get_post_list.rs b/backend/server/src/modules/blog/api/get_post_list.rs deleted file mode 100644 index 81800aa..0000000 --- a/backend/server/src/modules/blog/api/get_post_list.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::error::Error; - -use crate::constants::blog::PAGE_SIZE; - -use axum::{extract::Query, extract::State, http::StatusCode, Json}; -use chrono::{DateTime, Utc}; -use futures::future::join_all; -use serde::Deserialize; -use serde_json::{json, Map, Value}; -use sqlx::{postgres::PgRow, query, PgPool, Row}; -use tracing::error; -use uuid::Uuid; - -#[derive(Deserialize)] -pub struct Params { - page: Option, -} - -#[axum::debug_handler] -pub async fn get_post_list( - State(db_connection): State, - Query(params): Query, -) -> Result, (StatusCode, Json)> { - let search_param_page = params.page.unwrap_or(1); - if search_param_page < 1 { - return Err(( - StatusCode::BAD_REQUEST, - Json::from(json!({"error": "`page` should be greater than 0"})), - )); - } - - let offset = PAGE_SIZE * (search_param_page - 1); - - let post_list = sqlx::query("SELECT * FROM post ORDER BY first_update DESC LIMIT $1 OFFSET $2") - .bind(PAGE_SIZE as i32) - .bind(offset as i32) - .fetch_all(&db_connection) - .await; - let post_list = match post_list { - Ok(post_list) => post_list, - Err(e) => { - error!( - "Database query error on fetching post list: {}", - e.to_string() - ); - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json::from(json!({"error": e.to_string()})), - )); - } - }; - - let mut res = Vec::new(); - let posts: Vec<_> = post_list - .into_iter() - .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)); - Ok(Json::from(Value::from(res))) -} - -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"); - - Ok(json!({ - "post_id": post_id.to_string(), - "title": post.get::("title"), - "category": post.get::("category"), - "header_img": header_img, - "sub_title": post.get::("sub_title"), - "summary": post.get::("summary"), - "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 { - match query("SELECT tag FROM Tag WHERE post_id = $1") - .bind(post_id) - .fetch_all(db_connection) - .await - { - Ok(res) => res.iter().map(|tag| tag.get::("tag")).collect(), - Err(_) => vec![], - } -} diff --git a/backend/server/src/modules/blog/api/mod.rs b/backend/server/src/modules/blog/api/mod.rs index 5eb1f92..24fd435 100644 --- a/backend/server/src/modules/blog/api/mod.rs +++ b/backend/server/src/modules/blog/api/mod.rs @@ -1,6 +1,3 @@ -pub mod get_page_count; - // todo: remove exception tag #[allow(dead_code)] -pub mod get_post; -// pub mod get_post_list; +pub mod posts; diff --git a/backend/server/src/modules/blog/api/get_post/mod.rs b/backend/server/src/modules/blog/api/posts/get.rs similarity index 83% rename from backend/server/src/modules/blog/api/get_post/mod.rs rename to backend/server/src/modules/blog/api/posts/get.rs index a2b8df7..ad2b479 100644 --- a/backend/server/src/modules/blog/api/get_post/mod.rs +++ b/backend/server/src/modules/blog/api/posts/get.rs @@ -9,9 +9,6 @@ use serde_json::json; use sqlx::PgPool; use uuid::Uuid; -const GET_POST_BY_UUID: &str = include_str!("get_post_by_uuid.sql"); -const GET_POST_BY_STR_ID: &str = include_str!("get_post_by_str_id.sql"); - pub async fn handler( State(db_connection): State, Path(identifier): Path, diff --git a/backend/server/src/modules/blog/api/get_post/get_post_by_str_id.sql b/backend/server/src/modules/blog/api/posts/get_post_by_str_id.sql similarity index 100% rename from backend/server/src/modules/blog/api/get_post/get_post_by_str_id.sql rename to backend/server/src/modules/blog/api/posts/get_post_by_str_id.sql diff --git a/backend/server/src/modules/blog/api/get_post/get_post_by_uuid.sql b/backend/server/src/modules/blog/api/posts/get_post_by_uuid.sql similarity index 100% rename from backend/server/src/modules/blog/api/get_post/get_post_by_uuid.sql rename to backend/server/src/modules/blog/api/posts/get_post_by_uuid.sql diff --git a/backend/server/src/modules/blog/api/posts/mod.rs b/backend/server/src/modules/blog/api/posts/mod.rs new file mode 100644 index 0000000..602cb8e --- /dev/null +++ b/backend/server/src/modules/blog/api/posts/mod.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod pagination; diff --git a/backend/server/src/modules/blog/api/posts/pagination/mod.rs b/backend/server/src/modules/blog/api/posts/pagination/mod.rs new file mode 100644 index 0000000..4512ef3 --- /dev/null +++ b/backend/server/src/modules/blog/api/posts/pagination/mod.rs @@ -0,0 +1,42 @@ +use crate::{ + constants::blog::PAGE_SIZE, + utils::axum_response::{err_resp, succ_resp}, +}; + +use axum::{extract::State, http::StatusCode, response::Response}; +use serde::Serialize; +use serde_json::{Value, json}; +use sqlx::{FromRow, PgPool, Row, query_as}; + +#[derive(FromRow, Serialize)] +struct DBPaginationRow { + page_count: i32, + total_count: i64, +} + +impl From for Value { + fn from(value: DBPaginationRow) -> Self { + json!({ + "page_count": value.page_count, + "total_count": value.total_count + }) + } +} + +pub async fn handler(State(db_connection): State) -> Response { + match query_as::<_, DBPaginationRow>( + r#" + SELECT + cast(ceil(count(*) / cast($1 AS FLOAT)) AS INTEGER) AS page_count, + count(*) AS total_count + FROM posts; + "#, + ) + .bind(PAGE_SIZE as i64) + .fetch_one(&db_connection) + .await + { + Ok(res) => succ_resp(StatusCode::OK, res.into()), + Err(e) => err_resp(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + } +} diff --git a/backend/server/src/modules/blog/mod.rs b/backend/server/src/modules/blog/mod.rs index 707de44..cc47117 100644 --- a/backend/server/src/modules/blog/mod.rs +++ b/backend/server/src/modules/blog/mod.rs @@ -5,7 +5,7 @@ use crate::{ modules::{ Module, blog::{ - api::{get_page_count, get_post}, + api::{posts, posts::pagination}, components::{check_update, consistency_guard}, }, }, @@ -28,8 +28,10 @@ impl Module for Blog { // todo: remove debug comment Router::new() // .route("/get_post_list", get(get_post_list)) - .route("/get_page_count", get(get_page_count::handler)) - .route("/get_post/{identifier}", get(get_post::handler)) + // .route("/posts/pagination", get(...)) + // .route("/posts", get(...)) // get posts + .route("/posts/pagination", get(pagination::handler)) + .route("/posts/{identifier}", get(posts::get::handler)) .with_state(db_handler) } diff --git a/backend/server/src/utils/axum_response.rs b/backend/server/src/utils/axum_response.rs new file mode 100644 index 0000000..b2d2a9c --- /dev/null +++ b/backend/server/src/utils/axum_response.rs @@ -0,0 +1,31 @@ +use axum::{ + http::{StatusCode, header::CONTENT_TYPE}, + response::Response, +}; +use serde_json::{Value, json}; + +pub fn err_resp(code: StatusCode, err_msg: String) -> Response { + let payload = json!({ + "status": "error", + "data": { + "msg": err_msg, + } + }); + resp_json(code, payload) +} + +pub fn succ_resp(code: StatusCode, payload: Value) -> Response { + let payload = json!({ + "status": "success", + "data": payload + }); + resp_json(code, payload) +} + +pub fn resp_json(code: StatusCode, payload: Value) -> Response { + Response::builder() + .status(code) + .header(CONTENT_TYPE, "application/json") + .body(payload.to_string()) + .unwrap() +} diff --git a/backend/server/src/utils/mod.rs b/backend/server/src/utils/mod.rs index c2bf1c3..9ea0e92 100644 --- a/backend/server/src/utils/mod.rs +++ b/backend/server/src/utils/mod.rs @@ -1 +1,2 @@ +pub mod axum_response; pub mod git;