feat: atomic goal, watchlist, profile writes
This commit is contained in:
@@ -74,16 +74,17 @@ pub struct NoopGoalCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl crate::ports::GoalCommand for NoopGoalCommand {
|
||||
async fn save(&self, _: &crate::models::Goal) -> Result<(), DomainError> {
|
||||
async fn save(&self, _: &crate::models::Goal, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn update(&self, _: &crate::models::Goal) -> Result<(), DomainError> {
|
||||
async fn update(&self, _: &crate::models::Goal, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn delete(
|
||||
&self,
|
||||
_: &crate::value_objects::GoalId,
|
||||
_: &UserId,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -295,13 +295,18 @@ pub struct PanicWatchlistRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl WatchlistRepository for PanicWatchlistRepository {
|
||||
async fn add(&self, _: &WatchlistEntry) -> Result<(), DomainError> {
|
||||
async fn add(&self, _: &WatchlistEntry, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
panic!("PanicWatchlistRepository called")
|
||||
}
|
||||
async fn remove(&self, _: &UserId, _: &MovieId) -> Result<(), DomainError> {
|
||||
async fn remove(&self, _: &UserId, _: &MovieId, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
panic!("PanicWatchlistRepository called")
|
||||
}
|
||||
async fn remove_if_present(&self, _: &UserId, _: &MovieId) -> Result<bool, DomainError> {
|
||||
async fn remove_if_present(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: &MovieId,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<bool, DomainError> {
|
||||
panic!("PanicWatchlistRepository called")
|
||||
}
|
||||
async fn get_for_user(
|
||||
@@ -357,6 +362,7 @@ impl UserProfileFieldsRepository for PanicProfileFieldsRepo {
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: Vec<crate::models::ProfileField>,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
panic!("PanicProfileFieldsRepo called")
|
||||
}
|
||||
@@ -645,6 +651,7 @@ impl UserRepository for PanicUserRepository {
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: &crate::models::UserProfile,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
panic!("PanicUserRepository called")
|
||||
}
|
||||
@@ -675,16 +682,17 @@ pub struct PanicGoalCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl crate::ports::GoalCommand for PanicGoalCommand {
|
||||
async fn save(&self, _: &crate::models::Goal) -> Result<(), DomainError> {
|
||||
async fn save(&self, _: &crate::models::Goal, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
panic!("PanicGoalCommand called")
|
||||
}
|
||||
async fn update(&self, _: &crate::models::Goal) -> Result<(), DomainError> {
|
||||
async fn update(&self, _: &crate::models::Goal, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
panic!("PanicGoalCommand called")
|
||||
}
|
||||
async fn delete(
|
||||
&self,
|
||||
_: &crate::value_objects::GoalId,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
panic!("PanicGoalCommand called")
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ use crate::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::{
|
||||
Movie, Review, WatchlistEntry, WatchlistWithMovie,
|
||||
Goal, Movie, ProfileField, Review, WatchlistEntry, WatchlistWithMovie,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
ports::{EventPublisher, MovieCommand, ReviewRepository, WatchlistRepository},
|
||||
value_objects::{MovieId, ReviewId, UserId},
|
||||
ports::{
|
||||
EventPublisher, GoalCommand, MovieCommand, ReviewRepository, UserProfileFieldsRepository,
|
||||
WatchlistRepository,
|
||||
},
|
||||
value_objects::{GoalId, MovieId, ReviewId, UserId},
|
||||
};
|
||||
|
||||
fn refused() -> DomainError {
|
||||
@@ -54,13 +57,18 @@ pub struct RefusingWatchlistRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl WatchlistRepository for RefusingWatchlistRepository {
|
||||
async fn add(&self, _: &WatchlistEntry) -> Result<(), DomainError> {
|
||||
async fn add(&self, _: &WatchlistEntry, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn remove(&self, _: &UserId, _: &MovieId) -> Result<(), DomainError> {
|
||||
async fn remove(&self, _: &UserId, _: &MovieId, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn remove_if_present(&self, _: &UserId, _: &MovieId) -> Result<bool, DomainError> {
|
||||
async fn remove_if_present(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: &MovieId,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<bool, DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn get_for_user(
|
||||
@@ -75,6 +83,38 @@ impl WatchlistRepository for RefusingWatchlistRepository {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefusingGoalCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl GoalCommand for RefusingGoalCommand {
|
||||
async fn save(&self, _: &Goal, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn update(&self, _: &Goal, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn delete(&self, _: &GoalId, _: &UserId, _: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefusingProfileFieldsRepository;
|
||||
|
||||
#[async_trait]
|
||||
impl UserProfileFieldsRepository for RefusingProfileFieldsRepository {
|
||||
async fn get_fields(&self, _: &UserId) -> Result<Vec<ProfileField>, DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
async fn set_fields(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: Vec<ProfileField>,
|
||||
_: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
Err(refused())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefusingEventPublisher;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -2,6 +2,7 @@ use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::Goal,
|
||||
ports::{GoalCommand, GoalQuery},
|
||||
value_objects::{GoalId, UserId},
|
||||
@@ -12,7 +13,7 @@ use super::InMemoryStore;
|
||||
#[async_trait]
|
||||
impl GoalCommand for InMemoryStore {
|
||||
/// `goals` is UNIQUE(user_id, year).
|
||||
async fn save(&self, goal: &Goal) -> Result<(), DomainError> {
|
||||
async fn save(&self, goal: &Goal, events: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
let mut goals = self.goals.write().unwrap();
|
||||
let duplicate = goals.values().any(|g| {
|
||||
g.id() == goal.id() || (g.user_id() == goal.user_id() && g.year() == goal.year())
|
||||
@@ -23,14 +24,18 @@ impl GoalCommand for InMemoryStore {
|
||||
));
|
||||
}
|
||||
goals.insert(goal.id().value(), goal.clone());
|
||||
drop(goals);
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update(&self, goal: &Goal) -> Result<(), DomainError> {
|
||||
async fn update(&self, goal: &Goal, events: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
let mut store = self.goals.write().unwrap();
|
||||
match store.entry(goal.id().value()) {
|
||||
std::collections::hash_map::Entry::Occupied(mut e) => {
|
||||
e.insert(goal.clone());
|
||||
drop(store);
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(_) => {
|
||||
@@ -39,8 +44,14 @@ impl GoalCommand for InMemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &GoalId, _user_id: &UserId) -> Result<(), DomainError> {
|
||||
async fn delete(
|
||||
&self,
|
||||
id: &GoalId,
|
||||
_user_id: &UserId,
|
||||
events: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
self.goals.write().unwrap().remove(&id.value());
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +84,14 @@ impl UserRepository for InMemoryStore {
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
profile: &crate::models::UserProfile,
|
||||
events: &[crate::events::DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
let mut store = self.users.write().unwrap();
|
||||
if let Some(user) = store.get_mut(&user_id.value()) {
|
||||
user.update_profile(profile.clone());
|
||||
}
|
||||
drop(store);
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -104,11 +107,13 @@ impl UserProfileFieldsRepository for InMemoryStore {
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
fields: Vec<ProfileField>,
|
||||
events: &[crate::events::DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
self.profile_fields
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(user_id.value(), fields);
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::{
|
||||
RemoteWatchlistEntry, WatchlistEntry, WatchlistWithMovie,
|
||||
collections::{PageParams, Paginated},
|
||||
@@ -16,23 +17,30 @@ use super::InMemoryStore;
|
||||
#[async_trait]
|
||||
impl WatchlistRepository for InMemoryStore {
|
||||
/// `INSERT OR IGNORE` on UNIQUE(user_id, movie_id): adding twice is a no-op.
|
||||
async fn add(&self, entry: &WatchlistEntry) -> Result<(), DomainError> {
|
||||
async fn add(&self, entry: &WatchlistEntry, events: &[DomainEvent]) -> Result<(), DomainError> {
|
||||
let key = (entry.user_id.value(), entry.movie_id.value());
|
||||
self.watchlist
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(key)
|
||||
.or_insert_with(|| entry.clone());
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove(&self, user_id: &UserId, movie_id: &MovieId) -> Result<(), DomainError> {
|
||||
async fn remove(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
movie_id: &MovieId,
|
||||
events: &[DomainEvent],
|
||||
) -> Result<(), DomainError> {
|
||||
let key = (user_id.value(), movie_id.value());
|
||||
self.watchlist
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&key)
|
||||
.ok_or_else(|| DomainError::NotFound("watchlist entry".into()))?;
|
||||
self.record_events(events);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -40,9 +48,14 @@ impl WatchlistRepository for InMemoryStore {
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
movie_id: &MovieId,
|
||||
events: &[DomainEvent],
|
||||
) -> Result<bool, DomainError> {
|
||||
let key = (user_id.value(), movie_id.value());
|
||||
Ok(self.watchlist.write().unwrap().remove(&key).is_some())
|
||||
let removed = self.watchlist.write().unwrap().remove(&key).is_some();
|
||||
if removed {
|
||||
self.record_events(events);
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
async fn get_for_user(
|
||||
|
||||
Reference in New Issue
Block a user