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 find_by_type(&mut self, doc_type: &str) -> Result<Vec<Document>> {
129 use schema::documents::dsl::*;
130
131 documents
132 .filter(document_type.eq(doc_type))
133 .order(updated_at.desc())
134 .load(&mut self.connection)
135 .map_err(MetisError::Database)
136 }
137
138 pub fn find_by_tag(&mut self, tag_name: &str) -> Result<Vec<Document>> {
140 use schema::document_tags::dsl::*;
141 use schema::documents::dsl::*;
142
143 documents
144 .inner_join(document_tags.on(filepath.eq(document_filepath)))
145 .filter(tag.eq(tag_name))
146 .select(Document::as_select())
147 .load(&mut self.connection)
148 .map_err(MetisError::Database)
149 }
150
151 pub fn find_by_phase(&mut self, phase_name: &str) -> Result<Vec<Document>> {
153 use schema::documents::dsl::*;
154
155 documents
156 .filter(phase.eq(phase_name))
157 .order(updated_at.desc())
158 .load(&mut self.connection)
159 .map_err(MetisError::Database)
160 }
161
162 pub fn find_by_type_and_phase(
164 &mut self,
165 doc_type: &str,
166 phase_name: &str,
167 ) -> Result<Vec<Document>> {
168 use schema::documents::dsl::*;
169
170 documents
171 .filter(document_type.eq(doc_type))
172 .filter(phase.eq(phase_name))
173 .order(updated_at.desc())
174 .load(&mut self.connection)
175 .map_err(MetisError::Database)
176 }
177
178 pub fn find_by_strategy_id(&mut self, strategy_document_id: &str) -> Result<Vec<Document>> {
180 use schema::documents::dsl::*;
181
182 documents
183 .filter(strategy_id.eq(strategy_document_id))
184 .order(updated_at.desc())
185 .load(&mut self.connection)
186 .map_err(MetisError::Database)
187 }
188
189 pub fn find_by_initiative_id(&mut self, initiative_document_id: &str) -> Result<Vec<Document>> {
191 use schema::documents::dsl::*;
192
193 documents
194 .filter(initiative_id.eq(initiative_document_id))
195 .order(updated_at.desc())
196 .load(&mut self.connection)
197 .map_err(MetisError::Database)
198 }
199
200 pub fn get_tags_for_document(&mut self, doc_filepath: &str) -> Result<Vec<String>> {
202 use schema::document_tags::dsl::*;
203
204 document_tags
205 .filter(document_filepath.eq(doc_filepath))
206 .select(tag)
207 .load::<String>(&mut self.connection)
208 .map_err(MetisError::Database)
209 }
210
211 pub fn find_strategy_hierarchy(&mut self, strategy_document_id: &str) -> Result<Vec<Document>> {
213 use schema::documents::dsl::*;
214
215 documents
216 .filter(
217 id.eq(strategy_document_id)
218 .or(strategy_id.eq(strategy_document_id)),
219 )
220 .order((document_type.asc(), updated_at.desc()))
221 .load(&mut self.connection)
222 .map_err(MetisError::Database)
223 }
224
225 pub fn find_strategy_hierarchy_by_short_code(
227 &mut self,
228 strategy_short_code: &str,
229 ) -> Result<Vec<Document>> {
230 use schema::documents::dsl::*;
231
232 documents
233 .filter(
234 short_code
235 .eq(strategy_short_code)
236 .or(strategy_id.eq(strategy_short_code)),
237 )
238 .order((document_type.asc(), updated_at.desc()))
239 .load(&mut self.connection)
240 .map_err(MetisError::Database)
241 }
242
243 pub fn find_initiative_hierarchy(
245 &mut self,
246 initiative_document_id: &str,
247 ) -> Result<Vec<Document>> {
248 use schema::documents::dsl::*;
249
250 documents
251 .filter(
252 id.eq(initiative_document_id)
253 .or(initiative_id.eq(initiative_document_id)),
254 )
255 .order((document_type.asc(), updated_at.desc()))
256 .load(&mut self.connection)
257 .map_err(MetisError::Database)
258 }
259
260 pub fn find_initiative_hierarchy_by_short_code(
262 &mut self,
263 initiative_short_code: &str,
264 ) -> Result<Vec<Document>> {
265 use schema::documents::dsl::*;
266
267 documents
268 .filter(
269 short_code
270 .eq(initiative_short_code)
271 .or(initiative_id.eq(initiative_short_code)),
272 )
273 .order((document_type.asc(), updated_at.desc()))
274 .load(&mut self.connection)
275 .map_err(MetisError::Database)
276 }
277
278 pub fn generate_short_code(&mut self, doc_type: &str, db_path: &str) -> Result<String> {
280 let mut config_repo =
281 ConfigurationRepository::new(SqliteConnection::establish(db_path).map_err(|e| {
282 MetisError::ConfigurationError(
283 crate::domain::configuration::ConfigurationError::InvalidValue(e.to_string()),
284 )
285 })?);
286
287 config_repo.generate_short_code(doc_type)
288 }
289
290 pub fn find_by_short_code(&mut self, code: &str) -> Result<Option<Document>> {
292 use schema::documents::dsl::*;
293
294 documents
295 .filter(short_code.eq(code))
296 .first(&mut self.connection)
297 .optional()
298 .map_err(MetisError::Database)
299 }
300
301 pub fn resolve_short_code_to_document_id(&mut self, code: &str) -> Result<String> {
303 match self.find_by_short_code(code)? {
304 Some(doc) => Ok(doc.id.to_string()),
305 None => Err(MetisError::NotFound(format!(
306 "Document with short code '{}' not found",
307 code
308 ))),
309 }
310 }
311
312 pub fn resolve_short_code_to_filepath(&mut self, code: &str) -> Result<String> {
314 match self.find_by_short_code(code)? {
315 Some(doc) => Ok(doc.filepath),
316 None => Err(MetisError::NotFound(format!(
317 "Document with short code '{}' not found",
318 code
319 ))),
320 }
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::dal::Database;
328
329 fn setup_test_repository() -> DocumentRepository {
330 let db = Database::new(":memory:").expect("Failed to create test database");
331 db.into_repository()
332 }
333
334 fn create_test_document() -> NewDocument {
335 NewDocument {
336 filepath: "/test/doc.md".to_string(),
337 id: "test-doc-1".to_string(),
338 title: "Test Document".to_string(),
339 document_type: "vision".to_string(),
340 created_at: 1609459200.0, updated_at: 1609459200.0,
342 archived: false,
343 exit_criteria_met: false,
344 file_hash: "abc123".to_string(),
345 frontmatter_json: "{}".to_string(),
346 content: Some("Test content".to_string()),
347 phase: "draft".to_string(),
348 strategy_id: None,
349 initiative_id: None,
350 short_code: "TEST-V-0001".to_string(),
351 }
352 }
353
354 #[test]
355 fn test_create_and_find_document() {
356 let mut repo = setup_test_repository();
357
358 let new_doc = create_test_document();
359 let created = repo
360 .create_document(new_doc)
361 .expect("Failed to create document");
362
363 assert_eq!(created.filepath, "/test/doc.md");
364 assert_eq!(created.title, "Test Document");
365 assert_eq!(created.document_type, "vision");
366
367 let found = repo
369 .find_by_filepath("/test/doc.md")
370 .expect("Failed to find document")
371 .expect("Document not found");
372 assert_eq!(found.id, "test-doc-1");
373
374 let found_by_id = repo
376 .find_by_id("test-doc-1")
377 .expect("Failed to find document")
378 .expect("Document not found");
379 assert_eq!(found_by_id.filepath, "/test/doc.md");
380 }
381
382 #[test]
383 fn test_update_document() {
384 let mut repo = setup_test_repository();
385
386 let new_doc = create_test_document();
387 let mut created = repo
388 .create_document(new_doc)
389 .expect("Failed to create document");
390
391 created.title = "Updated Title".to_string();
393 created.updated_at = 1609462800.0; let updated = repo
396 .update_document("/test/doc.md", &created)
397 .expect("Failed to update document");
398
399 assert_eq!(updated.title, "Updated Title");
400 assert_eq!(updated.updated_at, 1609462800.0);
401 }
402
403 #[test]
404 fn test_delete_document() {
405 let mut repo = setup_test_repository();
406
407 let new_doc = create_test_document();
408 repo.create_document(new_doc)
409 .expect("Failed to create document");
410
411 let deleted = repo
413 .delete_document("/test/doc.md")
414 .expect("Failed to delete document");
415 assert!(deleted);
416
417 let found = repo
419 .find_by_filepath("/test/doc.md")
420 .expect("Failed to search for document");
421 assert!(found.is_none());
422
423 let deleted_again = repo
425 .delete_document("/test/doc.md")
426 .expect("Failed to delete document");
427 assert!(!deleted_again);
428 }
429
430 #[test]
431 fn test_document_relationships() {
432 let mut repo = setup_test_repository();
433
434 let parent_doc = NewDocument {
436 filepath: "/parent.md".to_string(),
437 id: "parent-1".to_string(),
438 title: "Parent Document".to_string(),
439 document_type: "strategy".to_string(),
440 created_at: 1609459200.0,
441 updated_at: 1609459200.0,
442 archived: false,
443 exit_criteria_met: false,
444 file_hash: "parent123".to_string(),
445 frontmatter_json: "{}".to_string(),
446 content: Some("Parent content".to_string()),
447 phase: "shaping".to_string(),
448 strategy_id: None,
449 initiative_id: None,
450 short_code: "TEST-S-0001".to_string(),
451 };
452 repo.create_document(parent_doc)
453 .expect("Failed to create parent");
454
455 let child_doc = NewDocument {
457 filepath: "/child.md".to_string(),
458 id: "child-1".to_string(),
459 title: "Child Document".to_string(),
460 document_type: "initiative".to_string(),
461 created_at: 1609459200.0,
462 updated_at: 1609459200.0,
463 archived: false,
464 exit_criteria_met: false,
465 file_hash: "child123".to_string(),
466 frontmatter_json: "{}".to_string(),
467 content: Some("Child content".to_string()),
468 phase: "discovery".to_string(),
469 strategy_id: Some("parent-1".to_string()),
470 initiative_id: None,
471 short_code: "TEST-I-0001".to_string(),
472 };
473 repo.create_document(child_doc)
474 .expect("Failed to create child");
475
476 let relationship = DocumentRelationship {
478 child_id: "child-1".to_string(),
479 parent_id: "parent-1".to_string(),
480 child_filepath: "/child.md".to_string(),
481 parent_filepath: "/parent.md".to_string(),
482 };
483 repo.create_relationship(relationship)
484 .expect("Failed to create relationship");
485
486 let children = repo
488 .find_children("parent-1")
489 .expect("Failed to find children");
490 assert_eq!(children.len(), 1);
491 assert_eq!(children[0].id, "child-1");
492
493 let parent = repo
495 .find_parent("child-1")
496 .expect("Failed to find parent")
497 .expect("Parent not found");
498 assert_eq!(parent.id, "parent-1");
499 }
500
501 #[test]
502 fn test_find_by_type() {
503 let mut repo = setup_test_repository();
504
505 let vision_doc = NewDocument {
507 document_type: "vision".to_string(),
508 filepath: "/vision.md".to_string(),
509 id: "vision-1".to_string(),
510 title: "Vision Doc".to_string(),
511 created_at: 1609459200.0,
512 updated_at: 1609459200.0,
513 archived: false,
514 exit_criteria_met: false,
515 file_hash: "vision123".to_string(),
516 frontmatter_json: "{}".to_string(),
517 content: None,
518 phase: "draft".to_string(),
519 strategy_id: None,
520 initiative_id: None,
521 short_code: "TEST-V-0002".to_string(),
522 };
523
524 let strategy_doc = NewDocument {
525 document_type: "strategy".to_string(),
526 filepath: "/strategy.md".to_string(),
527 id: "strategy-1".to_string(),
528 title: "Strategy Doc".to_string(),
529 created_at: 1609462800.0, updated_at: 1609462800.0,
531 archived: false,
532 exit_criteria_met: false,
533 file_hash: "strategy123".to_string(),
534 frontmatter_json: "{}".to_string(),
535 content: None,
536 phase: "shaping".to_string(),
537 strategy_id: None,
538 initiative_id: None,
539 short_code: "TEST-S-0002".to_string(),
540 };
541
542 repo.create_document(vision_doc)
543 .expect("Failed to create vision");
544 repo.create_document(strategy_doc)
545 .expect("Failed to create strategy");
546
547 let visions = repo.find_by_type("vision").expect("Failed to find visions");
549 assert_eq!(visions.len(), 1);
550 assert_eq!(visions[0].document_type, "vision");
551
552 let strategies = repo
553 .find_by_type("strategy")
554 .expect("Failed to find strategies");
555 assert_eq!(strategies.len(), 1);
556 assert_eq!(strategies[0].document_type, "strategy");
557
558 let _all_docs = repo.find_by_type("vision").expect("Failed to find docs");
560 }
563
564 #[test]
565 fn test_document_not_found() {
566 let mut repo = setup_test_repository();
567
568 let found = repo
569 .find_by_filepath("/nonexistent.md")
570 .expect("Failed to search for document");
571 assert!(found.is_none());
572
573 let found_by_id = repo
574 .find_by_id("nonexistent")
575 .expect("Failed to search for document");
576 assert!(found_by_id.is_none());
577 }
578}