style: cargo fmt --all
This commit is contained in:
@@ -56,7 +56,9 @@ impl Asset {
|
||||
|
||||
// --- AssetMetadata ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum MetadataSource {
|
||||
ExifExtracted,
|
||||
AiGenerated,
|
||||
@@ -133,7 +135,11 @@ impl AssetStack {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_member(&mut self, asset_id: SystemId, role: StackMemberRole) -> Result<(), DomainError> {
|
||||
pub fn add_member(
|
||||
&mut self,
|
||||
asset_id: SystemId,
|
||||
role: StackMemberRole,
|
||||
) -> Result<(), DomainError> {
|
||||
if self.members.iter().any(|m| m.asset_id == asset_id) {
|
||||
return Err(DomainError::Conflict(
|
||||
"Asset already exists in stack".to_string(),
|
||||
@@ -179,7 +185,11 @@ pub struct DerivativeAsset {
|
||||
}
|
||||
|
||||
impl DerivativeAsset {
|
||||
pub fn new_pending(parent: SystemId, profile: DerivativeProfile, path: impl Into<String>) -> Self {
|
||||
pub fn new_pending(
|
||||
parent: SystemId,
|
||||
profile: DerivativeProfile,
|
||||
path: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
derivative_id: SystemId::new(),
|
||||
parent_asset_id: parent,
|
||||
@@ -239,8 +249,14 @@ impl DuplicateGroup {
|
||||
detection_method: DetectionMethod::ExactHash,
|
||||
status: DuplicateStatus::Unresolved,
|
||||
candidates: vec![
|
||||
DuplicateCandidate { asset_id: asset_a, similarity_score: 1.0 },
|
||||
DuplicateCandidate { asset_id: asset_b, similarity_score: 1.0 },
|
||||
DuplicateCandidate {
|
||||
asset_id: asset_a,
|
||||
similarity_score: 1.0,
|
||||
},
|
||||
DuplicateCandidate {
|
||||
asset_id: asset_b,
|
||||
similarity_score: 1.0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use async_trait::async_trait;
|
||||
use super::entities::{
|
||||
Asset, AssetMetadata, AssetStack, DerivativeAsset, DerivativeProfile, DuplicateGroup,
|
||||
MetadataSource,
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Checksum, SystemId};
|
||||
use super::entities::{
|
||||
Asset, AssetMetadata, AssetStack, DerivativeAsset, DerivativeProfile,
|
||||
DuplicateGroup, MetadataSource,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
// --- AssetRepository ---
|
||||
|
||||
@@ -12,7 +12,12 @@ use super::entities::{
|
||||
pub trait AssetRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Asset>, DomainError>;
|
||||
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn find_by_owner(&self, owner_id: &SystemId, limit: u32, offset: u32) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn find_by_owner(
|
||||
&self,
|
||||
owner_id: &SystemId,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn save(&self, asset: &Asset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -22,9 +27,17 @@ pub trait AssetRepository: Send + Sync {
|
||||
#[async_trait]
|
||||
pub trait AssetMetadataRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetMetadata>, DomainError>;
|
||||
async fn find_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<Option<AssetMetadata>, DomainError>;
|
||||
async fn find_by_asset_and_source(
|
||||
&self,
|
||||
asset_id: &SystemId,
|
||||
source: MetadataSource,
|
||||
) -> Result<Option<AssetMetadata>, DomainError>;
|
||||
async fn save(&self, metadata: &AssetMetadata) -> Result<(), DomainError>;
|
||||
async fn delete_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<(), DomainError>;
|
||||
async fn delete_by_asset_and_source(
|
||||
&self,
|
||||
asset_id: &SystemId,
|
||||
source: MetadataSource,
|
||||
) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- AssetStackRepository ---
|
||||
@@ -41,8 +54,13 @@ pub trait AssetStackRepository: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait DerivativeRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DerivativeAsset>, DomainError>;
|
||||
async fn find_by_asset_and_profile(&self, asset_id: &SystemId, profile: DerivativeProfile) -> Result<Option<DerivativeAsset>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId)
|
||||
-> Result<Vec<DerivativeAsset>, DomainError>;
|
||||
async fn find_by_asset_and_profile(
|
||||
&self,
|
||||
asset_id: &SystemId,
|
||||
profile: DerivativeProfile,
|
||||
) -> Result<Option<DerivativeAsset>, DomainError>;
|
||||
async fn save(&self, derivative: &DerivativeAsset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::events::DomainEvent;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventPublisher: Send + Sync {
|
||||
|
||||
@@ -7,9 +7,10 @@ impl Checksum {
|
||||
pub fn new(hex: impl Into<String>) -> Result<Self, DomainError> {
|
||||
let hex = hex.into().to_lowercase();
|
||||
if hex.len() != 64 {
|
||||
return Err(DomainError::Validation(
|
||||
format!("Checksum must be 64 hex characters, got {}", hex.len()),
|
||||
));
|
||||
return Err(DomainError::Validation(format!(
|
||||
"Checksum must be 64 hex characters, got {}",
|
||||
hex.len()
|
||||
)));
|
||||
}
|
||||
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(DomainError::Validation(
|
||||
@@ -19,7 +20,9 @@ impl Checksum {
|
||||
Ok(Self(hex))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str { &self.0 }
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Checksum {
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct DateTimeStamp(DateTime<Utc>);
|
||||
|
||||
impl DateTimeStamp {
|
||||
pub fn now() -> Self { Self(Utc::now()) }
|
||||
pub fn from_datetime(dt: DateTime<Utc>) -> Self { Self(dt) }
|
||||
pub fn as_datetime(&self) -> &DateTime<Utc> { &self.0 }
|
||||
pub fn now() -> Self {
|
||||
Self(Utc::now())
|
||||
}
|
||||
pub fn from_datetime(dt: DateTime<Utc>) -> Self {
|
||||
Self(dt)
|
||||
}
|
||||
pub fn as_datetime(&self) -> &DateTime<Utc> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DateTimeStamp {
|
||||
@@ -16,5 +24,7 @@ impl std::fmt::Display for DateTimeStamp {
|
||||
}
|
||||
|
||||
impl From<DateTime<Utc>> for DateTimeStamp {
|
||||
fn from(dt: DateTime<Utc>) -> Self { Self(dt) }
|
||||
fn from(dt: DateTime<Utc>) -> Self {
|
||||
Self(dt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ impl Email {
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str { &self.0 }
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Email {
|
||||
|
||||
@@ -33,7 +33,11 @@ impl FilterCriteria {
|
||||
Self::Or(conditions)
|
||||
}
|
||||
|
||||
pub fn condition(field: impl Into<String>, op: FilterOperator, value: serde_json::Value) -> Self {
|
||||
pub fn condition(
|
||||
field: impl Into<String>,
|
||||
op: FilterOperator,
|
||||
value: serde_json::Value,
|
||||
) -> Self {
|
||||
Self::Condition(FilterCondition {
|
||||
field: field.into(),
|
||||
op,
|
||||
|
||||
@@ -9,6 +9,10 @@ impl std::fmt::Debug for PasswordHash {
|
||||
}
|
||||
|
||||
impl PasswordHash {
|
||||
pub fn from_hash(hash: String) -> Self { Self(hash) }
|
||||
pub fn as_str(&self) -> &str { &self.0 }
|
||||
pub fn from_hash(hash: String) -> Self {
|
||||
Self(hash)
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ pub enum MetadataValue {
|
||||
pub struct StructuredData(HashMap<String, MetadataValue>);
|
||||
|
||||
impl StructuredData {
|
||||
pub fn new() -> Self { Self(HashMap::new()) }
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: impl Into<String>, value: MetadataValue) {
|
||||
self.0.insert(key.into(), value);
|
||||
@@ -35,8 +37,12 @@ impl StructuredData {
|
||||
self.0.keys()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool { self.0.is_empty() }
|
||||
pub fn len(&self) -> usize { self.0.len() }
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn merge_from(&mut self, other: StructuredData) {
|
||||
self.0.extend(other.0);
|
||||
@@ -46,9 +52,13 @@ impl StructuredData {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &HashMap<String, MetadataValue> { &self.0 }
|
||||
pub fn inner(&self) -> &HashMap<String, MetadataValue> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StructuredData {
|
||||
fn default() -> Self { Self::new() }
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,21 @@ use uuid::Uuid;
|
||||
pub struct SystemId(Uuid);
|
||||
|
||||
impl SystemId {
|
||||
pub fn new() -> Self { Self(Uuid::new_v4()) }
|
||||
pub fn from_uuid(id: Uuid) -> Self { Self(id) }
|
||||
pub fn as_uuid(&self) -> &Uuid { &self.0 }
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
pub fn from_uuid(id: Uuid) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
pub fn as_uuid(&self) -> &Uuid {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SystemId {
|
||||
fn default() -> Self { Self::new() }
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SystemId {
|
||||
@@ -20,5 +28,7 @@ impl std::fmt::Display for SystemId {
|
||||
}
|
||||
|
||||
impl From<Uuid> for SystemId {
|
||||
fn from(id: Uuid) -> Self { Self(id) }
|
||||
fn from(id: Uuid) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Email, PasswordHash, SystemId};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// --- Permission ---
|
||||
|
||||
@@ -36,7 +36,10 @@ pub struct Permission {
|
||||
|
||||
impl Permission {
|
||||
pub fn new(action: PermissionAction, resource_type: ResourceType) -> Self {
|
||||
Self { action, resource_type }
|
||||
Self {
|
||||
action,
|
||||
resource_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,18 +52,39 @@ pub fn viewer_permissions() -> HashSet<Permission> {
|
||||
|
||||
pub fn contributor_permissions() -> HashSet<Permission> {
|
||||
let mut perms = viewer_permissions();
|
||||
perms.insert(Permission::new(PermissionAction::WriteMetadata, ResourceType::Global));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::WriteMetadata,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms
|
||||
}
|
||||
|
||||
pub fn admin_permissions() -> HashSet<Permission> {
|
||||
let mut perms = contributor_permissions();
|
||||
perms.insert(Permission::new(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageAccess, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageUsers, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageSystem, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ReadLocation, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ReadPerson, ResourceType::Global));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::DeleteAsset,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::ManageAccess,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::ManageUsers,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::ManageSystem,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::ReadLocation,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms.insert(Permission::new(
|
||||
PermissionAction::ReadPerson,
|
||||
ResourceType::Global,
|
||||
));
|
||||
perms
|
||||
}
|
||||
|
||||
@@ -75,7 +99,11 @@ pub struct Role {
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn new(name: impl Into<String>, permissions: HashSet<Permission>, is_system_default: bool) -> Self {
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
permissions: HashSet<Permission>,
|
||||
is_system_default: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
role_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
@@ -85,7 +113,8 @@ impl Role {
|
||||
}
|
||||
|
||||
pub fn has_permission(&self, action: PermissionAction, resource_type: ResourceType) -> bool {
|
||||
self.permissions.contains(&Permission::new(action, resource_type))
|
||||
self.permissions
|
||||
.contains(&Permission::new(action, resource_type))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +165,9 @@ impl Group {
|
||||
|
||||
pub fn add_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||
if self.members.contains(&user_id) {
|
||||
return Err(DomainError::Conflict(format!("User {user_id} is already a member")));
|
||||
return Err(DomainError::Conflict(format!(
|
||||
"User {user_id} is already a member"
|
||||
)));
|
||||
}
|
||||
self.members.insert(user_id);
|
||||
Ok(())
|
||||
@@ -144,10 +175,14 @@ impl Group {
|
||||
|
||||
pub fn remove_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||
if user_id == self.owner_user_id {
|
||||
return Err(DomainError::Validation("Cannot remove the group owner".to_string()));
|
||||
return Err(DomainError::Validation(
|
||||
"Cannot remove the group owner".to_string(),
|
||||
));
|
||||
}
|
||||
if !self.members.remove(&user_id) {
|
||||
return Err(DomainError::NotFound(format!("User {user_id} is not a member")));
|
||||
return Err(DomainError::NotFound(format!(
|
||||
"User {user_id} is not a member"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use super::entities::{Group, Role, User};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Email, PasswordHash, SystemId};
|
||||
use super::entities::{Group, Role, User};
|
||||
use async_trait::async_trait;
|
||||
|
||||
// --- UserRepository ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use super::entities::{Permission, PermissionAction, ResourceType, Role};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct PermissionChecker;
|
||||
|
||||
@@ -16,6 +16,9 @@ impl PermissionChecker {
|
||||
}
|
||||
|
||||
pub fn effective_permissions(roles: &[Role]) -> HashSet<Permission> {
|
||||
roles.iter().flat_map(|r| r.permissions.iter().copied()).collect()
|
||||
roles
|
||||
.iter()
|
||||
.flat_map(|r| r.permissions.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pub mod catalog;
|
||||
pub mod common;
|
||||
pub mod identity;
|
||||
pub mod storage;
|
||||
pub mod catalog;
|
||||
pub mod organization;
|
||||
pub mod processing;
|
||||
pub mod sharing;
|
||||
pub mod sidecar;
|
||||
pub mod processing;
|
||||
pub mod storage;
|
||||
|
||||
// Facade — old import paths still work
|
||||
pub mod errors {
|
||||
@@ -18,31 +18,31 @@ pub mod value_objects {
|
||||
pub use crate::common::value_objects::*;
|
||||
}
|
||||
pub mod entities {
|
||||
pub use crate::identity::entities::*;
|
||||
pub use crate::storage::entities::*;
|
||||
pub use crate::catalog::entities::*;
|
||||
pub use crate::identity::entities::*;
|
||||
pub use crate::organization::entities::*;
|
||||
pub use crate::processing::entities::*;
|
||||
pub use crate::sharing::entities::*;
|
||||
pub use crate::sidecar::entities::*;
|
||||
pub use crate::processing::entities::*;
|
||||
pub use crate::storage::entities::*;
|
||||
|
||||
// Sub-module alias for `domain::entities::permission::` imports
|
||||
pub mod permission {
|
||||
pub use crate::identity::entities::{
|
||||
Permission, PermissionAction, ResourceType,
|
||||
viewer_permissions, contributor_permissions, admin_permissions,
|
||||
Permission, PermissionAction, ResourceType, admin_permissions, contributor_permissions,
|
||||
viewer_permissions,
|
||||
};
|
||||
}
|
||||
}
|
||||
pub mod ports {
|
||||
pub use crate::catalog::ports::*;
|
||||
pub use crate::common::ports::*;
|
||||
pub use crate::identity::ports::*;
|
||||
pub use crate::storage::ports::*;
|
||||
pub use crate::catalog::ports::*;
|
||||
pub use crate::organization::ports::*;
|
||||
pub use crate::processing::ports::*;
|
||||
pub use crate::sharing::ports::*;
|
||||
pub use crate::sidecar::ports::*;
|
||||
pub use crate::processing::ports::*;
|
||||
pub use crate::storage::ports::*;
|
||||
}
|
||||
pub mod services {
|
||||
pub mod permission_service {
|
||||
|
||||
@@ -43,7 +43,8 @@ impl Album {
|
||||
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 };
|
||||
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,
|
||||
@@ -54,7 +55,10 @@ impl Album {
|
||||
}
|
||||
|
||||
pub fn remove_asset(&mut self, asset_id: &SystemId) -> Result<(), DomainError> {
|
||||
let idx = self.entries.iter().position(|e| &e.asset_id == asset_id)
|
||||
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(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use super::entities::{Album, AssetTag, Collection, Tag};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::SystemId;
|
||||
use super::entities::{Album, AssetTag, Collection, Tag};
|
||||
use async_trait::async_trait;
|
||||
|
||||
// --- AlbumRepository ---
|
||||
|
||||
@@ -19,10 +19,17 @@ pub trait AlbumRepository: Send + Sync {
|
||||
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 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>;
|
||||
async fn remove_asset_tag(
|
||||
&self,
|
||||
asset_id: &SystemId,
|
||||
tag_id: &SystemId,
|
||||
) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- CollectionRepository ---
|
||||
|
||||
@@ -88,9 +88,10 @@ impl Job {
|
||||
|
||||
pub fn start(&mut self) -> Result<(), DomainError> {
|
||||
if self.status != JobStatus::Queued {
|
||||
return Err(DomainError::Conflict(
|
||||
format!("Job can only start from Queued, currently {:?}", self.status),
|
||||
));
|
||||
return Err(DomainError::Conflict(format!(
|
||||
"Job can only start from Queued, currently {:?}",
|
||||
self.status
|
||||
)));
|
||||
}
|
||||
self.status = JobStatus::Processing;
|
||||
self.started_at = Some(DateTimeStamp::now());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use super::entities::{Job, JobBatch, Plugin, ProcessingPipeline};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::SystemId;
|
||||
use super::entities::{Job, JobBatch, Plugin, ProcessingPipeline};
|
||||
use async_trait::async_trait;
|
||||
|
||||
// --- JobRepository ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::Utc;
|
||||
use crate::common::value_objects::{DateTimeStamp, StructuredData, SystemId};
|
||||
use chrono::Utc;
|
||||
|
||||
// --- ShareScope ---
|
||||
|
||||
@@ -74,8 +74,18 @@ pub struct ShareTarget {
|
||||
}
|
||||
|
||||
impl ShareTarget {
|
||||
pub fn new(scope_id: SystemId, target_type: TargetType, target_id: SystemId, role_id: SystemId) -> Self {
|
||||
Self { scope_id, target_type, target_id, role_id }
|
||||
pub fn new(
|
||||
scope_id: SystemId,
|
||||
target_type: TargetType,
|
||||
target_id: SystemId,
|
||||
role_id: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
scope_id,
|
||||
target_type,
|
||||
target_id,
|
||||
role_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +109,11 @@ pub struct ShareLink {
|
||||
}
|
||||
|
||||
impl ShareLink {
|
||||
pub fn new(scope_id: SystemId, token: impl Into<String>, access_level: LinkAccessLevel) -> Self {
|
||||
pub fn new(
|
||||
scope_id: SystemId,
|
||||
token: impl Into<String>,
|
||||
access_level: LinkAccessLevel,
|
||||
) -> Self {
|
||||
Self {
|
||||
scope_id,
|
||||
token: token.into(),
|
||||
@@ -115,7 +129,10 @@ impl ShareLink {
|
||||
if !self.is_active {
|
||||
return false;
|
||||
}
|
||||
if self.expires_at.is_some_and(|exp| exp.as_datetime() < &Utc::now()) {
|
||||
if self
|
||||
.expires_at
|
||||
.is_some_and(|exp| exp.as_datetime() < &Utc::now())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.max_uses.is_some_and(|max| self.use_count >= max) {
|
||||
@@ -160,7 +177,10 @@ impl InviteCode {
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if self.expires_at.is_some_and(|exp| exp.as_datetime() < &Utc::now()) {
|
||||
if self
|
||||
.expires_at
|
||||
.is_some_and(|exp| exp.as_datetime() < &Utc::now())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.max_uses.is_some_and(|max| self.use_count >= max) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use super::entities::{InviteCode, ShareLink, ShareScope, ShareTarget, VisibilityFilter};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::SystemId;
|
||||
use super::entities::{InviteCode, ShareLink, ShareScope, ShareTarget, VisibilityFilter};
|
||||
use async_trait::async_trait;
|
||||
|
||||
// --- ShareRepository ---
|
||||
|
||||
@@ -9,12 +9,21 @@ use super::entities::{InviteCode, ShareLink, ShareScope, ShareTarget, Visibility
|
||||
pub trait ShareRepository: Send + Sync {
|
||||
async fn save_scope(&self, scope: &ShareScope) -> Result<(), DomainError>;
|
||||
async fn find_scope_by_id(&self, id: &SystemId) -> Result<Option<ShareScope>, DomainError>;
|
||||
async fn find_scopes_for_resource(&self, resource_id: &SystemId) -> Result<Vec<ShareScope>, DomainError>;
|
||||
async fn find_scopes_for_resource(
|
||||
&self,
|
||||
resource_id: &SystemId,
|
||||
) -> Result<Vec<ShareScope>, DomainError>;
|
||||
async fn delete_scope(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
|
||||
async fn save_target(&self, target: &ShareTarget) -> Result<(), DomainError>;
|
||||
async fn find_targets_for_scope(&self, scope_id: &SystemId) -> Result<Vec<ShareTarget>, DomainError>;
|
||||
async fn find_targets_for_user(&self, user_id: &SystemId) -> Result<Vec<ShareTarget>, DomainError>;
|
||||
async fn find_targets_for_scope(
|
||||
&self,
|
||||
scope_id: &SystemId,
|
||||
) -> Result<Vec<ShareTarget>, DomainError>;
|
||||
async fn find_targets_for_user(
|
||||
&self,
|
||||
user_id: &SystemId,
|
||||
) -> Result<Vec<ShareTarget>, DomainError>;
|
||||
|
||||
async fn save_link(&self, link: &ShareLink) -> Result<(), DomainError>;
|
||||
async fn find_link_by_token(&self, token: &str) -> Result<Option<ShareLink>, DomainError>;
|
||||
@@ -27,7 +36,11 @@ pub trait ShareRepository: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait VisibilityFilterRepository: Send + Sync {
|
||||
async fn find_by_scope_and_role(&self, scope_id: &SystemId, role_id: &SystemId) -> Result<Option<VisibilityFilter>, DomainError>;
|
||||
async fn find_by_scope_and_role(
|
||||
&self,
|
||||
scope_id: &SystemId,
|
||||
role_id: &SystemId,
|
||||
) -> Result<Option<VisibilityFilter>, DomainError>;
|
||||
async fn save(&self, filter: &VisibilityFilter) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use async_trait::async_trait;
|
||||
use super::entities::{SidecarRecord, SyncStatus};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{StructuredData, SystemId};
|
||||
use super::entities::{SidecarRecord, SyncStatus};
|
||||
use async_trait::async_trait;
|
||||
|
||||
// --- SidecarRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait SidecarRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Option<SidecarRecord>, DomainError>;
|
||||
async fn find_by_asset(
|
||||
&self,
|
||||
asset_id: &SystemId,
|
||||
) -> Result<Option<SidecarRecord>, DomainError>;
|
||||
async fn find_by_status(&self, status: SyncStatus) -> Result<Vec<SidecarRecord>, DomainError>;
|
||||
async fn save(&self, record: &SidecarRecord) -> Result<(), DomainError>;
|
||||
async fn delete(&self, asset_id: &SystemId) -> Result<(), DomainError>;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use super::entities::{
|
||||
IngestSession, LibraryPath, QuotaDefinition, StorageVolume, UsageLedgerEntry, UsageType,
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{DateTimeStamp, SystemId};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{self, BoxStream, StreamExt};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{DateTimeStamp, SystemId};
|
||||
use super::entities::{
|
||||
IngestSession, LibraryPath, QuotaDefinition, StorageVolume,
|
||||
UsageLedgerEntry, UsageType,
|
||||
};
|
||||
|
||||
// --- StorageVolumeRepository ---
|
||||
|
||||
@@ -24,7 +23,10 @@ pub trait StorageVolumeRepository: Send + Sync {
|
||||
pub trait LibraryPathRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<LibraryPath>, DomainError>;
|
||||
async fn find_by_volume(&self, volume_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn find_ingest_destinations(&self, owner_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn find_ingest_destinations(
|
||||
&self,
|
||||
owner_id: &SystemId,
|
||||
) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn save(&self, path: &LibraryPath) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -42,7 +44,10 @@ pub trait IngestSessionRepository: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait QuotaRepository: Send + Sync {
|
||||
async fn find_by_owner(&self, owner_id: &SystemId) -> Result<Option<QuotaDefinition>, DomainError>;
|
||||
async fn find_by_owner(
|
||||
&self,
|
||||
owner_id: &SystemId,
|
||||
) -> Result<Option<QuotaDefinition>, DomainError>;
|
||||
async fn save(&self, quota: &QuotaDefinition) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -114,7 +119,8 @@ pub trait StorageWriter: Send + Sync {
|
||||
|
||||
/// Convenience: stores an in-memory buffer at `key`. Wraps `put`.
|
||||
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), DomainError> {
|
||||
self.put(key, Box::pin(stream::once(async move { Ok(data) }))).await
|
||||
self.put(key, Box::pin(stream::once(async move { Ok(data) })))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
|
||||
use super::entities::{QuotaDefinition, TimePeriod, UsageType};
|
||||
use crate::common::value_objects::DateTimeStamp;
|
||||
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
|
||||
|
||||
pub struct QuotaCheckResult {
|
||||
pub allowed: bool,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use domain::entities::{
|
||||
Asset, AssetMetadata, AssetStack, AssetType, DerivativeAsset, DerivativeProfile,
|
||||
DetectionMethod, DuplicateGroup, DuplicateStatus, GenerationStatus,
|
||||
MetadataSource, SourceReference, StackMemberRole, StackType,
|
||||
DetectionMethod, DuplicateGroup, DuplicateStatus, GenerationStatus, MetadataSource,
|
||||
SourceReference, StackMemberRole, StackType,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{Checksum, MetadataValue, StructuredData, SystemId};
|
||||
@@ -63,7 +63,9 @@ fn new_stack_contains_primary() {
|
||||
fn add_motion_clip() {
|
||||
let mut stack = AssetStack::new(StackType::LivePhoto, SystemId::new(), SystemId::new());
|
||||
let clip_id = SystemId::new();
|
||||
stack.add_member(clip_id.clone(), StackMemberRole::MotionClip).unwrap();
|
||||
stack
|
||||
.add_member(clip_id.clone(), StackMemberRole::MotionClip)
|
||||
.unwrap();
|
||||
assert_eq!(stack.members.len(), 2);
|
||||
assert_eq!(stack.members[1].asset_id, clip_id);
|
||||
assert_eq!(stack.members[1].sort_order, 1);
|
||||
|
||||
@@ -20,7 +20,10 @@ fn user_edited_overrides_exif() {
|
||||
|
||||
#[test]
|
||||
fn ai_overrides_exif_but_not_user() {
|
||||
let exif = layer(MetadataSource::ExifExtracted, &[("desc", "raw"), ("camera", "Nikon")]);
|
||||
let exif = layer(
|
||||
MetadataSource::ExifExtracted,
|
||||
&[("desc", "raw"), ("camera", "Nikon")],
|
||||
);
|
||||
let ai = layer(MetadataSource::AiGenerated, &[("desc", "ai-desc")]);
|
||||
let user = layer(MetadataSource::UserEdited, &[("desc", "user-desc")]);
|
||||
let result = resolve_metadata(&[exif, ai, user]);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
mod value_objects;
|
||||
mod events;
|
||||
mod value_objects;
|
||||
|
||||
@@ -13,10 +13,14 @@ fn now_is_recent() {
|
||||
#[test]
|
||||
fn ordering() {
|
||||
let a = DateTimeStamp::from_datetime(
|
||||
chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z").unwrap().into(),
|
||||
chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
|
||||
.unwrap()
|
||||
.into(),
|
||||
);
|
||||
let b = DateTimeStamp::from_datetime(
|
||||
chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z").unwrap().into(),
|
||||
chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
|
||||
.unwrap()
|
||||
.into(),
|
||||
);
|
||||
assert!(a < b);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
mod catalog;
|
||||
mod common;
|
||||
mod identity;
|
||||
mod storage;
|
||||
mod catalog;
|
||||
mod organization;
|
||||
mod processing;
|
||||
mod sharing;
|
||||
mod sidecar;
|
||||
mod processing;
|
||||
mod storage;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use domain::entities::permission::{
|
||||
admin_permissions, contributor_permissions, viewer_permissions,
|
||||
Permission, PermissionAction, ResourceType,
|
||||
Permission, PermissionAction, ResourceType, admin_permissions, contributor_permissions,
|
||||
viewer_permissions,
|
||||
};
|
||||
use domain::entities::{Group, Role, User};
|
||||
use domain::errors::DomainError;
|
||||
@@ -36,8 +36,16 @@ fn role_checks_permission() {
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_unique_id() {
|
||||
let a = User::new("alice", Email::new("a@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||
let b = User::new("bob", Email::new("b@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||
let a = User::new(
|
||||
"alice",
|
||||
Email::new("a@example.com").unwrap(),
|
||||
PasswordHash::from_hash("h".into()),
|
||||
);
|
||||
let b = User::new(
|
||||
"bob",
|
||||
Email::new("b@example.com").unwrap(),
|
||||
PasswordHash::from_hash("h".into()),
|
||||
);
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.username, "alice");
|
||||
assert_eq!(b.username, "bob");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use domain::entities::{Permission, PermissionAction, ResourceType, Role};
|
||||
use domain::entities::permission::{admin_permissions, viewer_permissions};
|
||||
use domain::entities::{Permission, PermissionAction, ResourceType, Role};
|
||||
use domain::services::permission_service::PermissionChecker;
|
||||
|
||||
#[test]
|
||||
@@ -24,8 +24,24 @@ fn viewer_cannot_delete() {
|
||||
|
||||
#[test]
|
||||
fn roles_additive() {
|
||||
let r1 = Role::new("r1", [Permission::new(PermissionAction::ReadAsset, ResourceType::Global)].into(), false);
|
||||
let r2 = Role::new("r2", [Permission::new(PermissionAction::WriteMetadata, ResourceType::Global)].into(), false);
|
||||
let r1 = Role::new(
|
||||
"r1",
|
||||
[Permission::new(
|
||||
PermissionAction::ReadAsset,
|
||||
ResourceType::Global,
|
||||
)]
|
||||
.into(),
|
||||
false,
|
||||
);
|
||||
let r2 = Role::new(
|
||||
"r2",
|
||||
[Permission::new(
|
||||
PermissionAction::WriteMetadata,
|
||||
ResourceType::Global,
|
||||
)]
|
||||
.into(),
|
||||
false,
|
||||
);
|
||||
let eff = PermissionChecker::effective_permissions(&[r1, r2]);
|
||||
assert_eq!(eff.len(), 2);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,26 @@ use domain::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[test]
|
||||
fn not_expired_when_no_expiry() {
|
||||
let scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
let scope = ShareScope::new(
|
||||
ScopeType::Link,
|
||||
ShareableType::Album,
|
||||
SystemId::new(),
|
||||
SystemId::new(),
|
||||
);
|
||||
assert!(!scope.is_expired());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_when_past() {
|
||||
let mut scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
scope.expires_at = Some(DateTimeStamp::from_datetime(Utc::now() - Duration::hours(1)));
|
||||
let mut scope = ShareScope::new(
|
||||
ScopeType::Link,
|
||||
ShareableType::Album,
|
||||
SystemId::new(),
|
||||
SystemId::new(),
|
||||
);
|
||||
scope.expires_at = Some(DateTimeStamp::from_datetime(
|
||||
Utc::now() - Duration::hours(1),
|
||||
));
|
||||
assert!(scope.is_expired());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use domain::entities::{
|
||||
IngestSession, IngestStatus, LibraryPath, OwnershipPolicy,
|
||||
QuotaDefinition, StorageVolume, TimePeriod, UsageType,
|
||||
IngestSession, IngestStatus, LibraryPath, OwnershipPolicy, QuotaDefinition, StorageVolume,
|
||||
TimePeriod, UsageType,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::value_objects::{Checksum, SystemId};
|
||||
@@ -63,7 +63,11 @@ fn invalid_transition_rejected() {
|
||||
|
||||
#[test]
|
||||
fn can_fail_from_any_non_terminal() {
|
||||
for target in [IngestStatus::Uploading, IngestStatus::AwaitingProcessing, IngestStatus::Processing] {
|
||||
for target in [
|
||||
IngestStatus::Uploading,
|
||||
IngestStatus::AwaitingProcessing,
|
||||
IngestStatus::Processing,
|
||||
] {
|
||||
let mut s = make_session();
|
||||
if target == IngestStatus::AwaitingProcessing || target == IngestStatus::Processing {
|
||||
s.advance_to(IngestStatus::AwaitingProcessing).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user