domain: add Organization entities and ports (Album, Tag, Collection, FilterCriteria)

This commit is contained in:
2026-05-31 03:31:33 +02:00
parent ccb61b72d7
commit 1d3060fa12
16 changed files with 280 additions and 3 deletions

View File

@@ -0,0 +1,64 @@
use crate::errors::DomainError;
use crate::value_objects::{DateTimeStamp, SystemId};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AlbumEntry {
pub asset_id: SystemId,
pub sort_order: u32,
pub added_at: DateTimeStamp,
pub added_by_user_id: SystemId,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Album {
pub album_id: SystemId,
pub title: String,
pub description: String,
pub creator_user_id: SystemId,
pub cover_asset_id: Option<SystemId>,
pub start_date: Option<DateTimeStamp>,
pub end_date: Option<DateTimeStamp>,
pub entries: Vec<AlbumEntry>,
pub created_at: DateTimeStamp,
}
impl Album {
pub fn new(title: impl Into<String>, creator: SystemId) -> Self {
Self {
album_id: SystemId::new(),
title: title.into(),
description: String::new(),
creator_user_id: creator,
cover_asset_id: None,
start_date: None,
end_date: None,
entries: Vec::new(),
created_at: DateTimeStamp::now(),
}
}
pub fn add_asset(&mut self, asset_id: SystemId, added_by: SystemId) -> Result<(), DomainError> {
if self.entries.iter().any(|e| e.asset_id == asset_id) {
return Err(DomainError::Conflict("Asset already in album".to_string()));
}
let next_order = self.entries.iter().map(|e| e.sort_order).max().unwrap_or(0) + if self.entries.is_empty() { 0 } else { 1 };
self.entries.push(AlbumEntry {
asset_id,
sort_order: next_order,
added_at: DateTimeStamp::now(),
added_by_user_id: added_by,
});
Ok(())
}
pub fn remove_asset(&mut self, asset_id: &SystemId) -> Result<(), DomainError> {
let idx = self.entries.iter().position(|e| &e.asset_id == asset_id)
.ok_or_else(|| DomainError::NotFound("Asset not in album".to_string()))?;
self.entries.remove(idx);
Ok(())
}
pub fn asset_count(&self) -> usize {
self.entries.len()
}
}

View File

@@ -0,0 +1,22 @@
use crate::value_objects::{DateTimeStamp, FilterCriteria, SystemId};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Collection {
pub collection_id: SystemId,
pub name: String,
pub creator_user_id: SystemId,
pub criteria: FilterCriteria,
pub created_at: DateTimeStamp,
}
impl Collection {
pub fn new(name: impl Into<String>, creator: SystemId, criteria: FilterCriteria) -> Self {
Self {
collection_id: SystemId::new(),
name: name.into(),
creator_user_id: creator,
criteria,
created_at: DateTimeStamp::now(),
}
}
}

View File

@@ -25,3 +25,11 @@ pub use asset_metadata::{AssetMetadata, MetadataSource};
pub use asset_stack::{AssetStack, AssetStackMember, StackMemberRole, StackType};
pub use derivative_asset::{DerivativeAsset, DerivativeProfile, GenerationStatus};
pub use duplicate::{DetectionMethod, DuplicateCandidate, DuplicateGroup, DuplicateStatus};
mod album;
mod tag;
mod collection;
pub use album::{Album, AlbumEntry};
pub use tag::{AssetTag, Tag, TagSource};
pub use collection::Collection;

View File

@@ -0,0 +1,44 @@
use crate::value_objects::SystemId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TagSource {
UserManual,
AiGenerated,
ExifExtracted,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Tag {
pub tag_id: SystemId,
pub name: String,
pub tag_source: TagSource,
}
impl Tag {
pub fn new_manual(name: impl Into<String>) -> Self {
Self {
tag_id: SystemId::new(),
name: name.into(),
tag_source: TagSource::UserManual,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AssetTag {
pub asset_id: SystemId,
pub tag_id: SystemId,
pub tagged_by_user_id: Option<SystemId>,
pub confidence: f64,
}
impl AssetTag {
pub fn new_manual(asset_id: SystemId, tag_id: SystemId, user_id: SystemId) -> Self {
Self {
asset_id,
tag_id,
tagged_by_user_id: Some(user_id),
confidence: 1.0,
}
}
}

View File

@@ -0,0 +1,10 @@
use async_trait::async_trait;
use crate::{entities::Album, errors::DomainError, value_objects::SystemId};
#[async_trait]
pub trait AlbumRepository: Send + Sync {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Album>, DomainError>;
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Album>, DomainError>;
async fn save(&self, album: &Album) -> Result<(), DomainError>;
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
}

View File

@@ -0,0 +1,10 @@
use async_trait::async_trait;
use crate::{entities::Collection, errors::DomainError, value_objects::SystemId};
#[async_trait]
pub trait CollectionRepository: Send + Sync {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Collection>, DomainError>;
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Collection>, DomainError>;
async fn save(&self, collection: &Collection) -> Result<(), DomainError>;
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
}

View File

@@ -31,3 +31,11 @@ pub use asset_metadata_repo::AssetMetadataRepository;
pub use asset_stack_repo::AssetStackRepository;
pub use derivative_repo::DerivativeRepository;
pub use duplicate_repo::DuplicateRepository;
mod album_repo;
mod tag_repo;
mod collection_repo;
pub use album_repo::AlbumRepository;
pub use tag_repo::TagRepository;
pub use collection_repo::CollectionRepository;

View File

@@ -0,0 +1,12 @@
use async_trait::async_trait;
use crate::{entities::{AssetTag, Tag}, errors::DomainError, value_objects::SystemId};
#[async_trait]
pub trait TagRepository: Send + Sync {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Tag>, DomainError>;
async fn find_by_name(&self, name: &str) -> Result<Option<Tag>, DomainError>;
async fn find_tags_for_asset(&self, asset_id: &SystemId) -> Result<Vec<(Tag, AssetTag)>, DomainError>;
async fn save_tag(&self, tag: &Tag) -> Result<(), DomainError>;
async fn save_asset_tag(&self, asset_tag: &AssetTag) -> Result<(), DomainError>;
async fn remove_asset_tag(&self, asset_id: &SystemId, tag_id: &SystemId) -> Result<(), DomainError>;
}

View File

@@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterOperator {
Equals,
NotEquals,
Contains,
Between,
GreaterThan,
LessThan,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FilterCondition {
pub field: String,
pub op: FilterOperator,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterCriteria {
And(Vec<FilterCriteria>),
Or(Vec<FilterCriteria>),
Condition(FilterCondition),
}
impl FilterCriteria {
pub fn and(conditions: Vec<FilterCriteria>) -> Self {
Self::And(conditions)
}
pub fn or(conditions: Vec<FilterCriteria>) -> Self {
Self::Or(conditions)
}
pub fn condition(field: impl Into<String>, op: FilterOperator, value: serde_json::Value) -> Self {
Self::Condition(FilterCondition {
field: field.into(),
op,
value,
})
}
}

View File

@@ -1,6 +1,7 @@
mod checksum;
mod date_time_stamp;
mod email;
pub mod filter_criteria;
mod password;
mod structured_data;
mod system_id;
@@ -8,6 +9,7 @@ mod system_id;
pub use checksum::Checksum;
pub use date_time_stamp::DateTimeStamp;
pub use email::Email;
pub use filter_criteria::{FilterCondition, FilterCriteria, FilterOperator};
pub use password::PasswordHash;
pub use structured_data::{MetadataValue, StructuredData};
pub use system_id::SystemId;