megacommerce_shared/store/
errors.rs

1use std::{error::Error, fmt, sync::Arc};
2
3use regex::Regex;
4use sqlx::error::Error as SqlxError;
5use sqlx::postgres::PgDatabaseError;
6use tonic::Code;
7
8use crate::models::context::Context;
9use crate::models::errors::{
10  AppError, AppErrorErrors, ErrorType, InternalError, MSG_ID_ERR_INTERNAL,
11};
12
13#[derive(Debug)]
14pub struct DBError {
15  pub err_type: ErrorType,
16  pub err: Box<dyn Error + Send + Sync>,
17  pub msg: String,
18  pub path: String,
19  pub details: String,
20}
21
22impl fmt::Display for DBError {
23  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24    let mut parts = Vec::new();
25
26    if !self.path.is_empty() {
27      parts.push(format!("path: {}", self.path));
28    }
29
30    parts.push(format!("err_type: {}", self.err_type));
31
32    if !self.msg.is_empty() {
33      parts.push(format!("msg: {}", self.msg));
34    }
35
36    if !self.details.is_empty() {
37      parts.push(format!("details: {}", self.details));
38    }
39
40    parts.push(format!("err: {}", self.err));
41
42    write!(f, "{}", parts.join(", "))
43  }
44}
45
46impl From<InternalError> for DBError {
47  fn from(e: InternalError) -> Self {
48    DBError { err_type: e.err_type, err: e.err, msg: e.msg, path: e.path, details: "".into() }
49  }
50}
51
52impl Error for DBError {}
53
54impl DBError {
55  pub fn new(
56    err_type: ErrorType,
57    err: Box<dyn Error + Send + Sync>,
58    msg: impl Into<String>,
59    path: impl Into<String>,
60    details: impl Into<String>,
61  ) -> Self {
62    Self { err_type, err, msg: msg.into(), path: path.into(), details: details.into() }
63  }
64
65  pub fn to_app_error_internal(self, ctx: Arc<Context>, path: String) -> AppError {
66    let errors = AppErrorErrors { err: Some(self.err), ..Default::default() };
67    AppError::new(
68      ctx,
69      path,
70      MSG_ID_ERR_INTERNAL,
71      None,
72      self.details,
73      Code::Internal.into(),
74      Some(errors),
75    )
76  }
77}
78
79pub fn handle_db_error(err: SqlxError, path: &str) -> DBError {
80  match err {
81    SqlxError::Database(db_err) => {
82      let pg_err = db_err.downcast_ref::<PgDatabaseError>();
83
84      // Extract details before moving db_err into the Box
85      let details = pg_err.detail().unwrap_or("").to_string();
86      let msg = match pg_err.code() {
87        // Constraint violations
88        "23505" => {
89          // unique_violation
90          parse_duplicate_field_db_error(pg_err)
91        }
92        "23503" => {
93          // foreign_key_violation
94          "referenced record is not found".to_string()
95        }
96        "23502" => {
97          // not_null_violation
98          format!("{} cannot be null", parse_db_field_name(pg_err))
99        }
100        // Connection/availability errors
101        "08000" | "08003" | "08006" => "database connection exception".to_string(),
102        // Permission errors
103        "42501" => "insufficient permissions to perform an action".to_string(),
104        _ => "database error".to_string(),
105      };
106
107      let err_type = match pg_err.code() {
108        "23505" => ErrorType::UniqueViolation,
109        "23503" => ErrorType::ForeignKeyViolation,
110        "23502" => ErrorType::NotNullViolation,
111        "08000" | "08003" | "08006" => ErrorType::Connection,
112        "42501" => ErrorType::Privileges,
113        _ => ErrorType::Internal,
114      };
115
116      DBError::new(err_type, Box::new(SqlxError::Database(db_err)), msg, path, details)
117    }
118
119    SqlxError::RowNotFound => DBError::new(
120      ErrorType::NoRows,
121      Box::new(SqlxError::RowNotFound),
122      "the requested resource is not found",
123      path,
124      "",
125    ),
126
127    _ => DBError::new(ErrorType::Internal, Box::new(err), "database error", path, ""),
128  }
129}
130
131// Extract the duplicate field from error detail
132// Example: "Key (email)=(test@example.com) already exists.
133fn parse_duplicate_field_db_error(err: &PgDatabaseError) -> String {
134  if let Some(detail) = err.detail() {
135    if let Some(parts) = detail.split(")=(").next() {
136      let field = parts.trim_start_matches("Key (");
137      return format!("{} already exists", field);
138    }
139  }
140  err.detail().unwrap_or("").to_string()
141}
142
143// Extract field name from error message
144// Example: "null value in column \"email\" violates not-null constraint
145fn parse_db_field_name(err: &PgDatabaseError) -> String {
146  let re = Regex::new(r#"column "(.+?)""#).unwrap();
147  if let Some(captures) = re.captures(err.message()) {
148    if let Some(match_) = captures.get(1) {
149      return match_.as_str().to_string();
150    }
151  }
152  "field".to_string()
153}