1use thiserror::Error;
4use uuid::Uuid;
5
6pub type Result<T> = std::result::Result<T, StorageError>;
8
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ResourceType {
13 User,
14 Workflow,
15 WorkflowVersion,
16 Execution,
17 ApiKey,
18 Secret,
19 Quota,
20 Schedule,
21 Webhook,
22 Checkpoint,
23 AuditLog,
24 Metrics,
25 Other(String),
26}
27
28impl std::fmt::Display for ResourceType {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 ResourceType::User => write!(f, "User"),
32 ResourceType::Workflow => write!(f, "Workflow"),
33 ResourceType::WorkflowVersion => write!(f, "WorkflowVersion"),
34 ResourceType::Execution => write!(f, "Execution"),
35 ResourceType::ApiKey => write!(f, "ApiKey"),
36 ResourceType::Secret => write!(f, "Secret"),
37 ResourceType::Quota => write!(f, "Quota"),
38 ResourceType::Schedule => write!(f, "Schedule"),
39 ResourceType::Webhook => write!(f, "Webhook"),
40 ResourceType::Checkpoint => write!(f, "Checkpoint"),
41 ResourceType::AuditLog => write!(f, "AuditLog"),
42 ResourceType::Metrics => write!(f, "Metrics"),
43 ResourceType::Other(s) => write!(f, "{s}"),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum ResourceId {
52 Uuid(Uuid),
53 String(String),
54 Composite(Vec<String>),
55}
56
57impl std::fmt::Display for ResourceId {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 ResourceId::Uuid(id) => write!(f, "{id}"),
61 ResourceId::String(s) => write!(f, "{s}"),
62 ResourceId::Composite(parts) => write!(f, "{}", parts.join(":")),
63 }
64 }
65}
66
67impl From<Uuid> for ResourceId {
68 fn from(id: Uuid) -> Self {
69 ResourceId::Uuid(id)
70 }
71}
72
73impl From<String> for ResourceId {
74 fn from(s: String) -> Self {
75 ResourceId::String(s)
76 }
77}
78
79impl From<&str> for ResourceId {
80 fn from(s: &str) -> Self {
81 ResourceId::String(s.to_string())
82 }
83}
84
85#[derive(Error, Debug)]
87pub enum StorageError {
88 #[error("Database error: {0}")]
90 Database(#[from] sqlx::Error),
91
92 #[error("Serialization error: {0}")]
94 Serialization(#[from] serde_json::Error),
95
96 #[error("{resource_type} not found: {resource_id}")]
98 NotFound {
99 resource_type: ResourceType,
100 resource_id: ResourceId,
101 },
102
103 #[error("Resource not found: {0}")]
105 NotFoundLegacy(String),
106
107 #[error("Constraint violation: {0}")]
109 ConstraintViolation(String),
110
111 #[error("Concurrent modification: {0}")]
113 ConcurrentModification(String),
114
115 #[error("Migration error: {0}")]
117 Migration(String),
118
119 #[error("Encryption error: {0}")]
121 EncryptionError(String),
122
123 #[error("Validation error: {0}")]
125 ValidationError(String),
126
127 #[error("Backup error: {0}")]
129 BackupError(String),
130
131 #[error("Batch size {size} exceeds maximum {max}")]
133 BatchTooLarge { size: usize, max: usize },
134}
135
136impl StorageError {
137 pub fn not_found<T: Into<ResourceId>>(resource_type: ResourceType, resource_id: T) -> Self {
139 StorageError::NotFound {
140 resource_type,
141 resource_id: resource_id.into(),
142 }
143 }
144
145 pub fn constraint_violation<S: Into<String>>(message: S) -> Self {
147 StorageError::ConstraintViolation(message.into())
148 }
149
150 pub fn validation<S: Into<String>>(message: S) -> Self {
152 StorageError::ValidationError(message.into())
153 }
154
155 pub fn encryption<S: Into<String>>(message: S) -> Self {
157 StorageError::EncryptionError(message.into())
158 }
159
160 pub fn migration<S: Into<String>>(message: S) -> Self {
162 StorageError::Migration(message.into())
163 }
164
165 pub fn backup<S: Into<String>>(message: S) -> Self {
167 StorageError::BackupError(message.into())
168 }
169
170 pub fn is_not_found(&self) -> bool {
172 matches!(
173 self,
174 StorageError::NotFound { .. } | StorageError::NotFoundLegacy(_)
175 )
176 }
177
178 pub fn is_constraint_violation(&self) -> bool {
180 matches!(self, StorageError::ConstraintViolation(_))
181 }
182
183 pub fn is_validation_error(&self) -> bool {
185 matches!(self, StorageError::ValidationError(_))
186 }
187
188 pub fn is_database_error(&self) -> bool {
190 matches!(self, StorageError::Database(_))
191 }
192
193 pub fn resource_type(&self) -> Option<&ResourceType> {
195 match self {
196 StorageError::NotFound { resource_type, .. } => Some(resource_type),
197 _ => None,
198 }
199 }
200
201 pub fn resource_id(&self) -> Option<&ResourceId> {
203 match self {
204 StorageError::NotFound { resource_id, .. } => Some(resource_id),
205 _ => None,
206 }
207 }
208
209 pub fn is_retryable(&self) -> bool {
211 match self {
212 StorageError::Database(e) => {
213 if matches!(e, sqlx::Error::PoolTimedOut | sqlx::Error::PoolClosed) {
215 return true;
216 }
217
218 if matches!(e, sqlx::Error::Io(_)) {
220 return true;
221 }
222
223 e.as_database_error()
225 .and_then(sqlx::error::DatabaseError::code)
226 .is_some_and(|code| {
227 matches!(
234 code.as_ref(),
235 "40001" | "40P01" | "08006" | "08003" | "57P03"
236 )
237 })
238 }
239 _ => false,
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_not_found_error_creation() {
250 let error = StorageError::not_found(ResourceType::Workflow, Uuid::nil());
251 assert!(error.is_not_found());
252 assert_eq!(error.resource_type(), Some(&ResourceType::Workflow));
253 }
254
255 #[test]
256 fn test_validation_error_creation() {
257 let error = StorageError::validation("Invalid input");
258 assert!(error.is_validation_error());
259 assert!(!error.is_not_found());
260 }
261
262 #[test]
263 fn test_constraint_violation_error_creation() {
264 let error = StorageError::constraint_violation("Unique constraint violated");
265 assert!(error.is_constraint_violation());
266 }
267
268 #[test]
269 fn test_resource_type_serialization() {
270 let resource_type = ResourceType::Workflow;
271 let json = serde_json::to_string(&resource_type).unwrap();
272 assert_eq!(json, "\"workflow\"");
273
274 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
275 assert_eq!(deserialized, ResourceType::Workflow);
276 }
277
278 #[test]
279 fn test_resource_id_serialization() {
280 let resource_id = ResourceId::String("test-123".to_string());
281 let json = serde_json::to_string(&resource_id).unwrap();
282 let deserialized: ResourceId = serde_json::from_str(&json).unwrap();
283 assert_eq!(deserialized, resource_id);
284 }
285
286 #[test]
287 fn test_resource_id_from_uuid() {
288 let uuid = Uuid::nil();
289 let resource_id: ResourceId = uuid.into();
290 matches!(resource_id, ResourceId::Uuid(_));
291 }
292
293 #[test]
294 fn test_resource_id_from_string() {
295 let resource_id: ResourceId = "test".into();
296 matches!(resource_id, ResourceId::String(_));
297 }
298
299 #[test]
300 fn test_error_helper_methods() {
301 let migration_err = StorageError::migration("Migration failed");
302 assert!(!migration_err.is_not_found());
303 assert!(!migration_err.is_validation_error());
304
305 let encryption_err = StorageError::encryption("Encryption failed");
306 assert!(!encryption_err.is_constraint_violation());
307
308 let backup_err = StorageError::backup("Backup failed");
309 assert!(!backup_err.is_database_error());
310 }
311}