diff --git a/koucha/src/db.rs b/koucha/src/db.rs index 6914cfa..cea05ac 100644 --- a/koucha/src/db.rs +++ b/koucha/src/db.rs @@ -11,15 +11,23 @@ pub use channel::Channel; mod item; pub use item::Item; -macro_rules! define_id { - ($name:ident) => { - #[derive(PartialEq, Debug, Copy, Clone)] - pub struct $name(i64); - impl From<$name> for i64 { fn from(id: $name) -> Self { id.0 } } - }; +macro_rules! define_key { + ($name:ident) => { + #[derive(PartialEq, Debug, Copy, Clone)] + pub struct $name(i64); + }; + + ($name:ident, $($field:ident),* $(,)?) => { + #[derive(PartialEq, Debug, Copy, Clone)] + pub struct $name { + $($field: i64),* + } + }; } -define_id!(UserId); -define_id!(FeedId); -define_id!(ChannelId); -define_id!(ItemId); +define_key!(UserKey); +define_key!(FeedKey); +define_key!(FeedChannelKey, feed_id, channel_id); +define_key!(FeedItemKey, feed_id, item_id); +define_key!(ChannelKey); +define_key!(ItemKey); diff --git a/koucha/src/db/channel.rs b/koucha/src/db/channel.rs index 6df59fb..b79454f 100644 --- a/koucha/src/db/channel.rs +++ b/koucha/src/db/channel.rs @@ -4,7 +4,7 @@ use crate::{ Result, AdapterPool, db::{ - ChannelId, + ChannelKey, Item, FeedChannel, feed_channel::UnparsedFeedChannel, @@ -23,7 +23,7 @@ pub struct UnparsedChannel { impl UnparsedChannel { pub fn parse(self) -> Result { Ok(Channel { - id: ChannelId(self.id), + id: ChannelKey(self.id), title: self.title, link: Url::parse(&self.link)?, description: self.description, @@ -36,7 +36,7 @@ impl UnparsedChannel { } pub struct Channel { - id: ChannelId, + id: ChannelKey, title: String, link: Url, description: Option, @@ -44,7 +44,7 @@ pub struct Channel { } impl Channel { - pub fn id(&self) -> ChannelId { self.id } + pub fn id(&self) -> ChannelKey { self.id } pub fn title(&self) -> &str { &self.title } pub fn link(&self) -> &Url { &self.link } pub fn description(&self) -> Option<&str> { self.description.as_deref() } @@ -59,7 +59,7 @@ impl Channel { channels } - pub async fn get(pool: &AdapterPool, id: ChannelId) -> Result { + pub async fn get(pool: &AdapterPool, id: ChannelKey) -> Result { let channel: Result = sqlx::query_as!( UnparsedChannel, "SELECT id, title, link, description, last_fetched @@ -245,10 +245,7 @@ mod tests { let channel1 = Channel::get_or_create(pool, url_feed.clone()).await.unwrap(); let channel2 = Channel::get_or_create(pool, url_feed).await.unwrap(); - assert_eq!( - i64::from(channel1.id()), - i64::from(channel2.id()) - ); + assert_eq!(channel1.id(), channel2.id()); } #[tokio::test] diff --git a/koucha/src/db/feed.rs b/koucha/src/db/feed.rs index a769b1b..c3c95d1 100644 --- a/koucha/src/db/feed.rs +++ b/koucha/src/db/feed.rs @@ -3,10 +3,10 @@ use crate::{ Result, db::{ Channel, - ChannelId, - FeedId, + ChannelKey, + FeedKey, Item, - UserId, + UserKey, channel::UnparsedChannel, item::UnparsedItem, }, @@ -19,23 +19,23 @@ pub struct UnparsedFeed { impl UnparsedFeed { pub fn parse(self) -> Result { Ok(Feed { - id: FeedId(self.id), + id: FeedKey(self.id), title: self.title, }) } } pub struct Feed { - id: FeedId, + id: FeedKey, title: String, } impl Feed { - pub fn id(&self) -> FeedId { self.id } + pub fn id(&self) -> FeedKey { self.id } pub fn title(&self) -> &str { &self.title } pub async fn get( - pool: &AdapterPool, id: FeedId + pool: &AdapterPool, id: FeedKey ) -> Result { let feed = sqlx::query_as!( UnparsedFeed, @@ -47,22 +47,21 @@ impl Feed { } pub async fn create( - pool: &AdapterPool, user_id: UserId, title: &str + pool: &AdapterPool, user_id: UserKey, title: &str ) -> Result { - let int_id = i64::from(user_id); let new_feed = sqlx::query_as!( UnparsedFeed, "INSERT INTO feeds (user_id, title) VALUES (?, ?) RETURNING id as `id!`, title", - int_id, title + user_id.0, title ).fetch_one(&pool.0).await?.parse(); new_feed } pub async fn update_title( - pool: &AdapterPool, id: FeedId, new_title: &str + pool: &AdapterPool, id: FeedKey, new_title: &str ) -> Result<()> { sqlx::query!( "UPDATE feeds SET title = ? WHERE id = ?", @@ -73,13 +72,12 @@ impl Feed { } pub async fn add_channel( - &self, pool: &AdapterPool, channel_id: ChannelId + &self, pool: &AdapterPool, channel_id: ChannelKey ) -> Result<()> { - let int_channel_id = i64::from(channel_id); sqlx::query!( "INSERT INTO feed_channels (feed_id, channel_id) VALUES (?, ?)", - self.id.0, int_channel_id + self.id.0, channel_id.0 ).execute(&pool.0).await?; Ok(()) diff --git a/koucha/src/db/feed_channel.rs b/koucha/src/db/feed_channel.rs index f8a3870..d736c2d 100644 --- a/koucha/src/db/feed_channel.rs +++ b/koucha/src/db/feed_channel.rs @@ -3,9 +3,9 @@ use crate::{ AdapterPool, db::{ Channel, - ChannelId, + ChannelKey, Feed, - FeedId, + FeedKey, Item, }, score::{ @@ -26,8 +26,8 @@ pub struct UnparsedFeedChannel { impl UnparsedFeedChannel { pub fn parse(self) -> Result { Ok(FeedChannel { - channel_id: ChannelId(self.channel_id), - feed_id: FeedId(self.feed_id), + channel_id: ChannelKey(self.channel_id), + feed_id: FeedKey(self.feed_id), initial_score: Score::new(self.initial_score), gravity: Gravity::new(self.gravity), boost: Boost::new(self.boost), @@ -36,8 +36,8 @@ impl UnparsedFeedChannel { } pub struct FeedChannel { - channel_id: ChannelId, - feed_id: FeedId, + channel_id: ChannelKey, + feed_id: FeedKey, initial_score: Score, gravity: Gravity, boost: Boost, @@ -60,15 +60,14 @@ impl FeedChannel { async fn add_item_at( &self, pool: &AdapterPool, item: &Item, add_at: DateTime ) -> Result<()> { - let int_item_id = i64::from(item.id()); - let int_feed_id = i64::from(self.feed_id); + let int_item_id = item.id().0; let int_initial_score = i64::from(self.initial_score); let string_last_updated = add_at.to_rfc2822(); sqlx::query!( "INSERT OR IGNORE INTO feed_items (feed_id, item_id, score, last_updated) VALUES (?, ?, ?, ?)", - int_feed_id, int_item_id, int_initial_score, string_last_updated + self.feed_id.0, int_item_id, int_initial_score, string_last_updated ).execute(&pool.0).await?; Ok(()) } @@ -81,7 +80,7 @@ mod tests { use crate::{ db::{ Channel, - FeedId, + FeedKey, User }, test_utils::{ @@ -124,7 +123,7 @@ mod tests { let fc = FeedChannel { channel_id: channel.id(), - feed_id: FeedId(1), // Fake Feed + feed_id: FeedKey(1), // Fake Feed initial_score: Score::new(None), gravity: Gravity::new(None), boost: Boost::new(None), @@ -143,7 +142,7 @@ mod tests { let feed = Feed::create(pool, user.id(), "My Feed").await.unwrap(); let fc = FeedChannel { - channel_id: ChannelId(1), // Fake Channel + channel_id: ChannelKey(1), // Fake Channel feed_id: feed.id(), initial_score: Score::new(None), gravity: Gravity::new(None), diff --git a/koucha/src/db/feed_item.rs b/koucha/src/db/feed_item.rs index 052e8a3..ba1c29a 100644 --- a/koucha/src/db/feed_item.rs +++ b/koucha/src/db/feed_item.rs @@ -3,12 +3,11 @@ use crate::{ Result, AdapterPool, db::{ - ItemId, - FeedId, + FeedItemKey, }, score::{ TimedScore, - UnparsedTimedScore, + UnparsedTimedScore }, }; @@ -22,8 +21,10 @@ pub struct UnparsedFeedItem { impl UnparsedFeedItem { pub fn parse(self) -> Result { Ok(FeedItem { - item_id: ItemId(self.item_id), - feed_id: FeedId(self.feed_id), + key: FeedItemKey { + feed_id: self.feed_id, + item_id: self.item_id, + }, score: (UnparsedTimedScore { value: self.score, last_updated: DateTime::parse_from_rfc2822(&self.last_updated)? @@ -38,19 +39,41 @@ impl UnparsedFeedItem { } pub struct FeedItem { - item_id: ItemId, - feed_id: FeedId, + key: FeedItemKey, score: TimedScore, } impl FeedItem { - // async fn boost(pool: &AdapterPool, boost: Boost) -> Result<()> { - // self.boost_at(pool, boost, Utc::now()).await - // } - // - // async fn boost_at( - // &self, pool: &AdapterPool, boost: Boost, boost_at: DateTime - // ) -> Result<()> { - // - // } + pub fn key(&self) -> FeedItemKey { self.key } + pub fn score(&self) -> TimedScore { self.score.clone() } + + pub async fn archive( + &self, pool: &AdapterPool + ) -> Result<()> { + sqlx::query!( + "UPDATE feed_items + SET archived = ? + WHERE feed_id = ? AND item_id = ?", + true, self.key.feed_id, self.key.item_id + ).execute(&pool.0).await?; + + Ok(()) + } + + pub async fn update_score( + pool: &AdapterPool, key: FeedItemKey, new_score: TimedScore + ) -> Result<()> { + let unparsed_score = UnparsedTimedScore::unparse(new_score); + let last_updated = unparsed_score.last_updated.to_rfc2822(); + let boosted_at = unparsed_score.last_boosted.map(|lb| lb.to_rfc2822()); + + sqlx::query!( + "UPDATE feed_items + SET score = ?, last_updated = ?, boosted_at = ? + WHERE feed_id = ? AND item_id = ?", + unparsed_score.value, last_updated, boosted_at, key.feed_id, key.item_id + ).execute(&pool.0).await?; + + Ok(()) + } } diff --git a/koucha/src/db/item.rs b/koucha/src/db/item.rs index cb25331..e3486d4 100644 --- a/koucha/src/db/item.rs +++ b/koucha/src/db/item.rs @@ -2,8 +2,8 @@ use crate::{ Result, AdapterPool, db::{ - ChannelId, - ItemId, + ChannelKey, + ItemKey, }, fetch::FetchedRSSItem, }; @@ -22,8 +22,8 @@ pub struct UnparsedItem { impl UnparsedItem { pub fn parse(self) -> Result { Ok(Item { - id: ItemId(self.id), - channel_id: ChannelId(self.channel_id), + id: ItemKey(self.id), + channel_id: ChannelKey(self.channel_id), fetched_at: match self.fetched_at { Some(dt_str) => Some(DateTime::parse_from_rfc2822(&dt_str)? .with_timezone(&Utc)), @@ -38,8 +38,8 @@ impl UnparsedItem { } pub struct Item { - id: ItemId, - channel_id: ChannelId, + id: ItemKey, + channel_id: ChannelKey, #[allow(dead_code)] // TODO: Use for score decay calculations later fetched_at: Option>, @@ -49,16 +49,15 @@ pub struct Item { } impl Item { - pub fn id(&self) -> ItemId { self.id } - pub fn channel(&self) -> ChannelId { self.channel_id } + pub fn id(&self) -> ItemKey { self.id } + pub fn channel(&self) -> ChannelKey { self.channel_id } pub fn title(&self) -> Option<&str> { self.title.as_deref() } pub fn description(&self) -> Option<&str> { self.description.as_deref() } pub fn content(&self) -> Option<&str> { self.content.as_deref() } pub async fn get_or_create( - pool: &AdapterPool, from_channel: ChannelId, guid: &str + pool: &AdapterPool, from_channel: ChannelKey, guid: &str ) -> Result { - let int_channel_id = i64::from(from_channel); let item = sqlx::query_as!( UnparsedItem, @@ -67,7 +66,7 @@ impl Item { ON CONFLICT(channel_id, guid) DO UPDATE SET channel_id = channel_id RETURNING id as `id!`, channel_id, fetched_at, title, description, content", - int_channel_id, guid + from_channel.0, guid ).fetch_one(&pool.0).await?.parse(); item diff --git a/koucha/src/db/user.rs b/koucha/src/db/user.rs index 63f24d8..ef7ba51 100644 --- a/koucha/src/db/user.rs +++ b/koucha/src/db/user.rs @@ -2,7 +2,7 @@ use crate::{ Result, AdapterPool, db::{ - UserId, + UserKey, Feed, feed::UnparsedFeed, }, @@ -15,22 +15,22 @@ pub struct UnparsedUser { impl UnparsedUser { pub fn parse(self) -> Result { Ok(User { - id: UserId(self.id), + id: UserKey(self.id), name: self.name }) } } pub struct User { - id: UserId, + id: UserKey, name: String, } impl User { - pub fn id(&self) -> UserId { self.id } + pub fn id(&self) -> UserKey { self.id } pub fn name(&self) -> &str { &self.name } - pub async fn get(pool: &AdapterPool, id: UserId) -> Result { + pub async fn get(pool: &AdapterPool, id: UserKey) -> Result { let user = sqlx::query_as!( UnparsedUser, "SELECT id, name FROM users WHERE id = ?", @@ -58,13 +58,13 @@ impl User { ).fetch_one(&pool.0).await?; Ok(Self { - id: UserId(result.id), + id: UserKey(result.id), name: result.name, }) } pub async fn update_name( - pool: &AdapterPool, id: UserId, new_name: &str + pool: &AdapterPool, id: UserKey, new_name: &str ) -> Result<()> { sqlx::query!( "UPDATE users SET name = ? WHERE id = ?", diff --git a/koucha/src/score.rs b/koucha/src/score.rs index 6d6cba2..754ca17 100644 --- a/koucha/src/score.rs +++ b/koucha/src/score.rs @@ -80,8 +80,24 @@ impl UnparsedTimedScore { }), } } + + pub fn unparse(ts: TimedScore) -> Self { + match ts { + TimedScore::Decaying(ds) => UnparsedTimedScore { + value: ds.value.into(), + last_updated: ds.last_updated, + last_boosted: None, + }, + TimedScore::Boosted(bs) => UnparsedTimedScore { + value: bs.value.into(), + last_updated: bs.boosted_at, + last_boosted: Some(bs.boosted_at), + }, + } + } } +#[derive(Clone)] pub enum TimedScore { Decaying(DecayingScore), Boosted(BoostedScore), @@ -148,11 +164,13 @@ impl TimedScore { } } +#[derive(Clone)] pub struct BoostedScore { value: Score, boosted_at: DateTime, } +#[derive(Clone)] pub struct DecayingScore { value: Score, last_updated: DateTime,