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    /// Generate a short code for a document type (requires db_path)
165    pub fn generate_short_code(&mut self, doc_type: &str) -> Result<String> {
166        // Note: This requires the database path which is not stored in DatabaseService
167        // Callers should use this method from a service that has access to db_path
168        // For now, this is a placeholder that will be called from workspace-aware services
169        self.repository.generate_short_code(doc_type, ":memory:")
170    }
171
172    /// Set counter if the current value is lower than the provided value
173    /// This is a placeholder - actual implementation needs db_path
174    pub fn set_counter_if_lower(&mut self, _doc_type: &str, _min_value: u32) -> Result<bool> {
175        // This method needs access to ConfigurationRepository which requires db_path
176        // For collision resolution, we'll handle this differently
177        Ok(true)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::dal::Database;
185
186    fn setup_service() -> DatabaseService {
187        let db = Database::new(":memory:").expect("Failed to create test database");
188        DatabaseService::new(db.into_repository())
189    }
190
191    fn create_test_document() -> NewDocument {
192        NewDocument {
193            filepath: "/test/doc.md".to_string(),
194            id: "test-doc-1".to_string(),
195            title: "Test Document".to_string(),
196            document_type: "vision".to_string(),
197            created_at: 1609459200.0,
198            updated_at: 1609459200.0,
199            archived: false,
200            exit_criteria_met: false,
201            file_hash: "abc123".to_string(),
202            frontmatter_json: "{}".to_string(),
203            content: Some("Test content".to_string()),
204            phase: "draft".to_string(),
205            strategy_id: None,
206            initiative_id: None,
207            short_code: "TEST-V-0601".to_string(),
208        }
209    }
210
211    fn create_test_document_with_lineage(
212        id: &str,
213        doc_type: &str,
214        filepath: &str,
215        strategy_id: Option<String>,
216        initiative_id: Option<String>,
217    ) -> NewDocument {
218        NewDocument {
219            filepath: filepath.to_string(),
220            id: id.to_string(),
221            title: format!("Test {}", doc_type),
222            document_type: doc_type.to_string(),
223            created_at: 1609459200.0,
224            updated_at: 1609459200.0,
225            archived: false,
226            exit_criteria_met: false,
227            file_hash: "abc123".to_string(),
228            frontmatter_json: "{}".to_string(),
229            content: Some("Test content".to_string()),
230            phase: "draft".to_string(),
231            strategy_id,
232            initiative_id,
233            short_code: format!(
234                "TEST-{}-{:04}",
235                doc_type.chars().next().unwrap().to_uppercase(),
236                match doc_type {
237                    "vision" => 701,
238                    "strategy" => 702,
239                    "initiative" => 703,
240                    "task" => 704,
241                    "adr" => 705,
242                    _ => 799,
243                }
244            ),
245        }
246    }
247
248    #[test]
249    fn test_database_service_crud() {
250        let mut service = setup_service();
251
252        // Create
253        let new_doc = create_test_document();
254        let created = service.create_document(new_doc).expect("Failed to create");
255        assert_eq!(created.id, "test-doc-1");
256
257        // Read
258        let found = service
259            .find_by_id("test-doc-1")
260            .expect("Failed to find")
261            .expect("Document not found");
262        assert_eq!(found.filepath, "/test/doc.md");
263
264        // Update
265        let mut updated_doc = found.clone();
266        updated_doc.title = "Updated Title".to_string();
267        let updated = service
268            .update_document("/test/doc.md", &updated_doc)
269            .expect("Failed to update");
270        assert_eq!(updated.title, "Updated Title");
271
272        // Delete
273        let deleted = service
274            .delete_document("/test/doc.md")
275            .expect("Failed to delete");
276        assert!(deleted);
277
278        // Verify deleted
279        assert!(!service
280            .document_exists("/test/doc.md")
281            .expect("Failed to check existence"));
282    }
283
284    #[test]
285    fn test_database_service_relationships() {
286        let mut service = setup_service();
287
288        // Create parent and child documents
289        let parent = NewDocument {
290            id: "parent-1".to_string(),
291            filepath: "/parent.md".to_string(),
292            document_type: "strategy".to_string(),
293            short_code: "TEST-S-0601".to_string(),
294            ..create_test_document()
295        };
296
297        let child = NewDocument {
298            id: "child-1".to_string(),
299            filepath: "/child.md".to_string(),
300            document_type: "initiative".to_string(),
301            short_code: "TEST-I-0601".to_string(),
302            ..create_test_document()
303        };
304
305        service
306            .create_document(parent)
307            .expect("Failed to create parent");
308        service
309            .create_document(child)
310            .expect("Failed to create child");
311
312        // Create relationship
313        service
314            .create_relationship("parent-1", "child-1", "/parent.md", "/child.md")
315            .expect("Failed to create relationship");
316
317        // Test find children
318        let children = service
319            .find_children("parent-1")
320            .expect("Failed to find children");
321        assert_eq!(children.len(), 1);
322        assert_eq!(children[0].id, "child-1");
323
324        // Test find parent
325        let parent = service
326            .find_parent("child-1")
327            .expect("Failed to find parent")
328            .expect("Parent not found");
329        assert_eq!(parent.id, "parent-1");
330    }
331
332    #[test]
333    fn test_lineage_queries() {
334        let mut service = setup_service();
335
336        // Create strategy
337        let strategy = create_test_document_with_lineage(
338            "strategy-1",
339            "strategy",
340            "/strategies/strategy-1/strategy.md",
341            None,
342            None,
343        );
344        service
345            .create_document(strategy)
346            .expect("Failed to create strategy");
347
348        // Create initiative under strategy
349        let initiative = create_test_document_with_lineage(
350            "initiative-1",
351            "initiative",
352            "/strategies/strategy-1/initiatives/initiative-1/initiative.md",
353            Some("strategy-1".to_string()),
354            None,
355        );
356        service
357            .create_document(initiative)
358            .expect("Failed to create initiative");
359
360        // Create tasks under initiative
361        let mut task1 = create_test_document_with_lineage(
362            "task-1",
363            "task",
364            "/strategies/strategy-1/initiatives/initiative-1/tasks/task-1.md",
365            Some("strategy-1".to_string()),
366            Some("initiative-1".to_string()),
367        );
368        task1.short_code = "TEST-T-0704".to_string();
369
370        let mut task2 = create_test_document_with_lineage(
371            "task-2",
372            "task",
373            "/strategies/strategy-1/initiatives/initiative-1/tasks/task-2.md",
374            Some("strategy-1".to_string()),
375            Some("initiative-1".to_string()),
376        );
377        task2.short_code = "TEST-T-0705".to_string();
378        service
379            .create_document(task1)
380            .expect("Failed to create task1");
381        service
382            .create_document(task2)
383            .expect("Failed to create task2");
384
385        // Test find by strategy ID
386        let strategy_docs = service
387            .find_by_strategy_id("strategy-1")
388            .expect("Failed to find by strategy");
389        assert_eq!(strategy_docs.len(), 3); // initiative + 2 tasks
390
391        // Test find by initiative ID
392        let initiative_docs = service
393            .find_by_initiative_id("initiative-1")
394            .expect("Failed to find by initiative");
395        assert_eq!(initiative_docs.len(), 2); // 2 tasks
396
397        // Test strategy hierarchy (should include strategy itself + its children)
398        let strategy_hierarchy = service
399            .find_strategy_hierarchy("strategy-1")
400            .expect("Failed to find strategy hierarchy");
401        assert_eq!(strategy_hierarchy.len(), 4); // strategy + initiative + 2 tasks
402
403        // Test initiative hierarchy (should include initiative itself + its tasks)
404        let initiative_hierarchy = service
405            .find_initiative_hierarchy("initiative-1")
406            .expect("Failed to find initiative hierarchy");
407        assert_eq!(initiative_hierarchy.len(), 3); // initiative + 2 tasks
408
409        // Verify document types in strategy hierarchy
410        let doc_types: Vec<&str> = strategy_hierarchy
411            .iter()
412            .map(|d| d.document_type.as_str())
413            .collect();
414        assert!(doc_types.contains(&"strategy"));
415        assert!(doc_types.contains(&"initiative"));
416        assert!(doc_types.iter().filter(|&&t| t == "task").count() == 2);
417    }
418}