metis_core/application/services/
database.rs

1use crate::dal::database::{models::*, repository::DocumentRepository};
2use crate::domain::documents::types::DocumentType;
3use crate::Result;
4
5/// Database service - handles all database CRUD operations
6pub struct DatabaseService {
7    repository: DocumentRepository,
8}
9
10impl DatabaseService {
11    pub fn new(repository: DocumentRepository) -> Self {
12        Self { repository }
13    }
14
15    /// Create a new document in the database
16    pub fn create_document(&mut self, document: NewDocument) -> Result<Document> {
17        self.repository.create_document(document)
18    }
19
20    /// Find a document by filepath
21    pub fn find_by_filepath(&mut self, filepath: &str) -> Result<Option<Document>> {
22        self.repository.find_by_filepath(filepath)
23    }
24
25    /// Find a document by ID
26    pub fn find_by_id(&mut self, id: &str) -> Result<Option<Document>> {
27        self.repository.find_by_id(id)
28    }
29
30    /// Update an existing document
31    pub fn update_document(&mut self, filepath: &str, document: &Document) -> Result<Document> {
32        self.repository.update_document(filepath, document)
33    }
34
35    /// Delete a document from the database
36    pub fn delete_document(&mut self, filepath: &str) -> Result<bool> {
37        self.repository.delete_document(filepath)
38    }
39
40    /// Search documents using full-text search
41    pub fn search_documents(&mut self, query: &str) -> Result<Vec<Document>> {
42        self.repository.search_documents(query)
43    }
44
45    /// Get all documents of a specific type
46    pub fn find_by_type(&mut self, doc_type: DocumentType) -> Result<Vec<Document>> {
47        let type_str = doc_type.to_string();
48        self.repository.find_by_type(&type_str)
49    }
50
51    /// Get documents with a specific tag
52    pub fn find_by_tag(&mut self, tag: &str) -> Result<Vec<Document>> {
53        self.repository.find_by_tag(tag)
54    }
55
56    /// Get all children of a document
57    pub fn find_children(&mut self, parent_id: &str) -> Result<Vec<Document>> {
58        self.repository.find_children(parent_id)
59    }
60
61    /// Get the parent of a document
62    pub fn find_parent(&mut self, child_id: &str) -> Result<Option<Document>> {
63        self.repository.find_parent(child_id)
64    }
65
66    /// Create a parent-child relationship
67    pub fn create_relationship(
68        &mut self,
69        parent_id: &str,
70        child_id: &str,
71        parent_filepath: &str,
72        child_filepath: &str,
73    ) -> Result<()> {
74        let relationship = DocumentRelationship {
75            parent_id: parent_id.to_string(),
76            child_id: child_id.to_string(),
77            parent_filepath: parent_filepath.to_string(),
78            child_filepath: child_filepath.to_string(),
79        };
80        self.repository.create_relationship(relationship)
81    }
82
83    /// Check if a document exists by filepath
84    pub fn document_exists(&mut self, filepath: &str) -> Result<bool> {
85        Ok(self.repository.find_by_filepath(filepath)?.is_some())
86    }
87
88    /// Get document count by type
89    pub fn count_by_type(&mut self, doc_type: DocumentType) -> Result<usize> {
90        let docs = self.repository.find_by_type(&doc_type.to_string())?;
91        Ok(docs.len())
92    }
93
94    /// Get all document IDs and their filepaths (useful for validation)
95    pub fn get_all_id_filepath_pairs(&mut self) -> Result<Vec<(String, String)>> {
96        // This would need a custom query in the repository
97        // For now, we'll use find_by_type for each type
98        let mut pairs = Vec::new();
99
100        for doc_type in [
101            DocumentType::Vision,
102            DocumentType::Strategy,
103            DocumentType::Initiative,
104            DocumentType::Task,
105            DocumentType::Adr,
106        ] {
107            let docs = self.repository.find_by_type(&doc_type.to_string())?;
108            for doc in docs {
109                pairs.push((doc.id, doc.filepath));
110            }
111        }
112
113        Ok(pairs)
114    }
115
116    /// Get all documents belonging to a strategy
117    pub fn find_by_strategy_id(&mut self, strategy_id: &str) -> Result<Vec<Document>> {
118        self.repository.find_by_strategy_id(strategy_id)
119    }
120
121    /// Get all documents belonging to an initiative
122    pub fn find_by_initiative_id(&mut self, initiative_id: &str) -> Result<Vec<Document>> {
123        self.repository.find_by_initiative_id(initiative_id)
124    }
125
126    /// Get all documents in a strategy hierarchy (strategy + its initiatives + their tasks)
127    pub fn find_strategy_hierarchy(&mut self, strategy_id: &str) -> Result<Vec<Document>> {
128        self.repository.find_strategy_hierarchy(strategy_id)
129    }
130
131    /// Get all documents in an initiative hierarchy (initiative + its tasks)
132    pub fn find_initiative_hierarchy(&mut self, initiative_id: &str) -> Result<Vec<Document>> {
133        self.repository.find_initiative_hierarchy(initiative_id)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::dal::Database;
141
142    fn setup_service() -> DatabaseService {
143        let db = Database::new(":memory:").expect("Failed to create test database");
144        DatabaseService::new(db.into_repository())
145    }
146
147    fn create_test_document() -> NewDocument {
148        NewDocument {
149            filepath: "/test/doc.md".to_string(),
150            id: "test-doc-1".to_string(),
151            title: "Test Document".to_string(),
152            document_type: "vision".to_string(),
153            created_at: 1609459200.0,
154            updated_at: 1609459200.0,
155            archived: false,
156            exit_criteria_met: false,
157            file_hash: "abc123".to_string(),
158            frontmatter_json: "{}".to_string(),
159            content: Some("Test content".to_string()),
160            phase: "draft".to_string(),
161            strategy_id: None,
162            initiative_id: None,
163        }
164    }
165
166    fn create_test_document_with_lineage(
167        id: &str, 
168        doc_type: &str, 
169        filepath: &str,
170        strategy_id: Option<String>,
171        initiative_id: Option<String>
172    ) -> NewDocument {
173        NewDocument {
174            filepath: filepath.to_string(),
175            id: id.to_string(),
176            title: format!("Test {}", doc_type),
177            document_type: doc_type.to_string(),
178            created_at: 1609459200.0,
179            updated_at: 1609459200.0,
180            archived: false,
181            exit_criteria_met: false,
182            file_hash: "abc123".to_string(),
183            frontmatter_json: "{}".to_string(),
184            content: Some("Test content".to_string()),
185            phase: "draft".to_string(),
186            strategy_id,
187            initiative_id,
188        }
189    }
190
191    #[test]
192    fn test_database_service_crud() {
193        let mut service = setup_service();
194
195        // Create
196        let new_doc = create_test_document();
197        let created = service.create_document(new_doc).expect("Failed to create");
198        assert_eq!(created.id, "test-doc-1");
199
200        // Read
201        let found = service
202            .find_by_id("test-doc-1")
203            .expect("Failed to find")
204            .expect("Document not found");
205        assert_eq!(found.filepath, "/test/doc.md");
206
207        // Update
208        let mut updated_doc = found.clone();
209        updated_doc.title = "Updated Title".to_string();
210        let updated = service
211            .update_document("/test/doc.md", &updated_doc)
212            .expect("Failed to update");
213        assert_eq!(updated.title, "Updated Title");
214
215        // Delete
216        let deleted = service
217            .delete_document("/test/doc.md")
218            .expect("Failed to delete");
219        assert!(deleted);
220
221        // Verify deleted
222        assert!(!service
223            .document_exists("/test/doc.md")
224            .expect("Failed to check existence"));
225    }
226
227    #[test]
228    fn test_database_service_relationships() {
229        let mut service = setup_service();
230
231        // Create parent and child documents
232        let parent = NewDocument {
233            id: "parent-1".to_string(),
234            filepath: "/parent.md".to_string(),
235            document_type: "strategy".to_string(),
236            ..create_test_document()
237        };
238
239        let child = NewDocument {
240            id: "child-1".to_string(),
241            filepath: "/child.md".to_string(),
242            document_type: "initiative".to_string(),
243            ..create_test_document()
244        };
245
246        service
247            .create_document(parent)
248            .expect("Failed to create parent");
249        service
250            .create_document(child)
251            .expect("Failed to create child");
252
253        // Create relationship
254        service
255            .create_relationship("parent-1", "child-1", "/parent.md", "/child.md")
256            .expect("Failed to create relationship");
257
258        // Test find children
259        let children = service
260            .find_children("parent-1")
261            .expect("Failed to find children");
262        assert_eq!(children.len(), 1);
263        assert_eq!(children[0].id, "child-1");
264
265        // Test find parent
266        let parent = service
267            .find_parent("child-1")
268            .expect("Failed to find parent")
269            .expect("Parent not found");
270        assert_eq!(parent.id, "parent-1");
271    }
272
273    #[test]
274    fn test_lineage_queries() {
275        let mut service = setup_service();
276
277        // Create strategy
278        let strategy = create_test_document_with_lineage(
279            "strategy-1", 
280            "strategy", 
281            "/strategies/strategy-1/strategy.md",
282            None, 
283            None
284        );
285        service.create_document(strategy).expect("Failed to create strategy");
286
287        // Create initiative under strategy
288        let initiative = create_test_document_with_lineage(
289            "initiative-1", 
290            "initiative", 
291            "/strategies/strategy-1/initiatives/initiative-1/initiative.md",
292            Some("strategy-1".to_string()), 
293            None
294        );
295        service.create_document(initiative).expect("Failed to create initiative");
296
297        // Create tasks under initiative
298        let task1 = create_test_document_with_lineage(
299            "task-1", 
300            "task", 
301            "/strategies/strategy-1/initiatives/initiative-1/tasks/task-1.md",
302            Some("strategy-1".to_string()), 
303            Some("initiative-1".to_string())
304        );
305        let task2 = create_test_document_with_lineage(
306            "task-2", 
307            "task", 
308            "/strategies/strategy-1/initiatives/initiative-1/tasks/task-2.md",
309            Some("strategy-1".to_string()), 
310            Some("initiative-1".to_string())
311        );
312        service.create_document(task1).expect("Failed to create task1");
313        service.create_document(task2).expect("Failed to create task2");
314
315        // Test find by strategy ID
316        let strategy_docs = service.find_by_strategy_id("strategy-1").expect("Failed to find by strategy");
317        assert_eq!(strategy_docs.len(), 3); // initiative + 2 tasks
318
319        // Test find by initiative ID
320        let initiative_docs = service.find_by_initiative_id("initiative-1").expect("Failed to find by initiative");
321        assert_eq!(initiative_docs.len(), 2); // 2 tasks
322
323        // Test strategy hierarchy (should include strategy itself + its children)
324        let strategy_hierarchy = service.find_strategy_hierarchy("strategy-1").expect("Failed to find strategy hierarchy");
325        assert_eq!(strategy_hierarchy.len(), 4); // strategy + initiative + 2 tasks
326
327        // Test initiative hierarchy (should include initiative itself + its tasks)
328        let initiative_hierarchy = service.find_initiative_hierarchy("initiative-1").expect("Failed to find initiative hierarchy");
329        assert_eq!(initiative_hierarchy.len(), 3); // initiative + 2 tasks
330
331        // Verify document types in strategy hierarchy
332        let doc_types: Vec<&str> = strategy_hierarchy.iter().map(|d| d.document_type.as_str()).collect();
333        assert!(doc_types.contains(&"strategy"));
334        assert!(doc_types.contains(&"initiative"));
335        assert!(doc_types.iter().filter(|&&t| t == "task").count() == 2);
336    }
337}