categorization of feeds

This commit is contained in:
Greg Shuflin 2025-02-02 13:52:04 -08:00
parent e80aca5e60
commit ec2cdc98d5
3 changed files with 70 additions and 7 deletions

View File

@ -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;

View File

@ -1,6 +1,7 @@
use rocket::http::Status; 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 rocket_db_pools::Connection;
use sqlx::types::JsonValue;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@ -16,6 +17,7 @@ pub struct Feed {
user_id: Uuid, user_id: Uuid,
added_time: chrono::DateTime<chrono::Utc>, added_time: chrono::DateTime<chrono::Utc>,
last_checked_time: chrono::DateTime<chrono::Utc>, last_checked_time: chrono::DateTime<chrono::Utc>,
categorization: Vec<String>,
} }
impl Feed { impl Feed {
@ -28,6 +30,7 @@ impl Feed {
user_id, user_id,
added_time: now, added_time: now,
last_checked_time: now, last_checked_time: now,
categorization: Vec::new(),
} }
} }
} }
@ -35,7 +38,9 @@ impl Feed {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct NewFeed { pub struct NewFeed {
pub url: Url, // Only URL is required now pub url: Url,
#[serde(default)]
pub categorization: Vec<String>,
} }
#[post("/feeds", data = "<new_feed>")] #[post("/feeds", data = "<new_feed>")]
@ -74,11 +79,18 @@ pub async fn create_feed(
.map(|t| t.content) .map(|t| t.content)
.unwrap_or_else(|| new_feed.url.host_str().unwrap_or("Unknown").to_string()); .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( let query = sqlx::query(
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time) "INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time, categorization)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, json(?7))",
) )
.bind(feed.feed_id.to_string()) .bind(feed.feed_id.to_string())
.bind(&feed.name) .bind(&feed.name)
@ -86,6 +98,7 @@ pub async fn create_feed(
.bind(feed.user_id.to_string()) .bind(feed.user_id.to_string())
.bind(feed.added_time.to_rfc3339()) .bind(feed.added_time.to_rfc3339())
.bind(feed.last_checked_time.to_rfc3339()) .bind(feed.last_checked_time.to_rfc3339())
.bind(&categorization_json.to_string())
.execute(&mut **db) .execute(&mut **db)
.await; .await;
@ -118,7 +131,8 @@ pub async fn list_feeds(
url, url,
user_id as "user_id: String", user_id as "user_id: String",
added_time as "added_time: chrono::DateTime<chrono::Utc>", added_time as "added_time: chrono::DateTime<chrono::Utc>",
last_checked_time as "last_checked_time: chrono::DateTime<chrono::Utc>" last_checked_time as "last_checked_time: chrono::DateTime<chrono::Utc>",
categorization as "categorization: JsonValue"
FROM feeds FROM feeds
WHERE user_id = ? WHERE user_id = ?
ORDER BY added_time DESC ORDER BY added_time DESC
@ -136,7 +150,16 @@ pub async fn list_feeds(
.into_iter() .into_iter()
.map(|row| { .map(|row| {
// Parse URL from string // 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<String> = serde::json::from_value(row.categorization).map_err(|e| {
eprintln!("Failed to parse categorization: {}", e);
Status::InternalServerError
})?;
Ok(Feed { Ok(Feed {
feed_id: Uuid::parse_str(&row.feed_id).unwrap(), 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(), user_id: Uuid::parse_str(&row.user_id).unwrap(),
added_time: row.added_time, added_time: row.added_time,
last_checked_time: row.last_checked_time, last_checked_time: row.last_checked_time,
categorization,
}) })
}) })
.collect::<Result<Vec<_>, Status>>()?; .collect::<Result<Vec<_>, Status>>()?;

View File

@ -43,6 +43,8 @@
<div class="sidebar"> <div class="sidebar">
<h2>Feeds</h2> <h2>Feeds</h2>
<div id="feedList">
</div>
<ul> <ul>
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li><a href="/message">Message</a></li> <li><a href="/message">Message</a></li>