From 73e5e2cd6e011b1bb63978693c95ad571d635df0 Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Sun, 2 Feb 2025 01:41:38 -0800 Subject: [PATCH] Feed schema --- ...0450a54ba4b29712b21cadb47971d38db012b.json | 50 ++++++ migrations/20240320000002_create_feeds.sql | 14 ++ src/feeds.rs | 155 ++++++++++++++++++ src/main.rs | 6 +- 4 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 .sqlx/query-f2c677b85d02ab2698c88013a5f0450a54ba4b29712b21cadb47971d38db012b.json create mode 100644 migrations/20240320000002_create_feeds.sql create mode 100644 src/feeds.rs diff --git a/.sqlx/query-f2c677b85d02ab2698c88013a5f0450a54ba4b29712b21cadb47971d38db012b.json b/.sqlx/query-f2c677b85d02ab2698c88013a5f0450a54ba4b29712b21cadb47971d38db012b.json new file mode 100644 index 0000000..0de70ae --- /dev/null +++ b/.sqlx/query-f2c677b85d02ab2698c88013a5f0450a54ba4b29712b21cadb47971d38db012b.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT \n feed_id as \"feed_id: String\",\n name,\n url,\n user_id as \"user_id: String\",\n added_time as \"added_time: chrono::DateTime\",\n last_checked_time as \"last_checked_time: chrono::DateTime\"\n FROM feeds\n WHERE user_id = ?\n ORDER BY added_time DESC\n ", + "describe": { + "columns": [ + { + "name": "feed_id: String", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "url", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "user_id: String", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "added_time: chrono::DateTime", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "last_checked_time: chrono::DateTime", + "ordinal": 5, + "type_info": "Datetime" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f2c677b85d02ab2698c88013a5f0450a54ba4b29712b21cadb47971d38db012b" +} diff --git a/migrations/20240320000002_create_feeds.sql b/migrations/20240320000002_create_feeds.sql new file mode 100644 index 0000000..07ef598 --- /dev/null +++ b/migrations/20240320000002_create_feeds.sql @@ -0,0 +1,14 @@ +-- Create feeds table +CREATE TABLE feeds ( + feed_id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL, + user_id TEXT NOT NULL, + added_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_checked_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Create indexes +CREATE INDEX feeds_user_id_idx ON feeds(user_id); +CREATE UNIQUE INDEX feeds_user_url_idx ON feeds(user_id, url); \ No newline at end of file diff --git a/src/feeds.rs b/src/feeds.rs new file mode 100644 index 0000000..7dda440 --- /dev/null +++ b/src/feeds.rs @@ -0,0 +1,155 @@ +use chrono; +use rocket::http::Status; +use rocket::serde::{json::Json, Deserialize, Serialize}; +use rocket_db_pools::Connection; +use uuid::Uuid; + +use crate::Db; +use crate::user::AuthenticatedUser; + +#[derive(Debug, Serialize)] +#[serde(crate = "rocket::serde")] +pub struct Feed { + feed_id: Uuid, + name: String, + url: String, + user_id: Uuid, + added_time: chrono::DateTime, + last_checked_time: chrono::DateTime, +} + +impl Feed { + pub fn new(name: String, url: String, user_id: Uuid) -> Self { + let now = chrono::Utc::now(); + Feed { + feed_id: Uuid::new_v4(), + name, + url, + user_id, + added_time: now, + last_checked_time: now, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct NewFeed { + pub name: String, + pub url: String, +} + +#[post("/feeds", data = "")] +pub async fn create_feed( + mut db: Connection, + new_feed: Json, + user: AuthenticatedUser, +) -> Result, Status> { + let new_feed = new_feed.into_inner(); + let feed = Feed::new(new_feed.name, new_feed.url, user.user_id); + + 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)" + ) + .bind(feed.feed_id.to_string()) + .bind(&feed.name) + .bind(&feed.url) + .bind(feed.user_id.to_string()) + .bind(feed.added_time.to_rfc3339()) + .bind(feed.last_checked_time.to_rfc3339()) + .execute(&mut **db) + .await; + + match query { + Ok(_) => Ok(Json(feed)), + Err(e) => { + eprintln!("Database error: {}", e); + match e { + sqlx::Error::Database(db_err) if db_err.is_unique_violation() => { + Err(Status::Conflict) + } + _ => Err(Status::InternalServerError), + } + } + } +} + +#[get("/feeds")] +pub async fn list_feeds( + mut db: Connection, + user: AuthenticatedUser, +) -> Result>, Status> { + let user_id = user.user_id.to_string(); + + let query = sqlx::query!( + r#" + SELECT + feed_id as "feed_id: String", + name, + url, + user_id as "user_id: String", + added_time as "added_time: chrono::DateTime", + last_checked_time as "last_checked_time: chrono::DateTime" + FROM feeds + WHERE user_id = ? + ORDER BY added_time DESC + "#, + user_id + ) + .fetch_all(&mut **db) + .await + .map_err(|e| { + eprintln!("Database error: {}", e); + Status::InternalServerError + })?; + + let feeds = query + .into_iter() + .map(|row| Feed { + feed_id: Uuid::parse_str(&row.feed_id).unwrap(), + name: row.name, + url: row.url, + user_id: Uuid::parse_str(&row.user_id).unwrap(), + added_time: row.added_time, + last_checked_time: row.last_checked_time, + }) + .collect(); + + Ok(Json(feeds)) +} + +#[delete("/feeds/")] +pub async fn delete_feed( + mut db: Connection, + feed_id: &str, + user: AuthenticatedUser, +) -> Status { + // Validate UUID format + let feed_uuid = match Uuid::parse_str(feed_id) { + Ok(uuid) => uuid, + Err(_) => return Status::BadRequest, + }; + + let query = sqlx::query( + "DELETE FROM feeds WHERE feed_id = ? AND user_id = ?" + ) + .bind(feed_uuid.to_string()) + .bind(user.user_id.to_string()) + .execute(&mut **db) + .await; + + match query { + Ok(result) => { + if result.rows_affected() > 0 { + Status::NoContent + } else { + Status::NotFound + } + } + Err(e) => { + eprintln!("Database error: {}", e); + Status::InternalServerError + } + } +} diff --git a/src/main.rs b/src/main.rs index e325798..e2cdb56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ extern crate rocket; mod user; +mod feeds; use rocket::fs::FileServer; use rocket::response::Redirect; @@ -58,7 +59,10 @@ fn rocket() -> _ { user::get_users, user::delete_user, user::login, - user::logout + user::logout, + feeds::create_feed, + feeds::list_feeds, + feeds::delete_feed, ], ) .mount("/static", FileServer::from("static"))