use async_trait::async_trait; use domain::{ entities::{ Album, Asset, AssetFilters, AssetMetadata, AssetTag, DuplicateGroup, DuplicateStatus, Group, IngestSession, InviteCode, Job, JobBatch, JobStatus, LibraryPath, MetadataSource, Plugin, ProcessingPipeline, QuotaDefinition, RefreshToken, Role, ShareLink, ShareScope, ShareTarget, SidecarRecord, StorageVolume, SyncStatus, Tag, UsageLedgerEntry, UsageType, User, }, errors::DomainError, ports::{ AlbumRepository, AssetMetadataRepository, AssetRepository, DuplicateRepository, GroupRepository, IngestSessionRepository, IngestTransaction, JobBatchRepository, JobRepository, LibraryPathRepository, PipelineRepository, PluginRepository, QuotaRepository, RefreshTokenRepository, RoleRepository, ShareRepository, SidecarRepository, StorageVolumeRepository, TagRepository, UsageLedgerRepository, UserRepository, }, value_objects::{Checksum, DateTimeStamp, Email, SystemId}, }; use std::collections::HashMap; use tokio::sync::Mutex; macro_rules! in_memory_repo { ($name:ident, $entity:ty) => { pub struct $name { data: Mutex>, } impl $name { pub fn new() -> Self { Self { data: Mutex::new(HashMap::new()), } } } impl Default for $name { fn default() -> Self { Self::new() } } }; } // --- InMemoryUserRepository --- pub struct InMemoryUserRepository { users: Mutex>, } impl InMemoryUserRepository { pub fn new() -> Self { Self { users: Mutex::new(HashMap::new()), } } pub async fn all(&self) -> Vec { self.users.lock().await.values().cloned().collect() } } impl Default for InMemoryUserRepository { fn default() -> Self { Self::new() } } #[async_trait] impl UserRepository for InMemoryUserRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.users.lock().await.get(&id.to_string()).cloned()) } async fn find_by_email(&self, email: &Email) -> Result, DomainError> { Ok(self .users .lock() .await .values() .find(|u| u.email.as_str() == email.as_str()) .cloned()) } async fn find_by_username(&self, username: &str) -> Result, DomainError> { Ok(self .users .lock() .await .values() .find(|u| u.username == username) .cloned()) } async fn save(&self, user: &User) -> Result<(), DomainError> { self.users .lock() .await .insert(user.id.to_string(), user.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.users.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryAssetRepository, Asset); #[async_trait] impl AssetRepository for InMemoryAssetRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_checksum(&self, checksum: &Checksum) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|a| &a.source_reference.checksum == checksum) .cloned() .collect()) } async fn find_by_owner( &self, owner_id: &SystemId, limit: u32, offset: u32, ) -> Result, DomainError> { let all: Vec = self .data .lock() .await .values() .filter(|a| &a.owner_user_id == owner_id) .cloned() .collect(); Ok(all .into_iter() .skip(offset as usize) .take(limit as usize) .collect()) } async fn search( &self, owner_id: &SystemId, _filters: &AssetFilters, limit: u32, offset: u32, ) -> Result, DomainError> { self.find_by_owner(owner_id, limit, offset).await } async fn save(&self, asset: &Asset) -> Result<(), DomainError> { self.data .lock() .await .insert(asset.asset_id.to_string(), asset.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryAlbumRepository, Album); #[async_trait] impl AlbumRepository for InMemoryAlbumRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_creator(&self, creator_id: &SystemId) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|a| &a.creator_user_id == creator_id) .cloned() .collect()) } async fn save(&self, album: &Album) -> Result<(), DomainError> { self.data .lock() .await .insert(album.album_id.to_string(), album.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryJobRepository, Job); #[async_trait] impl JobRepository for InMemoryJobRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_next_queued(&self) -> Result, DomainError> { let data = self.data.lock().await; Ok(data .values() .filter(|j| j.status == JobStatus::Queued) .max_by_key(|j| j.priority) .cloned()) } async fn find_by_batch(&self, batch_id: &SystemId) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|j| j.batch_id.as_ref() == Some(batch_id)) .cloned() .collect()) } async fn find_all( &self, _status: Option<&str>, limit: u32, offset: u32, ) -> Result, DomainError> { let all: Vec = self.data.lock().await.values().cloned().collect(); Ok(all .into_iter() .skip(offset as usize) .take(limit as usize) .collect()) } async fn count(&self, _status: Option<&str>) -> Result { Ok(self.data.lock().await.len() as u64) } async fn save(&self, job: &Job) -> Result<(), DomainError> { self.data .lock() .await .insert(job.job_id.to_string(), job.clone()); Ok(()) } } in_memory_repo!(InMemoryRoleRepository, Role); #[async_trait] impl RoleRepository for InMemoryRoleRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_name(&self, name: &str) -> Result, DomainError> { Ok(self .data .lock() .await .values() .find(|r| r.name == name) .cloned()) } async fn find_defaults(&self) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|r| r.is_system_default) .cloned() .collect()) } async fn save(&self, role: &Role) -> Result<(), DomainError> { self.data .lock() .await .insert(role.role_id.to_string(), role.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryGroupRepository, Group); #[async_trait] impl GroupRepository for InMemoryGroupRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_user(&self, user_id: &SystemId) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|g| g.is_member(user_id)) .cloned() .collect()) } async fn save(&self, group: &Group) -> Result<(), DomainError> { self.data .lock() .await .insert(group.group_id.to_string(), group.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryStorageVolumeRepository, StorageVolume); #[async_trait] impl StorageVolumeRepository for InMemoryStorageVolumeRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_all(&self) -> Result, DomainError> { Ok(self.data.lock().await.values().cloned().collect()) } async fn save(&self, volume: &StorageVolume) -> Result<(), DomainError> { self.data .lock() .await .insert(volume.volume_id.to_string(), volume.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryLibraryPathRepository, LibraryPath); #[async_trait] impl LibraryPathRepository for InMemoryLibraryPathRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_volume(&self, volume_id: &SystemId) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|p| &p.volume_id == volume_id) .cloned() .collect()) } async fn find_ingest_destinations( &self, owner_id: &SystemId, ) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|p| p.is_ingest_destination && p.designated_owner_id.as_ref() == Some(owner_id)) .cloned() .collect()) } async fn save(&self, path: &LibraryPath) -> Result<(), DomainError> { self.data .lock() .await .insert(path.path_id.to_string(), path.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } in_memory_repo!(InMemoryIngestSessionRepository, IngestSession); #[async_trait] impl IngestSessionRepository for InMemoryIngestSessionRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_user(&self, user_id: &SystemId) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|s| &s.uploader_user_id == user_id) .cloned() .collect()) } async fn save(&self, session: &IngestSession) -> Result<(), DomainError> { self.data .lock() .await .insert(session.session_id.to_string(), session.clone()); Ok(()) } } in_memory_repo!(InMemoryQuotaRepository, QuotaDefinition); #[async_trait] impl QuotaRepository for InMemoryQuotaRepository { async fn find_by_owner( &self, owner_id: &SystemId, ) -> Result, DomainError> { Ok(self .data .lock() .await .values() .find(|q| &q.owner_scope == owner_id) .cloned()) } async fn save(&self, quota: &QuotaDefinition) -> Result<(), DomainError> { self.data .lock() .await .insert(quota.quota_id.to_string(), quota.clone()); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } } // --- InMemoryUsageLedgerRepository --- pub struct InMemoryUsageLedgerRepository { entries: Mutex>, } impl InMemoryUsageLedgerRepository { pub fn new() -> Self { Self { entries: Mutex::new(Vec::new()), } } } impl Default for InMemoryUsageLedgerRepository { fn default() -> Self { Self::new() } } #[async_trait] impl UsageLedgerRepository for InMemoryUsageLedgerRepository { async fn record(&self, entry: &UsageLedgerEntry) -> Result<(), DomainError> { self.entries.lock().await.push(entry.clone()); Ok(()) } async fn sum_usage( &self, user_id: &SystemId, usage_type: UsageType, since: Option, ) -> Result { let entries = self.entries.lock().await; let total = entries .iter() .filter(|e| &e.user_id == user_id && e.usage_type == usage_type) .filter(|e| match &since { Some(ts) => &e.timestamp >= ts, None => true, }) .map(|e| e.consumed_amount) .sum(); Ok(total) } } // --- InMemoryAssetMetadataRepository --- pub struct InMemoryAssetMetadataRepository { data: Mutex>, } impl InMemoryAssetMetadataRepository { pub fn new() -> Self { Self { data: Mutex::new(HashMap::new()), } } fn key(asset_id: &SystemId, source: MetadataSource) -> String { format!("{asset_id}:{source:?}") } } impl Default for InMemoryAssetMetadataRepository { fn default() -> Self { Self::new() } } #[async_trait] impl AssetMetadataRepository for InMemoryAssetMetadataRepository { async fn find_by_asset(&self, asset_id: &SystemId) -> Result, DomainError> { let prefix = format!("{asset_id}:"); Ok(self .data .lock() .await .iter() .filter(|(k, _)| k.starts_with(&prefix)) .map(|(_, v)| v.clone()) .collect()) } async fn find_by_assets( &self, asset_ids: &[SystemId], ) -> Result, DomainError> { let data = self.data.lock().await; let mut results = Vec::new(); for id in asset_ids { let prefix = format!("{id}:"); results.extend( data.iter() .filter(|(k, _)| k.starts_with(&prefix)) .map(|(_, v)| v.clone()), ); } Ok(results) } async fn find_by_asset_and_source( &self, asset_id: &SystemId, source: MetadataSource, ) -> Result, DomainError> { Ok(self .data .lock() .await .get(&Self::key(asset_id, source)) .cloned()) } async fn save(&self, metadata: &AssetMetadata) -> Result<(), DomainError> { let key = Self::key(&metadata.asset_id, metadata.metadata_source); self.data.lock().await.insert(key, metadata.clone()); Ok(()) } async fn delete_by_asset_and_source( &self, asset_id: &SystemId, source: MetadataSource, ) -> Result<(), DomainError> { self.data.lock().await.remove(&Self::key(asset_id, source)); Ok(()) } } // --- InMemoryShareRepository --- pub struct InMemoryShareRepository { scopes: Mutex>, targets: Mutex>, links: Mutex>, invites: Mutex>, } impl InMemoryShareRepository { pub fn new() -> Self { Self { scopes: Mutex::new(HashMap::new()), targets: Mutex::new(HashMap::new()), links: Mutex::new(HashMap::new()), invites: Mutex::new(HashMap::new()), } } } impl Default for InMemoryShareRepository { fn default() -> Self { Self::new() } } #[async_trait] impl ShareRepository for InMemoryShareRepository { async fn save_scope(&self, scope: &ShareScope) -> Result<(), DomainError> { self.scopes .lock() .await .insert(scope.scope_id.to_string(), scope.clone()); Ok(()) } async fn find_scope_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.scopes.lock().await.get(&id.to_string()).cloned()) } async fn find_scopes_for_resource( &self, resource_id: &SystemId, ) -> Result, DomainError> { Ok(self .scopes .lock() .await .values() .filter(|s| &s.shareable_id == resource_id) .cloned() .collect()) } async fn delete_scope(&self, id: &SystemId) -> Result<(), DomainError> { self.scopes.lock().await.remove(&id.to_string()); Ok(()) } async fn save_target(&self, target: &ShareTarget) -> Result<(), DomainError> { let key = format!("{}:{}", target.scope_id, target.target_id); self.targets.lock().await.insert(key, target.clone()); Ok(()) } async fn find_targets_for_scope( &self, scope_id: &SystemId, ) -> Result, DomainError> { Ok(self .targets .lock() .await .values() .filter(|t| &t.scope_id == scope_id) .cloned() .collect()) } async fn find_targets_for_user( &self, user_id: &SystemId, ) -> Result, DomainError> { Ok(self .targets .lock() .await .values() .filter(|t| &t.target_id == user_id) .cloned() .collect()) } async fn save_link(&self, link: &ShareLink) -> Result<(), DomainError> { self.links .lock() .await .insert(link.token.clone(), link.clone()); Ok(()) } async fn find_link_by_token(&self, token: &str) -> Result, DomainError> { Ok(self.links.lock().await.get(token).cloned()) } async fn save_invite(&self, invite: &InviteCode) -> Result<(), DomainError> { self.invites .lock() .await .insert(invite.code_id.to_string(), invite.clone()); Ok(()) } async fn find_invite_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.invites.lock().await.get(&id.to_string()).cloned()) } } // --- InMemoryTagRepository --- pub struct InMemoryTagRepository { tags: Mutex>, asset_tags: Mutex>, } impl InMemoryTagRepository { pub fn new() -> Self { Self { tags: Mutex::new(HashMap::new()), asset_tags: Mutex::new(HashMap::new()), } } } impl Default for InMemoryTagRepository { fn default() -> Self { Self::new() } } #[async_trait] impl TagRepository for InMemoryTagRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.tags.lock().await.get(&id.to_string()).cloned()) } async fn find_by_name(&self, name: &str) -> Result, DomainError> { Ok(self .tags .lock() .await .values() .find(|t| t.name == name) .cloned()) } async fn find_tags_for_asset( &self, asset_id: &SystemId, ) -> Result, DomainError> { let asset_tags = self.asset_tags.lock().await; let tags = self.tags.lock().await; let mut result = Vec::new(); for at in asset_tags.values() { if &at.asset_id == asset_id && let Some(tag) = tags.get(&at.tag_id.to_string()) { result.push((tag.clone(), at.clone())); } } Ok(result) } async fn save_tag(&self, tag: &Tag) -> Result<(), DomainError> { self.tags .lock() .await .insert(tag.tag_id.to_string(), tag.clone()); Ok(()) } async fn save_asset_tag(&self, asset_tag: &AssetTag) -> Result<(), DomainError> { let key = format!("{}:{}", asset_tag.asset_id, asset_tag.tag_id); self.asset_tags.lock().await.insert(key, asset_tag.clone()); Ok(()) } async fn remove_asset_tag( &self, asset_id: &SystemId, tag_id: &SystemId, ) -> Result<(), DomainError> { let key = format!("{asset_id}:{tag_id}"); self.asset_tags.lock().await.remove(&key); Ok(()) } } in_memory_repo!(InMemoryDuplicateRepository, DuplicateGroup); #[async_trait] impl DuplicateRepository for InMemoryDuplicateRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_unresolved( &self, limit: u32, offset: u32, ) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|g| g.status == DuplicateStatus::Unresolved) .skip(offset as usize) .take(limit as usize) .cloned() .collect()) } async fn find_by_asset(&self, asset_id: &SystemId) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|g| g.candidates.iter().any(|c| &c.asset_id == asset_id)) .cloned() .collect()) } async fn save(&self, group: &DuplicateGroup) -> Result<(), DomainError> { self.data .lock() .await .insert(group.group_id.to_string(), group.clone()); Ok(()) } } in_memory_repo!(InMemorySidecarRepository, SidecarRecord); #[async_trait] impl SidecarRepository for InMemorySidecarRepository { async fn find_by_asset( &self, asset_id: &SystemId, ) -> Result, DomainError> { Ok(self.data.lock().await.get(&asset_id.to_string()).cloned()) } async fn find_by_status(&self, status: SyncStatus) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|r| r.sync_status == status) .cloned() .collect()) } async fn save(&self, record: &SidecarRecord) -> Result<(), DomainError> { self.data .lock() .await .insert(record.asset_id.to_string(), record.clone()); Ok(()) } async fn delete(&self, asset_id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&asset_id.to_string()); Ok(()) } } in_memory_repo!(InMemoryJobBatchRepository, JobBatch); #[async_trait] impl JobBatchRepository for InMemoryJobBatchRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn save(&self, batch: &JobBatch) -> Result<(), DomainError> { self.data .lock() .await .insert(batch.batch_id.to_string(), batch.clone()); Ok(()) } } in_memory_repo!(InMemoryPluginRepository, Plugin); #[async_trait] impl PluginRepository for InMemoryPluginRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_enabled(&self) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|p| p.is_enabled) .cloned() .collect()) } async fn save(&self, plugin: &Plugin) -> Result<(), DomainError> { self.data .lock() .await .insert(plugin.plugin_id.to_string(), plugin.clone()); Ok(()) } } in_memory_repo!(InMemoryPipelineRepository, ProcessingPipeline); #[async_trait] impl PipelineRepository for InMemoryPipelineRepository { async fn find_by_id(&self, id: &SystemId) -> Result, DomainError> { Ok(self.data.lock().await.get(&id.to_string()).cloned()) } async fn find_by_trigger(&self, event: &str) -> Result, DomainError> { Ok(self .data .lock() .await .values() .filter(|p| p.trigger_event == event) .cloned() .collect()) } async fn save(&self, pipeline: &ProcessingPipeline) -> Result<(), DomainError> { self.data .lock() .await .insert(pipeline.pipeline_id.to_string(), pipeline.clone()); Ok(()) } } // --- InMemoryIngestTransaction --- pub struct InMemoryIngestTransaction { assets: Mutex>, sessions: Mutex>, quotas: Mutex>, ledger: Mutex>, } impl InMemoryIngestTransaction { pub fn new() -> Self { Self { assets: Mutex::new(HashMap::new()), sessions: Mutex::new(HashMap::new()), quotas: Mutex::new(HashMap::new()), ledger: Mutex::new(Vec::new()), } } /// Pre-seed a quota for testing. pub async fn insert_quota(&self, quota: &QuotaDefinition) { self.quotas .lock() .await .insert(quota.owner_scope.to_string(), quota.clone()); } } impl Default for InMemoryIngestTransaction { fn default() -> Self { Self::new() } } #[async_trait] impl IngestTransaction for InMemoryIngestTransaction { async fn save_asset(&self, asset: &Asset) -> Result<(), DomainError> { self.assets .lock() .await .insert(asset.asset_id.to_string(), asset.clone()); Ok(()) } async fn save_session(&self, session: &IngestSession) -> Result<(), DomainError> { self.sessions .lock() .await .insert(session.session_id.to_string(), session.clone()); Ok(()) } async fn find_quota( &self, owner_id: &SystemId, ) -> Result, DomainError> { Ok(self .quotas .lock() .await .values() .find(|q| &q.owner_scope == owner_id) .cloned()) } async fn sum_usage( &self, user_id: &SystemId, usage_type: UsageType, since: Option, ) -> Result { let entries = self.ledger.lock().await; let total = entries .iter() .filter(|e| &e.user_id == user_id && e.usage_type == usage_type) .filter(|e| match &since { Some(ts) => &e.timestamp >= ts, None => true, }) .map(|e| e.consumed_amount) .sum(); Ok(total) } async fn record_usage(&self, entry: &UsageLedgerEntry) -> Result<(), DomainError> { self.ledger.lock().await.push(entry.clone()); Ok(()) } } in_memory_repo!(InMemoryRefreshTokenRepository, RefreshToken); #[async_trait] impl RefreshTokenRepository for InMemoryRefreshTokenRepository { async fn save(&self, token: &RefreshToken) -> Result<(), DomainError> { self.data .lock() .await .insert(token.token_id.to_string(), token.clone()); Ok(()) } async fn find_by_hash(&self, token_hash: &str) -> Result, DomainError> { Ok(self .data .lock() .await .values() .find(|t| t.token_hash == token_hash) .cloned()) } async fn delete_by_user(&self, user_id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.retain(|_, t| &t.user_id != user_id); Ok(()) } async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { self.data.lock().await.remove(&id.to_string()); Ok(()) } }