diff --git a/src/reader/sync.rs b/src/reader/sync.rs index 1ac0908..5b45560 100644 --- a/src/reader/sync.rs +++ b/src/reader/sync.rs @@ -12,7 +12,7 @@ use crate::{ }, }; use actix_web::{web, HttpRequest, HttpResponse, Responder}; -use chrono::{DateTime, Local, NaiveDateTime}; +use chrono::{DateTime, Duration, Local, NaiveDateTime}; use dateparser::parse; use diesel::prelude::*; use rss::Item; @@ -124,9 +124,22 @@ fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) { } } +// Items without a `created_ts` (e.g. ones inserted before this column existed, +// or whose feed didn't provide a publish date) are left alone — `lt` never +// matches NULL, so there's nothing to special-case here. +fn delete_old_feed_items(connection: &mut PgConnection) { + let cutoff = Local::now().naive_local() - Duration::days(14); + let result = diesel::delete(feed_item::table.filter(feed_item::created_ts.lt(cutoff))) + .execute(connection); + + log::info!("Deleted old feed items (older than 2 weeks): {:?}", result); +} + pub async fn sync(_req: HttpRequest, data: web::Json) -> impl Responder { let mut connection: diesel::PgConnection = establish_connection(); + delete_old_feed_items(&mut connection); + let req_user_id: i32 = data.user_id; let feeds: Vec = feed::table @@ -248,6 +261,76 @@ mod tests { ); } + #[actix_web::test] + async fn delete_old_feed_items_removes_items_older_than_two_weeks_but_keeps_recent_ones() { + let mut connection = establish_connection(); + let suffix = unique_suffix(); + + let new_user = NewUser::new( + format!("cleanup_test_{suffix}"), + format!("cleanup_{suffix}@example.test"), + "secret".to_string(), + ); + let user: User = diesel::insert_into(users::table) + .values(&new_user) + .get_result(&mut connection) + .unwrap(); + + let new_feed = NewFeed::new( + format!("Cleanup test feed {suffix}"), + format!("https://example.test/feed/{suffix}"), + user.id, + ); + let feed: Feed = diesel::insert_into(feed::table) + .values(&new_feed) + .get_result(&mut connection) + .unwrap(); + + let now = Local::now().naive_local(); + let old_item = NewFeedItem::new( + feed.id, + "old content".to_string(), + format!("Old article {suffix}"), + format!("https://example.test/article/old-{suffix}"), + Some(now - Duration::days(20)), + ); + let recent_item = NewFeedItem::new( + feed.id, + "recent content".to_string(), + format!("Recent article {suffix}"), + format!("https://example.test/article/recent-{suffix}"), + Some(now - Duration::days(1)), + ); + diesel::insert_into(feed_item::table) + .values(&old_item) + .execute(&mut connection) + .unwrap(); + let recent: FeedItem = diesel::insert_into(feed_item::table) + .values(&recent_item) + .get_result(&mut connection) + .unwrap(); + + delete_old_feed_items(&mut connection); + + let remaining: Vec = feed_item::table + .filter(feed_id.eq(feed.id)) + .load(&mut connection) + .unwrap(); + + assert_eq!(1, remaining.len(), "only the recent item should survive cleanup"); + assert_eq!(recent.id, remaining[0].id); + + diesel::delete(feed_item::table.filter(feed_id.eq(feed.id))) + .execute(&mut connection) + .ok(); + diesel::delete(feed::table.filter(feed::id.eq(feed.id))) + .execute(&mut connection) + .ok(); + diesel::delete(users::table.filter(users::id.eq(user.id))) + .execute(&mut connection) + .ok(); + } + #[actix_web::test] async fn create_feed_item_does_not_duplicate_existing_items() { let mut connection = establish_connection();