mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
Compare commits
2
Commits
80a164625b
...
2f6cafa543
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f6cafa543 | ||
|
|
dc2c294617 |
No files matched your search
@@ -1,32 +1,5 @@
|
|||||||
use macros::dev_consume;
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
use axum::{
|
pub async fn handler() -> impl IntoResponse {
|
||||||
Json,
|
()
|
||||||
extract::{Path, State},
|
|
||||||
response::IntoResponse,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub async fn handler(
|
|
||||||
State(db_connection): State<PgPool>,
|
|
||||||
Path(identifier): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
if let Ok(id) = identifier.parse::<Uuid>() {
|
|
||||||
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!()
|
|
||||||
}
|
}
|
||||||
@@ -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<NaiveDate>,
|
||||||
|
category: String,
|
||||||
|
tags: Vec<String>,
|
||||||
|
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<String>,
|
||||||
|
header_img: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Debug)]
|
||||||
|
struct AdjacentIds {
|
||||||
|
prev: Option<Uuid>,
|
||||||
|
next: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Default)]
|
||||||
|
struct AdjacentPosts {
|
||||||
|
prev: Option<AdjacentPost>,
|
||||||
|
next: Option<AdjacentPost>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ResponseModel {
|
||||||
|
post: SelectedPost,
|
||||||
|
adjacent: AdjacentPosts,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
State(db_connection): State<PgPool>,
|
||||||
|
Path(identifier): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let post = match if let Ok(id) = identifier.parse::<Uuid>() {
|
||||||
|
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<SelectedPost, sqlx::Error> {
|
||||||
|
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<SelectedPost, sqlx::Error> {
|
||||||
|
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<AdjacentPost> {
|
||||||
|
query_as(SELECT_POST_WITH_ADJACENT_MODEL)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -1 +1,19 @@
|
|||||||
SELECT
|
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;
|
||||||
@@ -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
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod get;
|
pub mod get;
|
||||||
|
pub mod get_identifier;
|
||||||
pub mod pagination;
|
pub mod pagination;
|
||||||
@@ -4,18 +4,31 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use axum::{extract::State, http::StatusCode, response::Response};
|
use axum::{extract::State, http::StatusCode, response::Response};
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use sqlx::{FromRow, PgPool, Row, query_as};
|
use sqlx::{FromRow, PgPool, query_as};
|
||||||
|
|
||||||
#[derive(FromRow, Serialize)]
|
#[derive(FromRow)]
|
||||||
struct DBPaginationRow {
|
struct DBPaginationRow {
|
||||||
page_count: i32,
|
page_count: i32,
|
||||||
total_count: i64,
|
total_count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DBPaginationRow> for Value {
|
struct PaginationResponse {
|
||||||
|
page_count: i32,
|
||||||
|
total_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DBPaginationRow> for PaginationResponse {
|
||||||
fn from(value: DBPaginationRow) -> Self {
|
fn from(value: DBPaginationRow) -> Self {
|
||||||
|
Self {
|
||||||
|
page_count: value.page_count,
|
||||||
|
total_count: value.total_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PaginationResponse> for Value {
|
||||||
|
fn from(value: PaginationResponse) -> Self {
|
||||||
json!({
|
json!({
|
||||||
"page_count": value.page_count,
|
"page_count": value.page_count,
|
||||||
"total_count": value.total_count
|
"total_count": value.total_count
|
||||||
@@ -36,7 +49,7 @@ pub async fn handler(State(db_connection): State<PgPool>) -> Response<String> {
|
|||||||
.fetch_one(&db_connection)
|
.fetch_one(&db_connection)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(res) => succ_resp(StatusCode::OK, 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(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
modules::{
|
modules::{
|
||||||
Module,
|
Module,
|
||||||
blog::{
|
blog::{
|
||||||
api::{posts, posts::pagination},
|
api::{posts, posts::get, posts::pagination},
|
||||||
components::{check_update, consistency_guard},
|
components::{check_update, consistency_guard},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -27,11 +27,9 @@ impl Module for Blog {
|
|||||||
// todo: move resource init to new func
|
// todo: move resource init to new func
|
||||||
// todo: remove debug comment
|
// todo: remove debug comment
|
||||||
Router::new()
|
Router::new()
|
||||||
// .route("/get_post_list", get(get_post_list))
|
.route("/posts", get(get::handler))
|
||||||
// .route("/posts/pagination", get(...))
|
.route("/posts/{identifier}", get(posts::get_identifier::handler))
|
||||||
// .route("/posts", get(...)) // get posts
|
|
||||||
.route("/posts/pagination", get(pagination::handler))
|
.route("/posts/pagination", get(pagination::handler))
|
||||||
.route("/posts/{identifier}", get(posts::get::handler))
|
|
||||||
.with_state(db_handler)
|
.with_state(db_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ CREATE INDEX idx_categories_name ON categories (name);
|
|||||||
CREATE INDEX idx_posts_identifier ON posts (identifier);
|
CREATE INDEX idx_posts_identifier ON posts (identifier);
|
||||||
CREATE INDEX idx_posts_status ON posts (status);
|
CREATE INDEX idx_posts_status ON posts (status);
|
||||||
CREATE INDEX idx_posts_category ON posts (category);
|
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_post ON post_tag (post);
|
||||||
CREATE INDEX idx_post_tag_tag ON post_tag (tag);
|
CREATE INDEX idx_post_tag_tag ON post_tag (tag);
|
||||||
CREATE INDEX idx_metadata_pid ON metadata (pid);
|
CREATE INDEX idx_metadata_pid ON metadata (pid);
|
||||||
@@ -4,7 +4,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
pub fn err_resp(code: StatusCode, err_msg: String) -> Response<String> {
|
pub fn err_resp(code: StatusCode, err_msg: &str) -> Response<String> {
|
||||||
let payload = json!({
|
let payload = json!({
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"data": {
|
"data": {
|
||||||
|
|||||||
Reference in new issue
Block a user