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 RepositoryError {
48 pub fn from_source(e: impl Into<anyhow::Error>) -> Self {
54 let e: anyhow::Error = e.into();
55 let e = match e.downcast::<backbone_orm::QueryParamError>() {
57 Ok(refused) => {
58 return RepositoryError::Violations(vec![crate::violation::Violation::invalid_query(
59 &refused.parameter,
60 refused.reason.as_str(),
61 refused.message,
62 )])
63 }
64 Err(e) => e,
65 };
66 #[cfg(feature = "postgres")]
67 let e = match e.downcast::<sqlx::Error>() {
68 Ok(db) => return RepositoryError::from(db),
69 Err(e) => e,
70 };
71 RepositoryError::DatabaseError(e.to_string())
72 }
73
74 pub fn from_batch_source(e: impl Into<anyhow::Error>) -> Self {
78 let e: anyhow::Error = e.into();
79 match e.downcast::<backbone_orm::BatchItemError>() {
80 Ok(item) => Self::from_source(item.error).at_batch_index(item.index),
81 Err(e) => Self::from_source(e),
82 }
83 }
84
85 pub fn at_batch_index(self, index: usize) -> Self {
89 let item = |message: String| format!("item {index}: {message}");
90 match self {
91 RepositoryError::Violations(violations) => RepositoryError::Violations(
92 violations
93 .into_iter()
94 .map(|mut v| {
95 v.path = crate::violation::batch_item_path(index, &v.path);
96 v
97 })
98 .collect(),
99 ),
100 RepositoryError::NotFound => RepositoryError::ValidationError(item(
101 "the record no longer exists or was deleted".to_string(),
102 )),
103 RepositoryError::AlreadyExists(m) => RepositoryError::AlreadyExists(item(m)),
104 RepositoryError::ValidationError(m) => RepositoryError::ValidationError(item(m)),
105 RepositoryError::DatabaseError(m) => RepositoryError::DatabaseError(item(m)),
106 RepositoryError::SerializationError(m) => RepositoryError::SerializationError(item(m)),
107 RepositoryError::Conflict(m) => RepositoryError::Conflict(item(m)),
108 RepositoryError::InternalError(m) => RepositoryError::InternalError(item(m)),
109 }
110 }
111}
112
113impl From<serde_json::Error> for RepositoryError {
114 fn from(e: serde_json::Error) -> Self {
115 RepositoryError::SerializationError(e.to_string())
116 }
117}
118
119#[cfg(feature = "postgres")]
120impl From<sqlx::Error> for RepositoryError {
121 fn from(e: sqlx::Error) -> Self {
122 match e {
123 sqlx::Error::RowNotFound => RepositoryError::NotFound,
124 sqlx::Error::Database(db_err) => {
125 if let Some(v) = scope_refusal(db_err.code().as_deref(), db_err.message()) {
126 return RepositoryError::Violations(vec![v]);
127 }
128 let pg = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>();
129 if let Some(v) = constraint_violation(
130 db_err.kind(),
131 db_err.code().as_deref(),
132 db_err.constraint(),
133 pg.and_then(|p| p.detail()).unwrap_or(""),
134 pg.and_then(|p| p.column()),
135 pg.and_then(|p| p.table()),
136 ) {
137 return RepositoryError::Violations(vec![v]);
138 }
139 let msg = db_err.message().to_string();
140 if msg.contains("duplicate key") || msg.contains("unique constraint") {
141 RepositoryError::Violations(vec![taken_violation("", "taken")])
143 } else {
144 RepositoryError::DatabaseError(msg)
145 }
146 }
147 _ => RepositoryError::DatabaseError(e.to_string()),
148 }
149 }
150}
151
152#[cfg(feature = "postgres")]
159fn scope_refusal(sqlstate: Option<&str>, message: &str) -> Option<crate::violation::Violation> {
160 (sqlstate == Some("42501") && message.contains("row-level security policy")).then(|| {
161 crate::violation::Violation::new(
162 "",
163 "scope_refused",
164 "The record is outside the company or unit this request may write to",
165 )
166 })
167}
168
169#[cfg(feature = "postgres")]
171fn taken_violation(path: &str, code: &str) -> crate::violation::Violation {
172 let message = match path {
173 "" => "This value is already taken by another record".to_string(),
174 p => format!("{p} is already taken by another record"),
175 };
176 crate::violation::Violation::new(path, code, message)
177}
178
179#[cfg(feature = "postgres")]
195fn constraint_violation(
196 kind: sqlx::error::ErrorKind,
197 sqlstate: Option<&str>,
198 constraint: Option<&str>,
199 detail: &str,
200 column: Option<&str>,
201 table: Option<&str>,
202) -> Option<crate::violation::Violation> {
203 use crate::violation::{code_from_constraint, Violation};
204 use sqlx::error::ErrorKind;
205 if kind == ErrorKind::NotNullViolation || sqlstate == Some("23502") {
206 let column = column.filter(|c| !c.is_empty())?;
208 return Some(Violation::new(
209 column,
210 "required",
211 format!("{column} is required"),
212 ));
213 }
214 if kind == ErrorKind::UniqueViolation || sqlstate == Some("23505") {
215 let generated = constraint.and_then(generated_code);
216 let path = column
217 .filter(|c| !c.is_empty())
218 .map(str::to_string)
219 .or_else(|| key_column(detail))
220 .or_else(|| {
221 let (c, t) = (constraint?, table?);
222 constraint_column(c, t, generated.as_deref().unwrap_or(""))
223 })
224 .unwrap_or_default();
225 return Some(taken_violation(&path, generated.as_deref().unwrap_or("taken")));
226 }
227 let constraint = constraint?;
228 let code = code_from_constraint(constraint);
229 let path = column
230 .filter(|c| !c.is_empty())
231 .map(str::to_string)
232 .or_else(|| key_column(detail))
233 .or_else(|| table.and_then(|t| constraint_column(constraint, t, &code)))
234 .unwrap_or_default();
235 Some(match kind {
236 ErrorKind::ForeignKeyViolation if detail.contains("still referenced") => Violation::new(
237 "",
238 "still_referenced",
239 "This record is still referenced by another record, so it cannot be removed",
240 ),
241 ErrorKind::ForeignKeyViolation => {
242 let message = match path.as_str() {
243 "" => "The record refers to a record that does not exist".to_string(),
244 p => format!("{p} refers to a record that does not exist"),
245 };
246 Violation::new(path, "reference_missing", message)
247 }
248 ErrorKind::CheckViolation => {
249 let message = check_sentence(&path, &code);
250 Violation::new(path, code, message)
251 }
252 _ if sqlstate == Some("23P01") => Violation::new(
253 "",
254 code,
255 "The record overlaps another one it may not overlap",
256 ),
257 _ => return None,
258 })
259}
260
261#[cfg(feature = "postgres")]
266fn check_sentence(path: &str, code: &str) -> String {
267 let field = if path.is_empty() {
268 "A value".to_string()
269 } else {
270 path.to_string()
271 };
272 let predicate = match code {
273 "required" => "is required",
274 "max_length" => "is too long",
275 "min_length" => "is too short",
276 "length" => "does not have the required length",
277 "max" => "is too large",
278 "min" => "is too small",
279 "range" => "is out of the allowed range",
280 "positive" => "must be positive",
281 "non_negative" => "must not be negative",
282 "pattern" => "does not have the expected form",
283 "alpha_dash" => "may hold only letters, digits, '_' and '-'",
284 "alpha_num" => "may hold only letters and digits",
285 "values" => "is not one of the allowed values",
286 "email" => "must be an email address",
287 "url" => "must be an absolute URL",
288 "state_transition_not_declared" => "cannot move to that state from its current one",
289 _ if path.is_empty() => return "The record does not satisfy one of its rules".to_string(),
290 _ => "does not satisfy one of its rules",
291 };
292 format!("{field} {predicate}")
293}
294
295#[cfg(feature = "postgres")]
298fn generated_code(constraint: &str) -> Option<String> {
299 let code = crate::violation::code_from_constraint(constraint);
300 (code != constraint).then_some(code)
301}
302
303#[cfg(feature = "postgres")]
305const TENANT_KEYS: &[&str] = &["company_id", "tenant_id", "org_unit_id", "organization_id"];
306
307#[cfg(feature = "postgres")]
325fn constraint_column(constraint: &str, table: &str, code: &str) -> Option<String> {
326 let ident = |s: &str| {
327 (!s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
328 .then(|| s.to_string())
329 };
330 if let Some(rest) = constraint.strip_prefix("fk_") {
331 let column = rest.strip_prefix(table)?.strip_prefix('_')?;
332 return ident(column.strip_suffix("_fk").unwrap_or(column));
333 }
334 let head = ["ck_", "uq_", "ex_", "tg_"]
335 .iter()
336 .find_map(|p| constraint.strip_prefix(p))?
337 .split_once("__")?
338 .0;
339 if ends_in_digest(head) {
340 return None;
341 }
342 let mut rest = head.strip_prefix(table)?.strip_prefix('_')?;
343 if !code.is_empty() {
344 if let Some(field) = rest.strip_suffix(code).and_then(|f| f.strip_suffix('_')) {
345 if !field.is_empty() {
346 rest = field;
347 }
348 }
349 }
350 while let Some(next) = TENANT_KEYS.iter().find_map(|key| {
351 rest.strip_prefix(key)?.strip_prefix('_').filter(|r| !r.is_empty())
352 }) {
353 rest = next;
354 }
355 ident(rest)
356}
357
358#[cfg(feature = "postgres")]
361fn ends_in_digest(head: &str) -> bool {
362 let bytes = head.as_bytes();
363 bytes.len() > 9
364 && bytes[bytes.len() - 9] == b'_'
365 && bytes[bytes.len() - 8..].iter().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(b))
366}
367
368#[cfg(feature = "postgres")]
370const SCOPE_COLUMNS: &[&str] = &[
371 "company_id",
372 "tenant_id",
373 "org_unit_id",
374 "organization_id",
375 "deleted_at",
376];
377
378#[cfg(feature = "postgres")]
382fn key_column(detail: &str) -> Option<String> {
383 let rest = detail.strip_prefix("Key (")?;
384 let cols = &rest[..rest.find(")=")?];
385 let names: Vec<String> = cols
386 .split(',')
387 .filter_map(|part| {
388 let inner = part.rsplit('(').next().unwrap_or(part);
390 let ident: String = inner
391 .trim()
392 .trim_matches('"')
393 .chars()
394 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
395 .collect();
396 (!ident.is_empty()).then_some(ident)
397 })
398 .collect();
399 names
400 .iter()
401 .rev()
402 .find(|n| !SCOPE_COLUMNS.contains(&n.as_str()))
403 .or_else(|| (names.len() == 1).then(|| &names[0]))
404 .cloned()
405}
406
407#[cfg(all(test, feature = "postgres"))]
408mod constraint_violation_tests {
409 use super::*;
410 use sqlx::error::ErrorKind;
411
412 #[test]
413 fn a_missing_reference_names_the_column_and_never_the_table() {
414 let v = constraint_violation(
415 ErrorKind::ForeignKeyViolation,
416 Some("23503"),
417 Some("fk_subdistricts_district_id"),
418 "Key (district_id)=(1484f475-14be-4abc-985b-7291543d32bc) is not present in table \"districts\".",
419 None,
420 None,
421 )
422 .unwrap();
423 assert_eq!((v.path.as_str(), v.code.as_str()), ("district_id", "reference_missing"));
424 assert!(!v.message.contains("districts\"") && !v.message.contains("fk_"), "{}", v.message);
425 }
426
427 #[test]
428 fn removing_a_referenced_record_is_still_referenced() {
429 let v = constraint_violation(
430 ErrorKind::ForeignKeyViolation,
431 Some("23503"),
432 Some("fk_provinces_country_id"),
433 "Key (id)=(866f1b4e-a9f3-4626-95d1-13c183916b4b) is still referenced from table \"provinces\".",
434 None,
435 None,
436 )
437 .unwrap();
438 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "still_referenced"));
439 }
440
441 #[test]
442 fn a_rule_constraint_answers_its_rules_code() {
443 let check = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_countries_name_max_length__max_length"), "", None, Some("countries")).unwrap();
445 assert_eq!((check.path.as_str(), check.code.as_str()), ("name", "max_length"));
446 let unique = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_accounts__account_code_taken"), "Key (company_id, code)=(a, b) already exists.", None, Some("accounts")).unwrap();
447 assert_eq!((unique.path.as_str(), unique.code.as_str()), ("code", "account_code_taken"));
448 let overlap = constraint_violation(ErrorKind::Other, Some("23P01"), Some("ex_tax_rates__overlapping_validity"), "", None, Some("tax_rates")).unwrap();
449 assert_eq!(overlap.code, "overlapping_validity");
450 }
451
452 #[test]
453 fn a_missing_required_value_is_a_required_violation_on_its_column() {
454 let v = constraint_violation(
456 ErrorKind::NotNullViolation,
457 Some("23502"),
458 None,
459 "",
460 Some("employee_id"),
461 Some("payslips"),
462 )
463 .unwrap();
464 assert_eq!(
465 (v.path.as_str(), v.code.as_str()),
466 ("employee_id", "required")
467 );
468 assert_eq!(v.message, "employee_id is required");
469 }
470
471 #[test]
472 fn a_composite_unique_key_names_its_own_column_not_the_tenant() {
473 let v = constraint_violation(
474 ErrorKind::UniqueViolation,
475 Some("23505"),
476 Some("uq_accounts__code_taken"),
477 "Key (company_id, code)=(7c0a…, 1100) already exists.",
478 None,
479 Some("accounts"),
480 )
481 .unwrap();
482 assert_eq!(v.path, "code");
483 assert!(
484 v.message.starts_with("code "),
485 "a sentence with a subject: {}",
486 v.message
487 );
488 let scoped = constraint_violation(
489 ErrorKind::UniqueViolation,
490 Some("23505"),
491 Some("uq_items__sku_taken"),
492 "Key (org_unit_id, lower(sku::text))=(a, b) already exists.",
493 None,
494 Some("items"),
495 )
496 .unwrap();
497 assert_eq!(scoped.path, "sku");
498 let bare = constraint_violation(
499 ErrorKind::UniqueViolation,
500 Some("23505"),
501 Some("uq_x__taken"),
502 "",
503 None,
504 None,
505 )
506 .unwrap();
507 assert_eq!(bare.path, "");
508 assert!(
509 bare.message.starts_with(char::is_uppercase),
510 "{}",
511 bare.message
512 );
513 }
514
515 #[test]
516 fn a_check_reads_as_a_sentence_never_as_the_rule_identifier() {
517 let v = constraint_violation(
518 ErrorKind::CheckViolation,
519 Some("23514"),
520 Some("ck_tickets_title_max_length__max_length"),
521 "",
522 None,
523 Some("tickets"),
524 )
525 .unwrap();
526 assert_eq!(v.code, "max_length", "the code stays machine-readable");
527 assert!(
528 !v.message.contains('`') && !v.message.contains("max_length"),
529 "{}",
530 v.message
531 );
532 assert!(v.message.starts_with("title "), "{}", v.message);
533 let row = constraint_violation(
536 ErrorKind::CheckViolation,
537 Some("23514"),
538 Some("ck_journals_lines_must_balance_1f2e3d4c__unbalanced"),
539 "",
540 None,
541 Some("journals"),
542 )
543 .unwrap();
544 assert_eq!((row.path.as_str(), row.code.as_str()), ("", "unbalanced"));
545 assert!(
546 !row.message.contains("unbalanced") && !row.message.contains('`'),
547 "{}",
548 row.message
549 );
550 let tg = constraint_violation(
552 ErrorKind::CheckViolation,
553 Some("23514"),
554 Some("tg_journals__state_transition_not_declared"),
555 "",
556 Some("status"),
557 Some("journals"),
558 )
559 .unwrap();
560 assert_eq!(
561 (tg.path.as_str(), tg.code.as_str()),
562 ("status", "state_transition_not_declared")
563 );
564 assert!(
565 !tg.message.contains("state_transition_not_declared"),
566 "{}",
567 tg.message
568 );
569 }
570
571 #[test]
572 fn a_database_error_inside_any_error_converts_as_the_database_error_does() {
573 let wrapped = anyhow::Error::new(sqlx::Error::RowNotFound).context("loading the row");
574 assert!(matches!(RepositoryError::from_source(wrapped), RepositoryError::NotFound));
575 assert!(matches!(RepositoryError::from_source(sqlx::Error::RowNotFound), RepositoryError::NotFound));
576 let other = RepositoryError::from_source(anyhow::anyhow!("pool closed"));
577 assert!(matches!(other, RepositoryError::DatabaseError(ref m) if m == "pool closed"), "{other:?}");
578 }
579
580 #[test]
581 fn every_unique_key_answers_taken_and_never_names_its_index() {
582 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("users_email_key"), "Key (email)=(a@b.c) already exists.", None, Some("users")).unwrap();
584 assert_eq!((v.path.as_str(), v.code.as_str()), ("email", "taken"));
585 assert!(!v.message.contains("users_email_key"), "{}", v.message);
586 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("idx_projects_contact_email"), "", None, Some("projects")).unwrap();
588 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "taken"));
589 assert!(!v.message.contains("idx_"), "{}", v.message);
590 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_projects_contact_email__unique"), "", None, Some("projects")).unwrap();
592 assert_eq!((v.path.as_str(), v.code.as_str()), ("contact_email", "unique"));
593 assert!(!v.message.contains("uq_"), "{}", v.message);
594 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_tickets_title__unique"), "", None, Some("tickets")).unwrap();
595 assert_eq!((v.path.as_str(), v.code.as_str()), ("title", "unique"));
596 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_projects_company_id_code__code_taken"), "", None, Some("projects")).unwrap();
598 assert_eq!((v.path.as_str(), v.code.as_str()), ("code", "code_taken"));
599 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_items_company_id_org_unit_id_sku__unique"), "", None, Some("items")).unwrap();
600 assert_eq!(v.path, "sku");
601 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_members_org_unit_id__unique"), "", None, Some("members")).unwrap();
603 assert_eq!(v.path, "org_unit_id");
604 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_projects_company_id_some_long_fiel_0a1b2c3d__code_taken"), "", None, Some("projects")).unwrap();
606 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "code_taken"));
607 let v = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_tickets_org_unit_id_reference"), "", None, Some("tickets")).unwrap();
609 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "taken"));
610 assert!(!v.message.contains("uq_"), "{}", v.message);
611 }
612
613 #[test]
614 fn a_foreign_key_on_a_table_with_rls_names_its_column_from_the_constraint() {
615 let v = constraint_violation(ErrorKind::ForeignKeyViolation, Some("23503"), Some("fk_tickets_project_id"), "", None, Some("tickets")).unwrap();
617 assert_eq!((v.path.as_str(), v.code.as_str()), ("project_id", "reference_missing"));
618 assert!(!v.message.contains("fk_"), "{}", v.message);
619 let link = constraint_violation(ErrorKind::ForeignKeyViolation, Some("23503"), Some("fk_user_roles_role_id_fk"), "", None, Some("user_roles")).unwrap();
620 assert_eq!(link.path, "role_id");
621 let other = constraint_violation(ErrorKind::ForeignKeyViolation, Some("23503"), Some("fk_other_project_id"), "", None, Some("tickets")).unwrap();
623 assert_eq!(other.path, "");
624 }
625
626 #[test]
627 fn a_rule_check_about_a_field_answers_on_that_field() {
628 let v = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_projects_kind__website_required_for_client"), "", None, Some("projects")).unwrap();
629 assert_eq!((v.path.as_str(), v.code.as_str()), ("kind", "website_required_for_client"));
630 assert!(!v.message.contains("ck_") && !v.message.contains("website_required"), "{}", v.message);
631 let v = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_shifts_call_note_max_length__max_length"), "", None, Some("shifts")).unwrap();
633 assert_eq!(v.path, "call_note");
634 let v = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_projects_a_very_long_field_name_dead00ff__website_required_for_client"), "", None, Some("projects")).unwrap();
636 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "website_required_for_client"));
637 }
638
639 #[test]
640 fn a_row_security_refusal_is_scope_refused_and_a_missing_grant_is_not() {
641 let v = scope_refusal(Some("42501"), "new row violates row-level security policy for table \"projects\"").unwrap();
642 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "scope_refused"));
643 assert!(!v.message.contains("projects") && !v.message.contains("policy"), "{}", v.message);
644 assert!(scope_refusal(Some("42501"), "permission denied for table projects").is_none());
645 assert!(scope_refusal(Some("23505"), "row-level security policy").is_none());
646 }
647
648 #[test]
649 fn an_error_that_names_no_rule_keeps_its_old_answer() {
650 assert!(
651 constraint_violation(ErrorKind::Other, Some("42P01"), None, "", None, None).is_none()
652 );
653 assert!(constraint_violation(
655 ErrorKind::NotNullViolation,
656 Some("23502"),
657 None,
658 "",
659 None,
660 None
661 )
662 .is_none());
663 }
664}
665
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678pub struct Reference {
679 pub field: &'static str,
681 pub table: &'static str,
684 pub column: &'static str,
686}
687
688impl Reference {
689 pub const fn new(field: &'static str, table: &'static str, column: &'static str) -> Self {
691 Self { field, table, column }
692 }
693}
694
695pub trait PersistentEntity: Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static {
700 fn entity_id(&self) -> String;
702
703 fn set_entity_id(&mut self, id: String);
705
706 fn created_at(&self) -> Option<DateTime<Utc>>;
708
709 fn set_created_at(&mut self, ts: DateTime<Utc>);
711
712 fn updated_at(&self) -> Option<DateTime<Utc>>;
714
715 fn set_updated_at(&mut self, ts: DateTime<Utc>);
717
718 fn deleted_at(&self) -> Option<DateTime<Utc>>;
720
721 fn set_deleted_at(&mut self, ts: Option<DateTime<Utc>>);
723
724 fn is_deleted(&self) -> bool {
726 self.deleted_at().is_some()
727 }
728
729 fn mark_deleted(&mut self) {
731 self.set_deleted_at(Some(Utc::now()));
732 self.set_updated_at(Utc::now());
733 }
734
735 fn restore(&mut self) {
737 self.set_deleted_at(None);
738 self.set_updated_at(Utc::now());
739 }
740
741 fn touch(&mut self) {
743 self.set_updated_at(Utc::now());
744 }
745
746 fn generate_id() -> String {
748 uuid::Uuid::new_v4().to_string()
749 }
750
751 fn write_protected_fields() -> &'static [&'static str] {
762 &[]
763 }
764
765 fn state_fields() -> &'static [&'static str] {
775 &[]
776 }
777
778 fn references() -> &'static [Reference] {
790 &[]
791 }
792}
793
794pub trait PartialUpdatable: PersistentEntity {
796 fn apply_partial_update(&mut self, fields: &HashMap<String, serde_json::Value>) -> Result<(), RepositoryError>;
798}
799
800pub trait Versioned {
802 fn version(&self) -> u64;
803 fn set_version(&mut self, version: u64);
804 fn increment_version(&mut self) {
805 self.set_version(self.version() + 1);
806 }
807}
808
809#[async_trait]
818pub trait CrudRepository<E>: Send + Sync
819where
820 E: PersistentEntity,
821{
822 async fn create(&self, entity: E) -> Result<E, RepositoryError>;
824
825 async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError>;
827
828 async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError>;
830
831 async fn update(&self, entity: E) -> Result<E, RepositoryError>;
833
834 async fn update_changed(&self, before: &E, after: E) -> Result<Option<E>, RepositoryError> {
845 let _ = before;
846 match self.update(after).await {
847 Ok(saved) => Ok(Some(saved)),
848 Err(RepositoryError::NotFound) => Ok(None),
849 Err(e) => Err(e),
850 }
851 }
852
853 async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError>;
855
856 async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError>;
858
859 async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError>;
861
862 async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
864
865 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
867
868 async fn list_deleted_filtered(
874 &self,
875 page: u32,
876 limit: u32,
877 filters: HashMap<String, String>,
878 ) -> Result<(Vec<E>, u64), RepositoryError> {
879 let _ = filters;
880 self.list_deleted(page, limit).await
881 }
882
883 async fn count(&self) -> Result<u64, RepositoryError>;
885
886 async fn count_deleted(&self) -> Result<u64, RepositoryError>;
888
889 async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError>;
891
892 async fn empty_trash(&self) -> Result<u64, RepositoryError>;
894
895 async fn list_filtered(
900 &self,
901 page: u32,
902 limit: u32,
903 filters: HashMap<String, String>,
904 ) -> Result<(Vec<E>, u64), RepositoryError> {
905 let _ = filters; self.list(page, limit).await
907 }
908
909 async fn list_filtered_with_info(
914 &self,
915 page: u32,
916 limit: u32,
917 filters: HashMap<String, String>,
918 ) -> Result<(Vec<E>, backbone_orm::repository::PaginationInfo), RepositoryError> {
919 let (rows, total) = self.list_filtered(page, limit, filters).await?;
920 Ok((
921 rows,
922 backbone_orm::repository::PaginationInfo::new(page, limit, total),
923 ))
924 }
925
926 fn table_name(&self) -> Option<&str> {
938 None
939 }
940
941 fn write_pool(&self) -> Option<&backbone_orm::PgPool> {
949 None
950 }
951
952 async fn aggregate_filtered(
953 &self,
954 spec: &backbone_orm::repository::AggregateSpec,
955 filters: HashMap<String, String>,
956 ) -> Result<backbone_orm::repository::AggregateResult, RepositoryError> {
957 let _ = (spec, filters);
958 Err(RepositoryError::DatabaseError(
959 "aggregate is not supported by this repository".to_string(),
960 ))
961 }
962
963 async fn fetch_related_json(
968 &self,
969 _table: &str,
970 _ids: &[String],
971 ) -> Vec<serde_json::Value> {
972 Vec::new()
973 }
974
975 async fn reference_visible(
984 &self,
985 table: &str,
986 column: &str,
987 value: &serde_json::Value,
988 ) -> Result<Option<bool>, RepositoryError> {
989 let _ = (table, column, value);
990 Ok(None)
991 }
992
993 async fn exists(&self, id: &str) -> Result<bool, RepositoryError> {
995 Ok(self.find_by_id(id).await?.is_some())
996 }
997
998 async fn bulk_soft_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
1008 let mut n = 0;
1009 for id in ids {
1010 if self.soft_delete(id).await? {
1011 n += 1;
1012 }
1013 }
1014 Ok(n)
1015 }
1016
1017 async fn bulk_restore(&self, ids: &[String]) -> Result<Vec<E>, RepositoryError> {
1019 let mut out = Vec::with_capacity(ids.len());
1020 for id in ids {
1021 if let Some(e) = self.restore(id).await? {
1022 out.push(e);
1023 }
1024 }
1025 Ok(out)
1026 }
1027
1028 async fn bulk_hard_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
1030 let mut n = 0;
1031 for id in ids {
1032 if self.hard_delete(id).await? {
1033 n += 1;
1034 }
1035 }
1036 Ok(n)
1037 }
1038
1039 async fn restore_all(&self) -> Result<Vec<E>, RepositoryError> {
1041 let mut restored = Vec::new();
1042 loop {
1045 let (batch, _) = self.list_deleted(1, 500).await?;
1046 if batch.is_empty() {
1047 break;
1048 }
1049 for entity in &batch {
1050 if let Some(e) = self.restore(&entity.entity_id()).await? {
1051 restored.push(e);
1052 }
1053 }
1054 }
1055 Ok(restored)
1056 }
1057
1058 async fn bulk_update(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
1060 let mut out = Vec::with_capacity(entities.len());
1061 for entity in entities {
1062 out.push(self.update(entity).await?);
1063 }
1064 Ok(out)
1065 }
1066
1067 async fn bulk_update_changed(&self, pairs: Vec<(E, E)>) -> Result<Vec<E>, RepositoryError> {
1075 let mut out = Vec::with_capacity(pairs.len());
1076 for (index, (before, after)) in pairs.into_iter().enumerate() {
1077 match self.update_changed(&before, after).await {
1078 Ok(Some(saved)) => out.push(saved),
1079 Ok(None) => return Err(RepositoryError::NotFound.at_batch_index(index)),
1080 Err(e) => return Err(e.at_batch_index(index)),
1081 }
1082 }
1083 Ok(out)
1084 }
1085}
1086
1087#[async_trait]
1089pub trait SearchableRepository<E>: CrudRepository<E>
1090where
1091 E: PersistentEntity,
1092{
1093 async fn search(
1095 &self,
1096 filters: HashMap<String, String>,
1097 page: u32,
1098 limit: u32,
1099 ) -> Result<(Vec<E>, u64), RepositoryError>;
1100
1101 async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError>;
1103
1104 async fn find_all_by_field(
1106 &self,
1107 field: &str,
1108 value: &str,
1109 page: u32,
1110 limit: u32,
1111 ) -> Result<(Vec<E>, u64), RepositoryError>;
1112}
1113
1114#[cfg(feature = "postgres")]
1119pub use postgres_traits::*;
1120
1121#[cfg(feature = "postgres")]
1122mod postgres_traits {
1123 use super::*;
1124 use sqlx::postgres::PgRow;
1125 use sqlx::FromRow;
1126
1127 pub trait PostgresEntity: PersistentEntity + for<'r> FromRow<'r, PgRow> + Unpin {
1165 fn table_name() -> &'static str;
1167
1168 fn id_column() -> &'static str {
1170 "id"
1171 }
1172
1173 fn select_columns() -> &'static [&'static str];
1175
1176 fn insert_columns() -> &'static [&'static str];
1178
1179 fn update_columns() -> &'static [&'static str] {
1181 Self::insert_columns()
1182 }
1183
1184 fn bind_for_insert<'q>(
1188 entity: &'q Self,
1189 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
1190 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
1191
1192 fn bind_for_update<'q>(
1196 entity: &'q Self,
1197 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
1198 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
1199 Self::bind_for_insert(entity, query)
1201 }
1202
1203 fn select_by_id_query() -> String {
1205 format!(
1206 "SELECT {} FROM {} WHERE {} = $1 AND deleted_at IS NULL",
1207 Self::select_columns().join(", "),
1208 Self::table_name(),
1209 Self::id_column()
1210 )
1211 }
1212
1213 fn select_by_id_including_deleted_query() -> String {
1215 format!(
1216 "SELECT {} FROM {} WHERE {} = $1",
1217 Self::select_columns().join(", "),
1218 Self::table_name(),
1219 Self::id_column()
1220 )
1221 }
1222
1223 fn list_query() -> String {
1225 format!(
1226 "SELECT {} FROM {} WHERE deleted_at IS NULL ORDER BY created_at DESC, {} LIMIT $1 OFFSET $2",
1227 Self::select_columns().join(", "),
1228 Self::table_name(),
1229 Self::id_column()
1230 )
1231 }
1232
1233 fn count_query() -> String {
1235 format!(
1236 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NULL",
1237 Self::table_name()
1238 )
1239 }
1240
1241 fn list_deleted_query() -> String {
1243 format!(
1244 "SELECT {} FROM {} WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC, {} LIMIT $1 OFFSET $2",
1245 Self::select_columns().join(", "),
1246 Self::table_name(),
1247 Self::id_column()
1248 )
1249 }
1250
1251 fn count_deleted_query() -> String {
1253 format!(
1254 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NOT NULL",
1255 Self::table_name()
1256 )
1257 }
1258
1259 fn soft_delete_query() -> String {
1261 format!(
1262 "UPDATE {} SET deleted_at = NOW(), updated_at = NOW() WHERE {} = $1 AND deleted_at IS NULL",
1263 Self::table_name(),
1264 Self::id_column()
1265 )
1266 }
1267
1268 fn restore_query() -> String {
1270 format!(
1271 "UPDATE {} SET deleted_at = NULL, updated_at = NOW() WHERE {} = $1 AND deleted_at IS NOT NULL RETURNING {}",
1272 Self::table_name(),
1273 Self::id_column(),
1274 Self::select_columns().join(", ")
1275 )
1276 }
1277
1278 fn hard_delete_query() -> String {
1280 format!(
1281 "DELETE FROM {} WHERE {} = $1",
1282 Self::table_name(),
1283 Self::id_column()
1284 )
1285 }
1286
1287 fn empty_trash_query() -> String {
1289 format!(
1290 "DELETE FROM {} WHERE deleted_at IS NOT NULL",
1291 Self::table_name()
1292 )
1293 }
1294 }
1295}