diff --git a/backend/server/src/modules/blog/api/posts/get.rs b/backend/server/src/modules/blog/api/posts/get.rs index ad2b479..704b9eb 100644 --- a/backend/server/src/modules/blog/api/posts/get.rs +++ b/backend/server/src/modules/blog/api/posts/get.rs @@ -1,32 +1,5 @@ -use macros::dev_consume; +use axum::response::IntoResponse; -use axum::{ - Json, - extract::{Path, State}, - response::IntoResponse, -}; -use serde_json::json; -use sqlx::PgPool; -use uuid::Uuid; - -pub async fn handler( - State(db_connection): State, - Path(identifier): Path, -) -> impl IntoResponse { - if let Ok(id) = identifier.parse::() { - select_post_by_uuid(&db_connection, id).await - } else { - select_post_by_str_id(&db_connection, &identifier).await - }; - Json::from(json!({"res": "ok"})).into_response() -} - -async fn select_post_by_uuid(db: &PgPool, id: Uuid) { - dev_consume!(db, id); - todo!() -} - -async fn select_post_by_str_id(db: &PgPool, id: &str) { - dev_consume!(db, id); - todo!() +pub async fn handler() -> impl IntoResponse { + () } diff --git a/backend/server/src/modules/blog/api/posts/get_identifier.rs b/backend/server/src/modules/blog/api/posts/get_identifier.rs new file mode 100644 index 0000000..7226828 --- /dev/null +++ b/backend/server/src/modules/blog/api/posts/get_identifier.rs @@ -0,0 +1,130 @@ +use crate::utils::axum_response::{err_resp, succ_resp}; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use chrono::NaiveDate; +use futures::future::OptionFuture; +use serde_json::Value; +use sqlx::{FromRow, PgPool, query_as}; +use uuid::Uuid; + +const SELECT_POST_BY_UUID: &str = include_str!("get_post_by_uuid.sql"); +const SELECT_POST_BY_STR_ID: &str = include_str!("get_post_by_str_id.sql"); +const SELECT_POST_ADJACENT_IDS: &str = include_str!("get_post_adjacent_ids.sql"); +const SELECT_POST_WITH_ADJACENT_MODEL: &str = include_str!("get_post_with_adjacent_model.sql"); + +#[derive(serde::Serialize, FromRow)] +struct SelectedPost { + id: Uuid, + identifier: String, + title: String, + subtitle: String, + status: String, + date_created: NaiveDate, + date_updated: Option, + category: String, + tags: Vec, + content: String, + metadata: Value, +} + +#[derive(serde::Serialize, FromRow)] +struct AdjacentPost { + id: Uuid, + identifier: String, + title: String, + subtitle: String, + date_created: NaiveDate, + category: String, + tags: Vec, + header_img: Option, +} + +#[derive(FromRow, Debug)] +struct AdjacentIds { + prev: Option, + next: Option, +} + +#[derive(serde::Serialize, Default)] +struct AdjacentPosts { + prev: Option, + next: Option, +} + +#[derive(serde::Serialize)] +struct ResponseModel { + post: SelectedPost, + adjacent: AdjacentPosts, +} + +pub async fn handler( + State(db_connection): State, + Path(identifier): Path, +) -> impl IntoResponse { + let post = match if let Ok(id) = identifier.parse::() { + select_post_by_uuid(&db_connection, id).await + } else { + select_post_by_str_id(&db_connection, &identifier).await + } { + 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()), + }; + } + }; + + let adj_ids = query_as::<_, AdjacentIds>(SELECT_POST_ADJACENT_IDS) + .bind(post.id) + .fetch_one(&db_connection) + .await + .unwrap(); + let adjacent_prev = OptionFuture::from( + adj_ids + .prev + .map(|id| select_post_with_adjacent_model(&db_connection, id)), + ) + .await + .flatten(); + let adjacent_next = OptionFuture::from( + adj_ids + .next + .map(|id| select_post_with_adjacent_model(&db_connection, id)), + ) + .await + .flatten(); + let adjacent = AdjacentPosts { + prev: adjacent_prev, + next: adjacent_next, + }; + + let response = ResponseModel { post, adjacent }; + succ_resp(StatusCode::OK, serde_json::to_value(response).unwrap()) +} + +async fn select_post_by_uuid(db: &PgPool, id: Uuid) -> Result { + query_as::<_, SelectedPost>(SELECT_POST_BY_UUID) + .bind(id) + .fetch_one(db) + .await +} + +async fn select_post_by_str_id(db: &PgPool, id: &str) -> Result { + query_as::<_, SelectedPost>(SELECT_POST_BY_STR_ID) + .bind(id) + .fetch_one(db) + .await +} + +async fn select_post_with_adjacent_model(db: &PgPool, id: Uuid) -> Option { + query_as(SELECT_POST_WITH_ADJACENT_MODEL) + .bind(id) + .fetch_optional(db) + .await + .unwrap() +} diff --git a/backend/server/src/modules/blog/api/posts/get_post_adjacent_ids.sql b/backend/server/src/modules/blog/api/posts/get_post_adjacent_ids.sql new file mode 100644 index 0000000..33a0f0c --- /dev/null +++ b/backend/server/src/modules/blog/api/posts/get_post_adjacent_ids.sql @@ -0,0 +1,13 @@ +WITH target_create_date AS (SELECT p.date_created AS date FROM posts p WHERE p.id = $1) +SELECT (SELECT p.id + FROM posts p + WHERE p.date_created < t.date + ORDER BY p.date_created DESC + LIMIT 1) AS prev, + (SELECT p.id + FROM posts p + WHERE p.date_created > t.date + ORDER BY p.date_created + LIMIT 1) AS next +FROM target_create_date t + diff --git a/backend/server/src/modules/blog/api/posts/get_post_by_str_id.sql b/backend/server/src/modules/blog/api/posts/get_post_by_str_id.sql index e69de29..3b8e6a5 100644 --- a/backend/server/src/modules/blog/api/posts/get_post_by_str_id.sql +++ b/backend/server/src/modules/blog/api/posts/get_post_by_str_id.sql @@ -0,0 +1,19 @@ +SELECT p.id, + p.identifier, + p.title, + p.subtitle, + p.status, + p.date_created, + p.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, + p.content, + (SELECT json_object_agg(m.meta_key, m.meta_value) + FROM metadata m + WHERE m.pid = p.id) AS metadata +FROM posts p + JOIN categories c ON c.id = p.category +WHERE p.identifier = $1; diff --git a/backend/server/src/modules/blog/api/posts/get_post_by_uuid.sql b/backend/server/src/modules/blog/api/posts/get_post_by_uuid.sql index 7dc5107..19522d6 100644 --- a/backend/server/src/modules/blog/api/posts/get_post_by_uuid.sql +++ b/backend/server/src/modules/blog/api/posts/get_post_by_uuid.sql @@ -1 +1,19 @@ -SELECT \ No newline at end of file +SELECT p.id, + p.identifier, + p.title, + p.subtitle, + p.status, + p.date_created, + p.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, + p.content, + (SELECT json_object_agg(m.meta_key, m.meta_value) + FROM metadata m + WHERE m.pid = p.id) AS metadata +FROM posts p + JOIN categories c ON c.id = p.category +WHERE p.id = $1; diff --git a/backend/server/src/modules/blog/api/posts/get_post_with_adjacent_model.sql b/backend/server/src/modules/blog/api/posts/get_post_with_adjacent_model.sql new file mode 100644 index 0000000..366ccf0 --- /dev/null +++ b/backend/server/src/modules/blog/api/posts/get_post_with_adjacent_model.sql @@ -0,0 +1,17 @@ +SELECT p.id, + identifier, + title, + subtitle, + date_created, + 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 = $1 \ No newline at end of file diff --git a/backend/server/src/modules/blog/api/posts/mod.rs b/backend/server/src/modules/blog/api/posts/mod.rs index 602cb8e..41f62d5 100644 --- a/backend/server/src/modules/blog/api/posts/mod.rs +++ b/backend/server/src/modules/blog/api/posts/mod.rs @@ -1,2 +1,3 @@ pub mod get; +pub mod get_identifier; 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 index afed9b0..0897bd1 100644 --- a/backend/server/src/modules/blog/api/posts/pagination/mod.rs +++ b/backend/server/src/modules/blog/api/posts/pagination/mod.rs @@ -50,6 +50,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(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 cc47117..77e4539 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::{posts, posts::pagination}, + api::{posts, posts::get, posts::pagination}, components::{check_update, consistency_guard}, }, }, @@ -27,11 +27,9 @@ impl Module for Blog { // todo: move resource init to new func // todo: remove debug comment Router::new() - // .route("/get_post_list", get(get_post_list)) - // .route("/posts/pagination", get(...)) - // .route("/posts", get(...)) // get posts + .route("/posts", get(get::handler)) + .route("/posts/{identifier}", get(posts::get_identifier::handler)) .route("/posts/pagination", get(pagination::handler)) - .route("/posts/{identifier}", get(posts::get::handler)) .with_state(db_handler) } diff --git a/backend/server/src/modules/blog/schema.sql b/backend/server/src/modules/blog/schema.sql index c6ffbbe..1bf1246 100644 --- a/backend/server/src/modules/blog/schema.sql +++ b/backend/server/src/modules/blog/schema.sql @@ -60,6 +60,7 @@ CREATE INDEX idx_categories_name ON categories (name); CREATE INDEX idx_posts_identifier ON posts (identifier); CREATE INDEX idx_posts_status ON posts (status); CREATE INDEX idx_posts_category ON posts (category); +CREATE INDEX idx_posts_date_created ON posts (date_created); CREATE INDEX idx_post_tag_post ON post_tag (post); CREATE INDEX idx_post_tag_tag ON post_tag (tag); CREATE INDEX idx_metadata_pid ON metadata (pid); \ No newline at end of file diff --git a/backend/server/src/utils/axum_response.rs b/backend/server/src/utils/axum_response.rs index b2d2a9c..6cf6bd8 100644 --- a/backend/server/src/utils/axum_response.rs +++ b/backend/server/src/utils/axum_response.rs @@ -4,7 +4,7 @@ use axum::{ }; use serde_json::{Value, json}; -pub fn err_resp(code: StatusCode, err_msg: String) -> Response { +pub fn err_resp(code: StatusCode, err_msg: &str) -> Response { let payload = json!({ "status": "error", "data": {