From d15cdf216a99a8a4834b1b80e268e56a33720d86 Mon Sep 17 00:00:00 2001 From: Lumither Tao Date: Wed, 14 May 2025 01:47:21 +0800 Subject: [PATCH] feat(blog::api) impl GET /blog/posts --- .../server/src/modules/blog/api/posts/get.rs | 111 +++++++++++++++++- .../modules/blog/api/posts/get_identifier.rs | 8 +- .../api/posts/get_post_with_brief_model.sql | 19 +++ .../modules/blog/api/posts/pagination/mod.rs | 7 +- backend/server/src/utils/axum_response.rs | 30 ++++- 5 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 backend/server/src/modules/blog/api/posts/get_post_with_brief_model.sql diff --git a/backend/server/src/modules/blog/api/posts/get.rs b/backend/server/src/modules/blog/api/posts/get.rs index 704b9eb..aa3da53 100644 --- a/backend/server/src/modules/blog/api/posts/get.rs +++ b/backend/server/src/modules/blog/api/posts/get.rs @@ -1,5 +1,110 @@ -use axum::response::IntoResponse; +use crate::{ + constants::blog::PAGE_SIZE, + err_resp_log, + utils::axum_response::{err_resp, succ_resp}, +}; -pub async fn handler() -> impl IntoResponse { - () +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, +}; +use chrono::NaiveDate; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use sqlx::{query_as, FromRow, PgPool}; +use uuid::Uuid; + +#[derive(FromRow, Serialize)] +struct PostBriefModel { + id: Uuid, + identifier: String, + title: String, + subtitle: String, + tags: Vec, + category: String, + date_created: NaiveDate, + date_updated: Option, + header_img: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct Pagination { + page: Option, + page_size: Option, +} + +#[derive(Serialize)] +pub struct RespPagination { + page: u32, + page_size: u32, + page_count: u32, +} + +#[derive(Serialize)] +struct Response { + posts: Vec, + pagination: RespPagination, +} + +#[derive(FromRow)] +struct UuidRow { + id: Uuid, +} + +const GET_POST_WITH_BRIEF_MODEL: &str = include_str!("get_post_with_brief_model.sql"); + +pub async fn handler( + State(db): State, + Query(pagination): Query, +) -> impl IntoResponse { + let page = pagination.page.unwrap_or(1); + let page_size = pagination.page_size.unwrap_or(PAGE_SIZE); + let page_count = match sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM posts") + .fetch_one(&db) + .await + { + Ok(post_count) => ((post_count as f64) / (page_size as f64)).ceil() as u32, + Err(e) => return err_resp_log!(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + }; + + let pagination = RespPagination { + page, + page_size, + page_count, + }; + + if page > page_count { + return err_resp( + StatusCode::BAD_REQUEST, + "page number should not be greater than page count", + Some(json!({"pagination": pagination})), + ); + } + + let offset = page_size * (page - 1); + + let ids = match query_as::<_, UuidRow>( + "SELECT id FROM posts ORDER BY date_created DESC LIMIT $1 OFFSET $2", + ) + .bind(page_size as i32) + .bind(offset as i32) + .fetch_all(&db) + .await + { + Ok(res) => res.into_iter().map(|item| item.id).collect::>(), + Err(e) => return err_resp_log!(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + }; + + let posts = match query_as::<_, PostBriefModel>(GET_POST_WITH_BRIEF_MODEL) + .bind(&ids) + .fetch_all(&db) + .await + { + Ok(posts) => posts, + Err(e) => return err_resp_log!(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + }; + + let resp = Response { posts, pagination }; + succ_resp(StatusCode::OK, serde_json::to_value(resp).unwrap()) } diff --git a/backend/server/src/modules/blog/api/posts/get_identifier.rs b/backend/server/src/modules/blog/api/posts/get_identifier.rs index 7226828..05b103b 100644 --- a/backend/server/src/modules/blog/api/posts/get_identifier.rs +++ b/backend/server/src/modules/blog/api/posts/get_identifier.rs @@ -1,4 +1,4 @@ -use crate::utils::axum_response::{err_resp, succ_resp}; +use crate::{err_resp_log, utils::axum_response::succ_resp}; use axum::{ extract::{Path, State}, @@ -73,8 +73,10 @@ pub async fn handler( Ok(post) => post, Err(e) => { return match e { - sqlx::Error::RowNotFound => err_resp(StatusCode::NOT_FOUND, "no matching post"), - _ => err_resp(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + sqlx::Error::RowNotFound => { + err_resp_log!(StatusCode::NOT_FOUND, "no matching post") + } + _ => err_resp_log!(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), }; } }; diff --git a/backend/server/src/modules/blog/api/posts/get_post_with_brief_model.sql b/backend/server/src/modules/blog/api/posts/get_post_with_brief_model.sql new file mode 100644 index 0000000..33448ea --- /dev/null +++ b/backend/server/src/modules/blog/api/posts/get_post_with_brief_model.sql @@ -0,0 +1,19 @@ +SELECT p.id, + identifier, + title, + subtitle, + date_created, + date_updated, + c.name AS category, + (SELECT array_agg(t.name) + FROM tags t + JOIN post_tag pt ON pt.tag = t.id + WHERE pt.post = p.id) AS tags, + (SELECT m.meta_value + FROM metadata m + WHERE m.pid = p.id + AND m.meta_key = 'header_img') AS header_img +FROM posts p + JOIN public.categories c on c.id = p.category +WHERE p.id = ANY ($1) +ORDER BY p.date_created DESC; \ No newline at end of file diff --git a/backend/server/src/modules/blog/api/posts/pagination/mod.rs b/backend/server/src/modules/blog/api/posts/pagination/mod.rs index 0897bd1..6bb4d6f 100644 --- a/backend/server/src/modules/blog/api/posts/pagination/mod.rs +++ b/backend/server/src/modules/blog/api/posts/pagination/mod.rs @@ -1,7 +1,4 @@ -use crate::{ - constants::blog::PAGE_SIZE, - utils::axum_response::{err_resp, succ_resp}, -}; +use crate::{constants::blog::PAGE_SIZE, err_resp_log, utils::axum_response::succ_resp}; use axum::{extract::State, http::StatusCode, response::Response}; use serde_json::{Value, json}; @@ -50,6 +47,6 @@ pub async fn handler(State(db_connection): State) -> Response { .await { Ok(res) => succ_resp(StatusCode::OK, PaginationResponse::from(res).into()), - Err(e) => err_resp(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + Err(e) => err_resp_log!(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), } } diff --git a/backend/server/src/utils/axum_response.rs b/backend/server/src/utils/axum_response.rs index 6cf6bd8..28bbe27 100644 --- a/backend/server/src/utils/axum_response.rs +++ b/backend/server/src/utils/axum_response.rs @@ -4,13 +4,33 @@ use axum::{ }; use serde_json::{Value, json}; -pub fn err_resp(code: StatusCode, err_msg: &str) -> Response { - let payload = json!({ +#[macro_export] +macro_rules! err_resp_log { + ($code:expr, $msg:expr) => {{ + use tracing::error; + use $crate::utils::axum_response::err_resp; + + error!("{}", $msg); + err_resp($code, $msg, None) + }}; + ($code:expr, $msg:expr, $body:expr) => {{ + use tracing::error; + use $crate::utils::axum_response::err_resp; + + error!("{}", $msg); + err_resp($code, $msg, Some($body)) + }}; +} + +pub fn err_resp(code: StatusCode, err_msg: &str, body: Option) -> Response { + let mut payload = json!({ "status": "error", - "data": { - "msg": err_msg, - } + "msg": err_msg, }); + if let Some(body) = body { + payload["data"] = body; + } + resp_json(code, payload) }