1use async_trait::async_trait;
2use cqrs_es::{
3 persist::{
4 PersistedEventRepository, PersistenceError, ReplayStream, SerializedEvent,
5 SerializedSnapshot,
6 },
7 Aggregate,
8};
9use futures::StreamExt;
10use mongodb::{
11 bson::{self, doc, Document},
12 options::IndexOptions,
13 Cursor, IndexModel,
14};
15use mongodb::{options::FindOptions, Client, Collection};
16use serde_json::Value;
17
18use crate::error::MongoAggregateError;
19
20const DEFAULT_EVENT_COLLECTION: &str = "events";
21const DEFAULT_SNAPSHOT_COLLECTION: &str = "snapshots";
22
23const DEFAULT_STREAMING_CHANNEL_SIZE: usize = 100;
24
25pub struct MongoEventRepository {
26 client: Client,
27 event_collection: String,
28 snapshot_collection: String,
29 stream_channel_size: usize,
30}
31
32impl MongoEventRepository {
33 pub async fn new(client: Client) -> mongodb::error::Result<Self> {
34 let repository = Self::use_collection_names(
35 client,
36 DEFAULT_EVENT_COLLECTION,
37 DEFAULT_SNAPSHOT_COLLECTION,
38 );
39 repository.create_indexes().await?;
40 Ok(repository)
41 }
42
43 pub fn with_streaming_channel_size(self, stream_channel_size: usize) -> Self {
44 Self {
45 client: self.client,
46 event_collection: self.event_collection,
47 snapshot_collection: self.snapshot_collection,
48 stream_channel_size,
49 }
50 }
51
52 fn use_collection_names(
53 client: Client,
54 event_collection: &str,
55 snapshot_collection: &str,
56 ) -> Self {
57 Self {
58 client,
59 event_collection: event_collection.to_string(),
60 snapshot_collection: snapshot_collection.to_string(),
61 stream_channel_size: DEFAULT_STREAMING_CHANNEL_SIZE,
62 }
63 }
64
65 fn collection(&self, name: &str) -> Collection<Document> {
67 self.client
68 .default_database()
69 .expect("Default database not configured")
70 .collection::<Document>(name)
71 }
72
73 async fn create_indexes(&self) -> mongodb::error::Result<()> {
74 let event_collection = self.collection(&self.event_collection);
75
76 let index = IndexModel::builder()
77 .keys(doc! { "aggregate_id": 1, "sequence": 1 })
78 .options(
79 IndexOptions::builder()
80 .unique(true)
81 .build(),
83 )
84 .build();
85
86 event_collection.create_index(index).await?;
87
88 println!(
89 "Created compound index on `{}` collection",
90 self.event_collection
91 );
92
93 let snapshot_collection = self.collection(&self.snapshot_collection);
94
95 let index = IndexModel::builder()
96 .keys(doc! { "aggregate_id": 1, "current_snapshot": -1 })
97 .options(
98 IndexOptions::builder()
99 .unique(true)
100 .build(),
102 )
103 .build();
104
105 snapshot_collection.create_index(index).await?;
106
107 println!(
108 "Created compound index on `{}` collection",
109 self.snapshot_collection
110 );
111
112 Ok(())
113 }
114
115 pub(crate) async fn insert_events(
116 &self,
117 events: &[SerializedEvent],
118 ) -> Result<(), MongoAggregateError> {
119 if events.is_empty() {
120 return Ok(());
121 }
122
123 let collection: Collection<Document> = self.collection(&self.event_collection);
124
125 let (documents, _) = Self::build_event_upsert_documents(events);
126
127 let res = collection.insert_many(documents).await?;
128
129 println!(
130 "Inserted {} events into `{}` collection",
131 res.inserted_ids.len(),
132 self.event_collection
133 );
134
135 Ok(())
136 }
137
138 fn build_event_upsert_documents(events: &[SerializedEvent]) -> (Vec<Document>, usize) {
140 let mut current_sequence: usize = 0;
141 let mut documents: Vec<Document> = Vec::default();
142 for event in events {
143 current_sequence = event.sequence;
144 documents.push(doc! {
145 "aggregate_id": event.aggregate_id.clone(),
146 "aggregate_type": event.aggregate_type.clone(),
147 "sequence": event.sequence as i64,
148 "event_type": event.event_type.clone(),
149 "event_version": event.event_version.clone(),
150 "payload": bson::to_bson(&event.payload).unwrap(),
151 "metadata": bson::to_bson(&event.metadata).unwrap(),
152 });
153 }
154 (documents, current_sequence)
155 }
156
157 async fn query_events(
159 &self,
160 aggregate_type: &str,
161 aggregate_id: &str,
162 min_sequence: usize,
163 ) -> Result<Vec<SerializedEvent>, MongoAggregateError> {
164 let mut cursor = self
165 .query_collection(
166 aggregate_type,
167 aggregate_id,
168 &self.event_collection,
169 min_sequence as i64,
170 None,
171 )
172 .await?;
173 let mut events: Vec<SerializedEvent> = Default::default();
174 while cursor.advance().await? {
175 let document = cursor.deserialize_current()?;
176 events.push(serialized_event(&document)?);
177 }
178 Ok(events)
179 }
180
181 pub(crate) async fn update_snapshot<A: Aggregate>(
182 &self,
183 aggregate_payload: Value,
184 aggregate_id: String,
185 current_snapshot: usize,
186 events: &[SerializedEvent],
187 ) -> Result<(), MongoAggregateError> {
188 let (_, current_sequence) = Self::build_event_upsert_documents(events);
189 self.insert_events(events).await?;
190
191 let collection: Collection<Document> = self.collection(&self.snapshot_collection);
192
193 let expected_snapshot = current_snapshot - 1;
194
195 let snapshot = doc! {
196 "aggregate_type": A::aggregate_type(),
197 "aggregate_id": &aggregate_id,
198 "payload": bson::to_bson(&aggregate_payload).unwrap(),
199 "current_sequence": current_sequence as i64,
200 "current_snapshot": current_snapshot as i64,
201 };
202
203 let filter = doc! {
204 "aggregate_id": &aggregate_id,
205 "aggregate_type": A::aggregate_type(),
206 "current_snapshot": expected_snapshot as i64,
207 };
208
209 let res = collection
211 .replace_one(filter, snapshot)
212 .upsert(true)
213 .await
214 .map_err(MongoAggregateError::from)?;
215
216 if res.matched_count == 0 && res.upserted_id.is_none() {
217 return Err(MongoAggregateError::OptimisticLock);
218 }
219
220 println!(
221 "Inserted snapshot for `{}` with id `{}`",
222 A::aggregate_type(),
223 &aggregate_id
224 );
225
226 Ok(())
227 }
228
229 async fn query_collection(
231 &self,
232 aggregate_type: &str,
233 aggregate_id: &str,
234 collection: &str,
235 min_sequence: i64,
236 sort: Option<Document>,
237 ) -> Result<Cursor<Document>, MongoAggregateError> {
238 let filter = self.build_filter(aggregate_type, aggregate_id, min_sequence);
239 let collection = self.collection(collection);
240
241 let options = FindOptions::builder().sort(sort).build();
242
243 let cursor = collection.find(filter).with_options(options).await?;
244 Ok(cursor)
245 }
246
247 fn build_filter(
248 &self,
249 aggregate_type: &str,
250 aggregate_id: &str,
251 min_sequence: i64,
252 ) -> Document {
253 if min_sequence == 0 {
254 doc! {
255 "aggregate_type": aggregate_type,
256 "aggregate_id": aggregate_id,
257 }
258 } else {
259 doc! {
260 "aggregate_type": aggregate_type,
261 "aggregate_id": aggregate_id,
262 "sequence": { "$gte": min_sequence },
263 }
264 }
265 }
266}
267
268fn serialized_event(document: &Document) -> Result<SerializedEvent, MongoAggregateError> {
269 let aggregate_id = document.get_str("aggregate_id")?.to_string();
270 let sequence = document.get_i64("sequence")? as usize;
271 let aggregate_type = document.get_str("aggregate_type")?.to_string();
272 let event_type = document.get_str("event_type")?.to_string();
273 let event_version = document.get_str("event_version")?.to_string();
274 let payload = bson::from_bson(document.get("payload").unwrap().clone()).unwrap();
275 let metadata = bson::from_bson(document.get("metadata").unwrap().clone()).unwrap();
276
277 Ok(SerializedEvent {
278 aggregate_id,
279 sequence,
280 aggregate_type,
281 event_type,
282 event_version,
283 payload,
284 metadata,
285 })
286}
287
288#[async_trait]
289impl PersistedEventRepository for MongoEventRepository {
290 async fn get_events<A: Aggregate>(
291 &self,
292 aggregate_id: &str,
293 ) -> Result<Vec<SerializedEvent>, PersistenceError> {
294 let events = self
295 .query_events(&A::aggregate_type(), aggregate_id, 0)
296 .await?;
297 Ok(events)
298 }
299
300 async fn get_last_events<A: Aggregate>(
301 &self,
302 aggregate_id: &str,
303 last_sequence: usize,
304 ) -> Result<Vec<SerializedEvent>, PersistenceError> {
305 let events = self
306 .query_events(&A::aggregate_type(), aggregate_id, last_sequence)
307 .await?;
308 Ok(events)
309 }
310
311 async fn get_snapshot<A: Aggregate>(
312 &self,
313 aggregate_id: &str,
314 ) -> Result<Option<SerializedSnapshot>, PersistenceError> {
315 let mut cursor = self
316 .query_collection(
317 &A::aggregate_type(),
318 aggregate_id,
319 &self.snapshot_collection,
320 0,
321 Some(doc! { "current_snapshot": -1 }), )
323 .await?;
324
325 if let Some(result) = cursor.next().await {
326 let document = result.map_err(MongoAggregateError::from)?;
327 let payload = bson::from_bson(document.get("payload").unwrap().clone()).unwrap();
328 println!(
329 "Found snapshot for `{}` with id `{}`",
330 A::aggregate_type(),
331 aggregate_id
332 );
333 Ok(Some(SerializedSnapshot {
334 aggregate_id: aggregate_id.to_string(),
335 aggregate: payload,
336 current_sequence: document
337 .get_i64("current_sequence")
338 .map_err(MongoAggregateError::from)? as usize,
339 current_snapshot: document
340 .get_i64("current_snapshot")
341 .map_err(MongoAggregateError::from)? as usize,
342 }))
343 } else {
344 Ok(None)
345 }
346 }
347
348 async fn persist<A: Aggregate>(
349 &self,
350 events: &[SerializedEvent],
351 snapshot_update: Option<(String, Value, usize)>,
352 ) -> Result<(), PersistenceError> {
353 match snapshot_update {
354 None => {
355 self.insert_events(events).await?;
356 }
357 Some((aggregate_id, aggregate_payload, current_snapshot)) => {
358 self.update_snapshot::<A>(
359 aggregate_payload,
360 aggregate_id,
361 current_snapshot,
362 events,
363 )
364 .await?;
365 }
366 }
367 Ok(())
368 }
369
370 async fn stream_events<A: Aggregate>(
371 &self,
372 aggregate_id: &str,
373 ) -> Result<ReplayStream, PersistenceError> {
374 let query = self
375 .query_collection(
376 &A::aggregate_type(),
377 aggregate_id,
378 &self.event_collection,
379 0,
380 None,
381 )
382 .await?;
383 Ok(stream_events(query, self.stream_channel_size))
384 }
385
386 async fn stream_all_events<A: Aggregate>(&self) -> Result<ReplayStream, PersistenceError> {
389 todo!()
390 }
391}
392
393fn stream_events(_base_query: Cursor<Document>, channel_size: usize) -> ReplayStream {
394 let (mut _feed, stream) = ReplayStream::new(channel_size);
395 stream
396}
397
398#[cfg(test)]
399mod tests {
400 use cqrs_es::doc::{Customer, CustomerEvent};
401 use cqrs_es::persist::PersistedEventRepository;
402
403 use crate::error::MongoAggregateError;
404 use crate::utils::tests::{mongodb_client, test_event, test_snapshot_context};
405 use crate::MongoEventRepository;
406
407 #[tokio::test]
408 async fn test_event_repository_inserts_successfully() {
409 let client = mongodb_client().await;
410 let repository = MongoEventRepository::new(client)
411 .await
412 .unwrap()
413 .with_streaming_channel_size(1);
414 let aggregate_id = uuid::Uuid::new_v4().to_string();
415 let events = repository
416 .get_events::<Customer>(&aggregate_id)
417 .await
418 .unwrap();
419 assert!(events.is_empty());
420
421 repository
423 .insert_events(&[
424 test_event(
425 &aggregate_id,
426 1,
427 CustomerEvent::NameAdded {
428 name: "Ferris".to_string(),
429 },
430 ),
431 test_event(
432 &aggregate_id,
433 2,
434 CustomerEvent::EmailUpdated {
435 new_email: "ferris@example.test".to_string(),
436 },
437 ),
438 ])
439 .await
440 .unwrap();
441 let events = repository
442 .get_events::<Customer>(&aggregate_id)
443 .await
444 .unwrap();
445 assert_eq!(2, events.len());
446 events
447 .iter()
448 .for_each(|e| assert_eq!(&aggregate_id, &e.aggregate_id));
449 }
450
451 #[tokio::test]
452 async fn test_event_repository_invalid_sequence_throws_error() {
453 let client = mongodb_client().await;
454 let repository = MongoEventRepository::new(client)
455 .await
456 .unwrap()
457 .with_streaming_channel_size(1);
458 let aggregate_id = uuid::Uuid::new_v4().to_string();
459
460 repository
462 .insert_events(&[test_event(
463 &aggregate_id,
464 1,
465 CustomerEvent::NameAdded {
466 name: "Ferris".to_string(),
467 },
468 )])
469 .await
470 .unwrap();
471
472 let result = repository
474 .insert_events(&[test_event(
475 &aggregate_id,
476 1,
477 CustomerEvent::EmailUpdated {
478 new_email: "email@example.test".to_string(),
479 },
480 )])
481 .await
482 .unwrap_err();
483
484 match result {
485 MongoAggregateError::OptimisticLock => {}
486 _ => panic!("Expected OptimisticLockError, got {result:?}"),
487 }
488 }
489
490 #[tokio::test]
491 async fn test_snapshot_repository_empty_returns_none() {
492 let client = mongodb_client().await;
493 let repository = MongoEventRepository::new(client)
494 .await
495 .unwrap()
496 .with_streaming_channel_size(1);
497 let aggregate_id = uuid::Uuid::new_v4().to_string();
498
499 let snapshot = repository
500 .get_snapshot::<Customer>(&aggregate_id)
501 .await
502 .unwrap();
503 assert_eq!(None, snapshot);
504 }
505
506 #[tokio::test]
507 async fn test_snapshot_repository_inserts_successfully() {
508 let client = mongodb_client().await;
509 let repository = MongoEventRepository::new(client)
510 .await
511 .unwrap()
512 .with_streaming_channel_size(1);
513 let aggregate_id = uuid::Uuid::new_v4().to_string();
514
515 repository
516 .update_snapshot::<Customer>(
517 serde_json::to_value(Customer {
518 customer_id: "123".to_string(),
519 name: "Ferris".to_string(),
520 email: "email@example.test".to_string(),
521 })
522 .unwrap(),
523 aggregate_id.clone(),
524 1,
525 &[],
526 )
527 .await
528 .unwrap();
529
530 let snapshot = repository
531 .get_snapshot::<Customer>(&aggregate_id)
532 .await
533 .unwrap();
534
535 assert_eq!(
536 Some(test_snapshot_context(
537 aggregate_id.clone(),
538 serde_json::to_value(Customer {
539 customer_id: "123".to_string(),
540 name: "Ferris".to_string(),
541 email: "email@example.test".to_string(),
542 })
543 .unwrap(),
544 0,
545 1
546 )),
547 snapshot
548 );
549 }
550
551 #[tokio::test]
552 async fn test_snapshot_repository_returns_latest_snapshot() {
553 let client = mongodb_client().await;
554 let repository = MongoEventRepository::new(client)
555 .await
556 .unwrap()
557 .with_streaming_channel_size(1);
558 let aggregate_id = uuid::Uuid::new_v4().to_string();
559
560 repository
562 .update_snapshot::<Customer>(
563 serde_json::to_value(Customer {
564 customer_id: "123".to_string(),
565 name: "Ferris".to_string(),
566 email: "first@example.test".to_string(),
567 })
568 .unwrap(),
569 aggregate_id.clone(),
570 1,
571 &[],
572 )
573 .await
574 .unwrap();
575
576 repository
578 .update_snapshot::<Customer>(
579 serde_json::to_value(Customer {
580 customer_id: "123".to_string(),
581 name: "Ferris".to_string(),
582 email: "second@example.test".to_string(),
583 })
584 .unwrap(),
585 aggregate_id.clone(),
586 2,
587 &[],
588 )
589 .await
590 .unwrap();
591
592 let snapshot = repository
593 .get_snapshot::<Customer>(&aggregate_id)
594 .await
595 .unwrap();
596
597 assert_eq!(
598 Some(test_snapshot_context(
599 aggregate_id.clone(),
600 serde_json::to_value(Customer {
601 customer_id: "123".to_string(),
602 name: "Ferris".to_string(),
603 email: "second@example.test".to_string()
604 })
605 .unwrap(),
606 0,
607 2
608 )),
609 snapshot
610 );
611 }
612
613 #[tokio::test]
614 async fn test_snapshot_repository_invalid_sequence_returns_error() {
615 let client = mongodb_client().await;
616 let repository = MongoEventRepository::new(client)
617 .await
618 .unwrap()
619 .with_streaming_channel_size(1);
620 let aggregate_id = uuid::Uuid::new_v4().to_string();
621
622 repository
624 .update_snapshot::<Customer>(
625 serde_json::to_value(Customer {
626 customer_id: "123".to_string(),
627 name: "Ferris".to_string(),
628 email: "first@example.test".to_string(),
629 })
630 .unwrap(),
631 aggregate_id.clone(),
632 1,
633 &[],
634 )
635 .await
636 .unwrap();
637
638 let result = repository
640 .update_snapshot::<Customer>(
641 serde_json::to_value(Customer {
642 customer_id: "123".to_string(),
643 name: "Ferris".to_string(),
644 email: "second@example.test".to_string(),
645 })
646 .unwrap(),
647 aggregate_id.clone(),
648 1,
649 &[],
650 )
651 .await
652 .unwrap_err();
653
654 match result {
655 MongoAggregateError::OptimisticLock => {}
656 _ => panic!("Expected OptimisticLockError, got {result:?}"),
657 }
658
659 let snapshot = repository
660 .get_snapshot::<Customer>(&aggregate_id)
661 .await
662 .unwrap();
663
664 assert_eq!(
665 Some(test_snapshot_context(
666 aggregate_id.clone(),
667 serde_json::to_value(Customer {
668 customer_id: "123".to_string(),
669 name: "Ferris".to_string(),
670 email: "first@example.test".to_string()
671 })
672 .unwrap(),
673 0,
674 1
675 )),
676 snapshot
677 );
678 }
679}