From ec2cdc98d543a47520186626963adbcc0dfc2f16 Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Sun, 2 Feb 2025 13:52:04 -0800 Subject: [PATCH] categorization of feeds --- ...20240320000003_add_feed_categorization.sql | 37 ++++++++++++++++++ src/feeds.rs | 38 +++++++++++++++---- templates/index.html.tera | 2 + 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 migrations/20240320000003_add_feed_categorization.sql diff --git a/migrations/20240320000003_add_feed_categorization.sql b/migrations/20240320000003_add_feed_categorization.sql new file mode 100644 index 0000000..51ad2db --- /dev/null +++ b/migrations/20240320000003_add_feed_categorization.sql @@ -0,0 +1,37 @@ +-- Add categorization column +ALTER TABLE feeds ADD COLUMN categorization JSON NOT NULL DEFAULT '[]'; + +-- Add CHECK constraint +CREATE TRIGGER validate_feed_categorization +AFTER INSERT ON feeds +BEGIN + SELECT CASE + WHEN NOT ( + json_valid(NEW.categorization) + AND json_type(NEW.categorization) = 'array' + AND ( + SELECT COUNT(*) + FROM json_each(NEW.categorization) + WHERE json_type(value) != 'text' + ) = 0 + ) + THEN RAISE(ROLLBACK, 'categorization must be an array of strings') + END; +END; + +CREATE TRIGGER validate_feed_categorization_update +AFTER UPDATE OF categorization ON feeds +BEGIN + SELECT CASE + WHEN NOT ( + json_valid(NEW.categorization) + AND json_type(NEW.categorization) = 'array' + AND ( + SELECT COUNT(*) + FROM json_each(NEW.categorization) + WHERE json_type(value) != 'text' + ) = 0 + ) + THEN RAISE(ROLLBACK, 'categorization must be an array of strings') + END; +END; diff --git a/src/feeds.rs b/src/feeds.rs index 1375aa5..29ea447 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -1,6 +1,7 @@ use rocket::http::Status; -use rocket::serde::{json::Json, Deserialize, Serialize}; +use rocket::serde::{self, json::Json, Deserialize, Serialize}; use rocket_db_pools::Connection; +use sqlx::types::JsonValue; use url::Url; use uuid::Uuid; @@ -16,6 +17,7 @@ pub struct Feed { user_id: Uuid, added_time: chrono::DateTime, last_checked_time: chrono::DateTime, + categorization: Vec, } impl Feed { @@ -28,6 +30,7 @@ impl Feed { user_id, added_time: now, last_checked_time: now, + categorization: Vec::new(), } } } @@ -35,7 +38,9 @@ impl Feed { #[derive(Debug, Deserialize)] #[serde(crate = "rocket::serde")] pub struct NewFeed { - pub url: Url, // Only URL is required now + pub url: Url, + #[serde(default)] + pub categorization: Vec, } #[post("/feeds", data = "")] @@ -74,11 +79,18 @@ pub async fn create_feed( .map(|t| t.content) .unwrap_or_else(|| new_feed.url.host_str().unwrap_or("Unknown").to_string()); - let feed = Feed::new(name, new_feed.url, user.user_id); + let mut feed = Feed::new(name, new_feed.url, user.user_id); + feed.categorization = new_feed.categorization; + + // Convert categorization to JSON value + let categorization_json = serde::json::to_value(&feed.categorization).map_err(|e| { + eprintln!("Failed to serialize categorization: {}", e); + Status::InternalServerError + })?; let query = sqlx::query( - "INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))", ) .bind(feed.feed_id.to_string()) .bind(&feed.name) @@ -86,6 +98,7 @@ pub async fn create_feed( .bind(feed.user_id.to_string()) .bind(feed.added_time.to_rfc3339()) .bind(feed.last_checked_time.to_rfc3339()) + .bind(&categorization_json.to_string()) .execute(&mut **db) .await; @@ -118,7 +131,8 @@ pub async fn list_feeds( url, user_id as "user_id: String", added_time as "added_time: chrono::DateTime", - last_checked_time as "last_checked_time: chrono::DateTime" + last_checked_time as "last_checked_time: chrono::DateTime", + categorization as "categorization: JsonValue" FROM feeds WHERE user_id = ? ORDER BY added_time DESC @@ -136,7 +150,16 @@ pub async fn list_feeds( .into_iter() .map(|row| { // Parse URL from string - let url = Url::parse(&row.url).map_err(|_| Status::InternalServerError)?; + let url = Url::parse(&row.url).map_err(|e| { + eprintln!("Failed to parse URL '{}': {}", row.url, e); + Status::InternalServerError + })?; + + // Parse categorization from JSON value + let categorization: Vec = serde::json::from_value(row.categorization).map_err(|e| { + eprintln!("Failed to parse categorization: {}", e); + Status::InternalServerError + })?; Ok(Feed { feed_id: Uuid::parse_str(&row.feed_id).unwrap(), @@ -145,6 +168,7 @@ pub async fn list_feeds( user_id: Uuid::parse_str(&row.user_id).unwrap(), added_time: row.added_time, last_checked_time: row.last_checked_time, + categorization, }) }) .collect::, Status>>()?; diff --git a/templates/index.html.tera b/templates/index.html.tera index 61f1d57..3d1d34e 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -43,6 +43,8 @@