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(_) => Some("Check that the JWT is properly signed and not expired".into()),
230 Self::MissingAuth => Some("Provide a valid JWT in the Authorization header".into()),
231 Self::TableNotFound(_) => Some("Check the table name and schema".into()),
232 Self::UnknownColumn(_) => Some("Check column names against the table schema".into()),
233 Self::Database(db_err) => db_err.hint.clone(),
234 _ => None,
235 }
236 }
237}
238
239#[derive(Error, Debug)]
241#[error("Database error [{code}]: {message}")]
242pub struct DatabaseError {
243 pub code: String,
244 pub message: String,
245 pub details: Option<String>,
246 pub hint: Option<String>,
247 pub constraint: Option<String>,
248 pub table: Option<String>,
249 pub column: Option<String>,
250}
251
252impl DatabaseError {
253 pub fn status_code(&self) -> StatusCode {
255 match self.code.as_str() {
257 c if c.starts_with("23") => StatusCode::CONFLICT,
259 c if c.starts_with("42") => StatusCode::BAD_REQUEST,
261 c if c.starts_with("28") => StatusCode::FORBIDDEN,
263 c if c.starts_with("40") => StatusCode::CONFLICT,
265 c if c.starts_with("53") => StatusCode::SERVICE_UNAVAILABLE,
267 c if c.starts_with("54") => StatusCode::PAYLOAD_TOO_LARGE,
269 "P0001" => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR,
273 }
274 }
275
276 pub fn code(&self) -> &'static str {
278 match self.code.as_str() {
279 c if c.starts_with("23") => "PGRST503", c if c.starts_with("42") => "PGRST504", c if c.starts_with("28") => "PGRST505", _ => "PGRST500", }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_error_status_codes() {
293 assert_eq!(
294 Error::InvalidQueryParam("test".into()).status_code(),
295 StatusCode::BAD_REQUEST
296 );
297 assert_eq!(Error::MissingAuth.status_code(), StatusCode::UNAUTHORIZED);
298 assert_eq!(
299 Error::TableNotFound("users".into()).status_code(),
300 StatusCode::NOT_FOUND
301 );
302 assert_eq!(
303 Error::UnsupportedMethod("TRACE".into()).status_code(),
304 StatusCode::METHOD_NOT_ALLOWED
305 );
306 }
307
308 #[test]
309 fn test_error_codes() {
310 assert_eq!(Error::InvalidQueryParam("test".into()).code(), "PGRST101");
311 assert_eq!(Error::MissingAuth.code(), "PGRST202");
312 assert_eq!(Error::TableNotFound("users".into()).code(), "PGRST301");
313 }
314
315 #[test]
316 fn test_database_error_status() {
317 let constraint_error = DatabaseError {
318 code: "23505".into(), message: "Duplicate key".into(),
320 details: None,
321 hint: None,
322 constraint: Some("users_pkey".into()),
323 table: Some("users".into()),
324 column: None,
325 };
326 assert_eq!(constraint_error.status_code(), StatusCode::CONFLICT);
327 }
328
329 #[test]
330 fn test_error_to_json() {
331 let error = Error::InvalidQueryParam("bad filter".into());
332 let json = error.to_json();
333 assert_eq!(json["code"], "PGRST101");
334 assert!(json["message"].as_str().unwrap().contains("bad filter"));
335 }
336}