1use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{de::DeserializeOwned, Serialize};
9use std::collections::HashMap;
10use std::fmt::Debug;
11
12#[derive(Debug, thiserror::Error)]
18pub enum RepositoryError {
19 #[error("Entity not found")]
20 NotFound,
21
22 #[error("Entity already exists: {0}")]
23 AlreadyExists(String),
24
25 #[error("Validation error: {0}")]
26 ValidationError(String),
27
28 #[error("Database error: {0}")]
29 DatabaseError(String),
30
31 #[error("Serialization error: {0}")]
32 SerializationError(String),
33
34 #[error("Conflict: {0}")]
35 Conflict(String),
36
37 #[error("Internal error: {0}")]
38 InternalError(String),
39
40 #[error("validation failed: {}", .0.iter().map(|v| v.message.as_str()).collect::<Vec<_>>().join("; "))]
44 Violations(Vec<crate::violation::Violation>),
45}
46
47impl From<serde_json::Error> for RepositoryError {
48 fn from(e: serde_json::Error) -> Self {
49 RepositoryError::SerializationError(e.to_string())
50 }
51}
52
53#[cfg(feature = "postgres")]
54impl From<sqlx::Error> for RepositoryError {
55 fn from(e: sqlx::Error) -> Self {
56 match e {
57 sqlx::Error::RowNotFound => RepositoryError::NotFound,
58 sqlx::Error::Database(db_err) => {
59 let pg = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>();
60 if let Some(v) = constraint_violation(
61 db_err.kind(),
62 db_err.code().as_deref(),
63 db_err.constraint(),
64 pg.and_then(|p| p.detail()).unwrap_or(""),
65 pg.and_then(|p| p.column()),
66 ) {
67 return RepositoryError::Violations(vec![v]);
68 }
69 let msg = db_err.message().to_string();
70 if msg.contains("duplicate key") || msg.contains("unique constraint") {
71 RepositoryError::AlreadyExists(msg)
72 } else {
73 RepositoryError::DatabaseError(msg)
74 }
75 }
76 _ => RepositoryError::DatabaseError(e.to_string()),
77 }
78 }
79}
80
81#[cfg(feature = "postgres")]
89fn constraint_violation(
90 kind: sqlx::error::ErrorKind,
91 sqlstate: Option<&str>,
92 constraint: Option<&str>,
93 detail: &str,
94 column: Option<&str>,
95) -> Option<crate::violation::Violation> {
96 use crate::violation::{code_from_constraint, Violation};
97 use sqlx::error::ErrorKind;
98 let constraint = constraint?;
99 let path = column.map(str::to_string).or_else(|| key_column(detail)).unwrap_or_default();
100 let code = code_from_constraint(constraint);
101 let on = |path: &str| if path.is_empty() { String::new() } else { format!("`{path}` ") };
102 Some(match kind {
103 ErrorKind::ForeignKeyViolation if detail.contains("still referenced") => Violation::new(
104 "",
105 "still_referenced",
106 "This record is still referenced by another record, so it cannot be removed",
107 ),
108 ErrorKind::ForeignKeyViolation => {
109 let message = format!("{}refers to a record that does not exist", on(&path));
110 Violation::new(path, "reference_missing", message)
111 }
112 ErrorKind::CheckViolation => {
113 let message = format!("{}breaks the rule `{code}`", on(&path));
114 Violation::new(path, code, message)
115 }
116 ErrorKind::UniqueViolation if constraint.starts_with("uq_") => {
117 let message = format!("{}is already taken under the rule `{code}`", on(&path));
118 Violation::new(path, code, message)
119 }
120 _ if sqlstate == Some("23P01") => {
121 let message = format!("The record overlaps another one under the rule `{code}`");
122 Violation::new("", code, message)
123 }
124 _ => return None,
125 })
126}
127
128#[cfg(feature = "postgres")]
131fn key_column(detail: &str) -> Option<String> {
132 let rest = detail.strip_prefix("Key (")?;
133 let cols = &rest[..rest.find(")=")?];
134 (!cols.contains(',')).then(|| cols.trim().to_string())
135}
136
137#[cfg(all(test, feature = "postgres"))]
138mod constraint_violation_tests {
139 use super::*;
140 use sqlx::error::ErrorKind;
141
142 #[test]
143 fn a_missing_reference_names_the_column_and_never_the_table() {
144 let v = constraint_violation(
145 ErrorKind::ForeignKeyViolation,
146 Some("23503"),
147 Some("fk_subdistricts_district_id"),
148 "Key (district_id)=(1484f475-14be-4abc-985b-7291543d32bc) is not present in table \"districts\".",
149 None,
150 )
151 .unwrap();
152 assert_eq!((v.path.as_str(), v.code.as_str()), ("district_id", "reference_missing"));
153 assert!(!v.message.contains("districts\"") && !v.message.contains("fk_"), "{}", v.message);
154 }
155
156 #[test]
157 fn removing_a_referenced_record_is_still_referenced() {
158 let v = constraint_violation(
159 ErrorKind::ForeignKeyViolation,
160 Some("23503"),
161 Some("fk_provinces_country_id"),
162 "Key (id)=(866f1b4e-a9f3-4626-95d1-13c183916b4b) is still referenced from table \"provinces\".",
163 None,
164 )
165 .unwrap();
166 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "still_referenced"));
167 }
168
169 #[test]
170 fn a_rule_constraint_answers_its_rules_code() {
171 let check = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_countries_name__max_length"), "", Some("name")).unwrap();
172 assert_eq!((check.path.as_str(), check.code.as_str()), ("name", "max_length"));
173 let unique = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_accounts__account_code_taken"), "Key (company_id, code)=(a, b) already exists.", None).unwrap();
174 assert_eq!((unique.path.as_str(), unique.code.as_str()), ("", "account_code_taken"));
175 let overlap = constraint_violation(ErrorKind::Other, Some("23P01"), Some("ex_tax_rates__overlapping_validity"), "", None).unwrap();
176 assert_eq!(overlap.code, "overlapping_validity");
177 }
178
179 #[test]
180 fn an_error_that_names_no_rule_keeps_its_old_answer() {
181 assert!(constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("users_email_key"), "", None).is_none());
182 assert!(constraint_violation(ErrorKind::Other, Some("42P01"), None, "", None).is_none());
183 assert!(constraint_violation(ErrorKind::NotNullViolation, Some("23502"), Some("x"), "", None).is_none());
184 }
185}
186
187pub trait PersistentEntity: Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static {
196 fn entity_id(&self) -> String;
198
199 fn set_entity_id(&mut self, id: String);
201
202 fn created_at(&self) -> Option<DateTime<Utc>>;
204
205 fn set_created_at(&mut self, ts: DateTime<Utc>);
207
208 fn updated_at(&self) -> Option<DateTime<Utc>>;
210
211 fn set_updated_at(&mut self, ts: DateTime<Utc>);
213
214 fn deleted_at(&self) -> Option<DateTime<Utc>>;
216
217 fn set_deleted_at(&mut self, ts: Option<DateTime<Utc>>);
219
220 fn is_deleted(&self) -> bool {
222 self.deleted_at().is_some()
223 }
224
225 fn mark_deleted(&mut self) {
227 self.set_deleted_at(Some(Utc::now()));
228 self.set_updated_at(Utc::now());
229 }
230
231 fn restore(&mut self) {
233 self.set_deleted_at(None);
234 self.set_updated_at(Utc::now());
235 }
236
237 fn touch(&mut self) {
239 self.set_updated_at(Utc::now());
240 }
241
242 fn generate_id() -> String {
244 uuid::Uuid::new_v4().to_string()
245 }
246
247 fn write_protected_fields() -> &'static [&'static str] {
258 &[]
259 }
260}
261
262pub trait PartialUpdatable: PersistentEntity {
264 fn apply_partial_update(&mut self, fields: &HashMap<String, serde_json::Value>) -> Result<(), RepositoryError>;
266}
267
268pub trait Versioned {
270 fn version(&self) -> u64;
271 fn set_version(&mut self, version: u64);
272 fn increment_version(&mut self) {
273 self.set_version(self.version() + 1);
274 }
275}
276
277#[async_trait]
286pub trait CrudRepository<E>: Send + Sync
287where
288 E: PersistentEntity,
289{
290 async fn create(&self, entity: E) -> Result<E, RepositoryError>;
292
293 async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError>;
295
296 async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError>;
298
299 async fn update(&self, entity: E) -> Result<E, RepositoryError>;
301
302 async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError>;
304
305 async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError>;
307
308 async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError>;
310
311 async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
313
314 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
316
317 async fn count(&self) -> Result<u64, RepositoryError>;
319
320 async fn count_deleted(&self) -> Result<u64, RepositoryError>;
322
323 async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError>;
325
326 async fn empty_trash(&self) -> Result<u64, RepositoryError>;
328
329 async fn list_filtered(
334 &self,
335 page: u32,
336 limit: u32,
337 filters: HashMap<String, String>,
338 ) -> Result<(Vec<E>, u64), RepositoryError> {
339 let _ = filters; self.list(page, limit).await
341 }
342
343 async fn list_filtered_with_info(
348 &self,
349 page: u32,
350 limit: u32,
351 filters: HashMap<String, String>,
352 ) -> Result<(Vec<E>, backbone_orm::repository::PaginationInfo), RepositoryError> {
353 let (rows, total) = self.list_filtered(page, limit, filters).await?;
354 Ok((
355 rows,
356 backbone_orm::repository::PaginationInfo::new(page, limit, total),
357 ))
358 }
359
360 fn table_name(&self) -> Option<&str> {
372 None
373 }
374
375 async fn aggregate_filtered(
376 &self,
377 spec: &backbone_orm::repository::AggregateSpec,
378 filters: HashMap<String, String>,
379 ) -> Result<backbone_orm::repository::AggregateResult, RepositoryError> {
380 let _ = (spec, filters);
381 Err(RepositoryError::DatabaseError(
382 "aggregate is not supported by this repository".to_string(),
383 ))
384 }
385
386 async fn fetch_related_json(
391 &self,
392 _table: &str,
393 _ids: &[String],
394 ) -> Vec<serde_json::Value> {
395 Vec::new()
396 }
397
398 async fn exists(&self, id: &str) -> Result<bool, RepositoryError> {
400 Ok(self.find_by_id(id).await?.is_some())
401 }
402
403 async fn bulk_soft_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
413 let mut n = 0;
414 for id in ids {
415 if self.soft_delete(id).await? {
416 n += 1;
417 }
418 }
419 Ok(n)
420 }
421
422 async fn bulk_restore(&self, ids: &[String]) -> Result<Vec<E>, RepositoryError> {
424 let mut out = Vec::with_capacity(ids.len());
425 for id in ids {
426 if let Some(e) = self.restore(id).await? {
427 out.push(e);
428 }
429 }
430 Ok(out)
431 }
432
433 async fn bulk_hard_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
435 let mut n = 0;
436 for id in ids {
437 if self.hard_delete(id).await? {
438 n += 1;
439 }
440 }
441 Ok(n)
442 }
443
444 async fn restore_all(&self) -> Result<Vec<E>, RepositoryError> {
446 let mut restored = Vec::new();
447 loop {
450 let (batch, _) = self.list_deleted(1, 500).await?;
451 if batch.is_empty() {
452 break;
453 }
454 for entity in &batch {
455 if let Some(e) = self.restore(&entity.entity_id()).await? {
456 restored.push(e);
457 }
458 }
459 }
460 Ok(restored)
461 }
462
463 async fn bulk_update(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
465 let mut out = Vec::with_capacity(entities.len());
466 for entity in entities {
467 out.push(self.update(entity).await?);
468 }
469 Ok(out)
470 }
471}
472
473#[async_trait]
475pub trait SearchableRepository<E>: CrudRepository<E>
476where
477 E: PersistentEntity,
478{
479 async fn search(
481 &self,
482 filters: HashMap<String, String>,
483 page: u32,
484 limit: u32,
485 ) -> Result<(Vec<E>, u64), RepositoryError>;
486
487 async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError>;
489
490 async fn find_all_by_field(
492 &self,
493 field: &str,
494 value: &str,
495 page: u32,
496 limit: u32,
497 ) -> Result<(Vec<E>, u64), RepositoryError>;
498}
499
500#[cfg(feature = "postgres")]
505pub use postgres_traits::*;
506
507#[cfg(feature = "postgres")]
508mod postgres_traits {
509 use super::*;
510 use sqlx::postgres::PgRow;
511 use sqlx::FromRow;
512
513 pub trait PostgresEntity: PersistentEntity + for<'r> FromRow<'r, PgRow> + Unpin {
551 fn table_name() -> &'static str;
553
554 fn id_column() -> &'static str {
556 "id"
557 }
558
559 fn select_columns() -> &'static [&'static str];
561
562 fn insert_columns() -> &'static [&'static str];
564
565 fn update_columns() -> &'static [&'static str] {
567 Self::insert_columns()
568 }
569
570 fn bind_for_insert<'q>(
574 entity: &'q Self,
575 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
576 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
577
578 fn bind_for_update<'q>(
582 entity: &'q Self,
583 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
584 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
585 Self::bind_for_insert(entity, query)
587 }
588
589 fn select_by_id_query() -> String {
591 format!(
592 "SELECT {} FROM {} WHERE {} = $1 AND deleted_at IS NULL",
593 Self::select_columns().join(", "),
594 Self::table_name(),
595 Self::id_column()
596 )
597 }
598
599 fn select_by_id_including_deleted_query() -> String {
601 format!(
602 "SELECT {} FROM {} WHERE {} = $1",
603 Self::select_columns().join(", "),
604 Self::table_name(),
605 Self::id_column()
606 )
607 }
608
609 fn list_query() -> String {
611 format!(
612 "SELECT {} FROM {} WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2",
613 Self::select_columns().join(", "),
614 Self::table_name()
615 )
616 }
617
618 fn count_query() -> String {
620 format!(
621 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NULL",
622 Self::table_name()
623 )
624 }
625
626 fn list_deleted_query() -> String {
628 format!(
629 "SELECT {} FROM {} WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC LIMIT $1 OFFSET $2",
630 Self::select_columns().join(", "),
631 Self::table_name()
632 )
633 }
634
635 fn count_deleted_query() -> String {
637 format!(
638 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NOT NULL",
639 Self::table_name()
640 )
641 }
642
643 fn soft_delete_query() -> String {
645 format!(
646 "UPDATE {} SET deleted_at = NOW(), updated_at = NOW() WHERE {} = $1 AND deleted_at IS NULL",
647 Self::table_name(),
648 Self::id_column()
649 )
650 }
651
652 fn restore_query() -> String {
654 format!(
655 "UPDATE {} SET deleted_at = NULL, updated_at = NOW() WHERE {} = $1 AND deleted_at IS NOT NULL RETURNING {}",
656 Self::table_name(),
657 Self::id_column(),
658 Self::select_columns().join(", ")
659 )
660 }
661
662 fn hard_delete_query() -> String {
664 format!(
665 "DELETE FROM {} WHERE {} = $1",
666 Self::table_name(),
667 Self::id_column()
668 )
669 }
670
671 fn empty_trash_query() -> String {
673 format!(
674 "DELETE FROM {} WHERE deleted_at IS NOT NULL",
675 Self::table_name()
676 )
677 }
678 }
679}