Files
k-photos/crates/application/src/testing/repositories.rs
Gabriel Kaszewski bcaf49cc81 perf: scale fixes for 1M+ photo libraries
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.
2026-05-31 22:40:25 +02:00

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(())
}
}