This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher,
|
||||
ImageStorage,
|
||||
ImportProfileRepository, ImportSessionRepository,
|
||||
MetadataClient, MovieProfileRepository, MovieRepository, PasswordHasher, PosterFetcherClient,
|
||||
PersonCommand, PersonQuery, SearchCommand, SearchPort,
|
||||
ReviewRepository, StatsRepository, UserProfileFieldsRepository, UserRepository,
|
||||
WatchlistRepository,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use domain::ports::RemoteWatchlistRepository;
|
||||
use domain::ports::{
|
||||
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, ImageStorage,
|
||||
ImportProfileRepository, ImportSessionRepository, MetadataClient, MovieProfileRepository,
|
||||
MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
|
||||
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository,
|
||||
UserRepository, WatchlistRepository,
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
|
||||
@@ -51,10 +51,12 @@ impl PeriodicJob for EnrichmentStalenessJob {
|
||||
}
|
||||
tracing::info!("enrichment scan: {} stale movies", stale.len());
|
||||
for (movie_id, external_metadata_id) in stale {
|
||||
let event = DomainEvent::MovieEnrichmentRequested { movie_id, external_metadata_id };
|
||||
let event = DomainEvent::MovieEnrichmentRequested {
|
||||
movie_id,
|
||||
external_metadata_id,
|
||||
};
|
||||
self.ctx.event_publisher.publish(&event).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
pub mod commands;
|
||||
pub mod jobs;
|
||||
pub mod worker;
|
||||
pub mod config;
|
||||
pub mod context;
|
||||
pub mod jobs;
|
||||
pub mod movie_discovery_indexer;
|
||||
pub mod movie_resolver;
|
||||
pub mod ports;
|
||||
pub mod queries;
|
||||
pub mod use_cases;
|
||||
pub mod movie_discovery_indexer;
|
||||
pub mod search_cleanup;
|
||||
pub mod use_cases;
|
||||
pub mod worker;
|
||||
|
||||
pub use movie_discovery_indexer::MovieDiscoveryIndexer;
|
||||
pub use search_cleanup::SearchCleanupHandler;
|
||||
|
||||
@@ -13,12 +13,18 @@ use domain::{
|
||||
/// Enrichment will later overwrite this with the full document (cast, genres, etc.).
|
||||
pub struct MovieDiscoveryIndexer {
|
||||
movie_repository: Arc<dyn MovieRepository>,
|
||||
search_command: Arc<dyn SearchCommand>,
|
||||
search_command: Arc<dyn SearchCommand>,
|
||||
}
|
||||
|
||||
impl MovieDiscoveryIndexer {
|
||||
pub fn new(movie_repository: Arc<dyn MovieRepository>, search_command: Arc<dyn SearchCommand>) -> Self {
|
||||
Self { movie_repository, search_command }
|
||||
pub fn new(
|
||||
movie_repository: Arc<dyn MovieRepository>,
|
||||
search_command: Arc<dyn SearchCommand>,
|
||||
) -> Self {
|
||||
Self {
|
||||
movie_repository,
|
||||
search_command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +41,8 @@ impl EventHandler for MovieDiscoveryIndexer {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Err(e) = self.search_command
|
||||
if let Err(e) = self
|
||||
.search_command
|
||||
.index(IndexableDocument::Movie {
|
||||
id: movie_id.clone(),
|
||||
movie: Box::new(movie),
|
||||
|
||||
@@ -49,9 +49,10 @@ impl MovieResolver {
|
||||
) -> Result<(Movie, bool), DomainError> {
|
||||
for strategy in &self.strategies {
|
||||
if strategy.can_handle(input)
|
||||
&& let Some(result) = strategy.resolve(input, deps).await? {
|
||||
return Ok(result);
|
||||
}
|
||||
&& let Some(result) = strategy.resolve(input, deps).await?
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(DomainError::ValidationError(
|
||||
"Manual title required if TMDB fetch fails or is omitted".into(),
|
||||
@@ -108,13 +109,17 @@ impl ResolutionStrategy for TitleSearchStrategy {
|
||||
let title = input.manual_title.as_deref().unwrap();
|
||||
let criteria = MetadataSearchCriteria::Title {
|
||||
title: MovieTitle::new(title.to_string())?,
|
||||
year: input.manual_release_year.map(ReleaseYear::new).transpose()?,
|
||||
year: input
|
||||
.manual_release_year
|
||||
.map(ReleaseYear::new)
|
||||
.transpose()?,
|
||||
};
|
||||
match deps.metadata_client.fetch_movie_metadata(&criteria).await {
|
||||
Ok(m) => {
|
||||
// Movie may already exist in DB under this external_metadata_id
|
||||
if let Some(ext_id) = m.external_metadata_id() {
|
||||
if let Some(existing) = deps.repository.get_movie_by_external_id(ext_id).await? {
|
||||
if let Some(existing) = deps.repository.get_movie_by_external_id(ext_id).await?
|
||||
{
|
||||
return Ok(Some((existing, false)));
|
||||
}
|
||||
}
|
||||
@@ -164,8 +169,13 @@ impl ResolutionStrategy for ManualMovieStrategy {
|
||||
if let Some(existing) = matched {
|
||||
Ok(Some((existing, false)))
|
||||
} else {
|
||||
let new_movie =
|
||||
Movie::new(None, title, release_year, input.manual_director.clone(), None);
|
||||
let new_movie = Movie::new(
|
||||
None,
|
||||
title,
|
||||
release_year,
|
||||
input.manual_director.clone(),
|
||||
None,
|
||||
);
|
||||
Ok(Some((new_movie, true)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,10 +224,8 @@ pub trait HtmlRenderer: Send + Sync {
|
||||
fn render_import_upload_page(&self, data: ImportUploadPageData) -> Result<String, String>;
|
||||
fn render_import_mapping_page(&self, data: ImportMappingPageData) -> Result<String, String>;
|
||||
fn render_import_preview_page(&self, data: ImportPreviewPageData) -> Result<String, String>;
|
||||
fn render_profile_settings_page(
|
||||
&self,
|
||||
data: ProfileSettingsPageData,
|
||||
) -> Result<String, String>;
|
||||
fn render_profile_settings_page(&self, data: ProfileSettingsPageData)
|
||||
-> Result<String, String>;
|
||||
fn render_blocked_domains_page(&self, data: BlockedDomainsPageData) -> Result<String, String>;
|
||||
fn render_blocked_actors_page(&self, data: BlockedActorsPageData) -> Result<String, String>;
|
||||
fn render_watchlist_page(&self, data: WatchlistPageData) -> Result<String, String>;
|
||||
|
||||
@@ -10,12 +10,15 @@ use domain::{
|
||||
|
||||
pub struct SearchCleanupHandler {
|
||||
search_command: Arc<dyn SearchCommand>,
|
||||
person_query: Arc<dyn PersonQuery>,
|
||||
person_query: Arc<dyn PersonQuery>,
|
||||
}
|
||||
|
||||
impl SearchCleanupHandler {
|
||||
pub fn new(search_command: Arc<dyn SearchCommand>, person_query: Arc<dyn PersonQuery>) -> Self {
|
||||
Self { search_command, person_query }
|
||||
Self {
|
||||
search_command,
|
||||
person_query,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +30,11 @@ impl EventHandler for SearchCleanupHandler {
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
if let Err(e) = self.search_command.remove(EntityType::Movie, &movie_id).await {
|
||||
if let Err(e) = self
|
||||
.search_command
|
||||
.remove(EntityType::Movie, &movie_id)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("search cleanup failed for movie {movie_id}: {e}");
|
||||
}
|
||||
|
||||
@@ -41,7 +48,9 @@ impl EventHandler for SearchCleanupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!("failed to list orphaned persons after movie {movie_id} deletion: {e}"),
|
||||
Err(e) => tracing::warn!(
|
||||
"failed to list orphaned persons after movie {movie_id} deletion: {e}"
|
||||
),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -52,8 +52,17 @@ impl MovieRepository for RepoWithExternalMovie {
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: &domain::models::MovieFilter) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError> { panic!("unexpected") }
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_: &domain::models::collections::PageParams,
|
||||
_: &domain::models::MovieFilter,
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError>
|
||||
{
|
||||
panic!("unexpected")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -74,9 +83,20 @@ impl MovieRepository for RepoEmpty {
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: &domain::models::MovieFilter) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError> { panic!("unexpected") }
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_: &domain::models::collections::PageParams,
|
||||
_: &domain::models::MovieFilter,
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError>
|
||||
{
|
||||
panic!("unexpected")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -97,9 +117,20 @@ impl MovieRepository for RepoWithTitleMatch {
|
||||
) -> Result<Vec<Movie>, DomainError> {
|
||||
Ok(vec![self.0.clone()])
|
||||
}
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> { panic!("unexpected") }
|
||||
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: &domain::models::MovieFilter) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError> { panic!("unexpected") }
|
||||
async fn upsert_movie(&self, _: &Movie) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!("unexpected")
|
||||
}
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_: &domain::models::collections::PageParams,
|
||||
_: &domain::models::MovieFilter,
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError>
|
||||
{
|
||||
panic!("unexpected")
|
||||
}
|
||||
}
|
||||
|
||||
struct MetaReturnsMovie(Movie);
|
||||
@@ -107,10 +138,7 @@ struct MetaErrors;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MetadataClient for MetaReturnsMovie {
|
||||
async fn fetch_movie_metadata(
|
||||
&self,
|
||||
_: &MetadataSearchCriteria,
|
||||
) -> Result<Movie, DomainError> {
|
||||
async fn fetch_movie_metadata(&self, _: &MetadataSearchCriteria) -> Result<Movie, DomainError> {
|
||||
Ok(self.0.clone())
|
||||
}
|
||||
async fn get_poster_url(
|
||||
@@ -123,10 +151,7 @@ impl MetadataClient for MetaReturnsMovie {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MetadataClient for MetaErrors {
|
||||
async fn fetch_movie_metadata(
|
||||
&self,
|
||||
_: &MetadataSearchCriteria,
|
||||
) -> Result<Movie, DomainError> {
|
||||
async fn fetch_movie_metadata(&self, _: &MetadataSearchCriteria) -> Result<Movie, DomainError> {
|
||||
Err(DomainError::InfrastructureError(
|
||||
"metadata unavailable".into(),
|
||||
))
|
||||
@@ -299,7 +324,9 @@ async fn resolver_returns_error_when_no_strategy_matches() {
|
||||
metadata_client: &meta,
|
||||
};
|
||||
let input = make_input(None, None, None);
|
||||
let result = MovieResolver::default_pipeline().resolve(&input, &deps).await;
|
||||
let result = MovieResolver::default_pipeline()
|
||||
.resolve(&input, &deps)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,13 @@ pub async fn execute(ctx: &AppContext, cmd: AddToWatchlistCommand) -> Result<(),
|
||||
if is_new {
|
||||
ctx.movie_repository.upsert_movie(&movie).await?;
|
||||
if let Some(ext_id) = movie.external_metadata_id() {
|
||||
let _ = ctx.event_publisher.publish(&DomainEvent::MovieDiscovered {
|
||||
movie_id: movie.id().clone(),
|
||||
external_metadata_id: ext_id.clone(),
|
||||
}).await;
|
||||
let _ = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::MovieDiscovered {
|
||||
movie_id: movie.id().clone(),
|
||||
external_metadata_id: ext_id.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
movie
|
||||
@@ -43,14 +46,17 @@ pub async fn execute(ctx: &AppContext, cmd: AddToWatchlistCommand) -> Result<(),
|
||||
let entry = WatchlistEntry::new(user_id.clone(), movie.id().clone());
|
||||
ctx.watchlist_repository.add(&entry).await?;
|
||||
|
||||
let _ = ctx.event_publisher.publish(&DomainEvent::WatchlistEntryAdded {
|
||||
user_id,
|
||||
movie_id: movie.id().clone(),
|
||||
movie_title: movie.title().value().to_string(),
|
||||
release_year: movie.release_year().value(),
|
||||
external_metadata_id: movie.external_metadata_id().map(|e| e.value().to_string()),
|
||||
added_at: entry.added_at,
|
||||
}).await;
|
||||
let _ = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::WatchlistEntryAdded {
|
||||
user_id,
|
||||
movie_id: movie.id().clone(),
|
||||
movie_title: movie.title().value().to_string(),
|
||||
release_year: movie.release_year().value(),
|
||||
external_metadata_id: movie.external_metadata_id().map(|e| e.value().to_string()),
|
||||
added_at: entry.added_at,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,17 +6,23 @@ use domain::{
|
||||
|
||||
use crate::{commands::ApplyImportMappingCommand, context::AppContext};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, cmd: ApplyImportMappingCommand) -> Result<Vec<AnnotatedRow>, DomainError> {
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
cmd: ApplyImportMappingCommand,
|
||||
) -> Result<Vec<AnnotatedRow>, DomainError> {
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||
let mappings = cmd.mappings;
|
||||
let mut session = ctx.import_session_repository
|
||||
let mut session = ctx
|
||||
.import_session_repository
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||
|
||||
// clone to avoid borrow conflict when mutating session fields below
|
||||
let parsed = session.parsed_file.clone()
|
||||
let parsed = session
|
||||
.parsed_file
|
||||
.clone()
|
||||
.ok_or_else(|| DomainError::ValidationError("session has no parsed file".into()))?;
|
||||
|
||||
let mut annotated = ctx.document_parser.apply_mapping(&parsed, &mappings);
|
||||
@@ -35,17 +41,31 @@ pub async fn execute(ctx: &AppContext, cmd: ApplyImportMappingCommand) -> Result
|
||||
Ok(annotated)
|
||||
}
|
||||
|
||||
async fn check_duplicate(ctx: &AppContext, row: &domain::models::ImportRow) -> Result<bool, DomainError> {
|
||||
async fn check_duplicate(
|
||||
ctx: &AppContext,
|
||||
row: &domain::models::ImportRow,
|
||||
) -> Result<bool, DomainError> {
|
||||
if let Some(ext_id) = &row.external_metadata_id
|
||||
&& let Ok(eid) = ExternalMetadataId::new(ext_id.clone())
|
||||
&& ctx.movie_repository.get_movie_by_external_id(&eid).await?.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
&& ctx
|
||||
.movie_repository
|
||||
.get_movie_by_external_id(&eid)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
if let (Some(title), Some(year_str)) = (&row.title, &row.release_year) {
|
||||
let title_vo = MovieTitle::new(title.clone());
|
||||
let year_vo = year_str.parse::<u16>().ok().and_then(|y| ReleaseYear::new(y).ok());
|
||||
let year_vo = year_str
|
||||
.parse::<u16>()
|
||||
.ok()
|
||||
.and_then(|y| ReleaseYear::new(y).ok());
|
||||
if let (Ok(t), Some(y)) = (title_vo, year_vo) {
|
||||
let matches = ctx.movie_repository.get_movies_by_title_and_year(&t, &y).await?;
|
||||
let matches = ctx
|
||||
.movie_repository
|
||||
.get_movies_by_title_and_year(&t, &y)
|
||||
.await?;
|
||||
if !matches.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use domain::{errors::DomainError, value_objects::{ImportProfileId, ImportSessionId, UserId}};
|
||||
use crate::{commands::ApplyImportProfileCommand, context::AppContext};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
value_objects::{ImportProfileId, ImportSessionId, UserId},
|
||||
};
|
||||
|
||||
/// Copies the profile's field_mappings onto the session. Caller must then invoke
|
||||
/// apply_import_mapping to regenerate row_results with the new mappings.
|
||||
@@ -8,11 +11,15 @@ pub async fn execute(ctx: &AppContext, cmd: ApplyImportProfileCommand) -> Result
|
||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||
let profile_id = ImportProfileId::from_uuid(cmd.profile_id);
|
||||
|
||||
let profile = ctx.import_profile_repository
|
||||
.get(&profile_id, &user_id).await?
|
||||
let profile = ctx
|
||||
.import_profile_repository
|
||||
.get(&profile_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import profile".into()))?;
|
||||
let mut session = ctx.import_session_repository
|
||||
.get(&session_id, &user_id).await?
|
||||
let mut session = ctx
|
||||
.import_session_repository
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||
session.field_mappings = Some(profile.field_mappings);
|
||||
session.row_results = None;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use domain::errors::DomainError;
|
||||
use crate::context::AppContext;
|
||||
use domain::errors::DomainError;
|
||||
|
||||
pub async fn execute(ctx: &AppContext) -> Result<u64, DomainError> {
|
||||
ctx.import_session_repository.delete_expired().await
|
||||
|
||||
@@ -13,11 +13,17 @@ pub struct CreateSessionResult {
|
||||
pub sample_rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn execute(ctx: &AppContext, cmd: CreateImportSessionCommand) -> Result<CreateSessionResult, DomainError> {
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
cmd: CreateImportSessionCommand,
|
||||
) -> Result<CreateSessionResult, DomainError> {
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
ctx.import_session_repository.delete_expired_for_user(&user_id).await?;
|
||||
ctx.import_session_repository
|
||||
.delete_expired_for_user(&user_id)
|
||||
.await?;
|
||||
|
||||
let parsed = ctx.document_parser
|
||||
let parsed = ctx
|
||||
.document_parser
|
||||
.parse(&cmd.bytes, cmd.format)
|
||||
.map_err(|e| DomainError::ValidationError(e.to_string()))?;
|
||||
|
||||
@@ -31,5 +37,9 @@ pub async fn execute(ctx: &AppContext, cmd: CreateImportSessionCommand) -> Resul
|
||||
|
||||
ctx.import_session_repository.create(&session).await?;
|
||||
|
||||
Ok(CreateSessionResult { session_id, columns, sample_rows })
|
||||
Ok(CreateSessionResult {
|
||||
session_id,
|
||||
columns,
|
||||
sample_rows,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use domain::{errors::DomainError, value_objects::{ImportProfileId, UserId}};
|
||||
use crate::{commands::DeleteImportProfileCommand, context::AppContext};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
value_objects::{ImportProfileId, UserId},
|
||||
};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, cmd: DeleteImportProfileCommand) -> Result<(), DomainError> {
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
let profile_id = ImportProfileId::from_uuid(cmd.profile_id);
|
||||
|
||||
ctx.import_profile_repository
|
||||
.get(&profile_id, &user_id).await?
|
||||
.get(&profile_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import profile".into()))?;
|
||||
ctx.import_profile_repository.delete(&profile_id).await
|
||||
}
|
||||
|
||||
@@ -38,8 +38,12 @@ pub async fn execute(ctx: &AppContext, cmd: DeleteReviewCommand) -> Result<(), D
|
||||
let poster_path = history.movie().poster_path().cloned();
|
||||
ctx.movie_repository.delete_movie(&movie_id).await?;
|
||||
// best-effort: movie is already deleted, so publish failure is non-fatal
|
||||
if let Err(e) = ctx.event_publisher
|
||||
.publish(&DomainEvent::MovieDeleted { movie_id, poster_path })
|
||||
if let Err(e) = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::MovieDeleted {
|
||||
movie_id,
|
||||
poster_path,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to publish MovieDeleted event: {e}");
|
||||
|
||||
@@ -3,9 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{
|
||||
CastMember, CrewMember, ExternalPersonId, IndexableDocument, Person, PersonId,
|
||||
},
|
||||
models::{CastMember, CrewMember, ExternalPersonId, IndexableDocument, Person, PersonId},
|
||||
ports::{MovieProfileRepository, MovieRepository, PersonCommand, SearchCommand},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ use domain::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{commands::{ExecuteImportCommand, LogReviewCommand, MovieInput}, context::AppContext, use_cases::log_review};
|
||||
use crate::{
|
||||
commands::{ExecuteImportCommand, LogReviewCommand, MovieInput},
|
||||
context::AppContext,
|
||||
use_cases::log_review,
|
||||
};
|
||||
|
||||
pub struct ImportSummary {
|
||||
pub imported: usize,
|
||||
@@ -14,11 +18,15 @@ pub struct ImportSummary {
|
||||
pub failed: Vec<(usize, String)>,
|
||||
}
|
||||
|
||||
pub async fn execute(ctx: &AppContext, cmd: ExecuteImportCommand) -> Result<ImportSummary, DomainError> {
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
cmd: ExecuteImportCommand,
|
||||
) -> Result<ImportSummary, DomainError> {
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||
let confirmed_indices = cmd.confirmed_indices;
|
||||
let session = ctx.import_session_repository
|
||||
let session = ctx
|
||||
.import_session_repository
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||
@@ -36,17 +44,13 @@ pub async fn execute(ctx: &AppContext, cmd: ExecuteImportCommand) -> Result<Impo
|
||||
continue;
|
||||
}
|
||||
match annotated.result {
|
||||
RowResult::Valid(row) => {
|
||||
match row_to_command(&row, user_id.value()) {
|
||||
Ok(cmd) => {
|
||||
match log_review::execute(ctx, cmd).await {
|
||||
Ok(_) => imported += 1,
|
||||
Err(e) => failed.push((idx, e.to_string())),
|
||||
}
|
||||
}
|
||||
Err(e) => failed.push((idx, e)),
|
||||
}
|
||||
}
|
||||
RowResult::Valid(row) => match row_to_command(&row, user_id.value()) {
|
||||
Ok(cmd) => match log_review::execute(ctx, cmd).await {
|
||||
Ok(_) => imported += 1,
|
||||
Err(e) => failed.push((idx, e.to_string())),
|
||||
},
|
||||
Err(e) => failed.push((idx, e)),
|
||||
},
|
||||
RowResult::Invalid { errors, .. } => {
|
||||
failed.push((idx, errors.join("; ")));
|
||||
}
|
||||
@@ -55,20 +59,27 @@ pub async fn execute(ctx: &AppContext, cmd: ExecuteImportCommand) -> Result<Impo
|
||||
|
||||
ctx.import_session_repository.delete(&session_id).await?;
|
||||
|
||||
Ok(ImportSummary { imported, skipped_duplicates, failed })
|
||||
Ok(ImportSummary {
|
||||
imported,
|
||||
skipped_duplicates,
|
||||
failed,
|
||||
})
|
||||
}
|
||||
|
||||
fn row_to_command(row: &ImportRow, user_id: Uuid) -> Result<LogReviewCommand, String> {
|
||||
let rating = row.rating.as_deref()
|
||||
let rating = row
|
||||
.rating
|
||||
.as_deref()
|
||||
.ok_or("missing rating")?
|
||||
.parse::<u8>()
|
||||
.map_err(|_| "rating is not a valid u8".to_string())?;
|
||||
|
||||
let watched_at_str = row.watched_at.as_deref().ok_or("missing watched_at")?;
|
||||
let watched_at = NaiveDateTime::parse_from_str(&format!("{} 00:00:00", watched_at_str), "%Y-%m-%d %H:%M:%S")
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%d %H:%M:%S"))
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%dT%H:%M:%S"))
|
||||
.map_err(|_| format!("cannot parse watched_at: '{}'", watched_at_str))?;
|
||||
let watched_at =
|
||||
NaiveDateTime::parse_from_str(&format!("{} 00:00:00", watched_at_str), "%Y-%m-%d %H:%M:%S")
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%d %H:%M:%S"))
|
||||
.or_else(|_| NaiveDateTime::parse_from_str(watched_at_str, "%Y-%m-%dT%H:%M:%S"))
|
||||
.map_err(|_| format!("cannot parse watched_at: '{}'", watched_at_str))?;
|
||||
|
||||
Ok(LogReviewCommand {
|
||||
user_id,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{FeedEntry, Movie, MovieProfile, MovieStats, collections::{PageParams, Paginated}},
|
||||
models::{
|
||||
FeedEntry, Movie, MovieProfile, MovieStats,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
value_objects::MovieId,
|
||||
};
|
||||
|
||||
@@ -32,5 +35,10 @@ pub async fn execute(
|
||||
ctx.movie_profile_repository.get_by_movie_id(&movie_id),
|
||||
)?;
|
||||
|
||||
Ok(MovieSocialPageResult { movie, stats, reviews, profile })
|
||||
Ok(MovieSocialPageResult {
|
||||
movie,
|
||||
stats,
|
||||
reviews,
|
||||
profile,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ use domain::{
|
||||
|
||||
use crate::{context::AppContext, queries::GetMoviesQuery};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, query: GetMoviesQuery) -> Result<Paginated<MovieSummary>, DomainError> {
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
query: GetMoviesQuery,
|
||||
) -> Result<Paginated<MovieSummary>, DomainError> {
|
||||
let page = PageParams::new(query.limit, query.offset)?;
|
||||
let filter = MovieFilter {
|
||||
search: query.search,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use domain::{errors::DomainError, models::{Person, PersonId}};
|
||||
use crate::context::AppContext;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{Person, PersonId},
|
||||
};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, id: PersonId) -> Result<Option<Person>, DomainError> {
|
||||
ctx.person_query.get_by_id(&id).await
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use domain::{errors::DomainError, models::{PersonCredits, PersonId}};
|
||||
use crate::context::AppContext;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{PersonCredits, PersonId},
|
||||
};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, id: PersonId) -> Result<PersonCredits, DomainError> {
|
||||
ctx.person_query.get_credits(&id).await
|
||||
|
||||
@@ -2,6 +2,11 @@ use domain::{errors::DomainError, models::RemoteWatchlistEntry};
|
||||
|
||||
use crate::context::AppContext;
|
||||
|
||||
pub async fn execute(ctx: &AppContext, uuid: uuid::Uuid) -> Result<Vec<RemoteWatchlistEntry>, DomainError> {
|
||||
ctx.remote_watchlist_repository.get_by_derived_uuid(uuid).await
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
uuid: uuid::Uuid,
|
||||
) -> Result<Vec<RemoteWatchlistEntry>, DomainError> {
|
||||
ctx.remote_watchlist_repository
|
||||
.get_by_derived_uuid(uuid)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{WatchlistWithMovie, collections::{PageParams, Paginated}},
|
||||
models::{
|
||||
WatchlistWithMovie,
|
||||
collections::{PageParams, Paginated},
|
||||
},
|
||||
value_objects::UserId,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use domain::{errors::DomainError, models::ImportProfile, value_objects::UserId};
|
||||
use crate::context::AppContext;
|
||||
use domain::{errors::DomainError, models::ImportProfile, value_objects::UserId};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
user_id: &UserId,
|
||||
) -> Result<Vec<ImportProfile>, DomainError> {
|
||||
ctx.import_profile_repository.list_for_user(user_id).await
|
||||
}
|
||||
|
||||
@@ -39,14 +39,18 @@ pub async fn execute(ctx: &AppContext, cmd: LogReviewCommand) -> Result<(), Doma
|
||||
let review = Review::new(movie.id().clone(), user_id, rating, comment, cmd.watched_at)?;
|
||||
let review_event = ctx.review_repository.save_review(&review).await?;
|
||||
|
||||
let was_on_watchlist = ctx.watchlist_repository
|
||||
let was_on_watchlist = ctx
|
||||
.watchlist_repository
|
||||
.remove_if_present(review.user_id(), review.movie_id())
|
||||
.await?;
|
||||
if was_on_watchlist {
|
||||
let _ = ctx.event_publisher.publish(&DomainEvent::WatchlistEntryRemoved {
|
||||
user_id: review.user_id().clone(),
|
||||
movie_id: review.movie_id().clone(),
|
||||
}).await;
|
||||
let _ = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::WatchlistEntryRemoved {
|
||||
user_id: review.user_id().clone(),
|
||||
movie_id: review.movie_id().clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
publish_events(ctx, &movie, is_new_movie, review_event).await?;
|
||||
@@ -60,14 +64,13 @@ async fn publish_events(
|
||||
is_new_movie: bool,
|
||||
review_event: DomainEvent,
|
||||
) -> Result<(), DomainError> {
|
||||
if is_new_movie
|
||||
&& let Some(ext_id) = movie.external_metadata_id() {
|
||||
let discovery_event = DomainEvent::MovieDiscovered {
|
||||
movie_id: movie.id().clone(),
|
||||
external_metadata_id: ext_id.clone(),
|
||||
};
|
||||
ctx.event_publisher.publish(&discovery_event).await?;
|
||||
}
|
||||
if is_new_movie && let Some(ext_id) = movie.external_metadata_id() {
|
||||
let discovery_event = DomainEvent::MovieDiscovered {
|
||||
movie_id: movie.id().clone(),
|
||||
external_metadata_id: ext_id.clone(),
|
||||
};
|
||||
ctx.event_publisher.publish(&discovery_event).await?;
|
||||
}
|
||||
|
||||
if let Some(ext_id) = movie.external_metadata_id() {
|
||||
let enrichment_event = DomainEvent::MovieEnrichmentRequested {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
pub mod enrich_movie;
|
||||
pub mod add_to_watchlist;
|
||||
pub mod apply_import_mapping;
|
||||
pub mod apply_import_profile;
|
||||
pub mod cleanup_expired_import_sessions;
|
||||
pub mod create_import_session;
|
||||
pub mod delete_import_profile;
|
||||
pub mod delete_review;
|
||||
pub mod enrich_movie;
|
||||
pub mod execute_import;
|
||||
pub mod list_import_profiles;
|
||||
pub mod save_import_profile;
|
||||
pub mod export_diary;
|
||||
pub mod get_activity_feed;
|
||||
pub mod get_diary;
|
||||
@@ -15,19 +14,20 @@ pub mod get_movie_social_page;
|
||||
pub mod get_movies;
|
||||
pub mod get_person;
|
||||
pub mod get_person_credits;
|
||||
#[cfg(feature = "federation")]
|
||||
pub mod get_remote_watchlist;
|
||||
pub mod get_review_history;
|
||||
pub mod get_user_profile;
|
||||
pub mod get_users;
|
||||
pub mod get_watchlist;
|
||||
pub mod is_on_watchlist;
|
||||
pub mod list_import_profiles;
|
||||
pub mod log_review;
|
||||
pub mod login;
|
||||
pub mod register;
|
||||
pub mod remove_from_watchlist;
|
||||
pub mod save_import_profile;
|
||||
pub mod search;
|
||||
pub mod sync_poster;
|
||||
pub mod update_profile;
|
||||
pub mod update_profile_fields;
|
||||
pub mod add_to_watchlist;
|
||||
pub mod remove_from_watchlist;
|
||||
pub mod get_watchlist;
|
||||
pub mod is_on_watchlist;
|
||||
#[cfg(feature = "federation")]
|
||||
pub mod get_remote_watchlist;
|
||||
|
||||
@@ -11,10 +11,10 @@ pub async fn execute(ctx: &AppContext, cmd: RemoveFromWatchlistCommand) -> Resul
|
||||
let movie_id = MovieId::from_uuid(cmd.movie_id);
|
||||
ctx.watchlist_repository.remove(&user_id, &movie_id).await?;
|
||||
|
||||
let _ = ctx.event_publisher.publish(&DomainEvent::WatchlistEntryRemoved {
|
||||
user_id,
|
||||
movie_id,
|
||||
}).await;
|
||||
let _ = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::WatchlistEntryRemoved { user_id, movie_id })
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
use chrono::Utc;
|
||||
use domain::{errors::DomainError, models::ImportProfile, value_objects::{ImportProfileId, ImportSessionId, UserId}};
|
||||
use crate::{commands::SaveImportProfileCommand, context::AppContext};
|
||||
use chrono::Utc;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::ImportProfile,
|
||||
value_objects::{ImportProfileId, ImportSessionId, UserId},
|
||||
};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, cmd: SaveImportProfileCommand) -> Result<ImportProfileId, DomainError> {
|
||||
pub async fn execute(
|
||||
ctx: &AppContext,
|
||||
cmd: SaveImportProfileCommand,
|
||||
) -> Result<ImportProfileId, DomainError> {
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
let session_id = ImportSessionId::from_uuid(cmd.session_id);
|
||||
|
||||
let session = ctx.import_session_repository
|
||||
.get(&session_id, &user_id).await?
|
||||
let session = ctx
|
||||
.import_session_repository
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("import session".into()))?;
|
||||
let mappings = session.field_mappings
|
||||
.ok_or_else(|| DomainError::ValidationError("no mapping applied to this session yet".into()))?;
|
||||
let profile = ImportProfile::new(ImportProfileId::generate(), user_id, cmd.name, mappings, Utc::now().naive_utc());
|
||||
let mappings = session.field_mappings.ok_or_else(|| {
|
||||
DomainError::ValidationError("no mapping applied to this session yet".into())
|
||||
})?;
|
||||
let profile = ImportProfile::new(
|
||||
ImportProfileId::generate(),
|
||||
user_id,
|
||||
cmd.name,
|
||||
mappings,
|
||||
Utc::now().naive_utc(),
|
||||
);
|
||||
let id = profile.id.clone();
|
||||
ctx.import_profile_repository.save(&profile).await?;
|
||||
Ok(id)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use domain::{errors::DomainError, models::{SearchQuery, SearchResults}};
|
||||
use crate::context::AppContext;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{SearchQuery, SearchResults},
|
||||
};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, query: SearchQuery) -> Result<SearchResults, DomainError> {
|
||||
ctx.search_port.search(&query).await
|
||||
|
||||
@@ -42,8 +42,11 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
|
||||
.store(&movie_id.value().to_string(), &image_bytes)
|
||||
.await?;
|
||||
|
||||
if let Err(e) = ctx.event_publisher
|
||||
.publish(&DomainEvent::ImageStored { key: stored_path.clone() })
|
||||
if let Err(e) = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::ImageStored {
|
||||
key: stored_path.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to emit ImageStored for {stored_path}: {e}");
|
||||
@@ -56,8 +59,14 @@ pub async fn execute(ctx: &AppContext, cmd: SyncPosterCommand) -> Result<(), Dom
|
||||
|
||||
// Refresh search index so the new poster_path is reflected immediately.
|
||||
// Fetch existing profile if available for a complete index document.
|
||||
let profile = ctx.movie_profile_repository.get_by_movie_id(&movie_id).await.ok().flatten();
|
||||
if let Err(e) = ctx.search_command
|
||||
let profile = ctx
|
||||
.movie_profile_repository
|
||||
.get_by_movie_id(&movie_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Err(e) = ctx
|
||||
.search_command
|
||||
.index(IndexableDocument::Movie {
|
||||
id: movie_id.clone(),
|
||||
movie: Box::new(movie),
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
value_objects::UserId,
|
||||
};
|
||||
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
||||
|
||||
use crate::{commands::UpdateProfileCommand, context::AppContext};
|
||||
|
||||
@@ -19,14 +15,22 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
let new_avatar_path = if let Some(bytes) = cmd.avatar_bytes {
|
||||
let content_type = cmd.avatar_content_type.as_deref().unwrap_or("");
|
||||
if !["image/jpeg", "image/png", "image/webp"].contains(&content_type) {
|
||||
return Err(DomainError::ValidationError("Avatar must be jpeg, png, or webp".into()));
|
||||
return Err(DomainError::ValidationError(
|
||||
"Avatar must be jpeg, png, or webp".into(),
|
||||
));
|
||||
}
|
||||
if let Some(old_path) = user.avatar_path() {
|
||||
let _ = ctx.image_storage.delete(old_path).await;
|
||||
}
|
||||
let key = format!("avatars/{}", user_id.value());
|
||||
let stored = ctx.image_storage.store(&key, &bytes).await?;
|
||||
if let Err(e) = ctx.event_publisher.publish(&DomainEvent::ImageStored { key: stored.clone() }).await {
|
||||
if let Err(e) = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::ImageStored {
|
||||
key: stored.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to emit ImageStored for avatar {stored}: {e}");
|
||||
}
|
||||
Some(stored)
|
||||
@@ -38,14 +42,22 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
let new_banner_path = if let Some(bytes) = cmd.banner_bytes {
|
||||
let content_type = cmd.banner_content_type.as_deref().unwrap_or("");
|
||||
if !["image/jpeg", "image/png", "image/webp"].contains(&content_type) {
|
||||
return Err(DomainError::ValidationError("Banner must be jpeg, png, or webp".into()));
|
||||
return Err(DomainError::ValidationError(
|
||||
"Banner must be jpeg, png, or webp".into(),
|
||||
));
|
||||
}
|
||||
if let Some(old_path) = user.banner_path() {
|
||||
let _ = ctx.image_storage.delete(old_path).await;
|
||||
}
|
||||
let key = format!("banners/{}", user_id.value());
|
||||
let stored = ctx.image_storage.store(&key, &bytes).await?;
|
||||
if let Err(e) = ctx.event_publisher.publish(&DomainEvent::ImageStored { key: stored.clone() }).await {
|
||||
if let Err(e) = ctx
|
||||
.event_publisher
|
||||
.publish(&DomainEvent::ImageStored {
|
||||
key: stored.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::warn!("failed to emit ImageStored for banner {stored}: {e}");
|
||||
}
|
||||
Some(stored)
|
||||
@@ -54,7 +66,13 @@ pub async fn execute(ctx: &AppContext, cmd: UpdateProfileCommand) -> Result<(),
|
||||
};
|
||||
|
||||
ctx.user_repository
|
||||
.update_profile(&user_id, cmd.bio, new_avatar_path, new_banner_path, cmd.also_known_as)
|
||||
.update_profile(
|
||||
&user_id,
|
||||
cmd.bio,
|
||||
new_avatar_path,
|
||||
new_banner_path,
|
||||
cmd.also_known_as,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.event_publisher
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
value_objects::UserId,
|
||||
};
|
||||
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
||||
|
||||
use crate::{commands::UpdateProfileFieldsCommand, context::AppContext};
|
||||
|
||||
pub async fn execute(ctx: &AppContext, cmd: UpdateProfileFieldsCommand) -> Result<(), DomainError> {
|
||||
if cmd.fields.len() > 4 {
|
||||
return Err(DomainError::ValidationError("Maximum 4 profile fields allowed".into()));
|
||||
return Err(DomainError::ValidationError(
|
||||
"Maximum 4 profile fields allowed".into(),
|
||||
));
|
||||
}
|
||||
let user_id = UserId::from_uuid(cmd.user_id);
|
||||
ctx.profile_fields_repository.set_fields(&user_id, cmd.fields).await?;
|
||||
ctx.event_publisher.publish(&DomainEvent::UserUpdated { user_id }).await?;
|
||||
ctx.profile_fields_repository
|
||||
.set_fields(&user_id, cmd.fields)
|
||||
.await?;
|
||||
ctx.event_publisher
|
||||
.publish(&DomainEvent::UserUpdated { user_id })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user