app: add storage commands/queries + missing in-memory test repos
This commit is contained in:
@@ -2,13 +2,21 @@ use std::collections::HashMap;
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::Mutex;
|
||||
use domain::{
|
||||
entities::{Album, Asset, Group, Job, JobStatus, Role, User},
|
||||
entities::{
|
||||
Album, Asset, AssetMetadata, AssetTag, DuplicateGroup, DuplicateStatus,
|
||||
Group, IngestSession, InviteCode, Job, JobStatus, LibraryPath,
|
||||
MetadataSource, QuotaDefinition, Role, ShareLink, ShareScope, ShareTarget,
|
||||
StorageVolume, Tag, UsageLedgerEntry, UsageType, User,
|
||||
},
|
||||
errors::DomainError,
|
||||
ports::{
|
||||
AlbumRepository, AssetRepository, GroupRepository,
|
||||
JobRepository, RoleRepository, UserRepository,
|
||||
AlbumRepository, AssetMetadataRepository, AssetRepository,
|
||||
DuplicateRepository, GroupRepository, IngestSessionRepository,
|
||||
JobRepository, LibraryPathRepository, QuotaRepository,
|
||||
RoleRepository, ShareRepository, StorageVolumeRepository,
|
||||
TagRepository, UsageLedgerRepository, UserRepository,
|
||||
},
|
||||
value_objects::{Checksum, Email, SystemId},
|
||||
value_objects::{Checksum, DateTimeStamp, Email, SystemId},
|
||||
};
|
||||
|
||||
// --- InMemoryUserRepository ---
|
||||
@@ -276,3 +284,435 @@ impl GroupRepository for InMemoryGroupRepository {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryStorageVolumeRepository ---
|
||||
|
||||
pub struct InMemoryStorageVolumeRepository {
|
||||
data: Mutex<HashMap<String, StorageVolume>>,
|
||||
}
|
||||
|
||||
impl InMemoryStorageVolumeRepository {
|
||||
pub fn new() -> Self {
|
||||
Self { data: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryStorageVolumeRepository {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageVolumeRepository for InMemoryStorageVolumeRepository {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<StorageVolume>, DomainError> {
|
||||
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
|
||||
async fn find_all(&self) -> Result<Vec<StorageVolume>, 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(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryLibraryPathRepository ---
|
||||
|
||||
pub struct InMemoryLibraryPathRepository {
|
||||
data: Mutex<HashMap<String, LibraryPath>>,
|
||||
}
|
||||
|
||||
impl InMemoryLibraryPathRepository {
|
||||
pub fn new() -> Self {
|
||||
Self { data: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryLibraryPathRepository {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LibraryPathRepository for InMemoryLibraryPathRepository {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<LibraryPath>, DomainError> {
|
||||
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_volume(&self, volume_id: &SystemId) -> Result<Vec<LibraryPath>, 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<Vec<LibraryPath>, 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(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryIngestSessionRepository ---
|
||||
|
||||
pub struct InMemoryIngestSessionRepository {
|
||||
data: Mutex<HashMap<String, IngestSession>>,
|
||||
}
|
||||
|
||||
impl InMemoryIngestSessionRepository {
|
||||
pub fn new() -> Self {
|
||||
Self { data: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryIngestSessionRepository {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl IngestSessionRepository for InMemoryIngestSessionRepository {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<IngestSession>, DomainError> {
|
||||
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<IngestSession>, 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(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryQuotaRepository ---
|
||||
|
||||
pub struct InMemoryQuotaRepository {
|
||||
data: Mutex<HashMap<String, QuotaDefinition>>,
|
||||
}
|
||||
|
||||
impl InMemoryQuotaRepository {
|
||||
pub fn new() -> Self {
|
||||
Self { data: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryQuotaRepository {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QuotaRepository for InMemoryQuotaRepository {
|
||||
async fn find_by_owner(&self, owner_id: &SystemId) -> Result<Option<QuotaDefinition>, 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<Vec<UsageLedgerEntry>>,
|
||||
}
|
||||
|
||||
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<DateTimeStamp>,
|
||||
) -> Result<u64, DomainError> {
|
||||
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<HashMap<String, AssetMetadata>>,
|
||||
}
|
||||
|
||||
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<Vec<AssetMetadata>, 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_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<Option<AssetMetadata>, 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<HashMap<String, ShareScope>>,
|
||||
targets: Mutex<HashMap<String, ShareTarget>>,
|
||||
links: Mutex<HashMap<String, ShareLink>>,
|
||||
invites: Mutex<HashMap<String, InviteCode>>,
|
||||
}
|
||||
|
||||
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<Option<ShareScope>, DomainError> {
|
||||
Ok(self.scopes.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
|
||||
async fn find_scopes_for_resource(&self, resource_id: &SystemId) -> Result<Vec<ShareScope>, 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<Vec<ShareTarget>, 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<Vec<ShareTarget>, 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<Option<ShareLink>, 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<Option<InviteCode>, DomainError> {
|
||||
Ok(self.invites.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryTagRepository ---
|
||||
|
||||
pub struct InMemoryTagRepository {
|
||||
tags: Mutex<HashMap<String, Tag>>,
|
||||
asset_tags: Mutex<HashMap<String, AssetTag>>,
|
||||
}
|
||||
|
||||
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<Option<Tag>, DomainError> {
|
||||
Ok(self.tags.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Tag>, DomainError> {
|
||||
Ok(self.tags.lock().await.values()
|
||||
.find(|t| t.name == name)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn find_tags_for_asset(&self, asset_id: &SystemId) -> Result<Vec<(Tag, AssetTag)>, 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(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- InMemoryDuplicateRepository ---
|
||||
|
||||
pub struct InMemoryDuplicateRepository {
|
||||
data: Mutex<HashMap<String, DuplicateGroup>>,
|
||||
}
|
||||
|
||||
impl InMemoryDuplicateRepository {
|
||||
pub fn new() -> Self {
|
||||
Self { data: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryDuplicateRepository {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DuplicateRepository for InMemoryDuplicateRepository {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<DuplicateGroup>, DomainError> {
|
||||
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
||||
}
|
||||
|
||||
async fn find_unresolved(&self) -> Result<Vec<DuplicateGroup>, DomainError> {
|
||||
Ok(self.data.lock().await.values()
|
||||
.filter(|g| g.status == DuplicateStatus::Unresolved)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DuplicateGroup>, 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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user