db, add channel and feed; user.get_feeds

Adds the primary functionality for channel and feed

Also adds a function to user to get the feeds associated with a user.
This commit is contained in:
Julia Lange 2026-02-05 13:59:34 -08:00
parent 99321e9a5d
commit 2dc4c7bc99
Signed by: Julia
SSH key fingerprint: SHA256:5DJcfxa5/fKCYn57dcabJa2vN2e6eT0pBerYi5SUbto
7 changed files with 1400 additions and 17 deletions

191
koucha/src/db/feed.rs Normal file
View file

@ -0,0 +1,191 @@
use crate::{
AdapterPool,
Result,
db::{
Channel,
ChannelKey,
FeedKey,
UserKey,
channel::UnparsedChannel,
},
};
pub struct UnparsedFeed {
pub id: i64,
pub title: String,
}
impl UnparsedFeed {
pub fn parse(self) -> Result<Feed> {
Ok(Feed {
key: FeedKey(self.id),
title: self.title,
})
}
}
pub struct Feed {
key: FeedKey,
title: String,
}
impl Feed {
pub fn key(&self) -> FeedKey { self.key }
pub fn title(&self) -> &str { &self.title }
pub async fn get(
pool: &AdapterPool, key: FeedKey
) -> Result<Self> {
sqlx::query_as!(
UnparsedFeed,
"SELECT id, title FROM feeds WHERE id = ?",
key.0
).fetch_one(&pool.0).await?.parse()
}
pub async fn create(
pool: &AdapterPool, user_key: UserKey, title: &str
) -> Result<Self> {
sqlx::query_as!(
UnparsedFeed,
"INSERT INTO feeds (user_id, title)
VALUES (?, ?)
RETURNING id as `id!`, title",
user_key.0, title
).fetch_one(&pool.0).await?.parse()
}
pub async fn update_title(
pool: &AdapterPool, key: FeedKey, new_title: &str
) -> Result<()> {
sqlx::query!(
"UPDATE feeds SET title = ? WHERE id = ?",
new_title, key.0
).execute(&pool.0).await?;
Ok(())
}
pub async fn add_channel(
&self, pool: &AdapterPool, channel_key: ChannelKey
) -> Result<()> {
sqlx::query!(
"INSERT INTO feed_channels (feed_id, channel_id)
VALUES (?, ?)",
self.key.0, channel_key.0
).execute(&pool.0).await?;
Ok(())
}
pub async fn get_channels(
&self, pool: &AdapterPool
) -> Result<Vec<Channel>> {
sqlx::query_as!(
UnparsedChannel,
"SELECT c.id as `id!`, c.title, c.link, c.description, c.last_fetched
FROM channels c
JOIN feed_channels fc on c.id = fc.channel_id
WHERE fc.feed_id = ?",
self.key.0
).fetch_all(&pool.0).await?.into_iter()
.map(UnparsedChannel::parse).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
db::User,
test_utils::{
FEED_TITLE, USERNAME, FEED1, FEED2,
setup_adapter,
setup_feed,
setup_channel,
}
};
use reqwest::Url;
#[test]
fn parse() {
const FID: i64 = 1;
let uf = UnparsedFeed {
id: FID,
title: FEED_TITLE.to_string(),
};
let f = uf.parse().unwrap();
assert_eq!(f.key.0, FID);
assert_eq!(f.title, FEED_TITLE);
}
#[tokio::test]
async fn get() {
let adapter = setup_adapter().await;
let pool = adapter.get_pool();
let feed = setup_feed(pool).await;
let gotten_feed = Feed::get(pool, feed.key).await.unwrap();
assert_eq!(feed.key, gotten_feed.key);
assert_eq!(feed.title, gotten_feed.title);
}
#[tokio::test]
async fn create() {
let adapter = setup_adapter().await;
let pool = adapter.get_pool();
let user = User::create(pool, USERNAME).await.unwrap();
let feed = Feed::create(pool, user.key(), FEED_TITLE).await.unwrap();
assert!(feed.key().0 > 0);
assert_eq!(feed.title(), FEED_TITLE);
}
#[tokio::test]
async fn update_title() {
const NEW_FEED_TITLE: &str = "My NEW feed!";
let adapter = setup_adapter().await;
let pool = adapter.get_pool();
let feed = setup_feed(pool).await;
Feed::update_title(pool, feed.key(), NEW_FEED_TITLE).await.unwrap();
let updated = Feed::get(pool, feed.key()).await.unwrap();
assert_eq!(updated.title(), NEW_FEED_TITLE);
}
#[tokio::test]
async fn add_channel() {
let adapter = setup_adapter().await;
let pool = adapter.get_pool();
let feed = setup_feed(pool).await;
let channel = setup_channel(pool).await;
feed.add_channel(pool, channel.key()).await.unwrap();
let channels = feed.get_channels(pool).await.unwrap();
let gotten_channel = &channels[0];
assert_eq!(gotten_channel.key().0, channel.key().0);
}
#[tokio::test]
async fn get_channels() {
let adapter = setup_adapter().await;
let pool = adapter.get_pool();
let feed = setup_feed(pool).await;
let url1 = Url::parse(FEED1).unwrap();
let channel1 = Channel::get_or_create(pool, url1).await.unwrap();
let url2 = Url::parse(FEED2).unwrap();
let channel2 = Channel::get_or_create(pool, url2).await.unwrap();
feed.add_channel(pool, channel1.key()).await.unwrap();
feed.add_channel(pool, channel2.key()).await.unwrap();
let channels = feed.get_channels(pool).await.unwrap();
assert_eq!(channels.len(), 2);
}
}