mongo_es/
event_repository.rs

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    // helper: get collection by name from default database
66    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                    // .name("aggregate_id_sequence_unique".to_string())
82                    .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                    // .name("aggregate_id_current_snapshot_unique".to_string())
101                    .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    // helper: transforms serialized events into MongoDB documents
139    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    /// Returns all events for the given aggregate type and id, starting from the specified sequence.
158    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        // Replaces the snapshot entirely if it exists instead of patching it.
210        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    /// Queries the MongoDB collection and returns a cursor.
230    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 }), // sort descending
322            )
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    // https://github.com/serverlesstechnology/postgres-es/blob/main/src/event_repository.rs#L99C5-L100C96
387    // TODO: aggregate id is unused here, `stream_events` function needs to be broken up
388    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        // Insert events
422        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        // Insert event
461        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        // Expect error when using invalid sequence
473        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        // First snapshot
561        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        // Second snapshot
577        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        // First snapshot
623        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        // Second snapshot, but with same sequence number
639        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}