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 async 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).await?;
38        let signature = sign_hash(&hash, private_key).await?;
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 async 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).await?;
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 async 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).await?;
97        self.signature = sign_hash(&self.hash, private_key).await?;
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    #[tokio::test]
111    async 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)
118            .await
119            .unwrap();
120
121        assert_eq!(doc.id(), "test-id");
122        assert_eq!(doc.version(), crate::META_SENTINEL_VERSION);
123        assert_eq!(doc.data(), &data);
124        assert!(!doc.hash().is_empty());
125        assert!(!doc.signature().is_empty());
126        assert_eq!(doc.created_at(), doc.updated_at());
127    }
128
129    #[tokio::test]
130    async fn test_document_with_empty_data() {
131        let mut rng = OsRng;
132        let mut key_bytes = [0u8; 32];
133        rng.fill_bytes(&mut key_bytes);
134        let private_key = SigningKey::from_bytes(&key_bytes);
135        let data = serde_json::json!({});
136        let doc = Document::new("empty".to_string(), data.clone(), &private_key)
137            .await
138            .unwrap();
139
140        assert_eq!(doc.id(), "empty");
141        assert_eq!(doc.version(), crate::META_SENTINEL_VERSION);
142        assert!(doc.data().as_object().unwrap().is_empty());
143    }
144
145    #[tokio::test]
146    async fn test_document_with_complex_data() {
147        let mut rng = OsRng;
148        let mut key_bytes = [0u8; 32];
149        rng.fill_bytes(&mut key_bytes);
150        let private_key = SigningKey::from_bytes(&key_bytes);
151        let data = serde_json::json!({
152            "string": "value",
153            "number": 123,
154            "boolean": true,
155            "array": [1, 2, 3],
156            "object": {"nested": "value"}
157        });
158        let doc = Document::new("complex".to_string(), data.clone(), &private_key)
159            .await
160            .unwrap();
161
162        assert_eq!(doc.data()["string"], "value");
163        assert_eq!(doc.data()["number"], 123);
164        assert_eq!(doc.data()["boolean"], true);
165        assert_eq!(doc.data()["array"], serde_json::json!([1, 2, 3]));
166        assert_eq!(doc.data()["object"]["nested"], "value");
167    }
168
169    #[tokio::test]
170    async fn test_document_with_valid_filename_safe_ids() {
171        let mut rng = OsRng;
172        let mut key_bytes = [0u8; 32];
173        rng.fill_bytes(&mut key_bytes);
174        let private_key = SigningKey::from_bytes(&key_bytes);
175        // Test various valid filename-safe document IDs
176        let valid_ids = vec![
177            "user-123",
178            "user_456",
179            "user123",
180            "123",
181            "a",
182            "user-123_test",
183            "CamelCaseID",
184        ];
185
186        for id in valid_ids {
187            let data = serde_json::json!({"data": "test"});
188            let doc = Document::new(id.to_owned(), data.clone(), &private_key)
189                .await
190                .unwrap();
191
192            assert_eq!(doc.id(), id);
193            assert_eq!(doc.data(), &data);
194        }
195    }
196
197    #[tokio::test]
198    async fn test_set_data_updates_hash_and_signature() {
199        let mut rng = OsRng;
200        let mut key_bytes = [0u8; 32];
201        rng.fill_bytes(&mut key_bytes);
202        let private_key = SigningKey::from_bytes(&key_bytes);
203        let initial_data = serde_json::json!({"initial": "data"});
204        let mut doc = Document::new("test".to_string(), initial_data, &private_key)
205            .await
206            .unwrap();
207        let initial_hash = doc.hash().to_string();
208        let initial_signature = doc.signature().to_string();
209        let initial_updated_at = doc.updated_at();
210
211        let new_data = serde_json::json!({"new": "data"});
212        doc.set_data(new_data.clone(), &private_key).await.unwrap();
213
214        assert_eq!(doc.data(), &new_data);
215        assert_ne!(doc.hash(), initial_hash);
216        assert_ne!(doc.signature(), initial_signature);
217        assert!(doc.updated_at() > initial_updated_at);
218    }
219}