diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 612bbe0..f8e7c9a 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -12,7 +12,7 @@ tracing = { version = "0.1.41", features = ["log"] } git2 = "0.20.0" chrono = { version = "0.4.39", features = ["serde"] } cron = "0.15.0" -sqlx = { version = "0.8.3", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono"] } +sqlx = { version = "0.8.3", features = ["runtime-tokio", "tls-native-tls", "postgres", "chrono", "uuid"] } serde_json = "1.0.133" serde = { version = "1.0.215", features = ["derive"] } tracing-subscriber = "0.3.18" diff --git a/backend/server/src/constants/time.rs b/backend/server/src/constants/time.rs index d7b29ac..c5c6e21 100644 --- a/backend/server/src/constants/time.rs +++ b/backend/server/src/constants/time.rs @@ -1,2 +1,3 @@ pub const DEFAULT_TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z"; -pub const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d%zT%H%M%S"; + +pub const DEFAULT_NAIVE_DATE_FORMAT: &str = "%Y-%m-%d"; diff --git a/backend/server/src/modules/blog/components/check_update/apply.rs b/backend/server/src/modules/blog/components/check_update/apply.rs index 68f5da8..5529f36 100644 --- a/backend/server/src/modules/blog/components/check_update/apply.rs +++ b/backend/server/src/modules/blog/components/check_update/apply.rs @@ -1,12 +1,23 @@ -use crate::modules::blog::components::check_update::changes::{ - Change, Create, Delete, Move, Update, +use crate::modules::blog::components::check_update::{ + changes::{Change, Create, Delete, Move, Update}, + error::Error, + utils::{parse_naive_date_str, parse_post_identifier}, }; +use macros::dev_consume; use futures::{ + future::{join_all, BoxFuture}, FutureExt, - future::{BoxFuture, join_all}, }; -use sqlx::PgPool; +use sqlx::{query, PgPool, Postgres, Row, Transaction}; +use tracing::error; + +const DELETE_POST_BY_ID: &str = include_str!("sql/delete_post_by_id.sql"); +const GET_TAG_ID: &str = include_str!("sql/get_tag_id.sql"); +const GET_CATEGORY_ID: &str = include_str!("sql/get_category_id.sql"); +const CREATE_RETURN_TAG_ID: &str = include_str!("sql/create_return_tag_id.sql"); +const CREATE_RETURN_CATEGORY_ID: &str = include_str!("sql/create_return_category_id.sql"); +const CREATE_POST: &str = include_str!("sql/create_post.sql"); pub async fn apply_deltas(db_conn: &PgPool, changes: Vec) { let handlers = changes @@ -23,17 +34,148 @@ pub async fn apply_deltas(db_conn: &PgPool, changes: Vec) { } async fn handle_delete(db: &PgPool, del: Delete) { - todo!() + if let Err(e) = query(DELETE_POST_BY_ID).bind(del.uuid).execute(db).await { + error!( + "failed to delete post: {}, {}", + del.uuid.to_string(), + e.to_string() + ) + } } async fn handle_move(db: &PgPool, mv: Move) { + let identifier = parse_post_identifier(&mv.to).unwrap(); + + dev_consume!(db, mv, identifier); todo!() } async fn handle_update(db: &PgPool, update: Update) { + dev_consume!(db, update); todo!() } async fn handle_create(db: &PgPool, create: Create) { - todo!() + let mut tx = db.begin().await.unwrap(); + + let identifier = parse_post_identifier(&create.path).unwrap(); + + let tags_json = create.payload.meta["tags"].clone(); + let tags: Vec<_> = if let Some(tags) = tags_json.as_array() { + tags.iter().filter_map(|tag| tag.as_str()).collect() + } else { + vec![] + }; + let mut tag_ids = vec![]; + for tag in tags { + match get_or_create_tag_id(&mut tx, tag).await { + Ok(tid) => tag_ids.push(tid), + Err(e) => { + error!("failed to fetch or create tag ({}): {}", tag, e); + return; + } + } + } + + let category_json = create.payload.meta["category"].clone(); + let category = category_json.as_str().unwrap_or("Uncategorized"); + let category_id = match get_or_create_category_id(&mut tx, category).await { + Ok(id) => id, + Err(e) => { + error!( + "failed to fetch category id for post {}: {}", + &identifier, e + ); + return; + } + }; + + let meta = create.payload.meta; + let post_id = create.uuid; + let post_identifier = &identifier; + let post_title = meta + .get("title") + .map(|t| t.as_str().unwrap()) + .unwrap_or("Untitled"); + let post_subtitle = meta + .get("sub_title") + .map(|t| t.as_str().unwrap()) + .unwrap_or(""); + let post_date_created = if let Some(Some(date)) = meta + .get("date") + .map(|d| parse_naive_date_str(d.as_str().unwrap_or(""))) + { + date + } else { + error!("failed to parse `date` for post: {}", post_identifier); + return; + }; + let post_date_updated = meta + .get("update") + .map(|d| parse_naive_date_str(d.as_str().unwrap_or(""))) + .unwrap_or(None); + let post_status = meta.get("status").map(|v| v.as_str()).unwrap_or(None); + let post_content = create.payload.content; + + if let Err(e) = query(CREATE_POST) + .bind(post_id) + .bind(post_identifier) + .bind(post_title) + .bind(post_subtitle) + .bind(post_date_created) + .bind(post_date_updated) + .bind(post_status) + .bind(post_content) + .bind(category_id) + .bind(tag_ids) + .execute(&mut *tx) + .await + { + error!("failed to insert post {}: {}", &identifier, e); + return; + } + + tx.commit().await.unwrap(); +} + +async fn get_or_create_tag_id( + db: &mut Transaction<'_, Postgres>, + tag_name: &str, +) -> Result { + get_create_id(db, tag_name, GET_TAG_ID, CREATE_RETURN_TAG_ID).await +} + +async fn get_or_create_category_id( + db: &mut Transaction<'_, Postgres>, + category_name: &str, +) -> Result { + get_create_id( + db, + category_name, + GET_CATEGORY_ID, + CREATE_RETURN_CATEGORY_ID, + ) + .await +} + +async fn get_create_id( + db: &mut Transaction<'_, Postgres>, + field_name: &str, + fetch_query: &str, + create_fetch_query: &str, +) -> Result { + if let Some(id) = query(fetch_query) + .bind(field_name) + .fetch_optional(&mut **db) + .await? + { + Ok(id.get("id")) + } else { + let id = query(create_fetch_query) + .bind(field_name) + .fetch_one(&mut **db) + .await? + .get("id"); + Ok(id) + } } diff --git a/backend/server/src/modules/blog/components/check_update/mod.rs b/backend/server/src/modules/blog/components/check_update/mod.rs index f1bf54f..9470fa3 100644 --- a/backend/server/src/modules/blog/components/check_update/mod.rs +++ b/backend/server/src/modules/blog/components/check_update/mod.rs @@ -6,15 +6,15 @@ use std::{ use crate::{ constants::config::general::{default_value, var_name}, - modules::blog::components::check_update::{pull::fetch_deltas, utils::expand_path}, + modules::blog::components::check_update::{ + apply::apply_deltas, pull::fetch_deltas, utils::expand_path, + }, scheduler::task_func::AsyncTaskFunc, }; use macros::panic_with_log; -use crate::modules::blog::components::check_update::apply::apply_deltas; use sqlx::PgPool; -use tracing::log::error; -use tracing::{Level, trace, warn}; +use tracing::{log::error, trace, warn, Level}; // mod error; // mod index; diff --git a/backend/server/src/modules/blog/components/check_update/sql/create_post.sql b/backend/server/src/modules/blog/components/check_update/sql/create_post.sql new file mode 100644 index 0000000..2cf2700 --- /dev/null +++ b/backend/server/src/modules/blog/components/check_update/sql/create_post.sql @@ -0,0 +1,7 @@ +INSERT INTO posts +(id, identifier, title, subtitle, date_created, date_updated, status, content, category) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9); + +INSERT INTO post_tag (post, tag) +SELECT $1, tag +FROM UNNEST($10) AS t(tag); diff --git a/backend/server/src/modules/blog/components/check_update/sql/create_return_category_id.sql b/backend/server/src/modules/blog/components/check_update/sql/create_return_category_id.sql new file mode 100644 index 0000000..7ff139a --- /dev/null +++ b/backend/server/src/modules/blog/components/check_update/sql/create_return_category_id.sql @@ -0,0 +1,3 @@ +INSERT INTO categories(name) +VALUES ($1) +RETURNING id; diff --git a/backend/server/src/modules/blog/components/check_update/sql/create_return_tag_id.sql b/backend/server/src/modules/blog/components/check_update/sql/create_return_tag_id.sql new file mode 100644 index 0000000..96e49a1 --- /dev/null +++ b/backend/server/src/modules/blog/components/check_update/sql/create_return_tag_id.sql @@ -0,0 +1,3 @@ +INSERT INTO tags(name) +VALUES ($1) +RETURNING id; diff --git a/backend/server/src/modules/blog/components/check_update/sql/get_category_id.sql b/backend/server/src/modules/blog/components/check_update/sql/get_category_id.sql new file mode 100644 index 0000000..a404aea --- /dev/null +++ b/backend/server/src/modules/blog/components/check_update/sql/get_category_id.sql @@ -0,0 +1,3 @@ +SELECT id +FROM categories +WHERE name = $1; \ No newline at end of file diff --git a/backend/server/src/modules/blog/components/check_update/sql/get_tag_id.sql b/backend/server/src/modules/blog/components/check_update/sql/get_tag_id.sql new file mode 100644 index 0000000..965effa --- /dev/null +++ b/backend/server/src/modules/blog/components/check_update/sql/get_tag_id.sql @@ -0,0 +1,3 @@ +SELECT id +FROM tags +WHERE name = $1; \ No newline at end of file diff --git a/backend/server/src/modules/blog/components/check_update/utils.rs b/backend/server/src/modules/blog/components/check_update/utils.rs index 5b67a19..0f764d6 100644 --- a/backend/server/src/modules/blog/components/check_update/utils.rs +++ b/backend/server/src/modules/blog/components/check_update/utils.rs @@ -1,10 +1,11 @@ +use std::path::Path; + use crate::constants::{ config::general::{default_value, var_name}, - time::DEFAULT_DATE_FORMAT, + time::DEFAULT_NAIVE_DATE_FORMAT, }; -// use crate::modules::blog::components::check_update::index::index_file; -use chrono::{DateTime, FixedOffset, ParseResult}; +use chrono::NaiveDate; use dirs::home_dir; use tracing::warn; @@ -30,6 +31,28 @@ pub fn expand_path(path: String) -> String { } } -pub fn parse_date(date_str: &str) -> ParseResult> { - DateTime::parse_from_str(format!("{}T120000", date_str).as_str(), DEFAULT_DATE_FORMAT) +pub fn parse_post_identifier(path: &Path) -> Option { + if let Some(comp) = path.components().nth_back(1) { + comp.as_os_str().to_str().map(|s| s.to_string()) + } else { + None + } +} + +pub fn parse_naive_date_str(date_str: &str) -> Option { + NaiveDate::parse_from_str(date_str, DEFAULT_NAIVE_DATE_FORMAT).ok() +} + +#[cfg(test)] +mod tests { + use crate::modules::blog::components::check_update::utils::parse_naive_date_str; + use chrono::NaiveDate; + + #[test] + fn test_parse_naive_date() { + assert_eq!( + Some(NaiveDate::from_ymd(2015, 1, 1)), + parse_naive_date_str("2015-1-01") + ); + } }