From fd8b760b7fdf17099f121f8a0ca18dd0002d7161 Mon Sep 17 00:00:00 2001 From: Lumither Tao Date: Sun, 12 May 2024 17:00:37 +0800 Subject: [PATCH] ref(backend) major refactor - remove struct Post, use database schema and Map instead - add sub_title - replace NaiveDateTime with DateTime --- backend/Cargo.toml | 2 +- backend/src/api/get/get_post.rs | 48 +++++------ backend/src/api/get/get_post_list.rs | 6 +- backend/src/api/post/refresh.rs | 107 ++++++++++++------------ backend/src/constants.rs | 1 + backend/src/main.rs | 3 +- backend/src/markdown/meta.rs | 8 +- backend/src/model/mod.rs | 34 ++++---- backend/src/model/post.rs | 120 +++++++++++++++------------ 9 files changed, 170 insertions(+), 159 deletions(-) create mode 100644 backend/src/constants.rs diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 956a5f3..94c4830 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -13,5 +13,5 @@ serde_json = "1.0.116" tokio = { version = "1.37.0", features = ["full"] } uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } sqlx = { version = "0.7.4", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono", "uuid"] } -chrono = { version = "0.4.38", features = ["serde"] } +chrono = { version = "0.4.38", features = ["serde", "alloc"] } regex = "1.10.4" diff --git a/backend/src/api/get/get_post.rs b/backend/src/api/get/get_post.rs index d80efa9..b4b566b 100644 --- a/backend/src/api/get/get_post.rs +++ b/backend/src/api/get/get_post.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::Json; -use chrono::NaiveDateTime; +use chrono::{DateTime, Utc}; use serde_json::{json, Map, Value}; use sqlx::{Error, PgPool, query, Row}; use uuid::Uuid; @@ -19,42 +19,34 @@ FROM Post WHERE post_id = $1; "#, ) - .bind(Uuid::from_str(post_id.as_str()).expect("Failed to parse UUID")) - .fetch_one(&db_connection) - .await { - Ok(value) => { value } + .bind(Uuid::from_str(post_id.as_str()).expect("Failed to parse UUID")) + .fetch_one(&db_connection) + .await + { + Ok(value) => value, Err(e) => { return match e { - Error::RowNotFound => { - Err(( - StatusCode::BAD_REQUEST, - Json::from(json!(format!( - "{{error: {}}}", - e.to_string() - ))))) - } - _ => { - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json::from(json!(format!( - "{{error: {}}}", - e.to_string() - ))))) - } + Error::RowNotFound => Err(( + StatusCode::BAD_REQUEST, + Json::from(json!(format!("{{error: {}}}", e.to_string()))), + )), + _ => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(json!(format!("{{error: {}}}", e.to_string()))), + )), }; } }; - let tags = query( r#" SELECT tag FROM Tag WHERE post_id = $1; "#, ) - .bind(Uuid::from_str(post_id.as_str()).expect("Failed to parse UUID")) - .fetch_all(&db_connection) - .await - .expect("Failed to execute db query"); + .bind(Uuid::from_str(post_id.as_str()).expect("Failed to parse UUID")) + .fetch_all(&db_connection) + .await + .expect("Failed to execute db query"); let mut res = Map::new(); @@ -72,11 +64,11 @@ SELECT tag FROM Tag WHERE post_id = $1; ); res.insert( "last_update".to_string(), - json!(meta.get::("last_update")), + json!(meta.get::, &str>("last_update")), ); res.insert( "first_update".to_string(), - json!(meta.get::("first_update")), + json!(meta.get::, &str>("first_update")), ); let tags: Vec = tags diff --git a/backend/src/api/get/get_post_list.rs b/backend/src/api/get/get_post_list.rs index e324264..777b5d7 100644 --- a/backend/src/api/get/get_post_list.rs +++ b/backend/src/api/get/get_post_list.rs @@ -1,6 +1,6 @@ use axum::extract::State; use axum::Json; -use chrono::NaiveDateTime; +use chrono::{DateTime, Utc}; use serde_json::{json, Map, Value}; use sqlx::{PgPool, Row}; use uuid::Uuid; @@ -22,8 +22,8 @@ pub async fn get_post_list( let post_id: Uuid = post.get("post_id"); let title: String = post.get("title"); let summary: String = post.get("summary"); - let last_update: NaiveDateTime = post.get("last_update"); - let first_update: NaiveDateTime = post.get("first_update"); + let last_update: DateTime = post.get("last_update"); + let first_update: DateTime = post.get("first_update"); tmp.insert("post_id".to_string(), json!(post_id.to_string())); tmp.insert("title".to_string(), json!(title)); diff --git a/backend/src/api/post/refresh.rs b/backend/src/api/post/refresh.rs index 79e504a..6e9b416 100644 --- a/backend/src/api/post/refresh.rs +++ b/backend/src/api/post/refresh.rs @@ -1,14 +1,17 @@ use std::env; use std::path::Path; +use std::str::FromStr; use axum::extract::State; use axum::http::StatusCode; use axum::Json; -use serde_json::{json, Value}; +use chrono::DateTime; +use serde_json::{json, Map, Value}; use sqlx::{PgPool, query}; +use uuid::Uuid; -use crate::markdown::meta::parse_meta; -use crate::model::Post; +use crate::constants::GLOBAL_TIME_FORMAT; +use crate::model::{post, POST_DB_MODEL, TAG_DB_MODEL}; pub async fn refresh( State(db_connection): State, @@ -40,67 +43,59 @@ pub async fn refresh( // generate new idx - let post_list: Vec = md_list + let post_list: Vec> = md_list .into_iter() - .map(|post| Post::from_path(post).unwrap()) + .map(|post| post::from_path(post).unwrap()) .collect(); - query( - r##" -CREATE TABLE IF NOT EXISTS Post -( - post_id UUID PRIMARY KEY, - title VARCHAR(255) NOT NULL, - summary TEXT NOT NULL, - content TEXT NOT NULL, - last_update TIMESTAMP, - first_update TIMESTAMP NOT NULL -); - "##, - ) - .execute(&db_connection) - .await - .unwrap(); + query(POST_DB_MODEL).execute(&db_connection).await.unwrap(); - query( - r##" -CREATE TABLE IF NOT EXISTS Tag -( - post_id UUID NOT NULL, - tag VARCHAR(255) NOT NULL -); - "##, - ) - .execute(&db_connection) - .await - .unwrap(); + query(TAG_DB_MODEL).execute(&db_connection).await.unwrap(); // todo: concurrency for post in post_list { - let meta = parse_meta(&post.content); + let meta = &post["meta"]; // table Post match query( r##" -INSERT INTO Post (post_id, title, summary, content, last_update, first_update) -VALUES ($1, $2, $3, $4, $5, $6) -ON CONFLICT (post_id) - DO UPDATE SET title = excluded.title, - summary = excluded.summary, - content = excluded.content, - last_update = excluded.last_update, - first_update = excluded.first_update - +INSERT INTO Post (post_id, title, summary, content, last_update, first_update, sub_title) +VALUES ($1, $2, $3, $4, $5, $6, $7) "##, ) - .bind(post.post_id) - .bind(post.title) - .bind(post.summary) - .bind(&post.content) - .bind(post.last_update) - .bind(post.first_update) - .execute(&db_connection) - .await + .bind(Uuid::from_str(post["post_id"].as_str().unwrap()).unwrap()) + .bind(post["title"].as_str()) + .bind( + post.get("summary") + .and_then(|summary| summary.as_str()) + .unwrap_or(""), + ) + .bind(post["content"].as_str()) + .bind( + DateTime::parse_from_str( + post["last_update"] + .as_str() + .expect("Failed to parse `last_update`"), + GLOBAL_TIME_FORMAT, + ) + .expect("Failed to convert `last_update` to NaiveDateTime"), + ) + .bind( + DateTime::parse_from_str( + post["first_update"] + .as_str() + .expect("Failed to parse `first_update`"), + GLOBAL_TIME_FORMAT, + ) + .expect("Failed to convert `first_update` to NaiveDateTime"), + ) + .bind( + meta.get("sub_title") + .and_then(|sub_title| sub_title.as_str()) + .unwrap_or(""), + ) + .execute(&db_connection) + .await { Ok(_) => {} Err(e) => { @@ -114,7 +109,7 @@ ON CONFLICT (post_id) // table Tag if let Some(tags_string) = meta.get("tags").to_owned() { // parse tags string - let parsed: Value = serde_json::from_str(tags_string).unwrap(); + let parsed: Value = serde_json::from_str(tags_string.as_str().unwrap()).unwrap(); let string_array = parsed.as_array().unwrap(); let tags: Vec = string_array .iter() @@ -128,10 +123,10 @@ INSERT INTO Tag VALUES ($1, $2); "#, ) - .bind(post.post_id) - .bind(tag) - .execute(&db_connection) - .await + .bind(Uuid::from_str(post["post_id"].as_str().unwrap()).unwrap()) + .bind(tag) + .execute(&db_connection) + .await { Ok(_) => {} Err(e) => { diff --git a/backend/src/constants.rs b/backend/src/constants.rs new file mode 100644 index 0000000..1206c64 --- /dev/null +++ b/backend/src/constants.rs @@ -0,0 +1 @@ +pub const GLOBAL_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z"; diff --git a/backend/src/main.rs b/backend/src/main.rs index 347ea6f..d92c900 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -11,8 +11,9 @@ use crate::api::get::get_post_list::get_post_list; use crate::api::post::refresh::refresh; mod api; -mod model; +mod constants; mod markdown; +mod model; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/backend/src/markdown/meta.rs b/backend/src/markdown/meta.rs index 46e70b2..b2d7cac 100644 --- a/backend/src/markdown/meta.rs +++ b/backend/src/markdown/meta.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use regex::Regex; +/// return the meta info contained in the .md file front matter pub fn parse_meta(md_content: &str) -> HashMap { let mut fields: HashMap = HashMap::new(); let match_regex = Regex::new(r"(?s)---\n(.*\n)---\n").unwrap(); @@ -41,11 +42,14 @@ mod meta_test { title: Hello World! tags: [ "test", "hello_world" ] --- - "## + "##, ); dbg!(&parsed_meta); assert_eq!(parsed_meta.get("title"), Some(&"Hello World!".to_string())); - assert_eq!(parsed_meta.get("tags"), Some(&r#"[ "test", "hello_world" ]"#.to_string())); + assert_eq!( + parsed_meta.get("tags"), + Some(&r#"[ "test", "hello_world" ]"#.to_string()) + ); Ok(()) } } diff --git a/backend/src/model/mod.rs b/backend/src/model/mod.rs index fe50300..04f359d 100644 --- a/backend/src/model/mod.rs +++ b/backend/src/model/mod.rs @@ -1,18 +1,22 @@ -use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; -use uuid::Uuid; - pub mod post; -#[derive(Debug, Serialize, Deserialize, FromRow)] -pub struct Post { - pub post_id: Uuid, - pub title: String, - pub summary: String, - pub content: String, - pub last_update: NaiveDateTime, - pub first_update: NaiveDateTime, -} - +pub const POST_DB_MODEL: &str = r##" +CREATE TABLE IF NOT EXISTS Post +( + post_id UUID PRIMARY KEY, + title VARCHAR(255) NOT NULL, + summary TEXT, + sub_title TEXT, + content TEXT NOT NULL, + last_update TIMESTAMPTZ NOT NULL, + first_update TIMESTAMPTZ NOT NULL +); +"##; +pub const TAG_DB_MODEL: &str = r##" +CREATE TABLE IF NOT EXISTS Tag +( + post_id UUID NOT NULL, + tag VARCHAR(255) NOT NULL +); +"##; diff --git a/backend/src/model/post.rs b/backend/src/model/post.rs index 025a16f..0777b80 100644 --- a/backend/src/model/post.rs +++ b/backend/src/model/post.rs @@ -4,67 +4,81 @@ use std::process::Command; use std::str; use std::str::FromStr; -use chrono::{NaiveDate, NaiveDateTime, Utc}; +use chrono::{DateTime, Utc}; +use serde_json::{Map, Value}; use uuid::Uuid; +use crate::constants::GLOBAL_TIME_FORMAT; use crate::markdown::meta::parse_meta; -use crate::model::Post; -impl Post { - pub fn from_path(path: String) -> Result> { - let md_content = fs::read_to_string(&path)?; - let meta = parse_meta(&md_content); +pub fn from_path(path: String) -> Result, Box> { + let mut res = Map::new(); - let post_id = match meta.get("id") { - None => { Uuid::new_v4() } - Some(id) => { Uuid::from_str(id.as_str()).expect("Invalid UUID String") } - }; + let md_content = fs::read_to_string(&path)?; + res.insert("content".to_string(), Value::from(md_content.to_owned())); - let title = match meta.get("title") { - None => "Title Not Found".to_string(), - Some(title) => title.to_owned(), - }; - let summary = match meta.get("summary") { - None => "".to_string(), - Some(summary) => summary.to_owned(), - }; + let meta = parse_meta(&md_content); + let meta_map: Map = meta + .clone() + .into_iter() + .map(|(k, v)| (k, Value::from(v))) + .collect(); + res.insert("meta".to_string(), Value::from(meta_map)); - let git_output = Command::new("git") - .arg("--no-pager") - .arg("log") - .arg("-1") - .arg("--format=%cI") - .arg("--") - .current_dir(env::var("GIT_WORK_DIR")?) - .arg(&path) - .output() - .expect("Failed to execute git command"); + let post_id = match meta.get("id") { + None => Uuid::new_v4(), + Some(id) => Uuid::from_str(id.as_str()).expect("Invalid UUID String"), + }; + res.insert("post_id".to_string(), Value::from(post_id.to_string())); - let last_update = if git_output.status.success() { - NaiveDateTime::parse_from_str( - str::from_utf8(&git_output.stdout).unwrap().trim_end(), - "%Y-%m-%dT%H:%M:%S%z", - ) - .unwrap_or_else(|_| Utc::now().naive_utc()) - } else { - Utc::now().naive_utc() - }; + let title = match meta.get("title") { + None => "Title Not Found".to_string(), + Some(title) => title.to_owned(), + }; + res.insert("title".to_string(), Value::from(title)); - let first_update: NaiveDateTime = match meta.get("date") { - None => Utc::now().naive_utc(), - Some(date) => NaiveDateTime::from( - NaiveDate::parse_from_str(date, "%Y-%m-%d%z") - .expect("Failed to parse first_update time"), - ), - }; + let summary = match meta.get("summary") { + None => "".to_string(), + Some(summary) => summary.to_owned(), + }; + res.insert("summary".to_string(), Value::from(summary)); - Ok(Post { - post_id, - title, - summary, - content: md_content, - last_update, - first_update, - }) - } + let git_output = Command::new("git") + .arg("--no-pager") + .arg("log") + .arg("-1") + .arg("--format=%cI") + .arg("--") + .current_dir(env::var("GIT_WORK_DIR")?) + .arg(&path) + .output() + .expect("Failed to execute git command"); + let last_update: DateTime = if git_output.status.success() { + chrono::DateTime::parse_from_str( + str::from_utf8(&git_output.stdout).unwrap().trim_end(), + GLOBAL_TIME_FORMAT, + ) + .unwrap_or_else(|_| DateTime::from(Utc::now())) + .to_utc() + } else { + Utc::now().to_utc() + }; + res.insert( + "last_update".to_string(), + Value::from(last_update.format(GLOBAL_TIME_FORMAT).to_string()), + ); + + let first_update: DateTime = match meta.get("date") { + None => Utc::now().to_utc(), + Some(date) => DateTime::from( + DateTime::parse_from_str(format!("{}T000000", date).as_str(), "%Y-%m-%d%zT%H%M%S") + .expect("Failed to parse first_update time"), + ), + }; + res.insert( + "first_update".to_string(), + Value::from(first_update.format(GLOBAL_TIME_FORMAT).to_string()), + ); + + Ok(res) }