domain: add Processing entities and ports (Job, JobBatch, Plugin, Pipeline)
This commit is contained in:
123
crates/domain/src/entities/job.rs
Normal file
123
crates/domain/src/entities/job.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use crate::errors::DomainError;
|
||||||
|
use crate::value_objects::{DateTimeStamp, StructuredData, SystemId};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum JobType {
|
||||||
|
ScanDirectory,
|
||||||
|
ExtractMetadata,
|
||||||
|
GenerateDerivative,
|
||||||
|
SyncSidecar,
|
||||||
|
DetectDuplicates,
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for JobType {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::ScanDirectory, Self::ScanDirectory) => true,
|
||||||
|
(Self::ExtractMetadata, Self::ExtractMetadata) => true,
|
||||||
|
(Self::GenerateDerivative, Self::GenerateDerivative) => true,
|
||||||
|
(Self::SyncSidecar, Self::SyncSidecar) => true,
|
||||||
|
(Self::DetectDuplicates, Self::DetectDuplicates) => true,
|
||||||
|
(Self::Custom(a), Self::Custom(b)) => a == b,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for JobType {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum JobStatus {
|
||||||
|
Queued,
|
||||||
|
Processing,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Job {
|
||||||
|
pub job_id: SystemId,
|
||||||
|
pub job_type: JobType,
|
||||||
|
pub target_asset_id: Option<SystemId>,
|
||||||
|
pub batch_id: Option<SystemId>,
|
||||||
|
pub status: JobStatus,
|
||||||
|
pub priority: u32,
|
||||||
|
pub payload: StructuredData,
|
||||||
|
pub result_data: Option<StructuredData>,
|
||||||
|
pub retry_count: u32,
|
||||||
|
pub max_retries: u32,
|
||||||
|
pub created_at: DateTimeStamp,
|
||||||
|
pub started_at: Option<DateTimeStamp>,
|
||||||
|
pub completed_at: Option<DateTimeStamp>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Job {
|
||||||
|
pub fn new(job_type: JobType, priority: u32, payload: StructuredData) -> Self {
|
||||||
|
Self {
|
||||||
|
job_id: SystemId::new(),
|
||||||
|
job_type,
|
||||||
|
target_asset_id: None,
|
||||||
|
batch_id: None,
|
||||||
|
status: JobStatus::Queued,
|
||||||
|
priority,
|
||||||
|
payload,
|
||||||
|
result_data: None,
|
||||||
|
retry_count: 0,
|
||||||
|
max_retries: 3,
|
||||||
|
created_at: DateTimeStamp::now(),
|
||||||
|
started_at: None,
|
||||||
|
completed_at: None,
|
||||||
|
error_message: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_target(mut self, asset_id: SystemId) -> Self {
|
||||||
|
self.target_asset_id = Some(asset_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_batch(mut self, batch_id: SystemId) -> Self {
|
||||||
|
self.batch_id = Some(batch_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.status = JobStatus::Processing;
|
||||||
|
self.started_at = Some(DateTimeStamp::now());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(&mut self, result: StructuredData) {
|
||||||
|
self.status = JobStatus::Completed;
|
||||||
|
self.result_data = Some(result);
|
||||||
|
self.completed_at = Some(DateTimeStamp::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail(&mut self, error: impl Into<String>) {
|
||||||
|
self.retry_count += 1;
|
||||||
|
self.error_message = Some(error.into());
|
||||||
|
self.started_at = None;
|
||||||
|
if self.retry_count >= self.max_retries {
|
||||||
|
self.status = JobStatus::Failed;
|
||||||
|
} else {
|
||||||
|
self.status = JobStatus::Queued;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&mut self) {
|
||||||
|
self.status = JobStatus::Cancelled;
|
||||||
|
self.completed_at = Some(DateTimeStamp::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_retry(&self) -> bool {
|
||||||
|
self.retry_count < self.max_retries
|
||||||
|
}
|
||||||
|
}
|
||||||
59
crates/domain/src/entities/job_batch.rs
Normal file
59
crates/domain/src/entities/job_batch.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::value_objects::SystemId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum BatchStatus {
|
||||||
|
InProgress,
|
||||||
|
CompletedWithErrors,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct JobBatch {
|
||||||
|
pub batch_id: SystemId,
|
||||||
|
pub batch_type: String,
|
||||||
|
pub total_jobs: u32,
|
||||||
|
pub completed_count: u32,
|
||||||
|
pub failed_count: u32,
|
||||||
|
pub status: BatchStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobBatch {
|
||||||
|
pub fn new(batch_type: impl Into<String>, total_jobs: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
batch_id: SystemId::new(),
|
||||||
|
batch_type: batch_type.into(),
|
||||||
|
total_jobs,
|
||||||
|
completed_count: 0,
|
||||||
|
failed_count: 0,
|
||||||
|
status: BatchStatus::InProgress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_completion(&mut self) {
|
||||||
|
self.completed_count += 1;
|
||||||
|
self.check_finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_failure(&mut self) {
|
||||||
|
self.failed_count += 1;
|
||||||
|
self.check_finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn progress_percent(&self) -> f64 {
|
||||||
|
if self.total_jobs == 0 {
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
((self.completed_count + self.failed_count) as f64 / self.total_jobs as f64) * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_finished(&mut self) {
|
||||||
|
if self.completed_count + self.failed_count >= self.total_jobs {
|
||||||
|
self.status = if self.failed_count > 0 {
|
||||||
|
BatchStatus::CompletedWithErrors
|
||||||
|
} else {
|
||||||
|
BatchStatus::Completed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,39 @@
|
|||||||
|
// Identity & Access (Tasks 3-4)
|
||||||
pub mod permission;
|
pub mod permission;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
mod user;
|
mod user;
|
||||||
mod group;
|
mod group;
|
||||||
|
|
||||||
|
pub use permission::{Permission, PermissionAction, ResourceType};
|
||||||
|
pub use role::Role;
|
||||||
|
pub use user::User;
|
||||||
|
pub use group::Group;
|
||||||
|
|
||||||
|
// Storage & Sources (Task 6)
|
||||||
mod storage_volume;
|
mod storage_volume;
|
||||||
mod library_path;
|
mod library_path;
|
||||||
mod ingest_session;
|
mod ingest_session;
|
||||||
mod quota;
|
mod quota;
|
||||||
|
|
||||||
|
pub use storage_volume::StorageVolume;
|
||||||
|
pub use library_path::{LibraryPath, OwnershipPolicy};
|
||||||
|
pub use ingest_session::{IngestSession, IngestStatus};
|
||||||
|
pub use quota::{QuotaDefinition, QuotaRule, TimePeriod, UsageLedgerEntry, UsageType};
|
||||||
|
|
||||||
|
// Media Catalog (Task 8)
|
||||||
mod asset;
|
mod asset;
|
||||||
mod asset_metadata;
|
mod asset_metadata;
|
||||||
mod asset_stack;
|
mod asset_stack;
|
||||||
mod derivative_asset;
|
mod derivative_asset;
|
||||||
mod duplicate;
|
mod duplicate;
|
||||||
|
|
||||||
pub use permission::{Permission, PermissionAction, ResourceType};
|
|
||||||
pub use role::Role;
|
|
||||||
pub use user::User;
|
|
||||||
pub use group::Group;
|
|
||||||
pub use storage_volume::StorageVolume;
|
|
||||||
pub use library_path::{LibraryPath, OwnershipPolicy};
|
|
||||||
pub use ingest_session::{IngestSession, IngestStatus};
|
|
||||||
pub use quota::{QuotaDefinition, QuotaRule, TimePeriod, UsageLedgerEntry, UsageType};
|
|
||||||
pub use asset::{Asset, AssetType, SourceReference};
|
pub use asset::{Asset, AssetType, SourceReference};
|
||||||
pub use asset_metadata::{AssetMetadata, MetadataSource};
|
pub use asset_metadata::{AssetMetadata, MetadataSource};
|
||||||
pub use asset_stack::{AssetStack, AssetStackMember, StackMemberRole, StackType};
|
pub use asset_stack::{AssetStack, AssetStackMember, StackMemberRole, StackType};
|
||||||
pub use derivative_asset::{DerivativeAsset, DerivativeProfile, GenerationStatus};
|
pub use derivative_asset::{DerivativeAsset, DerivativeProfile, GenerationStatus};
|
||||||
pub use duplicate::{DetectionMethod, DuplicateCandidate, DuplicateGroup, DuplicateStatus};
|
pub use duplicate::{DetectionMethod, DuplicateCandidate, DuplicateGroup, DuplicateStatus};
|
||||||
|
|
||||||
|
// Organization (Task 10)
|
||||||
mod album;
|
mod album;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod collection;
|
mod collection;
|
||||||
@@ -34,6 +42,7 @@ pub use album::{Album, AlbumEntry};
|
|||||||
pub use tag::{AssetTag, Tag, TagSource};
|
pub use tag::{AssetTag, Tag, TagSource};
|
||||||
pub use collection::Collection;
|
pub use collection::Collection;
|
||||||
|
|
||||||
|
// Sharing (Task 11)
|
||||||
mod share_scope;
|
mod share_scope;
|
||||||
mod share_target;
|
mod share_target;
|
||||||
mod share_link;
|
mod share_link;
|
||||||
@@ -46,8 +55,20 @@ pub use share_link::{LinkAccessLevel, ShareLink};
|
|||||||
pub use invite_code::InviteCode;
|
pub use invite_code::InviteCode;
|
||||||
pub use visibility_filter::VisibilityFilter;
|
pub use visibility_filter::VisibilityFilter;
|
||||||
|
|
||||||
|
// Sidecar Sync (Task 12)
|
||||||
mod sidecar_record;
|
mod sidecar_record;
|
||||||
mod sidecar_config;
|
mod sidecar_config;
|
||||||
|
|
||||||
pub use sidecar_record::{SidecarRecord, SyncStatus};
|
pub use sidecar_record::{SidecarRecord, SyncStatus};
|
||||||
pub use sidecar_config::{ConflictPolicy, SidecarConfig, SyncMode};
|
pub use sidecar_config::{ConflictPolicy, SidecarConfig, SyncMode};
|
||||||
|
|
||||||
|
// Processing (Task 13)
|
||||||
|
mod job;
|
||||||
|
mod job_batch;
|
||||||
|
mod plugin;
|
||||||
|
mod processing_pipeline;
|
||||||
|
|
||||||
|
pub use job::{Job, JobStatus, JobType};
|
||||||
|
pub use job_batch::{BatchStatus, JobBatch};
|
||||||
|
pub use plugin::{Plugin, PluginType};
|
||||||
|
pub use processing_pipeline::{PipelineStep, ProcessingPipeline};
|
||||||
|
|||||||
37
crates/domain/src/entities/plugin.rs
Normal file
37
crates/domain/src/entities/plugin.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use crate::value_objects::{StructuredData, SystemId};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum PluginType {
|
||||||
|
MediaProcessor,
|
||||||
|
ScheduledTask,
|
||||||
|
SidecarWriter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Plugin {
|
||||||
|
pub plugin_id: SystemId,
|
||||||
|
pub name: String,
|
||||||
|
pub plugin_type: PluginType,
|
||||||
|
pub is_enabled: bool,
|
||||||
|
pub configuration: StructuredData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin {
|
||||||
|
pub fn new(name: impl Into<String>, plugin_type: PluginType) -> Self {
|
||||||
|
Self {
|
||||||
|
plugin_id: SystemId::new(),
|
||||||
|
name: name.into(),
|
||||||
|
plugin_type,
|
||||||
|
is_enabled: true,
|
||||||
|
configuration: StructuredData::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(&mut self) {
|
||||||
|
self.is_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable(&mut self) {
|
||||||
|
self.is_enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
crates/domain/src/entities/processing_pipeline.rs
Normal file
35
crates/domain/src/entities/processing_pipeline.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use crate::value_objects::{StructuredData, SystemId};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct PipelineStep {
|
||||||
|
pub plugin_id: SystemId,
|
||||||
|
pub step_order: u32,
|
||||||
|
pub configuration: StructuredData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct ProcessingPipeline {
|
||||||
|
pub pipeline_id: SystemId,
|
||||||
|
pub trigger_event: String,
|
||||||
|
pub steps: Vec<PipelineStep>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessingPipeline {
|
||||||
|
pub fn new(trigger_event: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
pipeline_id: SystemId::new(),
|
||||||
|
trigger_event: trigger_event.into(),
|
||||||
|
steps: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_step(&mut self, plugin_id: SystemId, config: StructuredData) {
|
||||||
|
let next_order = self.steps.iter().map(|s| s.step_order).max().unwrap_or(0)
|
||||||
|
+ if self.steps.is_empty() { 0 } else { 1 };
|
||||||
|
self.steps.push(PipelineStep {
|
||||||
|
plugin_id,
|
||||||
|
step_order: next_order,
|
||||||
|
configuration: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
crates/domain/src/ports/job_batch_repo.rs
Normal file
8
crates/domain/src/ports/job_batch_repo.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::{entities::JobBatch, errors::DomainError, value_objects::SystemId};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait JobBatchRepository: Send + Sync {
|
||||||
|
async fn find_by_id(&self, id: &SystemId) -> Result<Option<JobBatch>, DomainError>;
|
||||||
|
async fn save(&self, batch: &JobBatch) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
10
crates/domain/src/ports/job_repo.rs
Normal file
10
crates/domain/src/ports/job_repo.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::{entities::Job, errors::DomainError, value_objects::SystemId};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait JobRepository: Send + Sync {
|
||||||
|
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Job>, DomainError>;
|
||||||
|
async fn find_next_queued(&self) -> Result<Option<Job>, DomainError>;
|
||||||
|
async fn find_by_batch(&self, batch_id: &SystemId) -> Result<Vec<Job>, DomainError>;
|
||||||
|
async fn save(&self, job: &Job) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
@@ -1,19 +1,10 @@
|
|||||||
|
// Identity & Access (Tasks 4-5)
|
||||||
mod auth;
|
mod auth;
|
||||||
mod event_publisher;
|
mod event_publisher;
|
||||||
mod group_repo;
|
mod group_repo;
|
||||||
mod role_repo;
|
mod role_repo;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod user_repo;
|
mod user_repo;
|
||||||
mod storage_volume_repo;
|
|
||||||
mod library_path_repo;
|
|
||||||
mod ingest_session_repo;
|
|
||||||
mod quota_repo;
|
|
||||||
mod file_storage;
|
|
||||||
mod asset_repo;
|
|
||||||
mod asset_metadata_repo;
|
|
||||||
mod asset_stack_repo;
|
|
||||||
mod derivative_repo;
|
|
||||||
mod duplicate_repo;
|
|
||||||
|
|
||||||
pub use auth::{PasswordHasher, TokenIssuer};
|
pub use auth::{PasswordHasher, TokenIssuer};
|
||||||
pub use event_publisher::EventPublisher;
|
pub use event_publisher::EventPublisher;
|
||||||
@@ -21,17 +12,34 @@ pub use group_repo::GroupRepository;
|
|||||||
pub use role_repo::RoleRepository;
|
pub use role_repo::RoleRepository;
|
||||||
pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter};
|
pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter};
|
||||||
pub use user_repo::UserRepository;
|
pub use user_repo::UserRepository;
|
||||||
|
|
||||||
|
// Storage & Sources (Task 7)
|
||||||
|
mod storage_volume_repo;
|
||||||
|
mod library_path_repo;
|
||||||
|
mod ingest_session_repo;
|
||||||
|
mod quota_repo;
|
||||||
|
mod file_storage;
|
||||||
|
|
||||||
pub use storage_volume_repo::StorageVolumeRepository;
|
pub use storage_volume_repo::StorageVolumeRepository;
|
||||||
pub use library_path_repo::LibraryPathRepository;
|
pub use library_path_repo::LibraryPathRepository;
|
||||||
pub use ingest_session_repo::IngestSessionRepository;
|
pub use ingest_session_repo::IngestSessionRepository;
|
||||||
pub use quota_repo::{QuotaRepository, UsageLedgerRepository};
|
pub use quota_repo::{QuotaRepository, UsageLedgerRepository};
|
||||||
pub use file_storage::{FileEntry, FileStoragePort};
|
pub use file_storage::{FileEntry, FileStoragePort};
|
||||||
|
|
||||||
|
// Media Catalog (Task 9)
|
||||||
|
mod asset_repo;
|
||||||
|
mod asset_metadata_repo;
|
||||||
|
mod asset_stack_repo;
|
||||||
|
mod derivative_repo;
|
||||||
|
mod duplicate_repo;
|
||||||
|
|
||||||
pub use asset_repo::AssetRepository;
|
pub use asset_repo::AssetRepository;
|
||||||
pub use asset_metadata_repo::AssetMetadataRepository;
|
pub use asset_metadata_repo::AssetMetadataRepository;
|
||||||
pub use asset_stack_repo::AssetStackRepository;
|
pub use asset_stack_repo::AssetStackRepository;
|
||||||
pub use derivative_repo::DerivativeRepository;
|
pub use derivative_repo::DerivativeRepository;
|
||||||
pub use duplicate_repo::DuplicateRepository;
|
pub use duplicate_repo::DuplicateRepository;
|
||||||
|
|
||||||
|
// Organization (Task 10)
|
||||||
mod album_repo;
|
mod album_repo;
|
||||||
mod tag_repo;
|
mod tag_repo;
|
||||||
mod collection_repo;
|
mod collection_repo;
|
||||||
@@ -40,14 +48,27 @@ pub use album_repo::AlbumRepository;
|
|||||||
pub use tag_repo::TagRepository;
|
pub use tag_repo::TagRepository;
|
||||||
pub use collection_repo::CollectionRepository;
|
pub use collection_repo::CollectionRepository;
|
||||||
|
|
||||||
|
// Sharing (Task 11)
|
||||||
mod share_repo;
|
mod share_repo;
|
||||||
mod visibility_filter_repo;
|
mod visibility_filter_repo;
|
||||||
|
|
||||||
pub use share_repo::ShareRepository;
|
pub use share_repo::ShareRepository;
|
||||||
pub use visibility_filter_repo::VisibilityFilterRepository;
|
pub use visibility_filter_repo::VisibilityFilterRepository;
|
||||||
|
|
||||||
|
// Sidecar Sync (Task 12)
|
||||||
mod sidecar_repo;
|
mod sidecar_repo;
|
||||||
mod sidecar_writer;
|
mod sidecar_writer;
|
||||||
|
|
||||||
pub use sidecar_repo::SidecarRepository;
|
pub use sidecar_repo::SidecarRepository;
|
||||||
pub use sidecar_writer::SidecarWriterPort;
|
pub use sidecar_writer::SidecarWriterPort;
|
||||||
|
|
||||||
|
// Processing (Task 13)
|
||||||
|
mod job_repo;
|
||||||
|
mod job_batch_repo;
|
||||||
|
mod plugin_repo;
|
||||||
|
mod pipeline_repo;
|
||||||
|
|
||||||
|
pub use job_repo::JobRepository;
|
||||||
|
pub use job_batch_repo::JobBatchRepository;
|
||||||
|
pub use plugin_repo::PluginRepository;
|
||||||
|
pub use pipeline_repo::PipelineRepository;
|
||||||
|
|||||||
9
crates/domain/src/ports/pipeline_repo.rs
Normal file
9
crates/domain/src/ports/pipeline_repo.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::{entities::ProcessingPipeline, errors::DomainError, value_objects::SystemId};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PipelineRepository: Send + Sync {
|
||||||
|
async fn find_by_id(&self, id: &SystemId) -> Result<Option<ProcessingPipeline>, DomainError>;
|
||||||
|
async fn find_by_trigger(&self, event: &str) -> Result<Vec<ProcessingPipeline>, DomainError>;
|
||||||
|
async fn save(&self, pipeline: &ProcessingPipeline) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
9
crates/domain/src/ports/plugin_repo.rs
Normal file
9
crates/domain/src/ports/plugin_repo.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use crate::{entities::Plugin, errors::DomainError, value_objects::SystemId};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PluginRepository: Send + Sync {
|
||||||
|
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Plugin>, DomainError>;
|
||||||
|
async fn find_enabled(&self) -> Result<Vec<Plugin>, DomainError>;
|
||||||
|
async fn save(&self, plugin: &Plugin) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
53
crates/domain/tests/entities/job.rs
Normal file
53
crates/domain/tests/entities/job.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use domain::entities::{Job, JobStatus, JobType};
|
||||||
|
use domain::errors::DomainError;
|
||||||
|
use domain::value_objects::StructuredData;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn job_lifecycle_success() {
|
||||||
|
let mut job = Job::new(JobType::ExtractMetadata, 5, StructuredData::new());
|
||||||
|
assert_eq!(job.status, JobStatus::Queued);
|
||||||
|
|
||||||
|
job.start().unwrap();
|
||||||
|
assert_eq!(job.status, JobStatus::Processing);
|
||||||
|
assert!(job.started_at.is_some());
|
||||||
|
|
||||||
|
job.complete(StructuredData::new());
|
||||||
|
assert_eq!(job.status, JobStatus::Completed);
|
||||||
|
assert!(job.result_data.is_some());
|
||||||
|
assert!(job.completed_at.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retry_on_failure() {
|
||||||
|
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||||
|
job.start().unwrap();
|
||||||
|
|
||||||
|
job.fail("timeout");
|
||||||
|
assert_eq!(job.status, JobStatus::Queued);
|
||||||
|
assert_eq!(job.retry_count, 1);
|
||||||
|
assert!(job.can_retry());
|
||||||
|
assert!(job.started_at.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fails_after_max_retries() {
|
||||||
|
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||||
|
job.max_retries = 2;
|
||||||
|
|
||||||
|
job.start().unwrap();
|
||||||
|
job.fail("err1");
|
||||||
|
assert_eq!(job.status, JobStatus::Queued);
|
||||||
|
|
||||||
|
job.start().unwrap();
|
||||||
|
job.fail("err2");
|
||||||
|
assert_eq!(job.status, JobStatus::Failed);
|
||||||
|
assert!(!job.can_retry());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_start_from_processing() {
|
||||||
|
let mut job = Job::new(JobType::ScanDirectory, 1, StructuredData::new());
|
||||||
|
job.start().unwrap();
|
||||||
|
let result = job.start();
|
||||||
|
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||||
|
}
|
||||||
31
crates/domain/tests/entities/job_batch.rs
Normal file
31
crates/domain/tests/entities/job_batch.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use domain::entities::{BatchStatus, JobBatch};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_when_all_done() {
|
||||||
|
let mut batch = JobBatch::new("scan", 3);
|
||||||
|
batch.record_completion();
|
||||||
|
batch.record_completion();
|
||||||
|
batch.record_completion();
|
||||||
|
assert_eq!(batch.status, BatchStatus::Completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_with_errors() {
|
||||||
|
let mut batch = JobBatch::new("scan", 3);
|
||||||
|
batch.record_completion();
|
||||||
|
batch.record_failure();
|
||||||
|
batch.record_completion();
|
||||||
|
assert_eq!(batch.status, BatchStatus::CompletedWithErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn progress_tracking() {
|
||||||
|
let mut batch = JobBatch::new("scan", 4);
|
||||||
|
assert_eq!(batch.progress_percent(), 0.0);
|
||||||
|
|
||||||
|
batch.record_completion();
|
||||||
|
assert_eq!(batch.progress_percent(), 25.0);
|
||||||
|
|
||||||
|
batch.record_completion();
|
||||||
|
assert_eq!(batch.progress_percent(), 50.0);
|
||||||
|
}
|
||||||
@@ -16,3 +16,6 @@ mod tag;
|
|||||||
mod share_scope;
|
mod share_scope;
|
||||||
mod share_link;
|
mod share_link;
|
||||||
mod sidecar_record;
|
mod sidecar_record;
|
||||||
|
mod job;
|
||||||
|
mod job_batch;
|
||||||
|
mod processing_pipeline;
|
||||||
|
|||||||
17
crates/domain/tests/entities/processing_pipeline.rs
Normal file
17
crates/domain/tests/entities/processing_pipeline.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use domain::entities::ProcessingPipeline;
|
||||||
|
use domain::value_objects::{StructuredData, SystemId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn steps_ordered() {
|
||||||
|
let mut pipeline = ProcessingPipeline::new("asset.created");
|
||||||
|
assert!(pipeline.steps.is_empty());
|
||||||
|
|
||||||
|
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||||
|
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||||
|
pipeline.add_step(SystemId::new(), StructuredData::new());
|
||||||
|
|
||||||
|
assert_eq!(pipeline.steps.len(), 3);
|
||||||
|
assert_eq!(pipeline.steps[0].step_order, 0);
|
||||||
|
assert_eq!(pipeline.steps[1].step_order, 1);
|
||||||
|
assert_eq!(pipeline.steps[2].step_order, 2);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user