feat(blog::api) impl GET /blog/posts

This commit is contained in:
lumither committed 2025-05-14 01:47:21 +08:00
1 parent 2f6cafa543
commit d15cdf216a
5 files changed
+159 -16

No files matched your search

@@ -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<String>,
category: String,
date_created: NaiveDate,
date_updated: Option<NaiveDate>,
header_img: Option<String>,
}
#[derive(Deserialize, Serialize)]
pub struct Pagination {
page: Option<u32>,
page_size: Option<u32>,
}
#[derive(Serialize)]
pub struct RespPagination {
page: u32,
page_size: u32,
page_count: u32,
}
#[derive(Serialize)]
struct Response {
posts: Vec<PostBriefModel>,
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<PgPool>,
Query(pagination): Query<Pagination>,
) -> 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::<Vec<_>>(),
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())
} }
@@ -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::{ use axum::{
extract::{Path, State}, extract::{Path, State},
@@ -73,8 +73,10 @@ pub async fn handler(
Ok(post) => post, Ok(post) => post,
Err(e) => { Err(e) => {
return match e { return match e {
sqlx::Error::RowNotFound => err_resp(StatusCode::NOT_FOUND, "no matching post"), sqlx::Error::RowNotFound => {
_ => err_resp(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), err_resp_log!(StatusCode::NOT_FOUND, "no matching post")
}
_ => err_resp_log!(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}; };
} }
}; };
@@ -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;
@@ -1,7 +1,4 @@
use crate::{ use crate::{constants::blog::PAGE_SIZE, err_resp_log, utils::axum_response::succ_resp};
constants::blog::PAGE_SIZE,
utils::axum_response::{err_resp, succ_resp},
};
use axum::{extract::State, http::StatusCode, response::Response}; use axum::{extract::State, http::StatusCode, response::Response};
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -50,6 +47,6 @@ pub async fn handler(State(db_connection): State<PgPool>) -> Response<String> {
.await .await
{ {
Ok(res) => succ_resp(StatusCode::OK, PaginationResponse::from(res).into()), 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()),
} }
} }
+25 -5
View File
@@ -4,13 +4,33 @@ use axum::{
}; };
use serde_json::{Value, json}; use serde_json::{Value, json};
pub fn err_resp(code: StatusCode, err_msg: &str) -> Response<String> { #[macro_export]
let payload = json!({ macro_rules! err_resp_log {
"status": "error", ($code:expr, $msg:expr) => {{
"data": { use tracing::error;
"msg": err_msg, 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<Value>) -> Response<String> {
let mut payload = json!({
"status": "error",
"msg": err_msg,
}); });
if let Some(body) = body {
payload["data"] = body;
}
resp_json(code, payload) resp_json(code, payload)
} }