1use crate::dal::database::models::*;
2use crate::dal::database::schema;
3use crate::{MetisError, Result};
4use diesel::prelude::*;
5use diesel::sqlite::SqliteConnection;
6
7pub struct DocumentRepository {
9 connection: SqliteConnection,
10}
11
12impl DocumentRepository {
13 pub fn new(connection: SqliteConnection) -> Self {
14 Self { connection }
15 }
16
17 pub fn create_document(&mut self, doc: NewDocument) -> Result<Document> {
19 use schema::documents::dsl::*;
20
21 diesel::insert_into(documents)
22 .values(&doc)
23 .returning(Document::as_returning())
24 .get_result(&mut self.connection)
25 .map_err(MetisError::Database)
26 }
27
28 pub fn find_by_filepath(&mut self, file_path: &str) -> Result<Option<Document>> {
30 use schema::documents::dsl::*;
31
32 documents
33 .filter(filepath.eq(file_path))
34 .first(&mut self.connection)
35 .optional()
36 .map_err(MetisError::Database)
37 }
38
39 pub fn find_by_id(&mut self, document_id: &str) -> Result<Option<Document>> {
41 use schema::documents::dsl::*;
42
43 documents
44 .filter(id.eq(document_id))
45 .first(&mut self.connection)
46 .optional()
47 .map_err(MetisError::Database)
48 }
49
50 pub fn update_document(&mut self, file_path: &str, doc: &Document) -> Result<Document> {
52 use schema::documents::dsl::*;
53
54 diesel::update(documents.filter(filepath.eq(file_path)))
55 .set(doc)
56 .returning(Document::as_returning())
57 .get_result(&mut self.connection)
58 .map_err(MetisError::Database)
59 }
60
61 pub fn delete_document(&mut self, file_path: &str) -> Result<bool> {
63 use schema::documents::dsl::*;
64
65 let deleted_count = diesel::delete(documents.filter(filepath.eq(file_path)))
66 .execute(&mut self.connection)
67 .map_err(MetisError::Database)?;
68
69 Ok(deleted_count > 0)
70 }
71
72 pub fn find_children(&mut self, parent_document_id: &str) -> Result<Vec<Document>> {
74 use schema::document_relationships::dsl::*;
75 use schema::documents::dsl::*;
76
77 documents
78 .inner_join(document_relationships.on(id.eq(child_id)))
79 .filter(parent_id.eq(parent_document_id))
80 .select(Document::as_select())
81 .load(&mut self.connection)
82 .map_err(MetisError::Database)
83 }
84
85 pub fn find_parent(&mut self, child_document_id: &str) -> Result<Option<Document>> {
87 use schema::document_relationships::dsl::*;
88 use schema::documents::dsl::*;
89
90 documents
91 .inner_join(document_relationships.on(id.eq(parent_id)))
92 .filter(child_id.eq(child_document_id))
93 .select(Document::as_select())
94 .first(&mut self.connection)
95 .optional()
96 .map_err(MetisError::Database)
97 }
98
99 pub fn create_relationship(&mut self, relationship: DocumentRelationship) -> Result<()> {
101 use schema::document_relationships::dsl::*;
102
103 diesel::insert_into(document_relationships)
104 .values(&relationship)
105 .execute(&mut self.connection)
106 .map_err(MetisError::Database)?;
107
108 Ok(())
109 }
110
111 pub fn search_documents(&mut self, query: &str) -> Result<Vec<Document>> {
113 diesel::sql_query(
115 "
116 SELECT d.* FROM documents d
117 INNER JOIN document_search ds ON d.filepath = ds.document_filepath
118 WHERE document_search MATCH ?
119 ",
120 )
121 .bind::<diesel::sql_types::Text, _>(query)
122 .load::<Document>(&mut self.connection)
123 .map_err(MetisError::Database)
124 }
125
126 pub fn find_by_type(&mut self, doc_type: &str) -> Result<Vec<Document>> {
128 use schema::documents::dsl::*;
129
130 documents
131 .filter(document_type.eq(doc_type))
132 .order(updated_at.desc())
133 .load(&mut self.connection)
134 .map_err(MetisError::Database)
135 }
136
137 pub fn find_by_tag(&mut self, tag_name: &str) -> Result<Vec<Document>> {
139 use schema::document_tags::dsl::*;
140 use schema::documents::dsl::*;
141
142 documents
143 .inner_join(document_tags.on(filepath.eq(document_filepath)))
144 .filter(tag.eq(tag_name))
145 .select(Document::as_select())
146 .load(&mut self.connection)
147 .map_err(MetisError::Database)
148 }
149
150 pub fn find_by_phase(&mut self, phase_name: &str) -> Result<Vec<Document>> {
152 use schema::documents::dsl::*;
153
154 documents
155 .filter(phase.eq(phase_name))
156 .order(updated_at.desc())
157 .load(&mut self.connection)
158 .map_err(MetisError::Database)
159 }
160
161 pub fn find_by_type_and_phase(
163 &mut self,
164 doc_type: &str,
165 phase_name: &str,
166 ) -> Result<Vec<Document>> {
167 use schema::documents::dsl::*;
168
169 documents
170 .filter(document_type.eq(doc_type))
171 .filter(phase.eq(phase_name))
172 .order(updated_at.desc())
173 .load(&mut self.connection)
174 .map_err(MetisError::Database)
175 }
176
177 pub fn find_by_strategy_id(&mut self, strategy_document_id: &str) -> Result<Vec<Document>> {
179 use schema::documents::dsl::*;
180
181 documents
182 .filter(strategy_id.eq(strategy_document_id))
183 .order(updated_at.desc())
184 .load(&mut self.connection)
185 .map_err(MetisError::Database)
186 }
187
188 pub fn find_by_initiative_id(&mut self, initiative_document_id: &str) -> Result<Vec<Document>> {
190 use schema::documents::dsl::*;
191
192 documents
193 .filter(initiative_id.eq(initiative_document_id))
194 .order(updated_at.desc())
195 .load(&mut self.connection)
196 .map_err(MetisError::Database)
197 }
198
199 pub fn find_strategy_hierarchy(&mut self, strategy_document_id: &str) -> Result<Vec<Document>> {
201 use schema::documents::dsl::*;
202
203 documents
204 .filter(
205 id.eq(strategy_document_id)
206 .or(strategy_id.eq(strategy_document_id))
207 )
208 .order((document_type.asc(), updated_at.desc()))
209 .load(&mut self.connection)
210 .map_err(MetisError::Database)
211 }
212
213 pub fn find_initiative_hierarchy(&mut self, initiative_document_id: &str) -> Result<Vec<Document>> {
215 use schema::documents::dsl::*;
216
217 documents
218 .filter(
219 id.eq(initiative_document_id)
220 .or(initiative_id.eq(initiative_document_id))
221 )
222 .order((document_type.asc(), updated_at.desc()))
223 .load(&mut self.connection)
224 .map_err(MetisError::Database)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::dal::Database;
232
233 fn setup_test_repository() -> DocumentRepository {
234 let db = Database::new(":memory:").expect("Failed to create test database");
235 db.into_repository()
236 }
237
238 fn create_test_document() -> NewDocument {
239 NewDocument {
240 filepath: "/test/doc.md".to_string(),
241 id: "test-doc-1".to_string(),
242 title: "Test Document".to_string(),
243 document_type: "vision".to_string(),
244 created_at: 1609459200.0, updated_at: 1609459200.0,
246 archived: false,
247 exit_criteria_met: false,
248 file_hash: "abc123".to_string(),
249 frontmatter_json: "{}".to_string(),
250 content: Some("Test content".to_string()),
251 phase: "draft".to_string(),
252 strategy_id: None,
253 initiative_id: None,
254 }
255 }
256
257 #[test]
258 fn test_create_and_find_document() {
259 let mut repo = setup_test_repository();
260
261 let new_doc = create_test_document();
262 let created = repo
263 .create_document(new_doc)
264 .expect("Failed to create document");
265
266 assert_eq!(created.filepath, "/test/doc.md");
267 assert_eq!(created.title, "Test Document");
268 assert_eq!(created.document_type, "vision");
269
270 let found = repo
272 .find_by_filepath("/test/doc.md")
273 .expect("Failed to find document")
274 .expect("Document not found");
275 assert_eq!(found.id, "test-doc-1");
276
277 let found_by_id = repo
279 .find_by_id("test-doc-1")
280 .expect("Failed to find document")
281 .expect("Document not found");
282 assert_eq!(found_by_id.filepath, "/test/doc.md");
283 }
284
285 #[test]
286 fn test_update_document() {
287 let mut repo = setup_test_repository();
288
289 let new_doc = create_test_document();
290 let mut created = repo
291 .create_document(new_doc)
292 .expect("Failed to create document");
293
294 created.title = "Updated Title".to_string();
296 created.updated_at = 1609462800.0; let updated = repo
299 .update_document("/test/doc.md", &created)
300 .expect("Failed to update document");
301
302 assert_eq!(updated.title, "Updated Title");
303 assert_eq!(updated.updated_at, 1609462800.0);
304 }
305
306 #[test]
307 fn test_delete_document() {
308 let mut repo = setup_test_repository();
309
310 let new_doc = create_test_document();
311 repo.create_document(new_doc)
312 .expect("Failed to create document");
313
314 let deleted = repo
316 .delete_document("/test/doc.md")
317 .expect("Failed to delete document");
318 assert!(deleted);
319
320 let found = repo
322 .find_by_filepath("/test/doc.md")
323 .expect("Failed to search for document");
324 assert!(found.is_none());
325
326 let deleted_again = repo
328 .delete_document("/test/doc.md")
329 .expect("Failed to delete document");
330 assert!(!deleted_again);
331 }
332
333 #[test]
334 fn test_document_relationships() {
335 let mut repo = setup_test_repository();
336
337 let parent_doc = NewDocument {
339 filepath: "/parent.md".to_string(),
340 id: "parent-1".to_string(),
341 title: "Parent Document".to_string(),
342 document_type: "strategy".to_string(),
343 created_at: 1609459200.0,
344 updated_at: 1609459200.0,
345 archived: false,
346 exit_criteria_met: false,
347 file_hash: "parent123".to_string(),
348 frontmatter_json: "{}".to_string(),
349 content: Some("Parent content".to_string()),
350 phase: "shaping".to_string(),
351 strategy_id: None,
352 initiative_id: None,
353 };
354 repo.create_document(parent_doc)
355 .expect("Failed to create parent");
356
357 let child_doc = NewDocument {
359 filepath: "/child.md".to_string(),
360 id: "child-1".to_string(),
361 title: "Child Document".to_string(),
362 document_type: "initiative".to_string(),
363 created_at: 1609459200.0,
364 updated_at: 1609459200.0,
365 archived: false,
366 exit_criteria_met: false,
367 file_hash: "child123".to_string(),
368 frontmatter_json: "{}".to_string(),
369 content: Some("Child content".to_string()),
370 phase: "discovery".to_string(),
371 strategy_id: Some("parent-1".to_string()),
372 initiative_id: None,
373 };
374 repo.create_document(child_doc)
375 .expect("Failed to create child");
376
377 let relationship = DocumentRelationship {
379 child_id: "child-1".to_string(),
380 parent_id: "parent-1".to_string(),
381 child_filepath: "/child.md".to_string(),
382 parent_filepath: "/parent.md".to_string(),
383 };
384 repo.create_relationship(relationship)
385 .expect("Failed to create relationship");
386
387 let children = repo
389 .find_children("parent-1")
390 .expect("Failed to find children");
391 assert_eq!(children.len(), 1);
392 assert_eq!(children[0].id, "child-1");
393
394 let parent = repo
396 .find_parent("child-1")
397 .expect("Failed to find parent")
398 .expect("Parent not found");
399 assert_eq!(parent.id, "parent-1");
400 }
401
402 #[test]
403 fn test_find_by_type() {
404 let mut repo = setup_test_repository();
405
406 let vision_doc = NewDocument {
408 document_type: "vision".to_string(),
409 filepath: "/vision.md".to_string(),
410 id: "vision-1".to_string(),
411 title: "Vision Doc".to_string(),
412 created_at: 1609459200.0,
413 updated_at: 1609459200.0,
414 archived: false,
415 exit_criteria_met: false,
416 file_hash: "vision123".to_string(),
417 frontmatter_json: "{}".to_string(),
418 content: None,
419 phase: "draft".to_string(),
420 strategy_id: None,
421 initiative_id: None,
422 };
423
424 let strategy_doc = NewDocument {
425 document_type: "strategy".to_string(),
426 filepath: "/strategy.md".to_string(),
427 id: "strategy-1".to_string(),
428 title: "Strategy Doc".to_string(),
429 created_at: 1609462800.0, updated_at: 1609462800.0,
431 archived: false,
432 exit_criteria_met: false,
433 file_hash: "strategy123".to_string(),
434 frontmatter_json: "{}".to_string(),
435 content: None,
436 phase: "shaping".to_string(),
437 strategy_id: None,
438 initiative_id: None,
439 };
440
441 repo.create_document(vision_doc)
442 .expect("Failed to create vision");
443 repo.create_document(strategy_doc)
444 .expect("Failed to create strategy");
445
446 let visions = repo.find_by_type("vision").expect("Failed to find visions");
448 assert_eq!(visions.len(), 1);
449 assert_eq!(visions[0].document_type, "vision");
450
451 let strategies = repo
452 .find_by_type("strategy")
453 .expect("Failed to find strategies");
454 assert_eq!(strategies.len(), 1);
455 assert_eq!(strategies[0].document_type, "strategy");
456
457 let _all_docs = repo.find_by_type("vision").expect("Failed to find docs");
459 }
462
463 #[test]
464 fn test_document_not_found() {
465 let mut repo = setup_test_repository();
466
467 let found = repo
468 .find_by_filepath("/nonexistent.md")
469 .expect("Failed to search for document");
470 assert!(found.is_none());
471
472 let found_by_id = repo
473 .find_by_id("nonexistent")
474 .expect("Failed to search for document");
475 assert!(found_by_id.is_none());
476 }
477}