use chrono::{DateTime, Utc};
use sqlx::Pool;
use uuid::Uuid;
#[cfg(feature = "migrations")]
use sqlx::migrate::Migrator;
#[cfg(feature = "postgres")]
mod audit;
mod backend;
mod cache;
#[cfg(feature = "postgres")]
mod expiry;
#[cfg(feature = "postgres")]
mod mutation;
#[cfg(feature = "mysql")]
mod mysql;
#[cfg(feature = "postgres")]
mod query;
#[cfg(feature = "postgres")]
mod relation;
#[cfg(feature = "postgres")]
mod rows;
#[cfg(feature = "sqlite")]
mod sqlite;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
mod support;
mod timed;
mod types;
pub use backend::KeepsakeSqlxBackend;
#[cfg(feature = "mysql")]
pub use backend::MySqlBackend;
#[cfg(feature = "postgres")]
pub use backend::PostgresBackend;
#[cfg(feature = "sqlite")]
pub use backend::SqliteBackend;
#[cfg(feature = "cache")]
pub use cache::{LocalRelationCache, LocalRelationCacheConfig};
pub use cache::{NoopRelationCache, RelationCache};
pub use keepsake::ActiveRelation;
#[cfg(feature = "postgres")]
pub use timed::TimedKeepsakeRepository;
#[cfg(feature = "mysql")]
pub use timed::TimedMySqlKeepsakeRepository;
#[cfg(feature = "sqlite")]
pub use timed::TimedSqliteKeepsakeRepository;
pub use timed::TimedSqlxKeepsakeRepository;
pub use types::{
AppliedKeepsake, AuditCursor, AuditEventRecord, AuditOutboxCursor, AuditOutboxRecord,
FulfilledExpiryCandidate, MembershipCursor, TimedExpiryCandidate,
};
use backend::BackendMarker;
#[cfg(feature = "postgres")]
use rows::{
ActiveRelationRow, AppliedKeepsakeRow, AppliedKeepsakeWriteRow, AuditEventRow, RelationRow,
};
#[cfg(all(feature = "migrations", feature = "postgres"))]
static POSTGRES_MIGRATOR: Migrator = sqlx::migrate!("./migrations/postgres");
#[cfg(all(feature = "migrations", feature = "sqlite"))]
static SQLITE_MIGRATOR: Migrator = sqlx::migrate!("./migrations/sqlite");
#[cfg(all(feature = "migrations", feature = "mysql"))]
static MYSQL_MIGRATOR: Migrator = sqlx::migrate!("./migrations/mysql");
#[allow(dead_code)]
const MAX_BATCH_LIMIT: i64 = 10_000;
pub type RepositoryResult<T> = core::result::Result<T, RepositoryError>;
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum RepositoryError {
#[error(transparent)]
Sqlx(#[from] sqlx::Error),
#[cfg(feature = "migrations")]
#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Keepsake(#[from] keepsake::KeepsakeError),
#[error("schema backend mismatch: expected {expected}, found {actual}")]
BackendMismatch {
expected: &'static str,
actual: String,
},
#[error("relation {relation_id} is disabled")]
RelationDisabled {
relation_id: Uuid,
},
#[error(
"relation spec {kind}/{name} expected id {expected_relation_id}, but stored relation uses {stored_relation_id}"
)]
RelationSpecIdMismatch {
kind: String,
name: String,
expected_relation_id: Uuid,
stored_relation_id: Uuid,
},
#[error("relation definition {relation_id} was not found")]
RelationDefinitionMissing {
relation_id: Uuid,
},
#[error("limit {limit} is outside the accepted range 1..={max}")]
InvalidLimit {
limit: i64,
max: i64,
},
#[error("unknown lifecycle state {state}")]
InvalidLifecycleState {
state: String,
},
#[error("unknown audit event type {event_type}")]
InvalidAuditEventType {
event_type: String,
},
}
#[derive(Debug)]
pub struct SqlxKeepsakeRepository<B, C = NoopRelationCache>
where
B: KeepsakeSqlxBackend,
{
pool: Pool<B::Database>,
#[allow(dead_code)]
relation_cache: C,
backend: BackendMarker<B>,
}
impl<B, C> Clone for SqlxKeepsakeRepository<B, C>
where
B: KeepsakeSqlxBackend,
C: Clone,
{
fn clone(&self) -> Self {
Self {
pool: self.pool.clone(),
relation_cache: self.relation_cache.clone(),
backend: self.backend,
}
}
}
#[cfg(feature = "postgres")]
pub type PostgresKeepsakeRepository<C = NoopRelationCache> =
SqlxKeepsakeRepository<PostgresBackend, C>;
#[cfg(feature = "postgres")]
pub type KeepsakeRepository<C = NoopRelationCache> = PostgresKeepsakeRepository<C>;
#[cfg(feature = "sqlite")]
pub type SqliteKeepsakeRepository<C = NoopRelationCache> = SqlxKeepsakeRepository<SqliteBackend, C>;
#[cfg(feature = "mysql")]
pub type MySqlKeepsakeRepository<C = NoopRelationCache> = SqlxKeepsakeRepository<MySqlBackend, C>;
#[cfg(feature = "postgres")]
impl PostgresKeepsakeRepository<NoopRelationCache> {
#[must_use]
pub const fn new(pool: sqlx::PgPool) -> Self {
Self {
pool,
relation_cache: NoopRelationCache,
backend: BackendMarker::new(),
}
}
}
#[cfg(feature = "sqlite")]
impl SqliteKeepsakeRepository<NoopRelationCache> {
#[must_use]
pub const fn new(pool: sqlx::SqlitePool) -> Self {
Self {
pool,
relation_cache: NoopRelationCache,
backend: BackendMarker::new(),
}
}
}
#[cfg(feature = "mysql")]
impl MySqlKeepsakeRepository<NoopRelationCache> {
#[must_use]
pub const fn new(pool: sqlx::MySqlPool) -> Self {
Self {
pool,
relation_cache: NoopRelationCache,
backend: BackendMarker::new(),
}
}
}
impl<B, C> SqlxKeepsakeRepository<B, C>
where
B: KeepsakeSqlxBackend,
C: RelationCache,
{
pub const fn at(&self, at: DateTime<Utc>) -> TimedSqlxKeepsakeRepository<'_, B, C> {
TimedSqlxKeepsakeRepository {
repository: self,
at,
}
}
#[must_use]
pub fn with_relation_cache<Next>(self, cache: Next) -> SqlxKeepsakeRepository<B, Next>
where
Next: RelationCache,
{
SqlxKeepsakeRepository {
pool: self.pool,
relation_cache: cache,
backend: self.backend,
}
}
#[cfg(feature = "cache")]
#[must_use]
pub fn with_local_relation_cache(
self,
config: LocalRelationCacheConfig,
) -> SqlxKeepsakeRepository<B, LocalRelationCache> {
self.with_relation_cache(LocalRelationCache::new(config))
}
}
#[cfg(all(feature = "postgres", feature = "migrations"))]
impl<C> PostgresKeepsakeRepository<C>
where
C: RelationCache,
{
pub async fn migrate(&self) -> RepositoryResult<()> {
schema::postgres_schema_preflight(&self.pool).await?;
POSTGRES_MIGRATOR.run(&self.pool).await?;
Ok(())
}
}
#[cfg(all(feature = "sqlite", feature = "migrations"))]
impl<C> SqliteKeepsakeRepository<C>
where
C: RelationCache,
{
pub async fn migrate(&self) -> RepositoryResult<()> {
schema::sqlite_schema_preflight(&self.pool).await?;
SQLITE_MIGRATOR.run(&self.pool).await?;
Ok(())
}
}
#[cfg(all(feature = "mysql", feature = "migrations"))]
impl<C> MySqlKeepsakeRepository<C>
where
C: RelationCache,
{
pub async fn migrate(&self) -> RepositoryResult<()> {
schema::mysql_schema_preflight(&self.pool).await?;
MYSQL_MIGRATOR.run(&self.pool).await?;
Ok(())
}
}
#[allow(dead_code)]
fn validate_limit(limit: i64) -> RepositoryResult<i64> {
if (1..=MAX_BATCH_LIMIT).contains(&limit) {
Ok(limit)
} else {
Err(RepositoryError::InvalidLimit {
limit,
max: MAX_BATCH_LIMIT,
})
}
}
#[cfg(feature = "migrations")]
mod schema;
#[cfg(all(test, feature = "postgres"))]
mod tests;