fix: NewRemoteReview param struct, atomic review+orphan movie delete
This commit is contained in:
@@ -22,7 +22,7 @@ pub use k_ap::{
|
||||
|
||||
pub use event_handler::ActivityPubEventHandler;
|
||||
pub use federation_ports::ApServiceAdapter;
|
||||
pub use remote_review_repository::{RemoteReviewRepository, RemoteReviewUpdate};
|
||||
pub use remote_review_repository::{NewRemoteReview, RemoteReviewRepository, RemoteReviewUpdate};
|
||||
pub use review_handler::ReviewObjectHandler;
|
||||
pub use social_adapter::CompositeSocialAdapter;
|
||||
pub use user_adapter::DomainUserRepoAdapter;
|
||||
|
||||
@@ -14,19 +14,20 @@ pub struct RemoteReviewUpdate<'a> {
|
||||
pub spoilers: bool,
|
||||
}
|
||||
|
||||
/// A remote review to persist, with the events that ride the same write.
|
||||
pub struct NewRemoteReview<'a> {
|
||||
pub review: &'a Review,
|
||||
pub ap_id: &'a str,
|
||||
pub movie_title: &'a str,
|
||||
pub release_year: u16,
|
||||
pub external_metadata_id: Option<&'a str>,
|
||||
pub poster_url: Option<&'a str>,
|
||||
pub events: &'a [DomainEvent],
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait RemoteReviewRepository: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn save_remote_review(
|
||||
&self,
|
||||
review: &Review,
|
||||
ap_id: &str,
|
||||
movie_title: &str,
|
||||
release_year: u16,
|
||||
external_metadata_id: Option<&str>,
|
||||
poster_url: Option<&str>,
|
||||
events: &[DomainEvent],
|
||||
) -> Result<()>;
|
||||
async fn save_remote_review(&self, new: NewRemoteReview<'_>) -> Result<()>;
|
||||
|
||||
async fn delete_remote_review(&self, ap_id: &str, actor_url: &str) -> Result<()>;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use k_ap::{AS_PUBLIC, ApContentReader, ApObjectHandler, LocalObject};
|
||||
use url::Url;
|
||||
|
||||
use crate::objects::{ReviewApInput, ReviewObject, review_to_ap_object};
|
||||
use crate::remote_review_repository::RemoteReviewRepository;
|
||||
use crate::remote_review_repository::{NewRemoteReview, RemoteReviewRepository};
|
||||
use crate::urls::{actor_url, review_url};
|
||||
|
||||
pub struct ReviewObjectHandler {
|
||||
@@ -174,15 +174,15 @@ impl ApObjectHandler for ReviewObjectHandler {
|
||||
.collect();
|
||||
|
||||
self.review_store
|
||||
.save_remote_review(
|
||||
&review,
|
||||
obj.id.as_str(),
|
||||
&obj.movie_title,
|
||||
obj.release_year,
|
||||
obj.external_metadata_id.as_deref(),
|
||||
obj.poster_url.as_deref(),
|
||||
&events,
|
||||
)
|
||||
.save_remote_review(NewRemoteReview {
|
||||
review: &review,
|
||||
ap_id: obj.id.as_str(),
|
||||
movie_title: &obj.movie_title,
|
||||
release_year: obj.release_year,
|
||||
external_metadata_id: obj.external_metadata_id.as_deref(),
|
||||
poster_url: obj.poster_url.as_deref(),
|
||||
events: &events,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
use activitypub::{RemoteReviewRepository, RemoteReviewUpdate};
|
||||
use activitypub::{NewRemoteReview, RemoteReviewRepository, RemoteReviewUpdate};
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
events::DomainEvent,
|
||||
models::{Review, ReviewSource},
|
||||
};
|
||||
use domain::models::ReviewSource;
|
||||
|
||||
use super::SqliteFederationRepository;
|
||||
use sqlite::persistence::{datetime_to_str, enqueue_events};
|
||||
|
||||
#[async_trait]
|
||||
impl RemoteReviewRepository for SqliteFederationRepository {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn save_remote_review(
|
||||
&self,
|
||||
review: &Review,
|
||||
ap_id: &str,
|
||||
movie_title: &str,
|
||||
release_year: u16,
|
||||
external_metadata_id: Option<&str>,
|
||||
poster_url: Option<&str>,
|
||||
events: &[DomainEvent],
|
||||
) -> Result<()> {
|
||||
async fn save_remote_review(&self, new: NewRemoteReview<'_>) -> Result<()> {
|
||||
let NewRemoteReview {
|
||||
review,
|
||||
ap_id,
|
||||
movie_title,
|
||||
release_year,
|
||||
external_metadata_id,
|
||||
poster_url,
|
||||
events,
|
||||
} = new;
|
||||
let actor_url = match review.source() {
|
||||
ReviewSource::Remote { actor_url } => actor_url.clone(),
|
||||
ReviewSource::Local => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! A remote review and the enrichment request it triggers commit together.
|
||||
|
||||
use activitypub::RemoteReviewRepository;
|
||||
use activitypub::{NewRemoteReview, RemoteReviewRepository};
|
||||
use domain::{
|
||||
events::DomainEvent,
|
||||
models::{PersistedReview, Review, ReviewSource},
|
||||
@@ -56,15 +56,15 @@ fn enrichment_requested(movie_id: &MovieId) -> DomainEvent {
|
||||
|
||||
async fn save(pool: &SqlitePool, review: &Review, events: &[DomainEvent]) -> anyhow::Result<()> {
|
||||
SqliteFederationRepository::new(pool.clone())
|
||||
.save_remote_review(
|
||||
.save_remote_review(NewRemoteReview {
|
||||
review,
|
||||
AP_ID,
|
||||
"The Matrix",
|
||||
1999,
|
||||
Some("tmdb:603"),
|
||||
None,
|
||||
ap_id: AP_ID,
|
||||
movie_title: "The Matrix",
|
||||
release_year: 1999,
|
||||
external_metadata_id: Some("tmdb:603"),
|
||||
poster_url: None,
|
||||
events,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use domain::{
|
||||
events::DomainEvent,
|
||||
models::{Review, ReviewSource},
|
||||
ports::ReviewRepository,
|
||||
value_objects::{ReviewId, UserId},
|
||||
value_objects::{MovieId, ReviewId, UserId},
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@@ -114,12 +114,15 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
tx.commit().await.map_err(crate::shared::map_sqlx_error)
|
||||
}
|
||||
|
||||
async fn delete_review(
|
||||
async fn delete_review_and_orphaned_movie(
|
||||
&self,
|
||||
review_id: &ReviewId,
|
||||
events: &[DomainEvent],
|
||||
movie_id: &MovieId,
|
||||
review_events: &[DomainEvent],
|
||||
orphan_events: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
let id = review_id.value().to_string();
|
||||
let movie = movie_id.value().to_string();
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
@@ -130,7 +133,19 @@ impl ReviewRepository for SqliteReviewRepository {
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(crate::shared::map_sqlx_error)?;
|
||||
enqueue_events(&mut tx, events).await?;
|
||||
enqueue_events(&mut tx, review_events).await?;
|
||||
let orphaned = sqlx::query(
|
||||
"DELETE FROM movies WHERE id = ? \
|
||||
AND NOT EXISTS (SELECT 1 FROM reviews WHERE movie_id = ?)",
|
||||
)
|
||||
.bind(&movie)
|
||||
.bind(&movie)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(crate::shared::map_sqlx_error)?;
|
||||
if orphaned.rows_affected() > 0 {
|
||||
enqueue_events(&mut tx, orphan_events).await?;
|
||||
}
|
||||
tx.commit().await.map_err(crate::shared::map_sqlx_error)
|
||||
}
|
||||
|
||||
|
||||
@@ -135,36 +135,85 @@ async fn a_failing_enqueue_after_the_review_update_rolls_the_update_back() {
|
||||
assert_eq!(count(&pool, "event_queue").await, 0);
|
||||
}
|
||||
|
||||
fn deleted(review: &domain::models::Review) -> DomainEvent {
|
||||
DomainEvent::ReviewDeleted {
|
||||
review_id: review.id().clone(),
|
||||
user_id: review.user_id().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn movie_deleted(review: &domain::models::Review) -> DomainEvent {
|
||||
DomainEvent::MovieDeleted {
|
||||
movie_id: review.movie_id().clone(),
|
||||
poster_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_review_writes_the_removal_and_its_event_in_one_commit() {
|
||||
async fn deleting_the_last_review_removes_the_movie_and_enqueues_both_events_in_one_commit() {
|
||||
let (pool, review) = seeded().await;
|
||||
let repo = SqliteReviewRepository::new(pool.clone());
|
||||
repo.save_review(&review, &[]).await.unwrap();
|
||||
let event = DomainEvent::ReviewDeleted {
|
||||
review_id: review.id().clone(),
|
||||
user_id: review.user_id().clone(),
|
||||
};
|
||||
|
||||
repo.delete_review(review.id(), &[event]).await.unwrap();
|
||||
repo.delete_review_and_orphaned_movie(
|
||||
review.id(),
|
||||
review.movie_id(),
|
||||
&[deleted(&review)],
|
||||
&[movie_deleted(&review)],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count(&pool, "reviews").await, 0);
|
||||
assert_eq!(count(&pool, "movies").await, 0);
|
||||
assert_eq!(count(&pool, "event_queue").await, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_review_while_another_remains_keeps_the_movie_and_enqueues_only_review_deleted()
|
||||
{
|
||||
let (pool, review) = seeded().await;
|
||||
let repo = SqliteReviewRepository::new(pool.clone());
|
||||
repo.save_review(&review, &[]).await.unwrap();
|
||||
let other = test_review(review.movie_id(), review.user_id(), 3);
|
||||
repo.save_review(&other, &[]).await.unwrap();
|
||||
|
||||
repo.delete_review_and_orphaned_movie(
|
||||
review.id(),
|
||||
review.movie_id(),
|
||||
&[deleted(&review)],
|
||||
&[movie_deleted(&review)],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count(&pool, "reviews").await, 1);
|
||||
assert_eq!(count(&pool, "movies").await, 1);
|
||||
assert_eq!(count(&pool, "event_queue").await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_failing_enqueue_after_the_review_delete_rolls_the_delete_back() {
|
||||
async fn a_failing_enqueue_rolls_back_the_review_and_movie_delete() {
|
||||
let (pool, review) = seeded().await;
|
||||
let repo = SqliteReviewRepository::new(pool.clone());
|
||||
repo.save_review(&review, &[]).await.unwrap();
|
||||
fail_event_inserts(&pool).await;
|
||||
let event = DomainEvent::ReviewDeleted {
|
||||
review_id: review.id().clone(),
|
||||
user_id: review.user_id().clone(),
|
||||
};
|
||||
|
||||
let result = repo.delete_review(review.id(), &[event]).await;
|
||||
let result = repo
|
||||
.delete_review_and_orphaned_movie(
|
||||
review.id(),
|
||||
review.movie_id(),
|
||||
&[deleted(&review)],
|
||||
&[movie_deleted(&review)],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(count(&pool, "reviews").await, 1, "delete rolled back");
|
||||
assert_eq!(
|
||||
count(&pool, "reviews").await,
|
||||
1,
|
||||
"review delete rolled back"
|
||||
);
|
||||
assert_eq!(count(&pool, "movies").await, 1, "movie delete rolled back");
|
||||
assert_eq!(count(&pool, "event_queue").await, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user