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
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::dal::Database;
182
183 fn setup_test_repository() -> DocumentRepository {
184 let db = Database::new(":memory:").expect("Failed to create test database");
185 db.into_repository()
186 }
187
188 fn create_test_document() -> NewDocument {
189 NewDocument {
190 filepath: "/test/doc.md".to_string(),
191 id: "test-doc-1".to_string(),
192 title: "Test Document".to_string(),
193 document_type: "vision".to_string(),
194 created_at: 1609459200.0, updated_at: 1609459200.0,
196 archived: false,
197 exit_criteria_met: false,
198 file_hash: "abc123".to_string(),
199 frontmatter_json: "{}".to_string(),
200 content: Some("Test content".to_string()),
201 phase: "draft".to_string(),
202 }
203 }
204
205 #[test]
206 fn test_create_and_find_document() {
207 let mut repo = setup_test_repository();
208
209 let new_doc = create_test_document();
210 let created = repo
211 .create_document(new_doc)
212 .expect("Failed to create document");
213
214 assert_eq!(created.filepath, "/test/doc.md");
215 assert_eq!(created.title, "Test Document");
216 assert_eq!(created.document_type, "vision");
217
218 let found = repo
220 .find_by_filepath("/test/doc.md")
221 .expect("Failed to find document")
222 .expect("Document not found");
223 assert_eq!(found.id, "test-doc-1");
224
225 let found_by_id = repo
227 .find_by_id("test-doc-1")
228 .expect("Failed to find document")
229 .expect("Document not found");
230 assert_eq!(found_by_id.filepath, "/test/doc.md");
231 }
232
233 #[test]
234 fn test_update_document() {
235 let mut repo = setup_test_repository();
236
237 let new_doc = create_test_document();
238 let mut created = repo
239 .create_document(new_doc)
240 .expect("Failed to create document");
241
242 created.title = "Updated Title".to_string();
244 created.updated_at = 1609462800.0; let updated = repo
247 .update_document("/test/doc.md", &created)
248 .expect("Failed to update document");
249
250 assert_eq!(updated.title, "Updated Title");
251 assert_eq!(updated.updated_at, 1609462800.0);
252 }
253
254 #[test]
255 fn test_delete_document() {
256 let mut repo = setup_test_repository();
257
258 let new_doc = create_test_document();
259 repo.create_document(new_doc)
260 .expect("Failed to create document");
261
262 let deleted = repo
264 .delete_document("/test/doc.md")
265 .expect("Failed to delete document");
266 assert!(deleted);
267
268 let found = repo
270 .find_by_filepath("/test/doc.md")
271 .expect("Failed to search for document");
272 assert!(found.is_none());
273
274 let deleted_again = repo
276 .delete_document("/test/doc.md")
277 .expect("Failed to delete document");
278 assert!(!deleted_again);
279 }
280
281 #[test]
282 fn test_document_relationships() {
283 let mut repo = setup_test_repository();
284
285 let parent_doc = NewDocument {
287 filepath: "/parent.md".to_string(),
288 id: "parent-1".to_string(),
289 title: "Parent Document".to_string(),
290 document_type: "strategy".to_string(),
291 created_at: 1609459200.0,
292 updated_at: 1609459200.0,
293 archived: false,
294 exit_criteria_met: false,
295 file_hash: "parent123".to_string(),
296 frontmatter_json: "{}".to_string(),
297 content: Some("Parent content".to_string()),
298 phase: "shaping".to_string(),
299 };
300 repo.create_document(parent_doc)
301 .expect("Failed to create parent");
302
303 let child_doc = NewDocument {
305 filepath: "/child.md".to_string(),
306 id: "child-1".to_string(),
307 title: "Child Document".to_string(),
308 document_type: "initiative".to_string(),
309 created_at: 1609459200.0,
310 updated_at: 1609459200.0,
311 archived: false,
312 exit_criteria_met: false,
313 file_hash: "child123".to_string(),
314 frontmatter_json: "{}".to_string(),
315 content: Some("Child content".to_string()),
316 phase: "discovery".to_string(),
317 };
318 repo.create_document(child_doc)
319 .expect("Failed to create child");
320
321 let relationship = DocumentRelationship {
323 child_id: "child-1".to_string(),
324 parent_id: "parent-1".to_string(),
325 child_filepath: "/child.md".to_string(),
326 parent_filepath: "/parent.md".to_string(),
327 };
328 repo.create_relationship(relationship)
329 .expect("Failed to create relationship");
330
331 let children = repo
333 .find_children("parent-1")
334 .expect("Failed to find children");
335 assert_eq!(children.len(), 1);
336 assert_eq!(children[0].id, "child-1");
337
338 let parent = repo
340 .find_parent("child-1")
341 .expect("Failed to find parent")
342 .expect("Parent not found");
343 assert_eq!(parent.id, "parent-1");
344 }
345
346 #[test]
347 fn test_find_by_type() {
348 let mut repo = setup_test_repository();
349
350 let vision_doc = NewDocument {
352 document_type: "vision".to_string(),
353 filepath: "/vision.md".to_string(),
354 id: "vision-1".to_string(),
355 title: "Vision Doc".to_string(),
356 created_at: 1609459200.0,
357 updated_at: 1609459200.0,
358 archived: false,
359 exit_criteria_met: false,
360 file_hash: "vision123".to_string(),
361 frontmatter_json: "{}".to_string(),
362 content: None,
363 phase: "draft".to_string(),
364 };
365
366 let strategy_doc = NewDocument {
367 document_type: "strategy".to_string(),
368 filepath: "/strategy.md".to_string(),
369 id: "strategy-1".to_string(),
370 title: "Strategy Doc".to_string(),
371 created_at: 1609462800.0, updated_at: 1609462800.0,
373 archived: false,
374 exit_criteria_met: false,
375 file_hash: "strategy123".to_string(),
376 frontmatter_json: "{}".to_string(),
377 content: None,
378 phase: "shaping".to_string(),
379 };
380
381 repo.create_document(vision_doc)
382 .expect("Failed to create vision");
383 repo.create_document(strategy_doc)
384 .expect("Failed to create strategy");
385
386 let visions = repo.find_by_type("vision").expect("Failed to find visions");
388 assert_eq!(visions.len(), 1);
389 assert_eq!(visions[0].document_type, "vision");
390
391 let strategies = repo
392 .find_by_type("strategy")
393 .expect("Failed to find strategies");
394 assert_eq!(strategies.len(), 1);
395 assert_eq!(strategies[0].document_type, "strategy");
396
397 let _all_docs = repo.find_by_type("vision").expect("Failed to find docs");
399 }
402
403 #[test]
404 fn test_document_not_found() {
405 let mut repo = setup_test_repository();
406
407 let found = repo
408 .find_by_filepath("/nonexistent.md")
409 .expect("Failed to search for document");
410 assert!(found.is_none());
411
412 let found_by_id = repo
413 .find_by_id("nonexistent")
414 .expect("Failed to search for document");
415 assert!(found_by_id.is_none());
416 }
417}