1use http::StatusCode;
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Error, Debug)]
13pub enum Error {
14 #[error("Invalid path: {0}")]
18 InvalidPath(String),
19
20 #[error("Invalid query parameter: {0}")]
21 InvalidQueryParam(String),
22
23 #[error("Invalid header: {0}")]
24 InvalidHeader(&'static str),
25
26 #[error("Invalid request body: {0}")]
27 InvalidBody(String),
28
29 #[error("Unsupported HTTP method: {0}")]
30 UnsupportedMethod(String),
31
32 #[error("Unacceptable schema: {0}")]
33 UnacceptableSchema(String),
34
35 #[error("Unknown column: {0}")]
36 UnknownColumn(String),
37
38 #[error("Invalid range: {0}")]
39 InvalidRange(String),
40
41 #[error("Invalid media type: {0}")]
42 InvalidMediaType(String),
43
44 #[error("Missing required parameter: {0}")]
45 MissingParameter(String),
46
47 #[error("Ambiguous request: {0}")]
48 AmbiguousRequest(String),
49
50 #[error("Invalid JWT: {0}")]
54 InvalidJwt(String),
55
56 #[error("JWT expired")]
57 JwtExpired,
58
59 #[error("Missing authentication")]
60 MissingAuth,
61
62 #[error("Insufficient permissions: {0}")]
63 InsufficientPermissions(String),
64
65 #[error("Resource not found: {0}")]
69 NotFound(String),
70
71 #[error("Table not found: {0}")]
72 TableNotFound(String),
73
74 #[error("Function not found: {0}")]
75 FunctionNotFound(String),
76
77 #[error("Column not found: {0}")]
78 ColumnNotFound(String),
79
80 #[error("Relationship not found: {0}")]
81 RelationshipNotFound(String),
82
83 #[error("Schema cache not loaded")]
87 SchemaCacheNotLoaded,
88
89 #[error("Schema cache load failed: {0}")]
90 SchemaCacheLoadFailed(String),
91
92 #[error("Database error: {0}")]
96 Database(#[from] DatabaseError),
97
98 #[error("Connection pool error: {0}")]
99 ConnectionPool(String),
100
101 #[error("Internal error: {0}")]
105 Internal(String),
106
107 #[error("Configuration error: {0}")]
108 Config(String),
109
110 #[error("Invalid plan: {0}")]
114 InvalidPlan(String),
115
116 #[error("Embedding error: {0}")]
117 EmbeddingError(String),
118}
119
120impl Error {
121 pub fn status_code(&self) -> StatusCode {
123 match self {
124 Self::InvalidPath(_)
126 | Self::InvalidQueryParam(_)
127 | Self::InvalidHeader(_)
128 | Self::InvalidBody(_)
129 | Self::InvalidRange(_)
130 | Self::InvalidMediaType(_)
131 | Self::MissingParameter(_)
132 | Self::AmbiguousRequest(_)
133 | Self::UnknownColumn(_)
134 | Self::InvalidPlan(_)
135 | Self::EmbeddingError(_) => StatusCode::BAD_REQUEST,
136
137 Self::InvalidJwt(_) | Self::JwtExpired | Self::MissingAuth => StatusCode::UNAUTHORIZED,
139
140 Self::InsufficientPermissions(_) => StatusCode::FORBIDDEN,
142
143 Self::NotFound(_)
145 | Self::TableNotFound(_)
146 | Self::FunctionNotFound(_)
147 | Self::ColumnNotFound(_)
148 | Self::RelationshipNotFound(_) => StatusCode::NOT_FOUND,
149
150 Self::UnsupportedMethod(_) => StatusCode::METHOD_NOT_ALLOWED,
152
153 Self::UnacceptableSchema(_) => StatusCode::NOT_ACCEPTABLE,
155
156 Self::SchemaCacheNotLoaded
158 | Self::SchemaCacheLoadFailed(_)
159 | Self::ConnectionPool(_)
160 | Self::Internal(_)
161 | Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
162
163 Self::Database(db_err) => db_err.status_code(),
165 }
166 }
167
168 pub fn code(&self) -> &'static str {
170 match self {
171 Self::InvalidPath(_) => "PGRST100",
172 Self::InvalidQueryParam(_) => "PGRST101",
173 Self::InvalidHeader(_) => "PGRST102",
174 Self::InvalidBody(_) => "PGRST103",
175 Self::UnsupportedMethod(_) => "PGRST104",
176 Self::UnacceptableSchema(_) => "PGRST105",
177 Self::UnknownColumn(_) => "PGRST106",
178 Self::InvalidRange(_) => "PGRST107",
179 Self::InvalidMediaType(_) => "PGRST108",
180 Self::MissingParameter(_) => "PGRST109",
181 Self::AmbiguousRequest(_) => "PGRST110",
182
183 Self::InvalidJwt(_) => "PGRST200",
184 Self::JwtExpired => "PGRST201",
185 Self::MissingAuth => "PGRST202",
186 Self::InsufficientPermissions(_) => "PGRST203",
187
188 Self::NotFound(_) => "PGRST300",
189 Self::TableNotFound(_) => "PGRST301",
190 Self::FunctionNotFound(_) => "PGRST302",
191 Self::ColumnNotFound(_) => "PGRST303",
192 Self::RelationshipNotFound(_) => "PGRST304",
193
194 Self::SchemaCacheNotLoaded => "PGRST400",
195 Self::SchemaCacheLoadFailed(_) => "PGRST401",
196
197 Self::Database(e) => e.code(),
198 Self::ConnectionPool(_) => "PGRST500",
199
200 Self::Internal(_) => "PGRST900",
201 Self::Config(_) => "PGRST901",
202
203 Self::InvalidPlan(_) => "PGRST600",
204 Self::EmbeddingError(_) => "PGRST601",
205 }
206 }
207
208 pub fn to_json(&self) -> serde_json::Value {
210 serde_json::json!({
211 "code": self.code(),
212 "message": self.to_string(),
213 "details": self.details(),
214 "hint": self.hint(),
215 })
216 }
217
218 fn details(&self) -> Option<String> {
220 match self {
221 Self::Database(db_err) => db_err.details.clone(),
222 _ => None,
223 }
224 }
225
226 fn hint(&self) -> Option<String> {
228 match self {
229 Self::InvalidJwt(_) => {
230 Some("Check that the JWT is properly signed and not expired".into())
231 }
232 Self::MissingAuth => Some("Provide a valid JWT in the Authorization header".into()),
233 Self::TableNotFound(_) => Some("Check the table name and schema".into()),
234 Self::UnknownColumn(_) => Some("Check column names against the table schema".into()),
235 Self::Database(db_err) => db_err.hint.clone(),
236 _ => None,
237 }
238 }
239}
240
241#[derive(Error, Debug)]
243#[error("Database error [{code}]: {message}")]
244pub struct DatabaseError {
245 pub code: String,
246 pub message: String,
247 pub details: Option<String>,
248 pub hint: Option<String>,
249 pub constraint: Option<String>,
250 pub table: Option<String>,
251 pub column: Option<String>,
252}
253
254impl DatabaseError {
255 pub fn status_code(&self) -> StatusCode {
257 match self.code.as_str() {
259 c if c.starts_with("23") => StatusCode::CONFLICT,
261 c if c.starts_with("42") => StatusCode::BAD_REQUEST,
263 c if c.starts_with("28") => StatusCode::FORBIDDEN,
265 c if c.starts_with("40") => StatusCode::CONFLICT,
267 c if c.starts_with("53") => StatusCode::SERVICE_UNAVAILABLE,
269 c if c.starts_with("54") => StatusCode::PAYLOAD_TOO_LARGE,
271 "P0001" => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR,
275 }
276 }
277
278 pub fn code(&self) -> &'static str {
280 match self.code.as_str() {
281 c if c.starts_with("23") => "PGRST503", c if c.starts_with("42") => "PGRST504", c if c.starts_with("28") => "PGRST505", _ => "PGRST500", }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_error_status_codes() {
295 assert_eq!(
296 Error::InvalidQueryParam("test".into()).status_code(),
297 StatusCode::BAD_REQUEST
298 );
299 assert_eq!(Error::MissingAuth.status_code(), StatusCode::UNAUTHORIZED);
300 assert_eq!(
301 Error::TableNotFound("users".into()).status_code(),
302 StatusCode::NOT_FOUND
303 );
304 assert_eq!(
305 Error::UnsupportedMethod("TRACE".into()).status_code(),
306 StatusCode::METHOD_NOT_ALLOWED
307 );
308 }
309
310 #[test]
311 fn test_error_codes() {
312 assert_eq!(Error::InvalidQueryParam("test".into()).code(), "PGRST101");
313 assert_eq!(Error::MissingAuth.code(), "PGRST202");
314 assert_eq!(Error::TableNotFound("users".into()).code(), "PGRST301");
315 }
316
317 #[test]
318 fn test_database_error_status() {
319 let constraint_error = DatabaseError {
320 code: "23505".into(), message: "Duplicate key".into(),
322 details: None,
323 hint: None,
324 constraint: Some("users_pkey".into()),
325 table: Some("users".into()),
326 column: None,
327 };
328 assert_eq!(constraint_error.status_code(), StatusCode::CONFLICT);
329 }
330
331 #[test]
332 fn test_error_to_json() {
333 let error = Error::InvalidQueryParam("bad filter".into());
334 let json = error.to_json();
335 assert_eq!(json["code"], "PGRST101");
336 assert!(json["message"].as_str().unwrap().contains("bad filter"));
337 }
338}