1use crate::dal::database::configuration_repository::ConfigurationRepository;
2use crate::dal::database::models::*;
3use crate::dal::database::schema;
4use crate::{MetisError, Result};
5use diesel::prelude::*;
6use diesel::sqlite::SqliteConnection;
7
8pub struct DocumentRepository {
10 connection: SqliteConnection,
11}
12
13impl DocumentRepository {
14 pub fn new(connection: SqliteConnection) -> Self {
15 Self { connection }
16 }
17
18 pub fn create_document(&mut self, doc: NewDocument) -> Result<Document> {
20 use schema::documents::dsl::*;
21
22 diesel::insert_into(documents)
23 .values(&doc)
24 .returning(Document::as_returning())
25 .get_result(&mut self.connection)
26 .map_err(MetisError::Database)
27 }
28
29 pub fn find_by_filepath(&mut self, file_path: &str) -> Result<Option<Document>> {
31 use schema::documents::dsl::*;
32
33 documents
34 .filter(filepath.eq(file_path))
35 .first(&mut self.connection)
36 .optional()
37 .map_err(MetisError::Database)
38 }
39
40 pub fn find_by_id(&mut self, document_id: &str) -> Result<Option<Document>> {
42 use schema::documents::dsl::*;
43
44 documents
45 .filter(id.eq(document_id))
46 .first(&mut self.connection)
47 .optional()
48 .map_err(MetisError::Database)
49 }
50
51 pub fn update_document(&mut self, file_path: &str, doc: &Document) -> Result<Document> {
53 use schema::documents::dsl::*;
54
55 diesel::update(documents.filter(filepath.eq(file_path)))
56 .set(doc)
57 .returning(Document::as_returning())
58 .get_result(&mut self.connection)
59 .map_err(MetisError::Database)
60 }
61
62 pub fn delete_document(&mut self, file_path: &str) -> Result<bool> {
64 use schema::documents::dsl::*;
65
66 let deleted_count = diesel::delete(documents.filter(filepath.eq(file_path)))
67 .execute(&mut self.connection)
68 .map_err(MetisError::Database)?;
69
70 Ok(deleted_count > 0)
71 }
72
73 pub fn find_children(&mut self, parent_document_id: &str) -> Result<Vec<Document>> {
75 use schema::document_relationships::dsl::*;
76 use schema::documents::dsl::*;
77
78 documents
79 .inner_join(document_relationships.on(id.eq(child_id)))
80 .filter(parent_id.eq(parent_document_id))
81 .select(Document::as_select())
82 .load(&mut self.connection)
83 .map_err(MetisError::Database)
84 }
85
86 pub fn find_parent(&mut self, child_document_id: &str) -> Result<Option<Document>> {
88 use schema::document_relationships::dsl::*;
89 use schema::documents::dsl::*;
90
91 documents
92 .inner_join(document_relationships.on(id.eq(parent_id)))
93 .filter(child_id.eq(child_document_id))
94 .select(Document::as_select())
95 .first(&mut self.connection)
96 .optional()
97 .map_err(MetisError::Database)
98 }
99
100 pub fn create_relationship(&mut self, relationship: DocumentRelationship) -> Result<()> {
102 use schema::document_relationships::dsl::*;
103
104 diesel::insert_into(document_relationships)
105 .values(&relationship)
106 .execute(&mut self.connection)
107 .map_err(MetisError::Database)?;
108
109 Ok(())
110 }
111
112 pub fn search_documents(&mut self, query: &str) -> Result<Vec<Document>> {
114 diesel::sql_query(
116 "
117 SELECT d.* FROM documents d
118 INNER JOIN document_search ds ON d.filepath = ds.document_filepath
119 WHERE document_search MATCH ?
120 ",
121 )
122 .bind::<diesel::sql_types::Text, _>(query)
123 .load::<Document>(&mut self.connection)
124 .map_err(MetisError::Database)
125 }
126
127 pub fn search_documents_unarchived(&mut self, query: &str) -> Result<Vec<Document>> {
129 diesel::sql_query(
131 "
132 SELECT d.* FROM documents d
133 INNER JOIN document_search ds ON d.filepath = ds.document_filepath
134 WHERE document_search MATCH ? AND d.archived = 0
135 ",
136 )
137 .bind::<diesel::sql_types::Text, _>(query)
138 .load::<Document>(&mut self.connection)
139 .map_err(MetisError::Database)
140 }
141
142 pub fn find_by_type(&mut self, doc_type: &str) -> Result<Vec<Document>> {
144 use schema::documents::dsl::*;
145
146 documents
147 .filter(document_type.eq(doc_type))
148 .order(updated_at.desc())
149 .load(&mut self.connection)
150 .map_err(MetisError::Database)
151 }
152
153 pub fn find_by_type_unarchived(&mut self, doc_type: &str) -> Result<Vec<Document>> {
155 use schema::documents::dsl::*;
156
157 documents
158 .filter(document_type.eq(doc_type))
159 .filter(archived.eq(false))
160 .order(updated_at.desc())
161 .load(&mut self.connection)
162 .map_err(MetisError::Database)
163 }
164
165 pub fn find_by_tag(&mut self, tag_name: &str) -> Result<Vec<Document>> {
167 use schema::document_tags::dsl::*;
168 use schema::documents::dsl::*;
169
170 documents
171 .inner_join(document_tags.on(filepath.eq(document_filepath)))
172 .filter(tag.eq(tag_name))
173 .select(Document::as_select())
174 .load(&mut self.connection)
175 .map_err(MetisError::Database)
176 }
177
178 pub fn find_by_phase(&mut self, phase_name: &str) -> Result<Vec<Document>> {
180 use schema::documents::dsl::*;
181
182 documents
183 .filter(phase.eq(phase_name))
184 .order(updated_at.desc())
185 .load(&mut self.connection)
186 .map_err(MetisError::Database)
187 }
188
189 pub fn find_by_type_and_phase(
191 &mut self,
192 doc_type: &str,
193 phase_name: &str,
194 ) -> Result<Vec<Document>> {
195 use schema::documents::dsl::*;
196
197 documents
198 .filter(document_type.eq(doc_type))
199 .filter(phase.eq(phase_name))
200 .order(updated_at.desc())
201 .load(&mut self.connection)
202 .map_err(MetisError::Database)
203 }
204
205 pub fn find_by_strategy_id(&mut self, strategy_document_id: &str) -> Result<Vec<Document>> {
207 use schema::documents::dsl::*;
208
209 documents
210 .filter(strategy_id.eq(strategy_document_id))
211 .order(updated_at.desc())
212 .load(&mut self.connection)
213 .map_err(MetisError::Database)
214 }
215
216 pub fn find_by_initiative_id(&mut self, initiative_document_id: &str) -> Result<Vec<Document>> {
218 use schema::documents::dsl::*;
219
220 documents
221 .filter(initiative_id.eq(initiative_document_id))
222 .order(updated_at.desc())
223 .load(&mut self.connection)
224 .map_err(MetisError::Database)
225 }
226
227 pub fn get_tags_for_document(&mut self, doc_filepath: &str) -> Result<Vec<String>> {
229 use schema::document_tags::dsl::*;
230
231 document_tags
232 .filter(document_filepath.eq(doc_filepath))
233 .select(tag)
234 .load::<String>(&mut self.connection)
235 .map_err(MetisError::Database)
236 }
237
238 pub fn find_strategy_hierarchy(&mut self, strategy_document_id: &str) -> Result<Vec<Document>> {
240 use schema::documents::dsl::*;
241
242 documents
243 .filter(
244 id.eq(strategy_document_id)
245 .or(strategy_id.eq(strategy_document_id)),
246 )
247 .order((document_type.asc(), updated_at.desc()))
248 .load(&mut self.connection)
249 .map_err(MetisError::Database)
250 }
251
252 pub fn find_strategy_hierarchy_by_short_code(
254 &mut self,
255 strategy_short_code: &str,
256 ) -> Result<Vec<Document>> {
257 use schema::documents::dsl::*;
258
259 documents
260 .filter(
261 short_code
262 .eq(strategy_short_code)
263 .or(strategy_id.eq(strategy_short_code)),
264 )
265 .order((document_type.asc(), updated_at.desc()))
266 .load(&mut self.connection)
267 .map_err(MetisError::Database)
268 }
269
270 pub fn find_initiative_hierarchy(
272 &mut self,
273 initiative_document_id: &str,
274 ) -> Result<Vec<Document>> {
275 use schema::documents::dsl::*;
276
277 documents
278 .filter(
279 id.eq(initiative_document_id)
280 .or(initiative_id.eq(initiative_document_id)),
281 )
282 .order((document_type.asc(), updated_at.desc()))
283 .load(&mut self.connection)
284 .map_err(MetisError::Database)
285 }
286
287 pub fn find_initiative_hierarchy_by_short_code(
289 &mut self,
290 initiative_short_code: &str,
291 ) -> Result<Vec<Document>> {
292 use schema::documents::dsl::*;
293
294 documents
295 .filter(
296 short_code
297 .eq(initiative_short_code)
298 .or(initiative_id.eq(initiative_short_code)),
299 )
300 .order((document_type.asc(), updated_at.desc()))
301 .load(&mut self.connection)
302 .map_err(MetisError::Database)
303 }
304
305 pub fn generate_short_code(&mut self, doc_type: &str, db_path: &str) -> Result<String> {
307 let mut config_repo =
308 ConfigurationRepository::new(SqliteConnection::establish(db_path).map_err(|e| {
309 MetisError::ConfigurationError(
310 crate::domain::configuration::ConfigurationError::InvalidValue(e.to_string()),
311 )
312 })?);
313
314 config_repo.generate_short_code(doc_type)
315 }
316
317 pub fn find_by_short_code(&mut self, code: &str) -> Result<Option<Document>> {
319 use schema::documents::dsl::*;
320
321 documents
322 .filter(short_code.eq(code))
323 .first(&mut self.connection)
324 .optional()
325 .map_err(MetisError::Database)
326 }
327
328 pub fn resolve_short_code_to_document_id(&mut self, code: &str) -> Result<String> {
330 match self.find_by_short_code(code)? {
331 Some(doc) => Ok(doc.id.to_string()),
332 None => Err(MetisError::NotFound(format!(
333 "Document with short code '{}' not found",
334 code
335 ))),
336 }
337 }
338
339 pub fn resolve_short_code_to_filepath(&mut self, code: &str) -> Result<String> {
341 match self.find_by_short_code(code)? {
342 Some(doc) => Ok(doc.filepath),
343 None => Err(MetisError::NotFound(format!(
344 "Document with short code '{}' not found",
345 code
346 ))),
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::dal::Database;
355
356 fn setup_test_repository() -> DocumentRepository {
357 let db = Database::new(":memory:").expect("Failed to create test database");
358 db.into_repository()
359 }
360
361 fn create_test_document() -> NewDocument {
362 NewDocument {
363 filepath: "/test/doc.md".to_string(),
364 id: "test-doc-1".to_string(),
365 title: "Test Document".to_string(),
366 document_type: "vision".to_string(),
367 created_at: 1609459200.0, updated_at: 1609459200.0,
369 archived: false,
370 exit_criteria_met: false,
371 file_hash: "abc123".to_string(),
372 frontmatter_json: "{}".to_string(),
373 content: Some("Test content".to_string()),
374 phase: "draft".to_string(),
375 strategy_id: None,
376 initiative_id: None,
377 short_code: "TEST-V-0001".to_string(),
378 }
379 }
380
381 #[test]
382 fn test_create_and_find_document() {
383 let mut repo = setup_test_repository();
384
385 let new_doc = create_test_document();
386 let created = repo
387 .create_document(new_doc)
388 .expect("Failed to create document");
389
390 assert_eq!(created.filepath, "/test/doc.md");
391 assert_eq!(created.title, "Test Document");
392 assert_eq!(created.document_type, "vision");
393
394 let found = repo
396 .find_by_filepath("/test/doc.md")
397 .expect("Failed to find document")
398 .expect("Document not found");
399 assert_eq!(found.id, "test-doc-1");
400
401 let found_by_id = repo
403 .find_by_id("test-doc-1")
404 .expect("Failed to find document")
405 .expect("Document not found");
406 assert_eq!(found_by_id.filepath, "/test/doc.md");
407 }
408
409 #[test]
410 fn test_update_document() {
411 let mut repo = setup_test_repository();
412
413 let new_doc = create_test_document();
414 let mut created = repo
415 .create_document(new_doc)
416 .expect("Failed to create document");
417
418 created.title = "Updated Title".to_string();
420 created.updated_at = 1609462800.0; let updated = repo
423 .update_document("/test/doc.md", &created)
424 .expect("Failed to update document");
425
426 assert_eq!(updated.title, "Updated Title");
427 assert_eq!(updated.updated_at, 1609462800.0);
428 }
429
430 #[test]
431 fn test_delete_document() {
432 let mut repo = setup_test_repository();
433
434 let new_doc = create_test_document();
435 repo.create_document(new_doc)
436 .expect("Failed to create document");
437
438 let deleted = repo
440 .delete_document("/test/doc.md")
441 .expect("Failed to delete document");
442 assert!(deleted);
443
444 let found = repo
446 .find_by_filepath("/test/doc.md")
447 .expect("Failed to search for document");
448 assert!(found.is_none());
449
450 let deleted_again = repo
452 .delete_document("/test/doc.md")
453 .expect("Failed to delete document");
454 assert!(!deleted_again);
455 }
456
457 #[test]
458 fn test_document_relationships() {
459 let mut repo = setup_test_repository();
460
461 let parent_doc = NewDocument {
463 filepath: "/parent.md".to_string(),
464 id: "parent-1".to_string(),
465 title: "Parent Document".to_string(),
466 document_type: "strategy".to_string(),
467 created_at: 1609459200.0,
468 updated_at: 1609459200.0,
469 archived: false,
470 exit_criteria_met: false,
471 file_hash: "parent123".to_string(),
472 frontmatter_json: "{}".to_string(),
473 content: Some("Parent content".to_string()),
474 phase: "shaping".to_string(),
475 strategy_id: None,
476 initiative_id: None,
477 short_code: "TEST-S-0001".to_string(),
478 };
479 repo.create_document(parent_doc)
480 .expect("Failed to create parent");
481
482 let child_doc = NewDocument {
484 filepath: "/child.md".to_string(),
485 id: "child-1".to_string(),
486 title: "Child Document".to_string(),
487 document_type: "initiative".to_string(),
488 created_at: 1609459200.0,
489 updated_at: 1609459200.0,
490 archived: false,
491 exit_criteria_met: false,
492 file_hash: "child123".to_string(),
493 frontmatter_json: "{}".to_string(),
494 content: Some("Child content".to_string()),
495 phase: "discovery".to_string(),
496 strategy_id: Some("parent-1".to_string()),
497 initiative_id: None,
498 short_code: "TEST-I-0001".to_string(),
499 };
500 repo.create_document(child_doc)
501 .expect("Failed to create child");
502
503 let relationship = DocumentRelationship {
505 child_id: "child-1".to_string(),
506 parent_id: "parent-1".to_string(),
507 child_filepath: "/child.md".to_string(),
508 parent_filepath: "/parent.md".to_string(),
509 };
510 repo.create_relationship(relationship)
511 .expect("Failed to create relationship");
512
513 let children = repo
515 .find_children("parent-1")
516 .expect("Failed to find children");
517 assert_eq!(children.len(), 1);
518 assert_eq!(children[0].id, "child-1");
519
520 let parent = repo
522 .find_parent("child-1")
523 .expect("Failed to find parent")
524 .expect("Parent not found");
525 assert_eq!(parent.id, "parent-1");
526 }
527
528 #[test]
529 fn test_find_by_type() {
530 let mut repo = setup_test_repository();
531
532 let vision_doc = NewDocument {
534 document_type: "vision".to_string(),
535 filepath: "/vision.md".to_string(),
536 id: "vision-1".to_string(),
537 title: "Vision Doc".to_string(),
538 created_at: 1609459200.0,
539 updated_at: 1609459200.0,
540 archived: false,
541 exit_criteria_met: false,
542 file_hash: "vision123".to_string(),
543 frontmatter_json: "{}".to_string(),
544 content: None,
545 phase: "draft".to_string(),
546 strategy_id: None,
547 initiative_id: None,
548 short_code: "TEST-V-0002".to_string(),
549 };
550
551 let strategy_doc = NewDocument {
552 document_type: "strategy".to_string(),
553 filepath: "/strategy.md".to_string(),
554 id: "strategy-1".to_string(),
555 title: "Strategy Doc".to_string(),
556 created_at: 1609462800.0, updated_at: 1609462800.0,
558 archived: false,
559 exit_criteria_met: false,
560 file_hash: "strategy123".to_string(),
561 frontmatter_json: "{}".to_string(),
562 content: None,
563 phase: "shaping".to_string(),
564 strategy_id: None,
565 initiative_id: None,
566 short_code: "TEST-S-0002".to_string(),
567 };
568
569 repo.create_document(vision_doc)
570 .expect("Failed to create vision");
571 repo.create_document(strategy_doc)
572 .expect("Failed to create strategy");
573
574 let visions = repo.find_by_type("vision").expect("Failed to find visions");
576 assert_eq!(visions.len(), 1);
577 assert_eq!(visions[0].document_type, "vision");
578
579 let strategies = repo
580 .find_by_type("strategy")
581 .expect("Failed to find strategies");
582 assert_eq!(strategies.len(), 1);
583 assert_eq!(strategies[0].document_type, "strategy");
584
585 let _all_docs = repo.find_by_type("vision").expect("Failed to find docs");
587 }
590
591 #[test]
592 fn test_document_not_found() {
593 let mut repo = setup_test_repository();
594
595 let found = repo
596 .find_by_filepath("/nonexistent.md")
597 .expect("Failed to search for document");
598 assert!(found.is_none());
599
600 let found_by_id = repo
601 .find_by_id("nonexistent")
602 .expect("Failed to search for document");
603 assert!(found_by_id.is_none());
604 }
605}