1use cqrs_es::{
2 persist::{
3 PersistedEventRepository, PersistenceError, ReplayStream, SerializedEvent,
4 SerializedSnapshot,
5 },
6 Aggregate,
7};
8use futures::StreamExt;
9use mongodb::{
10 bson::{self, doc, Document},
11 options::IndexOptions,
12 Cursor, IndexModel,
13};
14use mongodb::{options::FindOptions, Client, Collection};
15use serde_json::Value;
16
17use crate::error::MongoAggregateError;
18
19const DEFAULT_EVENT_COLLECTION: &str = "events";
20const DEFAULT_SNAPSHOT_COLLECTION: &str = "snapshots";
21
22const DEFAULT_STREAMING_CHANNEL_SIZE: usize = 200;
23
24pub 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> {
35 let repository = Self {
36 client,
37 event_collection: DEFAULT_EVENT_COLLECTION.to_string(),
38 snapshot_collection: DEFAULT_SNAPSHOT_COLLECTION.to_string(),
39 stream_channel_size: DEFAULT_STREAMING_CHANNEL_SIZE,
40 };
41 repository.create_indexes().await?;
43 Ok(repository)
44 }
45
46 pub fn with_collection_names(self, event_collection: &str, snapshot_collection: &str) -> Self {
52 Self {
53 client: self.client,
54 event_collection: event_collection.to_string(),
55 snapshot_collection: snapshot_collection.to_string(),
56 stream_channel_size: self.stream_channel_size,
57 }
58 }
59
60 pub fn with_streaming_channel_size(self, stream_channel_size: usize) -> Self {
64 Self {
65 client: self.client,
66 event_collection: self.event_collection,
67 snapshot_collection: self.snapshot_collection,
68 stream_channel_size,
69 }
70 }
71
72 fn get_collection(&self, name: &str) -> Collection<Document> {
74 self.client
75 .default_database()
76 .expect("Default database not configured")
77 .collection::<Document>(name)
78 }
79
80 async fn create_indexes(&self) -> mongodb::error::Result<()> {
82 let event_collection = self.get_collection(&self.event_collection);
83
84 let index = IndexModel::builder()
85 .keys(doc! { "aggregate_id": 1, "sequence": 1 })
86 .options(
87 IndexOptions::builder()
88 .unique(true)
89 .build(),
91 )
92 .build();
93
94 event_collection.create_index(index).await?;
95
96 log::debug!(
97 "Created compound index on `{}` collection",
98 self.event_collection
99 );
100
101 let snapshot_collection = self.get_collection(&self.snapshot_collection);
102
103 let index = IndexModel::builder()
104 .keys(doc! { "aggregate_id": 1, "current_snapshot": -1 })
105 .options(
106 IndexOptions::builder()
107 .unique(true)
108 .build(),
110 )
111 .build();
112
113 snapshot_collection.create_index(index).await?;
114
115 log::debug!(
116 "Created compound index on `{}` collection",
117 self.snapshot_collection
118 );
119
120 Ok(())
121 }
122
123 pub(crate) async fn insert_events(
124 &self,
125 events: &[SerializedEvent],
126 ) -> Result<(), MongoAggregateError> {
127 if events.is_empty() {
128 return Ok(());
129 }
130
131 let collection: Collection<Document> = self.get_collection(&self.event_collection);
132
133 let (documents, _) = Self::build_event_upsert_documents(events);
134
135 let mut session = self.client.start_session().await?;
137 session.start_transaction().await?;
138 let res = collection
139 .insert_many(documents)
140 .session(&mut session)
141 .await?;
142 session.commit_transaction().await?;
143
144 log::debug!(
145 "Inserted {} events into `{}` collection",
146 res.inserted_ids.len(),
147 self.event_collection
148 );
149
150 Ok(())
151 }
152
153 fn build_event_upsert_documents(events: &[SerializedEvent]) -> (Vec<Document>, usize) {
155 let mut current_sequence: usize = 0;
156 let mut documents: Vec<Document> = Vec::default();
157 for event in events {
158 current_sequence = event.sequence;
159 documents.push(doc! {
160 "aggregate_id": event.aggregate_id.clone(),
161 "aggregate_type": event.aggregate_type.clone(),
162 "sequence": event.sequence as i64,
163 "event_type": event.event_type.clone(),
164 "event_version": event.event_version.clone(),
165 "payload": bson::to_bson(&event.payload).unwrap(),
166 "metadata": bson::to_bson(&event.metadata).unwrap(),
167 });
168 }
169 (documents, current_sequence)
170 }
171
172 async fn query_events(
174 &self,
175 aggregate_type: &str,
176 aggregate_id: &str,
177 min_sequence: usize,
178 ) -> Result<Vec<SerializedEvent>, MongoAggregateError> {
179 let mut cursor = self
180 .query_collection(
181 aggregate_type,
182 aggregate_id,
183 &self.event_collection,
184 min_sequence as i64,
185 None,
186 )
187 .await?;
188 let mut events: Vec<SerializedEvent> = Default::default();
189 while cursor.advance().await? {
190 let document = cursor.deserialize_current()?;
191 events.push(serialized_event(&document)?);
192 }
193 Ok(events)
194 }
195
196 pub(crate) async fn update_snapshot<A: Aggregate>(
197 &self,
198 aggregate_payload: Value,
199 aggregate_id: String,
200 current_snapshot: usize,
201 events: &[SerializedEvent],
202 ) -> Result<(), MongoAggregateError> {
203 let (_, current_sequence) = Self::build_event_upsert_documents(events);
204 self.insert_events(events).await?;
205
206 let collection: Collection<Document> = self.get_collection(&self.snapshot_collection);
207
208 let expected_snapshot = current_snapshot - 1;
209
210 let snapshot = doc! {
211 "aggregate_type": A::TYPE,
212 "aggregate_id": &aggregate_id,
213 "payload": bson::to_bson(&aggregate_payload).unwrap(),
214 "current_sequence": current_sequence as i64,
215 "current_snapshot": current_snapshot as i64,
216 };
217
218 let filter = doc! {
219 "aggregate_id": &aggregate_id,
220 "aggregate_type": A::TYPE,
221 "current_snapshot": expected_snapshot as i64,
222 };
223
224 let res = collection
226 .replace_one(filter, snapshot)
227 .upsert(true)
228 .await
229 .map_err(MongoAggregateError::from)?;
230
231 if res.matched_count == 0 && res.upserted_id.is_none() {
232 return Err(MongoAggregateError::OptimisticLock);
233 }
234
235 log::debug!(
236 "Inserted snapshot for `{}` with id `{}`",
237 A::TYPE,
238 &aggregate_id
239 );
240
241 Ok(())
242 }
243
244 async fn query_collection(
246 &self,
247 aggregate_type: &str,
248 aggregate_id: &str,
249 collection: &str,
250 min_sequence: i64,
251 sort: Option<Document>,
252 ) -> Result<Cursor<Document>, MongoAggregateError> {
253 let filter = self.build_filter(aggregate_type, aggregate_id, min_sequence);
254 let options = FindOptions::builder().sort(sort).build();
255 let cursor = self
256 .get_collection(collection)
257 .find(filter)
258 .with_options(options)
259 .await?;
260 Ok(cursor)
261 }
262
263 fn build_filter(
264 &self,
265 aggregate_type: &str,
266 aggregate_id: &str,
267 min_sequence: i64,
268 ) -> Document {
269 if min_sequence == 0 {
270 doc! {
271 "aggregate_type": aggregate_type,
272 "aggregate_id": aggregate_id,
273 }
274 } else {
275 doc! {
276 "aggregate_type": aggregate_type,
277 "aggregate_id": aggregate_id,
278 "sequence": { "$gte": min_sequence },
279 }
280 }
281 }
282}
283
284fn serialized_event(document: &Document) -> Result<SerializedEvent, MongoAggregateError> {
285 let aggregate_id = document.get_str("aggregate_id")?.to_string();
286 let sequence = document.get_i64("sequence")? as usize;
287 let aggregate_type = document.get_str("aggregate_type")?.to_string();
288 let event_type = document.get_str("event_type")?.to_string();
289 let event_version = document.get_str("event_version")?.to_string();
290 let payload = bson::from_bson(document.get("payload").into())?;
291 let metadata = bson::from_bson(document.get("metadata").into())?;
292
293 Ok(SerializedEvent {
294 aggregate_id,
295 sequence,
296 aggregate_type,
297 event_type,
298 event_version,
299 payload,
300 metadata,
301 })
302}
303
304impl PersistedEventRepository for MongoEventRepository {
305 async fn get_events<A: Aggregate>(
306 &self,
307 aggregate_id: &str,
308 ) -> Result<Vec<SerializedEvent>, PersistenceError> {
309 let events = self.query_events(A::TYPE, aggregate_id, 0).await?;
310 Ok(events)
311 }
312
313 async fn get_last_events<A: Aggregate>(
314 &self,
315 aggregate_id: &str,
316 last_sequence: usize,
317 ) -> Result<Vec<SerializedEvent>, PersistenceError> {
318 let events = self
319 .query_events(A::TYPE, aggregate_id, last_sequence)
320 .await?;
321 Ok(events)
322 }
323
324 async fn get_snapshot<A: Aggregate>(
325 &self,
326 aggregate_id: &str,
327 ) -> Result<Option<SerializedSnapshot>, PersistenceError> {
328 let mut cursor = self
329 .query_collection(
330 A::TYPE,
331 aggregate_id,
332 &self.snapshot_collection,
333 0,
334 Some(doc! { "current_snapshot": -1 }), )
336 .await?;
337
338 if let Some(result) = cursor.next().await {
339 let document = result.map_err(MongoAggregateError::from)?;
340 let payload = bson::from_bson(document.get("payload").unwrap().clone()).unwrap();
341 log::debug!(
342 "Found snapshot for `{}` with id `{}`",
343 A::TYPE,
344 aggregate_id
345 );
346 Ok(Some(SerializedSnapshot {
347 aggregate_id: aggregate_id.to_string(),
348 aggregate: payload,
349 current_sequence: document
350 .get_i64("current_sequence")
351 .map_err(MongoAggregateError::from)? as usize,
352 current_snapshot: document
353 .get_i64("current_snapshot")
354 .map_err(MongoAggregateError::from)? as usize,
355 }))
356 } else {
357 Ok(None)
358 }
359 }
360
361 async fn persist<A: Aggregate>(
362 &self,
363 events: &[SerializedEvent],
364 snapshot_update: Option<(String, Value, usize)>,
365 ) -> Result<(), PersistenceError> {
366 match snapshot_update {
367 None => {
368 self.insert_events(events).await?;
369 }
370 Some((aggregate_id, aggregate_payload, current_snapshot)) => {
371 self.update_snapshot::<A>(
372 aggregate_payload,
373 aggregate_id,
374 current_snapshot,
375 events,
376 )
377 .await?;
378 }
379 }
380 Ok(())
381 }
382
383 async fn stream_events<A: Aggregate>(
384 &self,
385 aggregate_id: &str,
386 ) -> Result<ReplayStream, PersistenceError> {
387 let query = self
388 .query_collection(A::TYPE, aggregate_id, &self.event_collection, 0, None)
389 .await?;
390 Ok(stream_events(query, self.stream_channel_size))
391 }
392
393 async fn stream_all_events<A: Aggregate>(&self) -> Result<ReplayStream, PersistenceError> {
394 let filter = doc! { "aggregate_type": A::TYPE };
395 let options = FindOptions::builder().sort(doc! { "sequence": 1 }).build();
396 let cursor = self
397 .get_collection(&self.event_collection)
398 .find(filter)
399 .with_options(options)
400 .await
401 .unwrap();
402 Ok(stream_events(cursor, self.stream_channel_size))
403 }
404}
405
406fn stream_events(mut cursor: Cursor<Document>, channel_size: usize) -> ReplayStream {
407 let (mut feed, stream) = ReplayStream::new(channel_size);
408 tokio::spawn(async move {
409 while let Some(result) = cursor.next().await {
410 match result {
411 Ok(document) => {
412 if let Ok(event) = serialized_event(&document) {
413 if feed.push(Ok(event)).await.is_err() {
414 log::warn!("Could not push event to feed. Stopping event stream.");
415 return;
416 };
417 } else {
418 log::error!("Failed to deserialize event: {:?}", document);
419 }
420 }
421 Err(e) => {
422 log::error!("Error while streaming events: {}", e);
423 let e: MongoAggregateError = e.into();
424 if feed.push(Err(e.into())).await.is_err() {
425 log::warn!("Could not push error to feed. Stopping event stream.");
426 return;
427 }
428 }
429 }
430 }
431 });
432 stream
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 use cqrs_es::doc::{Customer, CustomerEvent};
440
441 use crate::utils::tests::{mongodb_client, test_event, test_snapshot_context};
442
443 #[tokio::test]
444 async fn test_event_repository_inserts_successfully() {
445 let client = mongodb_client().await;
446 let repository = MongoEventRepository::new(client)
447 .await
448 .unwrap()
449 .with_streaming_channel_size(1);
450 let aggregate_id = uuid::Uuid::new_v4().to_string();
451 let events = repository
452 .get_events::<Customer>(&aggregate_id)
453 .await
454 .unwrap();
455 assert!(events.is_empty());
456
457 repository
459 .insert_events(&[
460 test_event(
461 &aggregate_id,
462 1,
463 CustomerEvent::NameAdded {
464 name: "Ferris".to_string(),
465 },
466 ),
467 test_event(
468 &aggregate_id,
469 2,
470 CustomerEvent::EmailUpdated {
471 new_email: "ferris@example.test".to_string(),
472 },
473 ),
474 ])
475 .await
476 .unwrap();
477 let events = repository
478 .get_events::<Customer>(&aggregate_id)
479 .await
480 .unwrap();
481 assert_eq!(2, events.len());
482 events
483 .iter()
484 .for_each(|e| assert_eq!(&aggregate_id, &e.aggregate_id));
485 }
486
487 #[tokio::test]
488 async fn test_event_repository_get_last_events() {
489 let client = mongodb_client().await;
490 let repository = MongoEventRepository::new(client)
491 .await
492 .unwrap()
493 .with_streaming_channel_size(1);
494 let aggregate_id = uuid::Uuid::new_v4().to_string();
495 let events = repository
496 .get_events::<Customer>(&aggregate_id)
497 .await
498 .unwrap();
499 assert!(events.is_empty());
500
501 repository
503 .insert_events(&[
504 test_event(
505 &aggregate_id,
506 1,
507 CustomerEvent::NameAdded {
508 name: "Ferris".to_string(),
509 },
510 ),
511 test_event(
512 &aggregate_id,
513 3,
514 CustomerEvent::EmailUpdated {
515 new_email: "second@example.test".to_string(),
516 },
517 ),
518 test_event(
519 &aggregate_id,
520 2,
521 CustomerEvent::EmailUpdated {
522 new_email: "first@example.test".to_string(),
523 },
524 ),
525 test_event(
526 &aggregate_id,
527 4,
528 CustomerEvent::NameAdded {
529 name: "F3rr1s".to_string(),
530 },
531 ),
532 ])
533 .await
534 .unwrap();
535
536 let events = repository
537 .get_last_events::<Customer>(&aggregate_id, 3)
538 .await
539 .unwrap();
540 assert_eq!(2, events.len());
541 assert_eq!(3, events[0].sequence);
542 assert_eq!(4, events[1].sequence);
543 }
544
545 #[tokio::test]
546 async fn test_event_repository_invalid_sequence_throws_error() {
547 let client = mongodb_client().await;
548 let repository = MongoEventRepository::new(client)
549 .await
550 .unwrap()
551 .with_streaming_channel_size(1);
552 let aggregate_id = uuid::Uuid::new_v4().to_string();
553
554 repository
556 .insert_events(&[test_event(
557 &aggregate_id,
558 1,
559 CustomerEvent::EmailUpdated {
560 new_email: "first@example.test".to_string(),
561 },
562 )])
563 .await
564 .unwrap();
565
566 let result = repository
568 .insert_events(&[
569 test_event(
570 &aggregate_id,
571 2,
572 CustomerEvent::EmailUpdated {
573 new_email: "second@example.test".to_string(),
574 },
575 ),
576 test_event(
577 &aggregate_id,
578 1,
579 CustomerEvent::NameAdded {
580 name: "Ferris".to_string(),
581 },
582 ),
583 ])
584 .await
585 .unwrap_err();
586
587 match result {
588 MongoAggregateError::OptimisticLock => {}
589 _ => panic!("Expected OptimisticLockError, got {result:?}"),
590 }
591
592 let events = repository
593 .get_events::<Customer>(&aggregate_id)
594 .await
595 .unwrap();
596 assert_eq!(1, events.len());
597 }
598
599 #[tokio::test]
600 async fn test_event_repository_replay_stream_aggregate_instance() {
601 let client = mongodb_client().await;
602 let repository = MongoEventRepository::new(client)
603 .await
604 .unwrap()
605 .with_streaming_channel_size(3);
606 let aggregate_id = uuid::Uuid::new_v4().to_string();
607
608 let events: Vec<SerializedEvent> = (1..=10)
610 .map(|i| {
611 test_event(
612 &aggregate_id,
613 i,
614 CustomerEvent::EmailUpdated {
615 new_email: format!("{i}@example.test").to_string(),
616 },
617 )
618 })
619 .collect();
620 repository.insert_events(&events).await.unwrap();
621
622 let mut stream = repository
623 .stream_events::<Customer>(&aggregate_id)
624 .await
625 .unwrap();
626 let mut num_events = 0;
627 while (stream.next::<Customer>(&[]).await).is_some() {
628 num_events += 1;
629 }
630 assert_eq!(10, num_events);
631 }
632
633 #[tokio::test]
634 async fn test_event_repository_replay_stream_all_aggregate_type() {
635 let client = mongodb_client().await;
636 let repository = MongoEventRepository::new(client)
637 .await
638 .unwrap()
639 .with_streaming_channel_size(3);
640 let aggregate_ids: Vec<String> = (0..3).map(|_| uuid::Uuid::new_v4().to_string()).collect();
641
642 clear_collection(repository.get_collection(&repository.event_collection)).await;
643
644 for aggregate_id in &aggregate_ids {
646 let events: Vec<SerializedEvent> = (1..=10)
647 .map(|i| {
648 test_event(
649 aggregate_id,
650 i,
651 CustomerEvent::EmailUpdated {
652 new_email: format!("{i}@example.test").to_string(),
653 },
654 )
655 })
656 .collect();
657 repository.insert_events(&events).await.unwrap();
658 }
659
660 let mut stream = repository.stream_all_events::<Customer>().await.unwrap();
661 let mut num_events = 0;
662 while (stream.next::<Customer>(&[]).await).is_some() {
663 num_events += 1;
664 }
665 assert_eq!(30, num_events);
666 }
667
668 #[tokio::test]
669 async fn test_snapshot_repository_empty_returns_none() {
670 let client = mongodb_client().await;
671 let repository = MongoEventRepository::new(client)
672 .await
673 .unwrap()
674 .with_streaming_channel_size(1);
675 let aggregate_id = uuid::Uuid::new_v4().to_string();
676
677 let snapshot = repository
678 .get_snapshot::<Customer>(&aggregate_id)
679 .await
680 .unwrap();
681 assert_eq!(None, snapshot);
682 }
683
684 #[tokio::test]
685 async fn test_snapshot_repository_inserts_successfully() {
686 let client = mongodb_client().await;
687 let repository = MongoEventRepository::new(client)
688 .await
689 .unwrap()
690 .with_streaming_channel_size(1);
691 let aggregate_id = uuid::Uuid::new_v4().to_string();
692
693 repository
694 .update_snapshot::<Customer>(
695 serde_json::to_value(Customer {
696 customer_id: "123".to_string(),
697 name: "Ferris".to_string(),
698 email: "email@example.test".to_string(),
699 data_populated: true,
700 })
701 .unwrap(),
702 aggregate_id.clone(),
703 1,
704 &[],
705 )
706 .await
707 .unwrap();
708
709 let snapshot = repository
710 .get_snapshot::<Customer>(&aggregate_id)
711 .await
712 .unwrap();
713
714 assert_eq!(
715 Some(test_snapshot_context(
716 aggregate_id.clone(),
717 serde_json::to_value(Customer {
718 customer_id: "123".to_string(),
719 name: "Ferris".to_string(),
720 email: "email@example.test".to_string(),
721 data_populated: true,
722 })
723 .unwrap(),
724 0,
725 1
726 )),
727 snapshot
728 );
729 }
730
731 #[tokio::test]
732 async fn test_snapshot_repository_returns_latest_snapshot() {
733 let client = mongodb_client().await;
734 let repository = MongoEventRepository::new(client)
735 .await
736 .unwrap()
737 .with_streaming_channel_size(1);
738 let aggregate_id = uuid::Uuid::new_v4().to_string();
739
740 repository
742 .update_snapshot::<Customer>(
743 serde_json::to_value(Customer {
744 customer_id: "123".to_string(),
745 name: "Ferris".to_string(),
746 email: "first@example.test".to_string(),
747 data_populated: true,
748 })
749 .unwrap(),
750 aggregate_id.clone(),
751 1,
752 &[],
753 )
754 .await
755 .unwrap();
756
757 repository
759 .update_snapshot::<Customer>(
760 serde_json::to_value(Customer {
761 customer_id: "123".to_string(),
762 name: "Ferris".to_string(),
763 email: "second@example.test".to_string(),
764 data_populated: true,
765 })
766 .unwrap(),
767 aggregate_id.clone(),
768 2,
769 &[],
770 )
771 .await
772 .unwrap();
773
774 let snapshot = repository
775 .get_snapshot::<Customer>(&aggregate_id)
776 .await
777 .unwrap();
778
779 assert_eq!(
780 Some(test_snapshot_context(
781 aggregate_id.clone(),
782 serde_json::to_value(Customer {
783 customer_id: "123".to_string(),
784 name: "Ferris".to_string(),
785 email: "second@example.test".to_string(),
786 data_populated: true,
787 })
788 .unwrap(),
789 0,
790 2
791 )),
792 snapshot
793 );
794 }
795
796 #[tokio::test]
797 async fn test_snapshot_repository_invalid_sequence_returns_error() {
798 let client = mongodb_client().await;
799 let repository = MongoEventRepository::new(client)
800 .await
801 .unwrap()
802 .with_streaming_channel_size(1);
803 let aggregate_id = uuid::Uuid::new_v4().to_string();
804
805 repository
807 .update_snapshot::<Customer>(
808 serde_json::to_value(Customer {
809 customer_id: "123".to_string(),
810 name: "Ferris".to_string(),
811 email: "first@example.test".to_string(),
812 data_populated: true,
813 })
814 .unwrap(),
815 aggregate_id.clone(),
816 1,
817 &[],
818 )
819 .await
820 .unwrap();
821
822 let result = repository
824 .update_snapshot::<Customer>(
825 serde_json::to_value(Customer {
826 customer_id: "123".to_string(),
827 name: "Ferris".to_string(),
828 email: "second@example.test".to_string(),
829 data_populated: true,
830 })
831 .unwrap(),
832 aggregate_id.clone(),
833 1,
834 &[],
835 )
836 .await
837 .unwrap_err();
838
839 match result {
840 MongoAggregateError::OptimisticLock => {}
841 _ => panic!("Expected OptimisticLockError, got {result:?}"),
842 }
843
844 let snapshot = repository
845 .get_snapshot::<Customer>(&aggregate_id)
846 .await
847 .unwrap();
848
849 assert_eq!(
850 Some(test_snapshot_context(
851 aggregate_id.clone(),
852 serde_json::to_value(Customer {
853 customer_id: "123".to_string(),
854 name: "Ferris".to_string(),
855 email: "first@example.test".to_string(),
856 data_populated: true,
857 })
858 .unwrap(),
859 0,
860 1
861 )),
862 snapshot
863 );
864 }
865
866 async fn clear_collection(collection: Collection<Document>) {
868 collection.delete_many(doc! {}).await.unwrap();
869 }
870}