Skip to main content

backbone_bucket/domain/entity/
file_version.rs

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