megacommerce_shared/store/
errors.rs1use 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 let details = pg_err.detail().unwrap_or("").to_string();
86 let msg = match pg_err.code() {
87 "23505" => {
89 parse_duplicate_field_db_error(pg_err)
91 }
92 "23503" => {
93 "referenced record is not found".to_string()
95 }
96 "23502" => {
97 format!("{} cannot be null", parse_db_field_name(pg_err))
99 }
100 "08000" | "08003" | "08006" => "database connection exception".to_string(),
102 "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
131fn 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
143fn 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}