sentinel_dbms/
document.rs

1use chrono::{DateTime, Utc};
2use sentinel_crypto::{hash_data, sign_hash, SigningKey};
3use serde_json::Value;
4use tracing::{debug, trace};
5
6use crate::Result;
7
8/// Represents a document in the database.
9#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone, PartialEq, Eq)]
10#[allow(
11    clippy::field_scoped_visibility_modifiers,
12    reason = "fields need to be pub(crate) for internal access"
13)]
14pub struct Document {
15    /// The unique identifier of the document.
16    pub(crate) id:         String,
17    /// The version of the document, represents the version of the client that created it.
18    pub(crate) version:    u32,
19    /// The timestamp when the document was created.
20    pub(crate) created_at: DateTime<Utc>,
21    /// The timestamp when the document was last updated.
22    pub(crate) updated_at: DateTime<Utc>,
23    /// The hash of the document data.
24    pub(crate) hash:       String,
25    /// The signature of the document data.
26    pub(crate) signature:  String,
27    /// The JSON data of the document.
28    pub(crate) data:       Value,
29}
30
31impl Document {
32    /// Creates a new document with the given id, version, and data.
33    /// Computes the hash and signature using the provided private key.
34    pub fn new(id: String, data: Value, private_key: &SigningKey) -> Result<Self> {
35        trace!("Creating new signed document with id: {}", id);
36        let now = Utc::now();
37        let hash = hash_data(&data)?;
38        let signature = sign_hash(&hash, private_key)?;
39        debug!("Document {} created with hash: {}", id, hash);
40        Ok(Self {
41            id,
42            version: crate::META_SENTINEL_VERSION,
43            created_at: now,
44            updated_at: now,
45            hash,
46            signature,
47            data,
48        })
49    }
50
51    /// Creates a new document with the given id and data.
52    /// Computes the hash but not the signature.
53    pub fn new_without_signature(id: String, data: Value) -> Result<Self> {
54        trace!("Creating new unsigned document with id: {}", id);
55        let now = Utc::now();
56        let hash = hash_data(&data)?;
57        debug!("Document {} created without signature, hash: {}", id, hash);
58        Ok(Self {
59            id,
60            version: crate::META_SENTINEL_VERSION,
61            created_at: now,
62            updated_at: now,
63            hash,
64            signature: String::new(),
65            data,
66        })
67    }
68
69    /// Returns the document ID.
70    pub fn id(&self) -> &str { &self.id }
71
72    /// Returns the document version.
73    pub const fn version(&self) -> u32 { self.version }
74
75    /// Returns the creation timestamp.
76    pub const fn created_at(&self) -> DateTime<Utc> { self.created_at }
77
78    /// Returns the last update timestamp.
79    pub const fn updated_at(&self) -> DateTime<Utc> { self.updated_at }
80
81    /// Returns the hash of the document data.
82    pub fn hash(&self) -> &str { &self.hash }
83
84    /// Returns the signature of the document data.
85    pub fn signature(&self) -> &str { &self.signature }
86
87    /// Returns a reference to the document data.
88    pub const fn data(&self) -> &Value { &self.data }
89
90    /// Sets the document data, updates the hash and signature, and refreshes the updated_at
91    /// timestamp.
92    pub fn set_data(&mut self, data: Value, private_key: &SigningKey) -> Result<()> {
93        trace!("Updating data for document: {}", self.id);
94        self.data = data;
95        self.updated_at = Utc::now();
96        self.hash = hash_data(&self.data)?;
97        self.signature = sign_hash(&self.hash, private_key)?;
98        debug!("Document {} data updated, new hash: {}", self.id, self.hash);
99        Ok(())
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use rand::{rngs::OsRng, RngCore};
106    use sentinel_crypto::SigningKey;
107
108    use super::*;
109
110    #[test]
111    fn test_document_creation() {
112        let mut rng = OsRng;
113        let mut key_bytes = [0u8; 32];
114        rng.fill_bytes(&mut key_bytes);
115        let private_key = SigningKey::from_bytes(&key_bytes);
116        let data = serde_json::json!({"name": "Test", "value": 42});
117        let doc = Document::new("test-id".to_string(), data.clone(), &private_key).unwrap();
118
119        assert_eq!(doc.id(), "test-id");
120        assert_eq!(doc.version(), crate::META_SENTINEL_VERSION);
121        assert_eq!(doc.data(), &data);
122        assert!(!doc.hash().is_empty());
123        assert!(!doc.signature().is_empty());
124        assert_eq!(doc.created_at(), doc.updated_at());
125    }
126
127    #[test]
128    fn test_document_with_empty_data() {
129        let mut rng = OsRng;
130        let mut key_bytes = [0u8; 32];
131        rng.fill_bytes(&mut key_bytes);
132        let private_key = SigningKey::from_bytes(&key_bytes);
133        let data = serde_json::json!({});
134        let doc = Document::new("empty".to_string(), data.clone(), &private_key).unwrap();
135
136        assert_eq!(doc.id(), "empty");
137        assert_eq!(doc.version(), crate::META_SENTINEL_VERSION);
138        assert!(doc.data().as_object().unwrap().is_empty());
139    }
140
141    #[test]
142    fn test_document_with_complex_data() {
143        let mut rng = OsRng;
144        let mut key_bytes = [0u8; 32];
145        rng.fill_bytes(&mut key_bytes);
146        let private_key = SigningKey::from_bytes(&key_bytes);
147        let data = serde_json::json!({
148            "string": "value",
149            "number": 123,
150            "boolean": true,
151            "array": [1, 2, 3],
152            "object": {"nested": "value"}
153        });
154        let doc = Document::new("complex".to_string(), data.clone(), &private_key).unwrap();
155
156        assert_eq!(doc.data()["string"], "value");
157        assert_eq!(doc.data()["number"], 123);
158        assert_eq!(doc.data()["boolean"], true);
159        assert_eq!(doc.data()["array"], serde_json::json!([1, 2, 3]));
160        assert_eq!(doc.data()["object"]["nested"], "value");
161    }
162
163    #[test]
164    fn test_document_with_valid_filename_safe_ids() {
165        let mut rng = OsRng;
166        let mut key_bytes = [0u8; 32];
167        rng.fill_bytes(&mut key_bytes);
168        let private_key = SigningKey::from_bytes(&key_bytes);
169        // Test various valid filename-safe document IDs
170        let valid_ids = vec![
171            "user-123",
172            "user_456",
173            "user123",
174            "123",
175            "a",
176            "user-123_test",
177            "CamelCaseID",
178        ];
179
180        for id in valid_ids {
181            let data = serde_json::json!({"data": "test"});
182            let doc = Document::new(id.to_owned(), data.clone(), &private_key).unwrap();
183
184            assert_eq!(doc.id(), id);
185            assert_eq!(doc.data(), &data);
186        }
187    }
188
189    #[test]
190    fn test_set_data_updates_hash_and_signature() {
191        let mut rng = OsRng;
192        let mut key_bytes = [0u8; 32];
193        rng.fill_bytes(&mut key_bytes);
194        let private_key = SigningKey::from_bytes(&key_bytes);
195        let initial_data = serde_json::json!({"initial": "data"});
196        let mut doc = Document::new("test".to_string(), initial_data, &private_key).unwrap();
197        let initial_hash = doc.hash().to_string();
198        let initial_signature = doc.signature().to_string();
199        let initial_updated_at = doc.updated_at();
200
201        let new_data = serde_json::json!({"new": "data"});
202        doc.set_data(new_data.clone(), &private_key).unwrap();
203
204        assert_eq!(doc.data(), &new_data);
205        assert_ne!(doc.hash(), initial_hash);
206        assert_ne!(doc.signature(), initial_signature);
207        assert!(doc.updated_at() > initial_updated_at);
208    }
209}