1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/// SQL database repository port traits.
///
/// Defines the abstract contracts (ports) that SQL-backed storage adapters must
/// implement. These traits live here in `paladin-ports` so that any crate
/// (e.g. `paladin-storage`) can implement them without depending on the facade
/// crate, preserving the hexagonal architecture's inward-only dependency rule.
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use paladin_core::platform::container::content::{ContentItem, ContentItemError};
use paladin_core::platform::container::content_list::{ContentList, ContentListError};
use thiserror::Error;
use uuid::Uuid;
/// Errors produced by repository operations.
#[derive(Debug, Clone, Error)]
pub enum RepositoryError {
/// A database connection could not be established or was lost.
#[error("Database connection error: {0}")]
ConnectionError(String),
/// A query failed to execute.
#[error("Query execution error: {0}")]
QueryError(String),
/// Serialization or deserialization of a stored value failed.
#[error("Serialization error: {0}")]
SerializationError(String),
/// The requested entity does not exist.
#[error("Entity not found: {0}")]
NotFound(String),
/// A uniqueness constraint was violated.
#[error("Duplicate entry: {0}")]
DuplicateEntry(String),
/// A transaction could not be committed or rolled back.
#[error("Transaction error: {0}")]
TransactionError(String),
/// A schema migration failed.
#[error("Migration error: {0}")]
MigrationError(String),
/// A [`ContentItem`] domain error was encountered during persistence.
#[error("Content item error: {0}")]
ContentItemError(#[from] ContentItemError),
/// A [`ContentList`] domain error was encountered during persistence.
#[error("Content list error: {0}")]
ContentListError(#[from] ContentListError),
}
/// Repository port for [`ContentItem`] persistence.
#[async_trait]
pub trait ContentRepository: Send + Sync {
/// Get a content item by its deduplication hash.
async fn get_by_hash(&self, hash: &str) -> Result<Option<ContentItem>, RepositoryError>;
/// Get a content item by UUID.
async fn get_by_id(&self, id: Uuid) -> Result<Option<ContentItem>, RepositoryError>;
/// Persist a new content item; returns the assigned UUID.
async fn create(&self, content: ContentItem) -> Result<Uuid, RepositoryError>;
/// Overwrite an existing content item.
async fn update(&self, content: &ContentItem) -> Result<(), RepositoryError>;
/// Remove a content item by UUID.
async fn delete(&self, id: Uuid) -> Result<(), RepositoryError>;
/// Return all content items (no pagination).
async fn list(&self) -> Result<Vec<ContentItem>, RepositoryError>;
/// Find content items that carry all of the given tags.
async fn find_by_tags(&self, tags: &[String]) -> Result<Vec<ContentItem>, RepositoryError>;
/// Find content items originating from a given source URL or path.
async fn find_by_source(&self, source: &str) -> Result<Vec<ContentItem>, RepositoryError>;
/// Return the total number of stored content items.
async fn count(&self) -> Result<u64, RepositoryError>;
/// Return `true` if a content item with the given hash already exists.
async fn exists_by_hash(&self, hash: &str) -> Result<bool, RepositoryError>;
}
/// Repository port for [`ContentList`] persistence.
///
/// NOTE: methods are currently synchronous. An async migration is tracked as a
/// TODO in the original `sql_store.rs`.
pub trait ContentListRepository {
/// Get a content list by UUID.
fn get_by_id(&self, id: Uuid) -> Result<Option<ContentList>, RepositoryError>;
/// Get a content list by name.
fn get_by_name(&self, name: &str) -> Result<Option<ContentList>, RepositoryError>;
/// Persist a content list.
fn save(&self, content_list: &ContentList) -> Result<(), RepositoryError>;
/// Overwrite an existing content list.
fn update(&self, content_list: &ContentList) -> Result<(), RepositoryError>;
/// Remove a content list by UUID.
fn delete(&self, id: Uuid) -> Result<(), RepositoryError>;
/// Return all content lists, with optional pagination.
fn find_all(
&self,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Vec<ContentList>, RepositoryError>;
/// Find content lists created within a date range.
fn find_by_date_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Result<Vec<ContentList>, RepositoryError>;
/// Return the total number of stored content lists.
fn count(&self) -> Result<u64, RepositoryError>;
/// Append an item to a content list.
fn add_item_to_list(&self, list_id: Uuid, item: &ContentItem) -> Result<(), RepositoryError>;
/// Remove an item from a content list.
fn remove_item_from_list(&self, list_id: Uuid, item_id: Uuid) -> Result<(), RepositoryError>;
}
/// Port for wrapping multiple repository operations in a single database transaction.
pub trait TransactionManager {
/// Execute `operation` atomically; roll back on any error.
fn with_transaction<F, R>(&self, operation: F) -> Result<R, RepositoryError>
where
F: FnOnce() -> Result<R, RepositoryError>;
}
/// Port for running database schema migrations.
#[async_trait]
pub trait MigrationManager: Send + Sync {
/// Apply any pending migrations.
async fn migrate(&self) -> Result<(), RepositoryError>;
/// Return `true` if the schema is already at the latest version.
async fn is_up_to_date(&self) -> Result<bool, RepositoryError>;
/// Return the identifier of the currently applied migration version.
async fn current_version(&self) -> Result<Option<String>, RepositoryError>;
}
/// General-purpose SQL store health and maintenance port.
#[async_trait]
pub trait SqlStore: Send + Sync {
/// Return aggregate statistics for the underlying store.
async fn get_stats(&self) -> Result<RepositoryStats, RepositoryError>;
/// Perform a lightweight connectivity / health check.
async fn health_check(&self) -> Result<bool, RepositoryError>;
/// Remove all content older than `older_than`; returns the number of rows deleted.
async fn cleanup(&self, older_than: DateTime<Utc>) -> Result<u64, RepositoryError>;
}
/// Aggregate statistics reported by a [`SqlStore`].
#[derive(Debug, Clone, PartialEq)]
pub struct RepositoryStats {
/// Total number of stored [`ContentItem`]s.
pub total_content_items: u64,
/// Total number of stored [`ContentList`]s.
pub total_content_lists: u64,
/// Number of items that still need to be fetched.
pub total_items_to_fetch: u64,
/// Physical database file size in bytes, if available.
pub database_size_bytes: Option<u64>,
/// Timestamp of the most recent write.
pub last_updated: DateTime<Utc>,
}