From ae9d8b94095d57b5295eb1f9c4577f0191b8ae60 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Tue, 19 Dec 2023 11:03:06 +0530 Subject: [PATCH 01/13] feat: add wrapper function to support MongoDB transactions --- .golangci.yml | 4 +- README.md | 6 ++- connection.go | 16 ++++--- docs/README.md | 3 +- docs/basic_usage.md | 6 ++- docs/transactions.md | 60 +++++++++++++++++++++++++ transaction.go | 33 ++++++++++++++ transaction_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 docs/transactions.md create mode 100644 transaction.go create mode 100644 transaction_test.go diff --git a/.golangci.yml b/.golangci.yml index 9e9b000..cea9bdf 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -243,7 +243,6 @@ linters: - nilnil # checks that there is no simultaneous return of nil error and an invalid value - noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives - - nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint @@ -269,8 +268,9 @@ linters: #- decorder # checks declaration order and count of types, constants, variables and functions #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega #- goheader # checks is file header matches to pattern - # - godox # detects FIXME, TODO and other comment keywords + #- godox # detects FIXME, TODO and other comment keywords #- ireturn # accept interfaces, return concrete types + #- nonamedreturns # reports all named returns #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated #- tagalign # checks that struct tags are well aligned #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope diff --git a/README.md b/README.md index 2f6a0de..4e46f04 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,10 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - // dbConn is the database connection obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(dbConn) + dbName := "mgod-test" + + // client is the MongoDB client obtained using Go Mongo Driver's Connect method. + mgod.SetDefaultConnection(client, dbName) } ``` diff --git a/connection.go b/connection.go index d6aab0c..818204b 100644 --- a/connection.go +++ b/connection.go @@ -8,27 +8,29 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +var mClient *mongo.Client var dbConn *mongo.Database var defaultTimeout = 10 * time.Second // ConnectionConfig is the configuration options available for a MongoDB connection. type ConnectionConfig struct { - // Timeout is the timeout for various operations performed on the MongoDB server like Connect, Ping, Session etc. + // Timeout is the timeout for various operations performed on the MongoDB server like Connect, Ping etc. Timeout time.Duration } // SetDefaultConnection sets the default connection to be used by the package. -func SetDefaultConnection(conn *mongo.Database) { - dbConn = conn +func SetDefaultConnection(client *mongo.Client, dbName string) { + mClient = client + dbConn = mClient.Database(dbName) } // ConfigureDefaultConnection opens a new connection using the provided config options and sets it as a default connection to be used by the package. -func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*options.ClientOptions) error { +func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*options.ClientOptions) (err error) { if cfg == nil { cfg = defaultConnectionConfig() } - client, err := newClient(cfg, opts...) + mClient, err = newClient(cfg, opts...) if err != nil { return err } @@ -37,12 +39,12 @@ func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*o defer cancel() // Ping the MongoDB server to check if connection is established. - err = client.Ping(ctx, nil) + err = mClient.Ping(ctx, nil) if err != nil { return err } - dbConn = client.Database(dbName) + dbConn = mClient.Database(dbName) return nil } diff --git a/docs/README.md b/docs/README.md index 94d2408..45bce26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,4 +18,5 @@ This directory contains user-facing documentation. For those who wish to underst * [Meta fields](./meta_fields.md) ### Advanced Guide -* [Unions](./union_types.md) \ No newline at end of file +* [Unions](./union_types.md) +* [Transactions](./transactions.md) \ No newline at end of file diff --git a/docs/basic_usage.md b/docs/basic_usage.md index c2cc8a9..d42df14 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -9,8 +9,10 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - // dbConn is the database connection obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(dbConn) + dbName := "mgod-test" + + // client is the MongoDB client obtained using Go Mongo Driver's Connect method. + mgod.SetDefaultConnection(client, dbName) } ``` diff --git a/docs/transactions.md b/docs/transactions.md new file mode 100644 index 0000000..8ce9c10 --- /dev/null +++ b/docs/transactions.md @@ -0,0 +1,60 @@ +--- +title: Transactions +--- + +`mgod` provides a wrapper function `WithTransaction` that supports MongoDB transactions, allowing users to perform a series of read and write operations as a single atomic unit. + +## Usage + +Configure default connection with `mgod`. + +```go +cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} +dbName := "mgod_test" +opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") + +err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) +``` + +:::info +To use Transactions, it is compulsory to run MongoDB daemon as a replica set. +Refer Community Forum Discussion - [Why replica set is mandatory for transactions in MongoDB?](https://www.mongodb.com/community/forums/t/why-replica-set-is-mandatory-for-transactions-in-mongodb/9533) +::: + +Create models to be used inside a MongoDB transaction. + +```go +type User struct { + Name string + EmailID string `bson:"emailId"` +} + +schemaOpts := schemaopt.SchemaOptions{ + Collection: "users", + Timestamps: true, +} + +userModel, _ := mgod.NewEntityMongoModel(User{}, schemaOpts) +``` + +Use `WithTransaction` function to perform multiple CRUD operations as an atomic unit. + +```go +userDoc1 := User{Name: "Gopher1", EmailID: "gopher1@mgod.com"} +userDoc2 := User{Name: "Gopher2", EmailID: "gopher2@mgod.com"} + +_, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + _, err1 := s.userModel.InsertOne(sc, userDoc1) + _, err2 := s.userModel.InsertOne(sc, userDoc2) + + if err1 != nil || err2 != nil { + return nil, errors.New("abort transaction") + } + + return nil, nil +}) +``` + +:::warning +Make sure to pass the session's context (`sc` here) only in EntityMongoModel's operation functions. +::: \ No newline at end of file diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..bd5c248 --- /dev/null +++ b/transaction.go @@ -0,0 +1,33 @@ +package mgod + +import ( + "context" + "log/slog" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" +) + +// TransactionFunc is the function that is executed in a MongoDB transaction. +// +// SessionContext(sc) combines the context.Context and mongo.Session interfaces. +type TransactionFunc func(sc mongo.SessionContext) (interface{}, error) + +// WithTransaction executes the given transaction function with a new session. +func WithTransaction(ctx context.Context, transactionFunc TransactionFunc) (interface{}, error) { + session, err := mClient.StartSession() + if err != nil { + slog.ErrorContext(ctx, "Error occurred during WithTransaction", err) + return nil, err + } + defer session.EndSession(ctx) + + // Reason behind using read preference: + // https://www.mongodb.com/community/forums/t/why-can-t-read-preference-be-secondary-in-a-transaction/204432 + payload, transactionErr := session.WithTransaction(ctx, transactionFunc, &options.TransactionOptions{ + ReadPreference: readpref.Primary(), + }) + + return payload, transactionErr +} diff --git a/transaction_test.go b/transaction_test.go new file mode 100644 index 0000000..02f2cff --- /dev/null +++ b/transaction_test.go @@ -0,0 +1,103 @@ +package mgod_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/Lyearn/mgod" + "github.com/Lyearn/mgod/schema/schemaopt" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type TransactionSuite struct { + suite.Suite + *require.Assertions + + userModel mgod.EntityMongoModel[TransactionTestUser] +} + +type TransactionTestUser struct { + Name string + EmailID string `bson:"emailId"` +} + +func TestTransactionSuite(t *testing.T) { + s := new(TransactionSuite) + suite.Run(t, s) +} + +func (s *TransactionSuite) SetupTest() { + s.Assertions = require.New(s.T()) + s.SetupConnectionAndModel() +} + +func (s *TransactionSuite) SetupConnectionAndModel() { + cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} + dbName := "mgod_test" + opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") + + err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + if err != nil { + s.T().Fatal(err) + } + + schemaOpts := schemaopt.SchemaOptions{ + Collection: "users", + Timestamps: true, + } + userModel, err := mgod.NewEntityMongoModel(TransactionTestUser{}, schemaOpts) + if err != nil { + s.T().Fatal(err) + } + + s.userModel = userModel +} + +func (s *TransactionSuite) TestWithTransaction() { + userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + + p, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + user, err := s.userModel.InsertOne(sc, userDoc) + return user, err + }) + + user, ok := p.(TransactionTestUser) + + s.True(ok) + s.NoError(err) + s.Equal(user.Name, userDoc.Name) + s.Equal(user.EmailID, userDoc.EmailID) + + userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + + s.NoError(err) + s.Equal(userCount, int64(1)) +} + +func (s *TransactionSuite) TestWithTransactionAbort() { + userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + + abortErr := errors.New("dummy error to abort transaction") + + _, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + _, err := s.userModel.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } + + return nil, abortErr + }) + + s.EqualError(err, abortErr.Error()) + + userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + + s.NoError(err) + s.Equal(userCount, int64(0)) +} From 86145c857eebeaa10b34e8cc56a713515d9bf23c Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Tue, 19 Dec 2023 11:06:16 +0530 Subject: [PATCH 02/13] doc: mark transactions feature as done in future scope section --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e46f04..5715409 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,9 @@ Inspired by the easy interface of MongoDB handling using [Mongoose](https://gith ## Future Scope The current version of mgod is a stable release. However, there are plans to add a lot more features like - -- [ ] Enable functionality to opt out of the default conversion of date fields to ISOString format. - [x] Implement a setup step for storing a default Mongo connection, eliminating the need to pass it during EntityMongoModel creation. -- [ ] Provide support for transactions following the integration of default Mongo connection logic. +- [x] Provide support for transactions following the integration of default Mongo connection logic. +- [ ] Enable functionality to opt out of the default conversion of date fields to ISOString format. - [ ] Develop easy to use wrapper functions around MongoDB Aggregation operation. - [ ] Introduce automatic MongoDB collection selection based on Go struct names as a default behavior. - [ ] Add test cases to improve code coverage. From a476567e4d1c68d74a61d7687e1fd928f6fb7081 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 12:24:22 +0530 Subject: [PATCH 03/13] fix: support multi tenancy --- README.md | 13 +- connection.go | 12 +- connection_cache.go | 21 +++ docs/README.md | 2 +- docs/basic_usage.md | 12 +- docs/meta_fields.md | 7 +- docs/schema_options.md | 19 --- docs/transactions.md | 10 +- entity_mongo_model.go | 10 +- entity_mongo_model_opts.go | 31 ++++ entity_mongo_model_test.go | 256 ++++++++++++----------------- schema/schemaopt/schema_options.go | 4 - transaction_test.go | 110 ++++++++++--- 13 files changed, 271 insertions(+), 236 deletions(-) create mode 100644 connection_cache.go create mode 100644 entity_mongo_model_opts.go diff --git a/README.md b/README.md index 5715409..48a29ea 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - Easily manage **meta fields** in models without cluttering Go structs. - Supports **union types**, expanding data capabilities. - Implement strict field requirements with struct tags for **data integrity**. +- Built-in support for **multi-tenant** systems. - Wrapper around the **official** Mongo Go Driver. ## Requirements @@ -42,10 +43,8 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - dbName := "mgod-test" - // client is the MongoDB client obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(client, dbName) + mgod.SetDefaultClient(client) } ``` @@ -61,10 +60,9 @@ import ( func init() { // `cfg` is optional. Can rely on default configurations by providing `nil` value in argument. cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} - dbName := "mgod-test" opts := options.Client().ApplyURI("mongodb://root:mgod123@localhost:27017") - err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + err := mgod.ConfigureDefaultClient(cfg, opts) } ``` @@ -86,11 +84,14 @@ import ( ) model := User{} +dbName := "mgoddb" +collection := "users" + schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } +opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) ``` diff --git a/connection.go b/connection.go index 818204b..6b0ac10 100644 --- a/connection.go +++ b/connection.go @@ -9,7 +9,6 @@ import ( ) var mClient *mongo.Client -var dbConn *mongo.Database var defaultTimeout = 10 * time.Second // ConnectionConfig is the configuration options available for a MongoDB connection. @@ -18,14 +17,13 @@ type ConnectionConfig struct { Timeout time.Duration } -// SetDefaultConnection sets the default connection to be used by the package. -func SetDefaultConnection(client *mongo.Client, dbName string) { +// SetDefaultClient sets the default MongoDB client to be used by the package. +func SetDefaultClient(client *mongo.Client, dbName string) { mClient = client - dbConn = mClient.Database(dbName) } -// ConfigureDefaultConnection opens a new connection using the provided config options and sets it as a default connection to be used by the package. -func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*options.ClientOptions) (err error) { +// ConfigureDefaultClient opens a new connection using the provided config options and sets the default MongoDB client to be used by the package. +func ConfigureDefaultClient(cfg *ConnectionConfig, opts ...*options.ClientOptions) (err error) { if cfg == nil { cfg = defaultConnectionConfig() } @@ -44,8 +42,6 @@ func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*o return err } - dbConn = mClient.Database(dbName) - return nil } diff --git a/connection_cache.go b/connection_cache.go new file mode 100644 index 0000000..cc8ed96 --- /dev/null +++ b/connection_cache.go @@ -0,0 +1,21 @@ +package mgod + +import "go.mongodb.org/mongo-driver/mongo" + +// dbConnCache is a cache of MongoDB database connections. +var dbConnCache map[string]*mongo.Database + +func init() { + dbConnCache = make(map[string]*mongo.Database) +} + +// getDBConn returns a MongoDB database connection from the cache. +// If the connection is not present in the cache, it creates a new connection and adds it to the cache (Write-through policy). +func getDBConn(dbName string) *mongo.Database { + // Initialize the cache entry if it is not present. + if dbConnCache[dbName] == nil { + dbConnCache[dbName] = mClient.Database(dbName) + } + + return dbConnCache[dbName] +} diff --git a/docs/README.md b/docs/README.md index 45bce26..c06c3a3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,4 +19,4 @@ This directory contains user-facing documentation. For those who wish to underst ### Advanced Guide * [Unions](./union_types.md) -* [Transactions](./transactions.md) \ No newline at end of file +* [Transactions](./transactions.md) diff --git a/docs/basic_usage.md b/docs/basic_usage.md index d42df14..b081489 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -9,10 +9,8 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - dbName := "mgod-test" - // client is the MongoDB client obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(client, dbName) + mgod.SetDefaultClient(client, dbName) } ``` @@ -28,10 +26,9 @@ import ( func init() { // `cfg` is optional. Can rely on default configurations by providing `nil` value in argument. cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} - dbName := "mgod-test" opts := options.Client().ApplyURI("mongodb://root:mgod123@localhost:27017") - err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + err := mgod.ConfigureDefaultClient(cfg, opts) } ``` @@ -59,11 +56,14 @@ import ( ) model := User{} +dbName := "mgoddb" +collection := "users" + schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } +opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) ``` diff --git a/docs/meta_fields.md b/docs/meta_fields.md index 021de52..1b1c4ea 100644 --- a/docs/meta_fields.md +++ b/docs/meta_fields.md @@ -25,7 +25,6 @@ It is the meta field that stores the timestamp of the document creation. This fi ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } @@ -33,6 +32,7 @@ userDoc := User{ Name: "Gopher", EmailID: "gopher@mgod.com", } + user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` @@ -59,7 +59,6 @@ It is the meta field that stores the timestamp of the document updation. This fi ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } @@ -98,7 +97,6 @@ It is the field that stores the version of the document. This field is automatic ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", VersionKey: true } @@ -106,6 +104,7 @@ userDoc := User{ Name: "Gopher", EmailID: "gopher@mgod.com", } + user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` @@ -124,7 +123,6 @@ If `VersionKey` is set to `false`. ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", VersionKey: false } @@ -132,6 +130,7 @@ userDoc := User{ Name: "Gopher", EmailID: "gopher@mgod.com", } + user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` diff --git a/docs/schema_options.md b/docs/schema_options.md index dbd18d7..41b09ff 100644 --- a/docs/schema_options.md +++ b/docs/schema_options.md @@ -6,21 +6,6 @@ Schema Options is Mongo Schema level options (which modifies actual MongoDB doc) `mgod` supports the following schema options - -## Collection - -- Accepts Type: `string` -- Is Optional: `No` - -It is the name of the mongo collection in which the entity is stored. For example, `users` collection of MongoDB for `User` model in Golang. - -### Usage - -```go -schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", // MongoDB collection name -} -``` - ## Timestamps - Accepts Type: `bool` @@ -33,7 +18,6 @@ It is used to track `createdAt` and `updatedAt` meta fields for the entity. See ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } ``` @@ -50,7 +34,6 @@ This reports whether to add a version key (`__v`) for the entity. See [Meta Fiel ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", VersionKey: true, } ``` @@ -67,7 +50,6 @@ It defines whether the entity is a union type. See [Union Types](union_types.md) ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "resources", IsUnionType: true, } ``` @@ -90,7 +72,6 @@ It is the key used to identify the underlying type in case of a union type entit ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "resources", IsUnionType: true, DiscriminatorKey: "type", } diff --git a/docs/transactions.md b/docs/transactions.md index 8ce9c10..171a8b2 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -10,10 +10,9 @@ Configure default connection with `mgod`. ```go cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} -dbName := "mgod_test" opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") -err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) +err := mgod.ConfigureDefaultClient(cfg, opts) ``` :::info @@ -29,12 +28,13 @@ type User struct { EmailID string `bson:"emailId"` } +dbName := "mgoddb" +collection := "users" schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } -userModel, _ := mgod.NewEntityMongoModel(User{}, schemaOpts) +userModel, _ := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) ``` Use `WithTransaction` function to perform multiple CRUD operations as an atomic unit. @@ -57,4 +57,4 @@ _, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContex :::warning Make sure to pass the session's context (`sc` here) only in EntityMongoModel's operation functions. -::: \ No newline at end of file +::: diff --git a/entity_mongo_model.go b/entity_mongo_model.go index e40d8c5..dcdb48c 100644 --- a/entity_mongo_model.go +++ b/entity_mongo_model.go @@ -74,12 +74,13 @@ type entityMongoModel[T any] struct { } // NewEntityMongoModel returns a new instance of EntityMongoModel for the provided model type and options. -func NewEntityMongoModel[T any](modelType T, schemaOpts schemaopt.SchemaOptions) (EntityMongoModel[T], error) { +func NewEntityMongoModel[T any](modelType T, opts entityMongoModelOptions) (EntityMongoModel[T], error) { + dbConn := getDBConn(opts.connOpts.db) if dbConn == nil { return nil, errors.ErrNoDatabaseConnection } - coll := dbConn.Collection(schemaOpts.Collection) + coll := dbConn.Collection(opts.connOpts.coll) modelName := schema.GetSchemaNameForModel(modelType) schemaCacheKey := GetSchemaCacheKey(coll.Name(), modelName) @@ -87,6 +88,11 @@ func NewEntityMongoModel[T any](modelType T, schemaOpts schemaopt.SchemaOptions) var entityModelSchema *schema.EntityModelSchema var err error + schemaOpts := schemaopt.SchemaOptions{} + if opts.schemaOpts != nil { + schemaOpts = *opts.schemaOpts + } + // build schema if not cached. if entityModelSchema, err = schema.EntityModelSchemaCacheInstance.GetSchema(schemaCacheKey); err != nil { entityModelSchema, err = schema.BuildSchemaForModel(modelType, schemaOpts) diff --git a/entity_mongo_model_opts.go b/entity_mongo_model_opts.go new file mode 100644 index 0000000..d0fa926 --- /dev/null +++ b/entity_mongo_model_opts.go @@ -0,0 +1,31 @@ +package mgod + +import ( + "github.com/Lyearn/mgod/schema/schemaopt" +) + +type entityMongoModelOptions struct { + connOpts connectionOptions + schemaOpts *schemaopt.SchemaOptions +} + +type connectionOptions struct { + db string + coll string +} + +// NewEntityMongoModelOptions creates a new entityMongoModelOptions instance. +// Its instance is used to provide necessary configuration options to the NewEntityMongoModel function. +// +// dbName is the name of the database in which the entity is stored. +// collection is the name of the mongo collection in which the entity is stored. +// schemaOpts is the schema level options for the entity. +func NewEntityMongoModelOptions(dbName string, collection string, schemaOpts *schemaopt.SchemaOptions) *entityMongoModelOptions { + return &entityMongoModelOptions{ + connOpts: connectionOptions{ + db: dbName, + coll: collection, + }, + schemaOpts: schemaOpts, + } +} diff --git a/entity_mongo_model_test.go b/entity_mongo_model_test.go index fc3948c..f0bf1bc 100644 --- a/entity_mongo_model_test.go +++ b/entity_mongo_model_test.go @@ -6,198 +6,146 @@ import ( "time" "github.com/Lyearn/mgod" - "github.com/Lyearn/mgod/dateformatter" "github.com/Lyearn/mgod/schema/schemaopt" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/integration/mtest" + "go.mongodb.org/mongo-driver/mongo/options" ) type EntityMongoModelSuite struct { suite.Suite *require.Assertions +} - dbName string - collName string - - mt *mtest.T - mtOpts *mtest.Options +type testEntity struct { + ID string `bson:"_id" mgoType:"id"` + Name string + Age *int `bson:",omitempty" mgoDefault:"18"` } func TestEntityMongoModelSuite(t *testing.T) { s := new(EntityMongoModelSuite) - s.dbName = "foo" - s.collName = "bar" - suite.Run(t, s) } +func (s *EntityMongoModelSuite) SetupSuite() { + s.setupConnection() + s.setupData() +} + func (s *EntityMongoModelSuite) SetupTest() { s.Assertions = require.New(s.T()) +} - mtOpts := mtest.NewOptions() - mtOpts = mtOpts.ClientType(mtest.Mock) - mtOpts = mtOpts.DatabaseName(s.dbName) - mtOpts = mtOpts.CollectionName(s.collName) +func (s *EntityMongoModelSuite) TearDownSuite() { + entityMongoModel := s.getModel() + _, err := entityMongoModel.DeleteMany(context.Background(), bson.D{}) + if err != nil { + s.T().Fatal(err) + } +} - mt := mtest.New(s.T(), mtOpts) +func (s *EntityMongoModelSuite) setupConnection() { + cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} + uri := "mongodb://localhost:27017/?replicaSet=replset&authSource=admin" - s.mt = mt - s.mtOpts = mtOpts + err := mgod.ConfigureDefaultClient(cfg, options.Client().ApplyURI(uri)) + if err != nil { + s.T().Fatal(err) + } } -func (s *EntityMongoModelSuite) ns() string { - return s.dbName + "." + s.collName +func (s *EntityMongoModelSuite) setupData() { + firstID := primitive.NewObjectID() + secondID := primitive.NewObjectID() + + age1 := 30 + age2 := 40 + + entities := []testEntity{ + { + ID: firstID.Hex(), + Name: "Default User 1", + Age: &age1, + }, + { + ID: secondID.Hex(), + Name: "Default User 2", + Age: &age2, + }, + } + + entityMongoModel := s.getModel() + _, err := entityMongoModel.InsertMany(context.Background(), entities) + if err != nil { + s.T().Fatal(err) + } } -type TestEntity struct { - ID string `bson:"_id" mgoType:"id"` - Name string - JoinedOn string `mgoType:"date"` - Age *int `bson:",omitempty" mgoDefault:"18"` +func (s *EntityMongoModelSuite) getModel() mgod.EntityMongoModel[testEntity] { + schemaOpts := schemaopt.SchemaOptions{Timestamps: true} + opts := mgod.NewEntityMongoModelOptions("mgoddb", "entityMongoModel", &schemaOpts) + model, err := mgod.NewEntityMongoModel(testEntity{}, *opts) + if err != nil { + s.T().Fatal(err) + } + + return model } func (s *EntityMongoModelSuite) TestFind() { - defer s.mt.Close() - - s.mt.RunOpts("find", s.mtOpts, func(mt *mtest.T) { - currentTime := time.Now() - currentTimeStr, _ := dateformatter.New(currentTime).GetISOString() - - firstID := primitive.NewObjectID() - secondID := primitive.NewObjectID() - - //nolint:govet // this is a mock entity. - firstEntity := TestEntity{ - ID: firstID.Hex(), - Name: "test1", - JoinedOn: currentTimeStr, - } - //nolint:govet // this is a mock entity. - secondEntity := TestEntity{ - ID: secondID.Hex(), - Name: "test2", - JoinedOn: currentTimeStr, - } - - first := mtest.CreateCursorResponse(1, s.ns(), mtest.FirstBatch, bson.D{ - {Key: "_id", Value: firstID}, - {Key: "name", Value: firstEntity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - }) - second := mtest.CreateCursorResponse(1, s.ns(), mtest.NextBatch, bson.D{ - {Key: "_id", Value: secondID}, - {Key: "name", Value: secondEntity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - }) - killCursors := mtest.CreateCursorResponse(0, s.ns(), mtest.NextBatch) - - mt.AddMockResponses(first, second, killCursors) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - testEntities, err := entityMongoModel.Find(context.Background(), bson.D{ - {Key: "name", Value: firstEntity.Name}, - }) - - s.Nil(err) - s.Equal(2, len(testEntities)) + entityMongoModel := s.getModel() + entities, err := entityMongoModel.Find(context.Background(), bson.M{ + "age": bson.M{ + "$gt": 20, + }, + "name": bson.M{ + "$regex": "Default", + }, }) + + s.NoError(err) + s.Equal(2, len(entities)) } func (s *EntityMongoModelSuite) TestFindOne() { - defer s.mt.Close() - - s.mt.RunOpts("find one", s.mtOpts, func(mt *mtest.T) { - currentTime := time.Now() - currentTimeStr, _ := dateformatter.New(currentTime).GetISOString() - - id := primitive.NewObjectID() - - //nolint:govet // this is a mock entity. - entity := TestEntity{ - ID: id.Hex(), - Name: "test", - JoinedOn: currentTimeStr, - } - - mt.AddMockResponses(mtest.CreateCursorResponse(1, s.ns(), mtest.FirstBatch, bson.D{ - {Key: "_id", Value: id}, - {Key: "name", Value: entity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - })) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - testEntity, err := entityMongoModel.FindOne(context.Background(), bson.D{ - {Key: "id", Value: entity.ID}, - }) - - s.Nil(err) - s.Equal(entity.ID, testEntity.ID) + entityMongoModel := s.getModel() + entity, err := entityMongoModel.FindOne(context.Background(), bson.M{ + "age": bson.M{ + "$gt": 30, + }, + "name": bson.M{ + "$regex": "Default", + }, }) + + s.NoError(err) + s.Equal("Default User 2", entity.Name) } func (s *EntityMongoModelSuite) TestInsertOne() { - defer s.mt.Close() - id := primitive.NewObjectID() - currentTime := time.Now() - currentTimeStr, _ := dateformatter.New(currentTime).GetISOString() - - s.mt.RunOpts("insert one", s.mtOpts, func(mt *mtest.T) { - entity := TestEntity{ - ID: id.Hex(), - Name: "test", - JoinedOn: currentTimeStr, - } - - mt.AddMockResponses(mtest.CreateCursorResponse(1, s.ns(), mtest.FirstBatch, bson.D{ - {Key: "_id", Value: id}, - {Key: "name", Value: entity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - {Key: "age", Value: 18}, - })) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - doc, err := entityMongoModel.InsertOne(context.Background(), entity) - - s.Nil(err) - s.Equal(entity.ID, doc.ID) - s.Equal(18, *doc.Age) - }) - - s.mt.RunOpts("insert one with error", s.mtOpts, func(mt *mtest.T) { - entity := TestEntity{ - ID: id.Hex(), - Name: "test", - JoinedOn: currentTimeStr, - } - - mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ - Index: 1, - Code: 11000, - Message: "duplicate key error", - })) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - docID, err := entityMongoModel.InsertOne(context.Background(), entity) - - s.Empty(docID) - s.NotNil(err) - s.True(mongo.IsDuplicateKeyError(err)) - }) + age := 18 + + entity := testEntity{ + ID: id.Hex(), + Name: "test", + Age: &age, + } + + entityMongoModel := s.getModel() + doc, err := entityMongoModel.InsertOne(context.Background(), entity) + + s.Nil(err) + s.Equal(entity.ID, doc.ID) + s.Equal(18, *doc.Age) + + // Test duplicate key error + docID, err := entityMongoModel.InsertOne(context.Background(), entity) + s.Empty(docID) + s.NotNil(err) + s.True(mongo.IsDuplicateKeyError(err)) } diff --git a/schema/schemaopt/schema_options.go b/schema/schemaopt/schema_options.go index cb68773..02c2f1d 100644 --- a/schema/schemaopt/schema_options.go +++ b/schema/schemaopt/schema_options.go @@ -1,11 +1,7 @@ package schemaopt // SchemaOptions is Mongo Schema level options (modifies actual MongoDB doc) that needs to be provided when creating a new EntityMongoModel. -// -// These options are used to identify the collection name, whether to add timestamps meta fields, etc. type SchemaOptions struct { - // Collection is the name of the mongo collection in which the entity is stored. - Collection string // Timestamps reports whether to add createdAt and updatedAt meta fields for the entity. Timestamps bool // VersionKey reports whether to add a version key for the entity. Defaults to true. diff --git a/transaction_test.go b/transaction_test.go index 02f2cff..f542031 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -3,6 +3,7 @@ package mgod_test import ( "context" "errors" + "fmt" "testing" "time" @@ -18,11 +19,9 @@ import ( type TransactionSuite struct { suite.Suite *require.Assertions - - userModel mgod.EntityMongoModel[TransactionTestUser] } -type TransactionTestUser struct { +type transactionTestUser struct { Name string EmailID string `bson:"emailId"` } @@ -32,61 +31,74 @@ func TestTransactionSuite(t *testing.T) { suite.Run(t, s) } +func (s *TransactionSuite) SetupSuite() { + s.setupConnection() +} + func (s *TransactionSuite) SetupTest() { s.Assertions = require.New(s.T()) - s.SetupConnectionAndModel() } -func (s *TransactionSuite) SetupConnectionAndModel() { +func (s *TransactionSuite) setupConnection() { cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} - dbName := "mgod_test" - opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") + // Can use the `mlaunch` tool to start a local replica set using command `mlaunch --repl`. + uri := "mongodb://localhost:27017/?replicaSet=replset&authSource=admin" - err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + err := mgod.ConfigureDefaultClient(cfg, options.Client().ApplyURI(uri)) if err != nil { s.T().Fatal(err) } +} - schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", - Timestamps: true, - } - userModel, err := mgod.NewEntityMongoModel(TransactionTestUser{}, schemaOpts) +func (s *TransactionSuite) getModelForDB(dbName string) mgod.EntityMongoModel[transactionTestUser] { + schemaOpts := schemaopt.SchemaOptions{Timestamps: true} + opts := mgod.NewEntityMongoModelOptions(dbName, "users", &schemaOpts) + userModel, err := mgod.NewEntityMongoModel(transactionTestUser{}, *opts) if err != nil { s.T().Fatal(err) } - s.userModel = userModel + return userModel } func (s *TransactionSuite) TestWithTransaction() { - userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + userModel := s.getModelForDB("mgod1") + userDoc := transactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} p, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { - user, err := s.userModel.InsertOne(sc, userDoc) - return user, err - }) + _, err := userModel.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } - user, ok := p.(TransactionTestUser) + userCount, err := userModel.CountDocuments(sc, bson.M{}) + if err != nil { + return nil, err + } - s.True(ok) - s.NoError(err) - s.Equal(user.Name, userDoc.Name) - s.Equal(user.EmailID, userDoc.EmailID) + _, err = userModel.DeleteOne(sc, bson.M{}) + if err != nil { + return nil, err + } - userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + return userCount, nil + }) + + userCount, ok := p.(int64) s.NoError(err) + s.True(ok) s.Equal(userCount, int64(1)) } func (s *TransactionSuite) TestWithTransactionAbort() { - userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + userModel := s.getModelForDB("mgod1") + userDoc := transactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} abortErr := errors.New("dummy error to abort transaction") _, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { - _, err := s.userModel.InsertOne(sc, userDoc) + _, err := userModel.InsertOne(sc, userDoc) if err != nil { return nil, err } @@ -96,8 +108,52 @@ func (s *TransactionSuite) TestWithTransactionAbort() { s.EqualError(err, abortErr.Error()) - userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + userCount, err := userModel.CountDocuments(context.Background(), bson.M{}) s.NoError(err) s.Equal(userCount, int64(0)) } + +func (s *TransactionSuite) TestWithTransactionForMultiTenancy() { + userModelTenant1 := s.getModelForDB("mgod1") + userModelTenant2 := s.getModelForDB("mgod2") + + userDoc := transactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + + p, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + _, err := userModelTenant1.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } + _, err = userModelTenant2.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } + + userCount1, err := userModelTenant1.CountDocuments(sc, bson.M{}) + if err != nil { + return nil, err + } + userCount2, err := userModelTenant2.CountDocuments(sc, bson.M{}) + if err != nil { + return nil, err + } + + _, err = userModelTenant1.DeleteOne(sc, bson.M{}) + if err != nil { + return nil, err + } + _, err = userModelTenant2.DeleteOne(sc, bson.M{}) + if err != nil { + return nil, err + } + + return fmt.Sprintf("%d%d", userCount1, userCount2), nil + }) + + userCountStr, ok := p.(string) + + s.NoError(err) + s.True(ok) + s.Equal(userCountStr, "11") +} From 1b3208064bfedf48d5ab8d3d65f53c820163af51 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 12:29:26 +0530 Subject: [PATCH 04/13] doc: fix mgod usage example in README --- README.md | 10 ++-------- docs/basic_usage.md | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 48a29ea..dae615a 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,8 @@ model := User{} dbName := "mgoddb" collection := "users" -schemaOpts := schemaopt.SchemaOptions{ - Timestamps: true, -} - -opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) -userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) +opts := mgod.NewEntityMongoModelOptions(dbName, collection, nil) +userModel, _ := mgod.NewEntityMongoModel(model, *opts) ``` Use the entity ODM to perform CRUD operations with ease. @@ -115,8 +111,6 @@ user, _ := userModel.InsertOne(context.TODO(), userDoc) "name": "Gopher", "emailId": "gopher@mgod.com", "joinedOn": ISODate("2023-12-01T11:32:19.290Z"), - "createdAt": ISODate("2023-12-01T11:32:19.290Z"), - "updatedAt": ISODate("2023-12-01T11:32:19.290Z"), "__v": 0 } ``` diff --git a/docs/basic_usage.md b/docs/basic_usage.md index b081489..cca3861 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -64,7 +64,7 @@ schemaOpts := schemaopt.SchemaOptions{ } opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) -userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) +userModel, _ := mgod.NewEntityMongoModel(model, *opts) ``` Use the entity ODM to perform CRUD operations with ease. From 9439185eb52b46e79d94e2cc89785648b29588e8 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 13:40:16 +0530 Subject: [PATCH 05/13] doc: multi tenancy advanced guide --- README.md | 2 +- docs/README.md | 1 + docs/multi_tenancy.md | 44 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 docs/multi_tenancy.md diff --git a/README.md b/README.md index dae615a..5b2230b 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` **Output:** -```json +```js { "_id": ObjectId("65697705d4cbed00e8aba717"), "name": "Gopher", diff --git a/docs/README.md b/docs/README.md index c06c3a3..3d05b44 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,5 +18,6 @@ This directory contains user-facing documentation. For those who wish to underst * [Meta fields](./meta_fields.md) ### Advanced Guide +* [Multi Tenancy](./multi_tenancy.md) * [Unions](./union_types.md) * [Transactions](./transactions.md) diff --git a/docs/multi_tenancy.md b/docs/multi_tenancy.md new file mode 100644 index 0000000..058b560 --- /dev/null +++ b/docs/multi_tenancy.md @@ -0,0 +1,44 @@ +--- +title: Multi Tenancy +--- + +`mgod` comes with the built-in support for multi-tenancy, enabling the use of a single Go struct with multiple databases. This feature allows creation of multiple `EntityMongoModel` of the same Go struct to be attached to different databases while using the same underlying MongoDB client connection. + +## Usage + +Create separate `EntityMongoModel` for different tenants using same Go struct and corresponding databases. + +```go +type User struct { + Name string + EmailID string `bson:"emailId"` + Amount float32 +} +collection := "users" + +tenant1DB := "tenant1" +tenant2DB := "tenant2" + +tenant1Model, _ := mgod.NewEntityMongoModelOptions(tenant1DB, collection, nil) +tenant2Model, _ := mgod.NewEntityMongoModelOptions(tenant2DB, collection, nil) +``` + +These models can now be used simultaneously inside the same service logic as well as in a transaction operation. + +```go +amount := 10000 + +tenant1Model.UpdateMany(context.TODO(), bson.M{"name": "Gopher Tenant 1"}, bson.M{"$inc": {"amount": -amount}}) +tenant2Model.UpdateMany(context.TODO(), bson.M{"name": "Gopher Tenant 2"}, bson.M{"$inc": {"amount": amount}}) +``` + +:::note +The `EntityMongoModel` is always bound to the specified database at the time of its declaration and, as such, cannot be used to perform operations across multiple databases. +::: + +```go +amount := 10000 + +result, _ := tenant1Model.FindOne(context.TODO(), bson.M{"name": "Gopher Tenant 2"}) +// result will be value in this case +``` From a24a590638650966d25293a4a6b50804c9d24ea1 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 14:21:38 +0530 Subject: [PATCH 06/13] chore: add ci workflow to test and lint --- .github/workflows/test-module.yml | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/test-module.yml diff --git a/.github/workflows/test-module.yml b/.github/workflows/test-module.yml new file mode 100644 index 0000000..3d486f6 --- /dev/null +++ b/.github/workflows/test-module.yml @@ -0,0 +1,65 @@ +name: Lint & Test + +on: [push, pull_request] + +jobs: + lint-module: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: 1.18.x + + - name: Setup cache + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go-version }}- + + - name: Run linter + uses: golangci/golangci-lint-action@v3 + with: + version: v1.54 + + test-module: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.18', '1.19', '1.20', '1.21'] + mongodb-version: ['3.6', '4.0', '5.0', '6.0', '7.0'] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + + - name: Start MongoDB Replica Set + uses: supercharge/mongodb-github-action@v1.10.0 + with: + mongodb-version: ${{ matrix.mongodb-version }} + mongodb-replica-set: replset + mongodb-db: mgoddb + + - name: Setup cache + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go-version }}- + + - name: Run tests + run: go test ./... \ No newline at end of file From 5024438db2ce0577ea84d2c823b1f1bc51fd07ae Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 15:16:29 +0530 Subject: [PATCH 07/13] fix: remove slog usage --- bsondoc/build_bson_doc.go | 29 +++++++++++++++------------- errors/errors.go | 10 ++++++++++ schema/entity_model_schema_cache.go | 5 +++-- schema/fieldopt/default_value_opt.go | 7 ++++++- transaction.go | 2 -- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/bsondoc/build_bson_doc.go b/bsondoc/build_bson_doc.go index d1da554..5926217 100644 --- a/bsondoc/build_bson_doc.go +++ b/bsondoc/build_bson_doc.go @@ -4,7 +4,6 @@ package bsondoc import ( "context" "fmt" - "log/slog" "github.com/Lyearn/mgod/errors" "github.com/Lyearn/mgod/schema" @@ -33,7 +32,6 @@ func Build( } if bsonDoc == nil && len(entityModelSchema.Root.Children) != 0 { - slog.ErrorContext(ctx, "BSON doc is nil but entity model schema is not empty") return errors.NewBadRequestError(errors.BadRequestError{ Underlying: "bson doc", Got: "nil", @@ -64,7 +62,7 @@ func build( return nil } - schemaNode, err := getSchemaNodeForPath(ctx, parent, schemaNodes, translateTo) + schemaNode, err := getSchemaNodeForPath(parent, schemaNodes, translateTo) if err != nil { return err } else if schemaNode == nil { @@ -100,7 +98,7 @@ func build( uniqVisitedSchemaNodes := lo.Uniq(visitedSchemaNodes) if len(uniqVisitedSchemaNodes) != len(immediateChildren) { - err := addMissingNodes(ctx, bsonElem, immediateChildren, uniqVisitedSchemaNodes, schemaNodes, translateTo) + err := addMissingNodes(bsonElem, immediateChildren, uniqVisitedSchemaNodes, schemaNodes, translateTo) if err != nil { return err } @@ -157,7 +155,10 @@ func build( case TranslateToEnumEntityModel: modifiedBSONNodeVal, err = transformer.TransformForEntityModelDoc(elemVal) default: - err = fmt.Errorf("unknown translateTo enum value %s", translateTo) + err = errors.NewBadRequestError(errors.BadRequestError{ + Underlying: "translateTo enum", + Got: string(translateTo), + }) } if err != nil { @@ -220,7 +221,6 @@ func getConvertedValueForNode( // addMissingNodes appends missing nodes in bson doc which have default value. func addMissingNodes( - ctx context.Context, bsonElem *bson.D, immediateChildren []string, uniqVisitedSchemaNodes []string, @@ -229,7 +229,7 @@ func addMissingNodes( ) error { missingSchemaPaths, _ := lo.Difference(immediateChildren, uniqVisitedSchemaNodes) for _, missingSchemaPath := range missingSchemaPaths { - missingSchemaNode, err := getSchemaNodeForPath(ctx, missingSchemaPath, schemaNodes, translateTo) + missingSchemaNode, err := getSchemaNodeForPath(missingSchemaPath, schemaNodes, translateTo) if err != nil { return err } else if missingSchemaNode == nil { @@ -245,7 +245,11 @@ func addMissingNodes( // throw error if schema node is not _id field (special field) and is required but has no default value. if !isIDField && missingSchemaNode.Props.Options.Default == nil { - return fmt.Errorf("required field at path %s is missing in bson doc", missingSchemaPath) + return errors.NewBadRequestError(errors.BadRequestError{ + Underlying: "bson doc", + Got: "nil", + Expected: fmt.Sprintf("field at path - %s", missingSchemaPath), + }) } var bsonNodeToAppend bson.E @@ -278,7 +282,6 @@ func addMissingNodes( } func getSchemaNodeForPath( - ctx context.Context, path string, schemaNodes map[string]*schema.TreeNode, translateTo TranslateToEnum, @@ -291,10 +294,10 @@ func getSchemaNodeForPath( return nil, nil } - slog.ErrorContext(ctx, fmt.Sprintf( - "schema doesn't contains any node at path %s found in bsonDoc", path)) - - return nil, fmt.Errorf("unknown path %s found in bson doc", path) + return nil, errors.NewNotFoundError(errors.NotFoundError{ + Underlying: "bson doc", + Value: fmt.Sprintf("path - %s", path), + }) } return schemaNode, nil diff --git a/errors/errors.go b/errors/errors.go index cea1f4f..97cdca2 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -19,6 +19,16 @@ func NewBadRequestError(e BadRequestError) Error { return Error(fmt.Sprintf("%s: expected %s, got %s", e.Underlying, e.Expected, e.Got)) } +type NotFoundError struct { + Value string + Underlying string +} + +func NewNotFoundError(e NotFoundError) Error { + return Error(fmt.Sprintf("%s not found for %s", e.Value, e.Underlying)) +} + const ( ErrNoDatabaseConnection = Error("no database connection") + ErrSchemaNotCached = Error("schema not cached") ) diff --git a/schema/entity_model_schema_cache.go b/schema/entity_model_schema_cache.go index 43768ab..8afd2a7 100644 --- a/schema/entity_model_schema_cache.go +++ b/schema/entity_model_schema_cache.go @@ -1,8 +1,9 @@ package schema import ( - "fmt" "sync" + + "github.com/Lyearn/mgod/errors" ) // EntityModelSchemaCache is the cache implementation than can hold [EntityModelSchema]. @@ -31,7 +32,7 @@ func (c *entityModelSchemaCache) GetSchema(schemaName string) (*EntityModelSchem return schema, nil } - return nil, fmt.Errorf("%s schema not found in cache", schemaName) + return nil, errors.ErrSchemaNotCached } func (c *entityModelSchemaCache) SetSchema(schemaName string, schema *EntityModelSchema) { diff --git a/schema/fieldopt/default_value_opt.go b/schema/fieldopt/default_value_opt.go index 73f2766..1f3aa33 100644 --- a/schema/fieldopt/default_value_opt.go +++ b/schema/fieldopt/default_value_opt.go @@ -5,6 +5,7 @@ import ( "reflect" "strconv" + "github.com/Lyearn/mgod/errors" "go.mongodb.org/mongo-driver/bson" ) @@ -73,6 +74,10 @@ func (o defaultValueOption) GetValue(field reflect.StructField) (interface{}, er return bson.A{}, nil default: - return nil, fmt.Errorf("unsupported type %v", fieldType) + return nil, errors.NewBadRequestError(errors.BadRequestError{ + Underlying: "default value option", + Got: fmt.Sprintf("%v", fieldType), + Expected: "string, int, float32, float64, bool, slice or array", + }) } } diff --git a/transaction.go b/transaction.go index bd5c248..812bc61 100644 --- a/transaction.go +++ b/transaction.go @@ -2,7 +2,6 @@ package mgod import ( "context" - "log/slog" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -18,7 +17,6 @@ type TransactionFunc func(sc mongo.SessionContext) (interface{}, error) func WithTransaction(ctx context.Context, transactionFunc TransactionFunc) (interface{}, error) { session, err := mClient.StartSession() if err != nil { - slog.ErrorContext(ctx, "Error occurred during WithTransaction", err) return nil, err } defer session.EndSession(ctx) From 80ca7a5b089405c24e00e89d67ce4abd0f3cc3d6 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 15:24:02 +0530 Subject: [PATCH 08/13] chore: bump monogdb requirement from 3.6 -> 4.4 --- .github/workflows/test-module.yml | 2 +- README.md | 2 +- docs/installation.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-module.yml b/.github/workflows/test-module.yml index 3d486f6..b7cf77e 100644 --- a/.github/workflows/test-module.yml +++ b/.github/workflows/test-module.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: go-version: ['1.18', '1.19', '1.20', '1.21'] - mongodb-version: ['3.6', '4.0', '5.0', '6.0', '7.0'] + mongodb-version: ['4.4', '5.0', '6.0', '7.0'] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/README.md b/README.md index 5b2230b..075714f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ ## Requirements - Go 1.18 or higher. -- MongoDB 3.6 and higher. +- MongoDB 4.4 and higher. ## Installation ``` diff --git a/docs/installation.md b/docs/installation.md index fcbc528..2e51178 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,7 +5,7 @@ title: Installation ## Requirements - Go 1.18 or higher -- MongoDB 3.6 and higher +- MongoDB 4.4 and higher ## Installation From b3592a96f7872451eb9348dfb0d1e177a0fdabce Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Thu, 21 Dec 2023 12:48:53 +0530 Subject: [PATCH 09/13] fix: add support from mongodb 3.6 with warning note --- .github/workflows/test-module.yml | 2 +- README.md | 6 +++++- docs/installation.md | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-module.yml b/.github/workflows/test-module.yml index b7cf77e..8a53729 100644 --- a/.github/workflows/test-module.yml +++ b/.github/workflows/test-module.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: go-version: ['1.18', '1.19', '1.20', '1.21'] - mongodb-version: ['4.4', '5.0', '6.0', '7.0'] + mongodb-version: ['4.4', '5.0', '6.0', '7.0'] # add 3.6 as well after fixing the tests (collection creation) steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/README.md b/README.md index 075714f..17265ed 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,11 @@ ## Requirements - Go 1.18 or higher. -- MongoDB 4.4 and higher. +- MongoDB 3.6 and higher. + +> [!WARNING] +> For MongoDB version **<4.4**, please create the collection in MongoDB before creating an `EntityMongoModel` using `mgod` for the same. +> Refer to [this MongoDB docs](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. ## Installation ``` diff --git a/docs/installation.md b/docs/installation.md index 2e51178..e097e9b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,7 +5,12 @@ title: Installation ## Requirements - Go 1.18 or higher -- MongoDB 4.4 and higher +- MongoDB 3.6 and higher + +:::warning +For MongoDB version **<4.4**, please create the collection in MongoDB before creating an `EntityMongoModel` using `mgod` for the same. +Refer to [this MongoDB docs](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. +::: ## Installation From 635e354bf7f3f26993d46d21a52ac5be0d81e673 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Thu, 21 Dec 2023 12:50:08 +0530 Subject: [PATCH 10/13] doc: refactor docs -> limitations --- README.md | 2 +- docs/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 17265ed..b8209be 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ > [!WARNING] > For MongoDB version **<4.4**, please create the collection in MongoDB before creating an `EntityMongoModel` using `mgod` for the same. -> Refer to [this MongoDB docs](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. +> Refer to [this MongoDB limitations](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. ## Installation ``` diff --git a/docs/installation.md b/docs/installation.md index e097e9b..63c8cf1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -9,7 +9,7 @@ title: Installation :::warning For MongoDB version **<4.4**, please create the collection in MongoDB before creating an `EntityMongoModel` using `mgod` for the same. -Refer to [this MongoDB docs](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. +Refer to [this MongoDB limitations](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. ::: ## Installation From 80eec7de7297fb22eb61b45b40b1908a88f6485d Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Thu, 21 Dec 2023 12:51:12 +0530 Subject: [PATCH 11/13] fix: remove slog usage --- transaction.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/transaction.go b/transaction.go index bd5c248..812bc61 100644 --- a/transaction.go +++ b/transaction.go @@ -2,7 +2,6 @@ package mgod import ( "context" - "log/slog" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -18,7 +17,6 @@ type TransactionFunc func(sc mongo.SessionContext) (interface{}, error) func WithTransaction(ctx context.Context, transactionFunc TransactionFunc) (interface{}, error) { session, err := mClient.StartSession() if err != nil { - slog.ErrorContext(ctx, "Error occurred during WithTransaction", err) return nil, err } defer session.EndSession(ctx) From 5ae10f20a5e41f1131c1c54781e4e6be19faa51d Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Thu, 21 Dec 2023 12:53:37 +0530 Subject: [PATCH 12/13] style: escape less than in installtion markdown file --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 63c8cf1..fabf9de 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,7 +8,7 @@ title: Installation - MongoDB 3.6 and higher :::warning -For MongoDB version **<4.4**, please create the collection in MongoDB before creating an `EntityMongoModel` using `mgod` for the same. +For MongoDB version **\<4.4**, please create the collection in MongoDB before creating an `EntityMongoModel` using `mgod` for the same. Refer to [this MongoDB limitations](https://www.mongodb.com/docs/manual/reference/limits/#operations) for more information. ::: From 5a41d6f2d69d96e2c43d04d37d90588ba88b2948 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Thu, 21 Dec 2023 13:03:38 +0530 Subject: [PATCH 13/13] chore: limit test workflow trigger for commits against main branch only --- .github/workflows/test-module.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-module.yml b/.github/workflows/test-module.yml index 8a53729..ab154f6 100644 --- a/.github/workflows/test-module.yml +++ b/.github/workflows/test-module.yml @@ -1,6 +1,18 @@ name: Lint & Test -on: [push, pull_request] +on: + pull_request: + branches: + - main + paths-ignore: + - 'docs/**' + - 'website/**' + push: + branches: + - main + paths-ignore: + - 'docs/**' + - 'website/**' jobs: lint-module: