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