Indexes: share_targets.target_id, duplicate_groups.status, GIN on stacks members + duplicate candidates JSONB, composite (owner_user_id, created_at DESC) on assets. N+1 elimination: batch metadata loading via find_by_assets(ids) using WHERE asset_id = ANY($1), used in timeline + sidecar export. Visibility: cache find_targets_for_user per request via OnceCell, extract filter_visible helper to reduce duplication. Streaming: FileStoragePort.open_file() returns (DataStream, u64), LocalFileStorage uses ReaderStream instead of loading full file. serve_file/serve_derivative use Body::from_stream(). Unbounded queries: sidecar full_export/import batched in 500-row chunks instead of u32::MAX. find_unresolved paginated with limit/offset. list_duplicates API accepts pagination params.
1073 lines
29 KiB
Rust
1073 lines
29 KiB
Rust
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<HashMap<String, $entity>>,
|
|
}
|
|
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<HashMap<String, User>>,
|
|
}
|
|
|
|
impl InMemoryUserRepository {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
users: Mutex::new(HashMap::new()),
|
|
}
|
|
}
|
|
|
|
pub async fn all(&self) -> Vec<User> {
|
|
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<Option<User>, DomainError> {
|
|
Ok(self.users.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, 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<Option<User>, 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<Option<Asset>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, 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<Vec<Asset>, DomainError> {
|
|
let all: Vec<Asset> = 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<Vec<Asset>, 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<Option<Album>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Album>, 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<Option<Job>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_next_queued(&self) -> Result<Option<Job>, 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<Vec<Job>, 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<Vec<Job>, DomainError> {
|
|
let all: Vec<Job> = 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<u64, DomainError> {
|
|
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<Option<Role>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_name(&self, name: &str) -> Result<Option<Role>, DomainError> {
|
|
Ok(self
|
|
.data
|
|
.lock()
|
|
.await
|
|
.values()
|
|
.find(|r| r.name == name)
|
|
.cloned())
|
|
}
|
|
|
|
async fn find_defaults(&self) -> Result<Vec<Role>, 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<Option<Group>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<Group>, 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<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(())
|
|
}
|
|
}
|
|
|
|
in_memory_repo!(InMemoryLibraryPathRepository, LibraryPath);
|
|
|
|
#[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(())
|
|
}
|
|
}
|
|
|
|
in_memory_repo!(InMemoryIngestSessionRepository, IngestSession);
|
|
|
|
#[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(())
|
|
}
|
|
}
|
|
|
|
in_memory_repo!(InMemoryQuotaRepository, QuotaDefinition);
|
|
|
|
#[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_assets(
|
|
&self,
|
|
asset_ids: &[SystemId],
|
|
) -> Result<Vec<AssetMetadata>, 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<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(())
|
|
}
|
|
}
|
|
|
|
in_memory_repo!(InMemoryDuplicateRepository, DuplicateGroup);
|
|
|
|
#[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,
|
|
limit: u32,
|
|
offset: u32,
|
|
) -> Result<Vec<DuplicateGroup>, 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<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(())
|
|
}
|
|
}
|
|
|
|
in_memory_repo!(InMemorySidecarRepository, SidecarRecord);
|
|
|
|
#[async_trait]
|
|
impl SidecarRepository for InMemorySidecarRepository {
|
|
async fn find_by_asset(
|
|
&self,
|
|
asset_id: &SystemId,
|
|
) -> Result<Option<SidecarRecord>, DomainError> {
|
|
Ok(self.data.lock().await.get(&asset_id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_status(&self, status: SyncStatus) -> Result<Vec<SidecarRecord>, 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<Option<JobBatch>, 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<Option<Plugin>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_enabled(&self) -> Result<Vec<Plugin>, 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<Option<ProcessingPipeline>, DomainError> {
|
|
Ok(self.data.lock().await.get(&id.to_string()).cloned())
|
|
}
|
|
|
|
async fn find_by_trigger(&self, event: &str) -> Result<Vec<ProcessingPipeline>, 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<HashMap<String, Asset>>,
|
|
sessions: Mutex<HashMap<String, IngestSession>>,
|
|
quotas: Mutex<HashMap<String, QuotaDefinition>>,
|
|
ledger: Mutex<Vec<UsageLedgerEntry>>,
|
|
}
|
|
|
|
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<Option<QuotaDefinition>, 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<DateTimeStamp>,
|
|
) -> Result<u64, DomainError> {
|
|
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<Option<RefreshToken>, 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(())
|
|
}
|
|
}
|