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