use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Debug, thiserror::Error)]
pub enum RepositoryError {
#[error("Entity not found")]
NotFound,
#[error("Entity already exists: {0}")]
AlreadyExists(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Internal error: {0}")]
InternalError(String),
}
impl From<serde_json::Error> for RepositoryError {
fn from(e: serde_json::Error) -> Self {
RepositoryError::SerializationError(e.to_string())
}
}
#[cfg(feature = "postgres")]
impl From<sqlx::Error> for RepositoryError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => RepositoryError::NotFound,
sqlx::Error::Database(db_err) => {
let msg = db_err.message().to_string();
if msg.contains("duplicate key") || msg.contains("unique constraint") {
RepositoryError::AlreadyExists(msg)
} else {
RepositoryError::DatabaseError(msg)
}
}
_ => RepositoryError::DatabaseError(e.to_string()),
}
}
}
pub trait PersistentEntity: Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static {
fn entity_id(&self) -> String;
fn set_entity_id(&mut self, id: String);
fn created_at(&self) -> Option<DateTime<Utc>>;
fn set_created_at(&mut self, ts: DateTime<Utc>);
fn updated_at(&self) -> Option<DateTime<Utc>>;
fn set_updated_at(&mut self, ts: DateTime<Utc>);
fn deleted_at(&self) -> Option<DateTime<Utc>>;
fn set_deleted_at(&mut self, ts: Option<DateTime<Utc>>);
fn is_deleted(&self) -> bool {
self.deleted_at().is_some()
}
fn mark_deleted(&mut self) {
self.set_deleted_at(Some(Utc::now()));
self.set_updated_at(Utc::now());
}
fn restore(&mut self) {
self.set_deleted_at(None);
self.set_updated_at(Utc::now());
}
fn touch(&mut self) {
self.set_updated_at(Utc::now());
}
fn generate_id() -> String {
uuid::Uuid::new_v4().to_string()
}
fn write_protected_fields() -> &'static [&'static str] {
&[]
}
}
pub trait PartialUpdatable: PersistentEntity {
fn apply_partial_update(&mut self, fields: &HashMap<String, serde_json::Value>) -> Result<(), RepositoryError>;
}
pub trait Versioned {
fn version(&self) -> u64;
fn set_version(&mut self, version: u64);
fn increment_version(&mut self) {
self.set_version(self.version() + 1);
}
}
#[async_trait]
pub trait CrudRepository<E>: Send + Sync
where
E: PersistentEntity,
{
async fn create(&self, entity: E) -> Result<E, RepositoryError>;
async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError>;
async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError>;
async fn update(&self, entity: E) -> Result<E, RepositoryError>;
async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError>;
async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError>;
async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError>;
async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
async fn count(&self) -> Result<u64, RepositoryError>;
async fn count_deleted(&self) -> Result<u64, RepositoryError>;
async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError>;
async fn empty_trash(&self) -> Result<u64, RepositoryError>;
async fn list_filtered(
&self,
page: u32,
limit: u32,
filters: HashMap<String, String>,
) -> Result<(Vec<E>, u64), RepositoryError> {
let _ = filters; self.list(page, limit).await
}
async fn list_filtered_with_info(
&self,
page: u32,
limit: u32,
filters: HashMap<String, String>,
) -> Result<(Vec<E>, backbone_orm::repository::PaginationInfo), RepositoryError> {
let (rows, total) = self.list_filtered(page, limit, filters).await?;
Ok((
rows,
backbone_orm::repository::PaginationInfo::new(page, limit, total),
))
}
fn table_name(&self) -> Option<&str> {
None
}
async fn aggregate_filtered(
&self,
spec: &backbone_orm::repository::AggregateSpec,
filters: HashMap<String, String>,
) -> Result<backbone_orm::repository::AggregateResult, RepositoryError> {
let _ = (spec, filters);
Err(RepositoryError::DatabaseError(
"aggregate is not supported by this repository".to_string(),
))
}
async fn fetch_related_json(
&self,
_table: &str,
_ids: &[String],
) -> Vec<serde_json::Value> {
Vec::new()
}
async fn exists(&self, id: &str) -> Result<bool, RepositoryError> {
Ok(self.find_by_id(id).await?.is_some())
}
async fn bulk_soft_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
let mut n = 0;
for id in ids {
if self.soft_delete(id).await? {
n += 1;
}
}
Ok(n)
}
async fn bulk_restore(&self, ids: &[String]) -> Result<Vec<E>, RepositoryError> {
let mut out = Vec::with_capacity(ids.len());
for id in ids {
if let Some(e) = self.restore(id).await? {
out.push(e);
}
}
Ok(out)
}
async fn bulk_hard_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
let mut n = 0;
for id in ids {
if self.hard_delete(id).await? {
n += 1;
}
}
Ok(n)
}
async fn restore_all(&self) -> Result<Vec<E>, RepositoryError> {
let mut restored = Vec::new();
loop {
let (batch, _) = self.list_deleted(1, 500).await?;
if batch.is_empty() {
break;
}
for entity in &batch {
if let Some(e) = self.restore(&entity.entity_id()).await? {
restored.push(e);
}
}
}
Ok(restored)
}
async fn bulk_update(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
let mut out = Vec::with_capacity(entities.len());
for entity in entities {
out.push(self.update(entity).await?);
}
Ok(out)
}
}
#[async_trait]
pub trait SearchableRepository<E>: CrudRepository<E>
where
E: PersistentEntity,
{
async fn search(
&self,
filters: HashMap<String, String>,
page: u32,
limit: u32,
) -> Result<(Vec<E>, u64), RepositoryError>;
async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError>;
async fn find_all_by_field(
&self,
field: &str,
value: &str,
page: u32,
limit: u32,
) -> Result<(Vec<E>, u64), RepositoryError>;
}
#[cfg(feature = "postgres")]
pub use postgres_traits::*;
#[cfg(feature = "postgres")]
mod postgres_traits {
use super::*;
use sqlx::postgres::PgRow;
use sqlx::FromRow;
pub trait PostgresEntity: PersistentEntity + for<'r> FromRow<'r, PgRow> + Unpin {
fn table_name() -> &'static str;
fn id_column() -> &'static str {
"id"
}
fn select_columns() -> &'static [&'static str];
fn insert_columns() -> &'static [&'static str];
fn update_columns() -> &'static [&'static str] {
Self::insert_columns()
}
fn bind_for_insert<'q>(
entity: &'q Self,
query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
fn bind_for_update<'q>(
entity: &'q Self,
query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
Self::bind_for_insert(entity, query)
}
fn select_by_id_query() -> String {
format!(
"SELECT {} FROM {} WHERE {} = $1 AND deleted_at IS NULL",
Self::select_columns().join(", "),
Self::table_name(),
Self::id_column()
)
}
fn select_by_id_including_deleted_query() -> String {
format!(
"SELECT {} FROM {} WHERE {} = $1",
Self::select_columns().join(", "),
Self::table_name(),
Self::id_column()
)
}
fn list_query() -> String {
format!(
"SELECT {} FROM {} WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2",
Self::select_columns().join(", "),
Self::table_name()
)
}
fn count_query() -> String {
format!(
"SELECT COUNT(*) FROM {} WHERE deleted_at IS NULL",
Self::table_name()
)
}
fn list_deleted_query() -> String {
format!(
"SELECT {} FROM {} WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC LIMIT $1 OFFSET $2",
Self::select_columns().join(", "),
Self::table_name()
)
}
fn count_deleted_query() -> String {
format!(
"SELECT COUNT(*) FROM {} WHERE deleted_at IS NOT NULL",
Self::table_name()
)
}
fn soft_delete_query() -> String {
format!(
"UPDATE {} SET deleted_at = NOW(), updated_at = NOW() WHERE {} = $1 AND deleted_at IS NULL",
Self::table_name(),
Self::id_column()
)
}
fn restore_query() -> String {
format!(
"UPDATE {} SET deleted_at = NULL, updated_at = NOW() WHERE {} = $1 AND deleted_at IS NOT NULL RETURNING {}",
Self::table_name(),
Self::id_column(),
Self::select_columns().join(", ")
)
}
fn hard_delete_query() -> String {
format!(
"DELETE FROM {} WHERE {} = $1",
Self::table_name(),
Self::id_column()
)
}
fn empty_trash_query() -> String {
format!(
"DELETE FROM {} WHERE deleted_at IS NOT NULL",
Self::table_name()
)
}
}
}