Repository: looplab/eventhorizon (go)

Issue:
Custom MongoDB Collection name for `events`
**Is your feature request related to a problem? Please describe.**
Currently the eventstore forces you to use `events` as your MongoDB collection name. Possibility to specify this by developer would make a lot.

**Describe the solution you'd like**
Add option to configure the value. I am unable to provide enough good solution to this, so leaving feature request here.

**Additional context**

https://github.com/looplab/eventhorizon/blob/main/eventstore/mongodb_v2/eventstore.go#L69-L70

https://github.com/looplab/eventhorizon/blob/main/eventstore/mongodb/eventstore.go#L65-L68



Pull request description:
Feature / custom mongodb event collection names
### Description

This PR adds the ability to configure in which collection the mongo event store saves events. This would be useful, for example, when I use the namespace package and want to store events in different collections for each tenant. Currently I can only separate them on a "per database" basis.

### Affected Components

- Event Store (Mongo v1, Mongo v2)

### Related Issues

Closes #328 

### Solution and Design

Using an optional function to provide the custom collection name.

### Steps to test and verify

1. Run the TestWithCollectionNameIntegration tests in eventstore/mongodb and eventstore/mongodb_v2.


Patch:
diff --git a/AUTHORS b/AUTHORS
index 0204d42..841ac75 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -19,3 +19,4 @@ Michael E Brown
 Oleg Yashchuk
 Bradley Weston
 Johannes Kolata
+Maximilian Breida
diff --git a/eventstore/mongodb/eventstore.go b/eventstore/mongodb/eventstore.go
index 6cda628..17e21fb 100644
--- a/eventstore/mongodb/eventstore.go
+++ b/eventstore/mongodb/eventstore.go
@@ -38,6 +38,7 @@ import (
 // as values.
 type EventStore struct {
 	client                *mongo.Client
+	db                    *mongo.Database
 	aggregates            *mongo.Collection
 	eventHandlerAfterSave eh.EventHandler
 	eventHandlerInTX      eh.EventHandler
@@ -59,14 +60,17 @@ func NewEventStore(uri, db string, options ...Option) (*EventStore, error) {
 }
 
 // NewEventStoreWithClient creates a new EventStore with a client.
-func NewEventStoreWithClient(client *mongo.Client, db string, options ...Option) (*EventStore, error) {
+func NewEventStoreWithClient(client *mongo.Client, dbName string, options ...Option) (*EventStore, error) {
 	if client == nil {
 		return nil, fmt.Errorf("missing DB client")
 	}
 
+	db := client.Database(dbName)
+
 	s := &EventStore{
 		client:     client,
-		aggregates: client.Database(db).Collection("events"),
+		db:         db,
+		aggregates: db.Collection("events"),
 	}
 
 	for _, option := range options {
@@ -123,6 +127,19 @@ func WithEventHandlerInTX(h eh.EventHandler) Option {
 	}
 }
 
+// WithCollectionName uses a different event collection than the default "events".
+func WithCollectionName(eventsColl string) Option {
+	return func(s *EventStore) error {
+		if eventsColl == "" {
+			return fmt.Errorf("missing collection name")
+		}
+
+		s.aggregates = s.db.Collection(eventsColl)
+
+		return nil
+	}
+}
+
 // Save implements the Save method of the eventhorizon.EventStore interface.
 func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersion int) error {
 	if len(events) == 0 {
diff --git a/eventstore/mongodb_v2/eventstore.go b/eventstore/mongodb_v2/eventstore.go
index b6c12da..13911a8 100644
--- a/eventstore/mongodb_v2/eventstore.go
+++ b/eventstore/mongodb_v2/eventstore.go
@@ -38,6 +38,7 @@ import (
 // keep tracks of the global position of events, stored as metadata.
 type EventStore struct {
 	client                *mongo.Client
+	db                    *mongo.Database
 	events                *mongo.Collection
 	streams               *mongo.Collection
 	eventHandlerAfterSave eh.EventHandler
@@ -68,6 +69,7 @@ func NewEventStoreWithClient(client *mongo.Client, dbName string, options ...Opt
 	db := client.Database(dbName)
 	s := &EventStore{
 		client:  client,
+		db:      db,
 		events:  db.Collection("events"),
 		streams: db.Collection("streams"),
 	}
@@ -148,6 +150,25 @@ func WithEventHandlerInTX(h eh.EventHandler) Option {
 	}
 }
 
+// WithCollectionNames uses different collections from the default "events" and "streams" collections.
+// Will return an error if provided parameters are equal.
+func WithCollectionNames(eventsColl, streamsColl string) Option {
+	return func(s *EventStore) error {
+		if eventsColl == streamsColl {
+			return fmt.Errorf("custom collection names are equal")
+		}
+
+		if eventsColl == "" || streamsColl == "" {
+			return fmt.Errorf("missing collection name")
+		}
+
+		s.events = s.db.Collection(eventsColl)
+		s.streams = s.db.Collection(streamsColl)
+
+		return nil
+	}
+}
+
 // Save implements the Save method of the eventhorizon.EventStore interface.
 func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersion int) error {
 	if len(events) == 0 {


Test patch:
diff --git a/eventstore/mongodb/eventstore_test.go b/eventstore/mongodb/eventstore_test.go
index 7156777..c73e1e8 100644
--- a/eventstore/mongodb/eventstore_test.go
+++ b/eventstore/mongodb/eventstore_test.go
@@ -65,6 +65,56 @@ func TestEventStoreIntegration(t *testing.T) {
 	eventstore.AcceptanceTest(t, store, context.Background())
 }
 
+func TestWithCollectionNameIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	// Use MongoDB in Docker with fallback to localhost.
+	url := os.Getenv("MONGODB_ADDR")
+	if url == "" {
+		url = "localhost:27017"
+	}
+
+	url = "mongodb://" + url
+
+	// Get a random DB name.
+	b := make([]byte, 4)
+	if _, err := rand.Read(b); err != nil {
+		t.Fatal(err)
+	}
+
+	db := "test-" + hex.EncodeToString(b)
+	collName := "foo_events"
+
+	t.Log("using DB:", db)
+
+	store, err := NewEventStore(url, db,
+		WithCollectionName(collName),
+	)
+	if err != nil {
+		t.Fatal("there should be no error:", err)
+	}
+
+	if store == nil {
+		t.Fatal("there should be a store")
+	}
+
+	defer store.Close()
+
+	if store.aggregates.Name() != collName {
+		t.Fatal("events collection should use custom collection name")
+	}
+
+	// providing empty collection names should result in an error
+	_, err = NewEventStore(url, db,
+		WithCollectionName(""),
+	)
+	if err == nil || err.Error() != "error while applying option: missing collection name" {
+		t.Fatal("there should be an error")
+	}
+}
+
 func TestWithEventHandlerIntegration(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skipping integration test")
diff --git a/eventstore/mongodb_v2/eventstore_test.go b/eventstore/mongodb_v2/eventstore_test.go
index ee0d546..f739cde 100644
--- a/eventstore/mongodb_v2/eventstore_test.go
+++ b/eventstore/mongodb_v2/eventstore_test.go
@@ -65,6 +65,77 @@ func TestEventStoreIntegration(t *testing.T) {
 	eventstore.AcceptanceTest(t, store, context.Background())
 }
 
+func TestWithCollectionNamesIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	// Use MongoDB in Docker with fallback to localhost.
+	url := os.Getenv("MONGODB_ADDR")
+	if url == "" {
+		url = "localhost:27017"
+	}
+
+	url = "mongodb://" + url
+
+	// Get a random DB name.
+	b := make([]byte, 4)
+	if _, err := rand.Read(b); err != nil {
+		t.Fatal(err)
+	}
+
+	db := "test-" + hex.EncodeToString(b)
+	eventsColl := "foo_events"
+	streamsColl := "bar_streams"
+
+	t.Log("using DB:", db)
+
+	store, err := NewEventStore(url, db,
+		WithCollectionNames(eventsColl, streamsColl),
+	)
+	if err != nil {
+		t.Fatal("there should be no error:", err)
+	}
+
+	if store == nil {
+		t.Fatal("there should be a store")
+	}
+
+	defer store.Close()
+
+	if store.events.Name() != eventsColl {
+		t.Fatal("events collection should use custom collection name")
+	}
+
+	if store.streams.Name() != streamsColl {
+		t.Fatal("streams collection should use custom collection name")
+	}
+
+	// providing the same collection name should result in an error
+	_, err = NewEventStore(url, db,
+		WithCollectionNames("my-collection", "my-collection"),
+	)
+	if err == nil || err.Error() != "error while applying option: custom collection names are equal" {
+		t.Fatal("there should be an error")
+	}
+
+	// providing empty collection names should result in an error
+	_, err = NewEventStore(url, db,
+		WithCollectionNames("", "my-collection"),
+	)
+	if err == nil || err.Error() != "error while applying option: missing collection name" {
+		t.Fatal("there should be an error")
+	}
+
+	// providing empty collection names should result in an error
+	_, err = NewEventStore(url, db,
+		WithCollectionNames("my-collection", ""),
+	)
+	if err == nil || err.Error() != "error while applying option: missing collection name" {
+		t.Fatal("there should be an error")
+	}
+}
+
 func TestWithEventHandlerIntegration(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skipping integration test")