1use chrono::{DateTime, Utc};
4use sqlx::Pool;
5use uuid::Uuid;
6
7#[cfg(feature = "migrations")]
8use sqlx::migrate::Migrator;
9
10#[cfg(feature = "postgres")]
11mod audit;
12mod backend;
13mod cache;
14#[cfg(feature = "postgres")]
15mod expiry;
16#[cfg(feature = "postgres")]
17mod mutation;
18#[cfg(feature = "mysql")]
19mod mysql;
20#[cfg(feature = "postgres")]
21mod query;
22#[cfg(feature = "postgres")]
23mod relation;
24#[cfg(feature = "postgres")]
25mod rows;
26#[cfg(feature = "sqlite")]
27mod sqlite;
28#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
29mod support;
30mod timed;
31mod types;
32
33pub use backend::KeepsakeSqlxBackend;
34#[cfg(feature = "mysql")]
35pub use backend::MySqlBackend;
36#[cfg(feature = "postgres")]
37pub use backend::PostgresBackend;
38#[cfg(feature = "sqlite")]
39pub use backend::SqliteBackend;
40#[cfg(feature = "cache")]
41pub use cache::{LocalRelationCache, LocalRelationCacheConfig};
42pub use cache::{NoopRelationCache, RelationCache};
43pub use keepsake::ActiveRelation;
44#[cfg(feature = "postgres")]
45pub use timed::TimedKeepsakeRepository;
46#[cfg(feature = "mysql")]
47pub use timed::TimedMySqlKeepsakeRepository;
48#[cfg(feature = "sqlite")]
49pub use timed::TimedSqliteKeepsakeRepository;
50pub use timed::TimedSqlxKeepsakeRepository;
51pub use types::{
52 AppliedKeepsake, AuditCursor, AuditEventRecord, AuditOutboxCursor, AuditOutboxRecord,
53 FulfilledExpiryCandidate, MembershipCursor, TimedExpiryCandidate,
54};
55
56use backend::BackendMarker;
57#[cfg(feature = "postgres")]
58use rows::{
59 ActiveRelationRow, AppliedKeepsakeRow, AppliedKeepsakeWriteRow, AuditEventRow, RelationRow,
60};
61
62#[cfg(all(feature = "migrations", feature = "postgres"))]
63static POSTGRES_MIGRATOR: Migrator = sqlx::migrate!("./migrations/postgres");
64
65#[cfg(all(feature = "migrations", feature = "sqlite"))]
66static SQLITE_MIGRATOR: Migrator = sqlx::migrate!("./migrations/sqlite");
67
68#[cfg(all(feature = "migrations", feature = "mysql"))]
69static MYSQL_MIGRATOR: Migrator = sqlx::migrate!("./migrations/mysql");
70
71#[allow(dead_code)]
72const MAX_BATCH_LIMIT: i64 = 10_000;
73
74pub type RepositoryResult<T> = core::result::Result<T, RepositoryError>;
76
77#[non_exhaustive]
79#[derive(Debug, thiserror::Error)]
80pub enum RepositoryError {
81 #[error(transparent)]
83 Sqlx(#[from] sqlx::Error),
84
85 #[cfg(feature = "migrations")]
87 #[error(transparent)]
88 Migration(#[from] sqlx::migrate::MigrateError),
89
90 #[error(transparent)]
92 Json(#[from] serde_json::Error),
93
94 #[error(transparent)]
96 Keepsake(#[from] keepsake::KeepsakeError),
97
98 #[error("schema backend mismatch: expected {expected}, found {actual}")]
100 BackendMismatch {
101 expected: &'static str,
103 actual: String,
105 },
106
107 #[error("relation {relation_id} is disabled")]
109 RelationDisabled {
110 relation_id: Uuid,
112 },
113
114 #[error(
116 "relation spec {kind}/{name} expected id {expected_relation_id}, but stored relation uses {stored_relation_id}"
117 )]
118 RelationSpecIdMismatch {
119 kind: String,
121 name: String,
123 expected_relation_id: Uuid,
125 stored_relation_id: Uuid,
127 },
128
129 #[error("relation definition {relation_id} was not found")]
131 RelationDefinitionMissing {
132 relation_id: Uuid,
134 },
135
136 #[error("limit {limit} is outside the accepted range 1..={max}")]
138 InvalidLimit {
139 limit: i64,
141 max: i64,
143 },
144
145 #[error("unknown lifecycle state {state}")]
147 InvalidLifecycleState {
148 state: String,
150 },
151
152 #[error("unknown audit event type {event_type}")]
154 InvalidAuditEventType {
155 event_type: String,
157 },
158}
159
160#[derive(Debug)]
162pub struct SqlxKeepsakeRepository<B, C = NoopRelationCache>
163where
164 B: KeepsakeSqlxBackend,
165{
166 pool: Pool<B::Database>,
167 #[allow(dead_code)]
168 relation_cache: C,
169 backend: BackendMarker<B>,
170}
171
172impl<B, C> Clone for SqlxKeepsakeRepository<B, C>
173where
174 B: KeepsakeSqlxBackend,
175 C: Clone,
176{
177 fn clone(&self) -> Self {
178 Self {
179 pool: self.pool.clone(),
180 relation_cache: self.relation_cache.clone(),
181 backend: self.backend,
182 }
183 }
184}
185
186#[cfg(feature = "postgres")]
188pub type PostgresKeepsakeRepository<C = NoopRelationCache> =
189 SqlxKeepsakeRepository<PostgresBackend, C>;
190
191#[cfg(feature = "postgres")]
193pub type KeepsakeRepository<C = NoopRelationCache> = PostgresKeepsakeRepository<C>;
194
195#[cfg(feature = "sqlite")]
197pub type SqliteKeepsakeRepository<C = NoopRelationCache> = SqlxKeepsakeRepository<SqliteBackend, C>;
198
199#[cfg(feature = "mysql")]
201pub type MySqlKeepsakeRepository<C = NoopRelationCache> = SqlxKeepsakeRepository<MySqlBackend, C>;
202
203#[cfg(feature = "postgres")]
204impl PostgresKeepsakeRepository<NoopRelationCache> {
205 #[must_use]
207 pub const fn new(pool: sqlx::PgPool) -> Self {
208 Self {
209 pool,
210 relation_cache: NoopRelationCache,
211 backend: BackendMarker::new(),
212 }
213 }
214}
215
216#[cfg(feature = "sqlite")]
217impl SqliteKeepsakeRepository<NoopRelationCache> {
218 #[must_use]
220 pub const fn new(pool: sqlx::SqlitePool) -> Self {
221 Self {
222 pool,
223 relation_cache: NoopRelationCache,
224 backend: BackendMarker::new(),
225 }
226 }
227}
228
229#[cfg(feature = "mysql")]
230impl MySqlKeepsakeRepository<NoopRelationCache> {
231 #[must_use]
233 pub const fn new(pool: sqlx::MySqlPool) -> Self {
234 Self {
235 pool,
236 relation_cache: NoopRelationCache,
237 backend: BackendMarker::new(),
238 }
239 }
240}
241
242impl<B, C> SqlxKeepsakeRepository<B, C>
243where
244 B: KeepsakeSqlxBackend,
245 C: RelationCache,
246{
247 pub const fn at(&self, at: DateTime<Utc>) -> TimedSqlxKeepsakeRepository<'_, B, C> {
252 TimedSqlxKeepsakeRepository {
253 repository: self,
254 at,
255 }
256 }
257
258 #[must_use]
260 pub fn with_relation_cache<Next>(self, cache: Next) -> SqlxKeepsakeRepository<B, Next>
261 where
262 Next: RelationCache,
263 {
264 SqlxKeepsakeRepository {
265 pool: self.pool,
266 relation_cache: cache,
267 backend: self.backend,
268 }
269 }
270
271 #[cfg(feature = "cache")]
277 #[must_use]
278 pub fn with_local_relation_cache(
279 self,
280 config: LocalRelationCacheConfig,
281 ) -> SqlxKeepsakeRepository<B, LocalRelationCache> {
282 self.with_relation_cache(LocalRelationCache::new(config))
283 }
284}
285
286#[cfg(all(feature = "postgres", feature = "migrations"))]
287impl<C> PostgresKeepsakeRepository<C>
288where
289 C: RelationCache,
290{
291 pub async fn migrate(&self) -> RepositoryResult<()> {
293 schema::postgres_schema_preflight(&self.pool).await?;
294 POSTGRES_MIGRATOR.run(&self.pool).await?;
295 Ok(())
296 }
297}
298
299#[cfg(all(feature = "sqlite", feature = "migrations"))]
300impl<C> SqliteKeepsakeRepository<C>
301where
302 C: RelationCache,
303{
304 pub async fn migrate(&self) -> RepositoryResult<()> {
306 schema::sqlite_schema_preflight(&self.pool).await?;
307 SQLITE_MIGRATOR.run(&self.pool).await?;
308 Ok(())
309 }
310}
311
312#[cfg(all(feature = "mysql", feature = "migrations"))]
313impl<C> MySqlKeepsakeRepository<C>
314where
315 C: RelationCache,
316{
317 pub async fn migrate(&self) -> RepositoryResult<()> {
319 schema::mysql_schema_preflight(&self.pool).await?;
320 MYSQL_MIGRATOR.run(&self.pool).await?;
321 Ok(())
322 }
323}
324
325#[allow(dead_code)]
326fn validate_limit(limit: i64) -> RepositoryResult<i64> {
327 if (1..=MAX_BATCH_LIMIT).contains(&limit) {
328 Ok(limit)
329 } else {
330 Err(RepositoryError::InvalidLimit {
331 limit,
332 max: MAX_BATCH_LIMIT,
333 })
334 }
335}
336
337#[cfg(feature = "migrations")]
338mod schema;
339#[cfg(all(test, feature = "postgres"))]
340mod tests;