feat(blog::api) pagination

This commit is contained in:
lumither committed 2025-05-12 01:07:23 +08:00
1 parent 2c11af4a25
commit 80a164625b
13 files changed
+83 -168

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
.idea
.vscode
# exclude post repo
# exclude content repo
/blog_posts_git
# rustc ICE dump
@@ -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<PgPool>) -> Result<Json<Value>, Json<Value>> {
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()})))
}
}
}
@@ -1,2 +0,0 @@
SELECT cast(ceil(count(*) / cast($1 AS FLOAT)) AS INTEGER) AS count
FROM posts;
@@ -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<u32>,
}
#[axum::debug_handler]
pub async fn get_post_list(
State(db_connection): State<PgPool>,
Query(params): Query<Params>,
) -> Result<Json<Value>, (StatusCode, Json<Value>)> {
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<Value, Box<dyn Error + Send>> {
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::<String, _>("title"),
"category": post.get::<String, _>("category"),
"header_img": header_img,
"sub_title": post.get::<String, _>("sub_title"),
"summary": post.get::<String, _>("summary"),
"last_update": last_update,
"first_update": post.get::<DateTime<Utc>, _>("first_update"),
"tags": tags,
}))
}
async fn fetch_post_meta(db_connection: &PgPool, post_id: Uuid) -> Option<Value> {
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::<Value, _>("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<String> {
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::<String, _>("tag")).collect(),
Err(_) => vec![],
}
}
+1 -4
View File
@@ -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;
@@ -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<PgPool>,
Path(identifier): Path<String>,
@@ -0,0 +1,2 @@
pub mod get;
pub mod pagination;
@@ -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<DBPaginationRow> 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<PgPool>) -> Response<String> {
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()),
}
}
+5 -3
View File
@@ -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)
}
+31
View File
@@ -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<String> {
let payload = json!({
"status": "error",
"data": {
"msg": err_msg,
}
});
resp_json(code, payload)
}
pub fn succ_resp(code: StatusCode, payload: Value) -> Response<String> {
let payload = json!({
"status": "success",
"data": payload
});
resp_json(code, payload)
}
pub fn resp_json(code: StatusCode, payload: Value) -> Response<String> {
Response::builder()
.status(code)
.header(CONTENT_TYPE, "application/json")
.body(payload.to_string())
.unwrap()
}
+1
View File
@@ -1 +1,2 @@
pub mod axum_response;
pub mod git;