Skip to main content

backbone_bucket/domain/entity/
upload_session.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::UploadStatus;
7use super::StorageBackend;
8use super::AuditMetadata;
9
10use super::*;
11
12use crate::domain::state_machine::{UploadSessionStateMachine, UploadSessionState, StateMachineError};
13
14use thiserror::Error;
15
16/// Domain error for uploaderror operations
17#[derive(Debug, Clone, Error)]
18pub enum UploadError {
19    #[error("{0}")]
20    Message(String),
21
22    #[error("Not found: {0}")]
23    NotFound(String),
24
25    #[error("Validation failed: {0}")]
26    ValidationFailed(String),
27
28    #[error("Conflict: {0}")]
29    Conflict(String),
30}
31
32impl From<String> for UploadError {
33    fn from(msg: String) -> Self { Self::Message(msg) }
34}
35
36impl From<&str> for UploadError {
37    fn from(msg: &str) -> Self { Self::Message(msg.to_string()) }
38}
39
40
41/// Strongly-typed ID for UploadSession
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct UploadSessionId(pub Uuid);
45
46impl UploadSessionId {
47    pub fn new(id: Uuid) -> Self { Self(id) }
48    pub fn generate() -> Self { Self(Uuid::new_v4()) }
49    pub fn into_inner(self) -> Uuid { self.0 }
50}
51
52impl std::fmt::Display for UploadSessionId {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "{}", self.0)
55    }
56}
57
58impl std::str::FromStr for UploadSessionId {
59    type Err = uuid::Error;
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        Ok(Self(Uuid::parse_str(s)?))
62    }
63}
64
65impl From<Uuid> for UploadSessionId {
66    fn from(id: Uuid) -> Self { Self(id) }
67}
68
69impl From<UploadSessionId> for Uuid {
70    fn from(id: UploadSessionId) -> Self { id.0 }
71}
72
73impl AsRef<Uuid> for UploadSessionId {
74    fn as_ref(&self) -> &Uuid { &self.0 }
75}
76
77impl std::ops::Deref for UploadSessionId {
78    type Target = Uuid;
79    fn deref(&self) -> &Self::Target { &self.0 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
83pub struct UploadSession {
84    pub id: Uuid,
85    pub bucket_id: Uuid,
86    pub user_id: Uuid,
87    pub path: String,
88    pub filename: String,
89    pub mime_type: Option<String>,
90    pub file_size: i64,
91    pub chunk_size: i32,
92    pub total_chunks: i32,
93    pub uploaded_chunks: i32,
94    pub(crate) status: UploadStatus,
95    pub storage_backend: StorageBackend,
96    pub completed_parts: Vec<i32>,
97    pub part_etags: Option<serde_json::Value>,
98    pub expires_at: DateTime<Utc>,
99    #[serde(default)]
100    #[sqlx(json)]
101    pub metadata: AuditMetadata,
102}
103
104impl UploadSession {
105    /// Create a builder for UploadSession
106    pub fn builder() -> UploadSessionBuilder {
107        <UploadSessionBuilder as Default>::default()
108    }
109
110    /// Create a new UploadSession with required fields
111    pub fn new(bucket_id: Uuid, user_id: Uuid, path: String, filename: String, file_size: i64, chunk_size: i32, total_chunks: i32, uploaded_chunks: i32, status: UploadStatus, storage_backend: StorageBackend, completed_parts: Vec<i32>, expires_at: DateTime<Utc>) -> Self {
112        Self {
113            id: Uuid::new_v4(),
114            bucket_id,
115            user_id,
116            path,
117            filename,
118            mime_type: None,
119            file_size,
120            chunk_size,
121            total_chunks,
122            uploaded_chunks,
123            status,
124            storage_backend,
125            completed_parts,
126            part_etags: None,
127            expires_at,
128            metadata: AuditMetadata::default(),
129        }
130    }
131
132    /// Get the entity's unique identifier
133    pub fn id(&self) -> &Uuid {
134        &self.id
135    }
136
137    /// Get a strongly-typed ID for this entity
138    pub fn typed_id(&self) -> UploadSessionId {
139        UploadSessionId(self.id)
140    }
141
142    /// Get when this entity was created
143    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
144        self.metadata.created_at.as_ref()
145    }
146
147    /// Get when this entity was last updated
148    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
149        self.metadata.updated_at.as_ref()
150    }
151
152    /// Check if this entity is soft deleted
153    pub fn is_deleted(&self) -> bool {
154        self.metadata.deleted_at.is_some()
155    }
156
157    /// Check if this entity is active (not deleted)
158    pub fn is_active(&self) -> bool {
159        self.metadata.deleted_at.is_none()
160    }
161
162    /// Get when this entity was deleted
163    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
164        self.metadata.deleted_at.as_ref()
165    }
166
167    /// Get who created this entity
168    pub fn created_by(&self) -> Option<&Uuid> {
169        self.metadata.created_by.as_ref()
170    }
171
172    /// Get who last updated this entity
173    pub fn updated_by(&self) -> Option<&Uuid> {
174        self.metadata.updated_by.as_ref()
175    }
176
177    /// Get who deleted this entity
178    pub fn deleted_by(&self) -> Option<&Uuid> {
179        self.metadata.deleted_by.as_ref()
180    }
181
182    /// Get the current status
183    pub fn status(&self) -> &UploadStatus {
184        &self.status
185    }
186
187
188    // ==========================================================
189    // Fluent Setters (with_* for optional fields)
190    // ==========================================================
191
192    /// Set the mime_type field (chainable)
193    pub fn with_mime_type(mut self, value: String) -> Self {
194        self.mime_type = Some(value);
195        self
196    }
197
198    /// Set the part_etags field (chainable)
199    pub fn with_part_etags(mut self, value: serde_json::Value) -> Self {
200        self.part_etags = Some(value);
201        self
202    }
203
204    // ==========================================================
205    // State Machine
206    // ==========================================================
207
208    /// Transition to a new state via the status state machine.
209    ///
210    /// Returns `Err` if the transition is not permitted from the current state.
211    /// Use this method instead of assigning `self.status` directly.
212    pub fn transition_to(&mut self, new_state: UploadSessionState) -> Result<(), StateMachineError> {
213        let current = self.status.to_string().parse::<UploadSessionState>()?;
214        let mut sm = UploadSessionStateMachine::from_state(current);
215        sm.transition_to_state(new_state)?;
216        self.status = new_state.to_string().parse::<UploadStatus>()
217            .map_err(|e| StateMachineError::InvalidState(e.to_string()))?;
218        Ok(())
219    }
220
221    // ==========================================================
222    // Partial Update
223    // ==========================================================
224
225    /// Apply partial updates from a map of field name to JSON value
226    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
227        for (key, value) in fields {
228            match key.as_str() {
229                "bucket_id" => {
230                    if let Ok(v) = serde_json::from_value(value) { self.bucket_id = v; }
231                }
232                "user_id" => {
233                    if let Ok(v) = serde_json::from_value(value) { self.user_id = v; }
234                }
235                "path" => {
236                    if let Ok(v) = serde_json::from_value(value) { self.path = v; }
237                }
238                "filename" => {
239                    if let Ok(v) = serde_json::from_value(value) { self.filename = v; }
240                }
241                "mime_type" => {
242                    if let Ok(v) = serde_json::from_value(value) { self.mime_type = v; }
243                }
244                "file_size" => {
245                    if let Ok(v) = serde_json::from_value(value) { self.file_size = v; }
246                }
247                "chunk_size" => {
248                    if let Ok(v) = serde_json::from_value(value) { self.chunk_size = v; }
249                }
250                "total_chunks" => {
251                    if let Ok(v) = serde_json::from_value(value) { self.total_chunks = v; }
252                }
253                "uploaded_chunks" => {
254                    if let Ok(v) = serde_json::from_value(value) { self.uploaded_chunks = v; }
255                }
256                "storage_backend" => {
257                    if let Ok(v) = serde_json::from_value(value) { self.storage_backend = v; }
258                }
259                "completed_parts" => {
260                    if let Ok(v) = serde_json::from_value(value) { self.completed_parts = v; }
261                }
262                "part_etags" => {
263                    if let Ok(v) = serde_json::from_value(value) { self.part_etags = v; }
264                }
265                "expires_at" => {
266                    if let Ok(v) = serde_json::from_value(value) { self.expires_at = v; }
267                }
268                _ => {} // ignore unknown fields
269            }
270        }
271    }
272
273}
274
275impl super::Entity for UploadSession {
276    type Id = Uuid;
277
278    fn entity_id(&self) -> &Self::Id {
279        &self.id
280    }
281
282    fn entity_type() -> &'static str {
283        "UploadSession"
284    }
285}
286
287impl backbone_core::PersistentEntity for UploadSession {
288    fn entity_id(&self) -> String {
289        self.id.to_string()
290    }
291    fn set_entity_id(&mut self, id: String) {
292        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
293            self.id = uuid;
294        }
295    }
296    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
297        self.metadata.created_at
298    }
299    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
300        self.metadata.created_at = Some(ts);
301    }
302    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
303        self.metadata.updated_at
304    }
305    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
306        self.metadata.updated_at = Some(ts);
307    }
308    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
309        self.metadata.deleted_at
310    }
311    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
312        self.metadata.deleted_at = ts;
313    }
314}
315
316impl backbone_orm::EntityRepoMeta for UploadSession {
317    fn column_types() -> std::collections::HashMap<String, String> {
318        let mut m = std::collections::HashMap::new();
319        m.insert("id".to_string(), "uuid".to_string());
320        m.insert("bucket_id".to_string(), "uuid".to_string());
321        m.insert("user_id".to_string(), "uuid".to_string());
322        m.insert("status".to_string(), "upload_status".to_string());
323        m.insert("storage_backend".to_string(), "storage_backend".to_string());
324        m.insert("expires_at".to_string(), "timestamptz".to_string());
325        m
326    }
327    fn search_fields() -> &'static [&'static str] {
328        &["path", "filename"]
329    }
330}
331
332/// Builder for UploadSession entity
333///
334/// Provides a fluent API for constructing UploadSession instances.
335/// System fields (id, metadata, timestamps) are auto-initialized.
336#[derive(Debug, Clone, Default)]
337pub struct UploadSessionBuilder {
338    bucket_id: Option<Uuid>,
339    user_id: Option<Uuid>,
340    path: Option<String>,
341    filename: Option<String>,
342    mime_type: Option<String>,
343    file_size: Option<i64>,
344    chunk_size: Option<i32>,
345    total_chunks: Option<i32>,
346    uploaded_chunks: Option<i32>,
347    status: Option<UploadStatus>,
348    storage_backend: Option<StorageBackend>,
349    completed_parts: Option<Vec<i32>>,
350    part_etags: Option<serde_json::Value>,
351    expires_at: Option<DateTime<Utc>>,
352}
353
354impl UploadSessionBuilder {
355    /// Set the bucket_id field (required)
356    pub fn bucket_id(mut self, value: Uuid) -> Self {
357        self.bucket_id = Some(value);
358        self
359    }
360
361    /// Set the user_id field (required)
362    pub fn user_id(mut self, value: Uuid) -> Self {
363        self.user_id = Some(value);
364        self
365    }
366
367    /// Set the path field (required)
368    pub fn path(mut self, value: String) -> Self {
369        self.path = Some(value);
370        self
371    }
372
373    /// Set the filename field (required)
374    pub fn filename(mut self, value: String) -> Self {
375        self.filename = Some(value);
376        self
377    }
378
379    /// Set the mime_type field (optional)
380    pub fn mime_type(mut self, value: String) -> Self {
381        self.mime_type = Some(value);
382        self
383    }
384
385    /// Set the file_size field (required)
386    pub fn file_size(mut self, value: i64) -> Self {
387        self.file_size = Some(value);
388        self
389    }
390
391    /// Set the chunk_size field (required)
392    pub fn chunk_size(mut self, value: i32) -> Self {
393        self.chunk_size = Some(value);
394        self
395    }
396
397    /// Set the total_chunks field (required)
398    pub fn total_chunks(mut self, value: i32) -> Self {
399        self.total_chunks = Some(value);
400        self
401    }
402
403    /// Set the uploaded_chunks field (default: `0`)
404    pub fn uploaded_chunks(mut self, value: i32) -> Self {
405        self.uploaded_chunks = Some(value);
406        self
407    }
408
409    /// Set the status field (default: `UploadStatus::default()`)
410    pub fn status(mut self, value: UploadStatus) -> Self {
411        self.status = Some(value);
412        self
413    }
414
415    /// Set the storage_backend field (default: `StorageBackend::default()`)
416    pub fn storage_backend(mut self, value: StorageBackend) -> Self {
417        self.storage_backend = Some(value);
418        self
419    }
420
421    /// Set the completed_parts field (required)
422    pub fn completed_parts(mut self, value: Vec<i32>) -> Self {
423        self.completed_parts = Some(value);
424        self
425    }
426
427    /// Set the part_etags field (optional)
428    pub fn part_etags(mut self, value: serde_json::Value) -> Self {
429        self.part_etags = Some(value);
430        self
431    }
432
433    /// Set the expires_at field (required)
434    pub fn expires_at(mut self, value: DateTime<Utc>) -> Self {
435        self.expires_at = Some(value);
436        self
437    }
438
439    /// Build the UploadSession entity
440    ///
441    /// Returns Err if any required field without a default is missing.
442    pub fn build(self) -> Result<UploadSession, String> {
443        let bucket_id = self.bucket_id.ok_or_else(|| "bucket_id is required".to_string())?;
444        let user_id = self.user_id.ok_or_else(|| "user_id is required".to_string())?;
445        let path = self.path.ok_or_else(|| "path is required".to_string())?;
446        let filename = self.filename.ok_or_else(|| "filename is required".to_string())?;
447        let file_size = self.file_size.ok_or_else(|| "file_size is required".to_string())?;
448        let chunk_size = self.chunk_size.ok_or_else(|| "chunk_size is required".to_string())?;
449        let total_chunks = self.total_chunks.ok_or_else(|| "total_chunks is required".to_string())?;
450        let completed_parts = self.completed_parts.ok_or_else(|| "completed_parts is required".to_string())?;
451        let expires_at = self.expires_at.ok_or_else(|| "expires_at is required".to_string())?;
452
453        Ok(UploadSession {
454            id: Uuid::new_v4(),
455            bucket_id,
456            user_id,
457            path,
458            filename,
459            mime_type: self.mime_type,
460            file_size,
461            chunk_size,
462            total_chunks,
463            uploaded_chunks: self.uploaded_chunks.unwrap_or(0),
464            status: self.status.unwrap_or_default(),
465            storage_backend: self.storage_backend.unwrap_or_default(),
466            completed_parts,
467            part_etags: self.part_etags,
468            expires_at,
469            metadata: AuditMetadata::default(),
470        })
471    }
472}
473
474// This file's extension (hand-written; ADR-0031).
475#[path = "upload_session.ext.rs"]
476mod upload_session_ext;
477pub use upload_session_ext::*;