use crate::error::Result; use crate::index::FieldIndex; use crate::store::{HeroDbClient, Object}; /// Generic storage layer for OSIRIS objects #[derive(Debug, Clone)] pub struct GenericStore { client: HeroDbClient, index: FieldIndex, } impl GenericStore { /// Create a new generic store pub fn new(client: HeroDbClient) -> Self { let index = FieldIndex::new(client.clone()); Self { client, index, } } /// Store an object pub async fn put(&self, obj: &T) -> Result<()> { // Serialize object to JSON let json = obj.to_json()?; let key = format!("obj:{}:{}", obj.namespace(), obj.id()); // Store in HeroDB self.client.set(&key, &json).await?; // Index the object self.index_object(obj).await?; Ok(()) } /// Get an object by ID pub async fn get(&self, ns: &str, id: &str) -> Result { let key = format!("obj:{}:{}", ns, id); let json = self.client.get(&key).await? .ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))?; T::from_json(&json) } /// Get raw JSON data by ID (for generic access without type) pub async fn get_raw(&self, ns: &str, id: &str) -> Result { let key = format!("obj:{}:{}", ns, id); self.client.get(&key).await? .ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id))) } /// Delete an object pub async fn delete(&self, obj: &T) -> Result { let key = format!("obj:{}:{}", obj.namespace(), obj.id()); // Deindex first self.deindex_object(obj).await?; // Delete from HeroDB self.client.del(&key).await } /// Check if an object exists pub async fn exists(&self, ns: &str, id: &str) -> Result { let key = format!("obj:{}:{}", ns, id); self.client.exists(&key).await } /// Index an object async fn index_object(&self, obj: &T) -> Result<()> { let index_keys = obj.index_keys(); for key in index_keys { let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value); self.client.sadd(&field_key, &obj.id().to_string()).await?; } // Add to scan index for full-text search let scan_key = format!("scan:{}", obj.namespace()); self.client.sadd(&scan_key, &obj.id().to_string()).await?; Ok(()) } /// Deindex an object async fn deindex_object(&self, obj: &T) -> Result<()> { let index_keys = obj.index_keys(); for key in index_keys { let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value); self.client.srem(&field_key, &obj.id().to_string()).await?; } // Remove from scan index let scan_key = format!("scan:{}", obj.namespace()); self.client.srem(&scan_key, &obj.id().to_string()).await?; Ok(()) } /// Get all IDs matching an index key pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result> { let field_key = format!("idx:{}:{}:{}", ns, field, value); self.client.smembers(&field_key).await } /// Get all IDs in a namespace pub async fn get_all_ids(&self, ns: &str) -> Result> { let scan_key = format!("scan:{}", ns); self.client.smembers(&scan_key).await } } #[cfg(test)] mod tests { use super::*; use crate::objects::Note; #[tokio::test] #[ignore] async fn test_generic_store() { let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap(); let store = GenericStore::new(client); let note = Note::new("test".to_string()) .set_title("Test Note") .set_content("This is a test"); store.put(¬e).await.unwrap(); let retrieved: Note = store.get("test", note.id()).await.unwrap(); assert_eq!(retrieved.title, note.title); } }