From 51454279216dcb9f91a239a42b63219e8e5bed54 Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Wed, 6 Dec 2023 10:34:14 +0530 Subject: [PATCH] feat: add documentation for mgod module + remove unused code * doc: add documentation for mgod module * fix: documentation links and format * doc: add missing documentation for package fields and functions * doc: add package level comments --- bsondoc/bson_doc_util.go | 1 + bsondoc/build_bson_doc.go | 19 +- dateformatter/date_formatter.go | 4 + entity_mongo_model.go | 182 ++++-------------- entity_mongo_model_test.go | 12 +- entity_mongo_model_util.go | 164 +++++++++++++++- entity_mongo_options.go | 14 +- errors/errors.go | 1 + schema/entity_model_schema.go | 42 ++-- schema/entity_model_schema_cache.go | 6 + schema/entity_model_schema_options.go | 21 -- schema/entity_model_schema_test.go | 16 +- schema/entity_model_schema_utils.go | 3 + schema/fieldopt/default_value_opt.go | 24 ++- schema/fieldopt/field_opt_tags.go | 10 + schema/fieldopt/field_opts.go | 33 +++- schema/fieldopt/required_opt.go | 23 ++- schema/fieldopt/xid_opt.go | 23 ++- schema/metafield/created_at.go | 27 +-- schema/metafield/doc_version.go | 25 +-- schema/metafield/meta_field.go | 23 ++- schema/metafield/meta_field_keys.go | 1 + schema/metafield/updated_at.go | 27 +-- .../metafield/validate_and_add_field_util.go | 3 +- schema/schemaopt/schema_options.go | 3 + schema/transformer/convert_field_utils.go | 5 + schema/transformer/date_transformer.go | 14 +- schema/transformer/id_transformer.go | 14 +- schema/transformer/transformer.go | 17 +- 29 files changed, 438 insertions(+), 319 deletions(-) create mode 100644 schema/fieldopt/field_opt_tags.go diff --git a/bsondoc/bson_doc_util.go b/bsondoc/bson_doc_util.go index 94ffcf2..6748766 100644 --- a/bsondoc/bson_doc_util.go +++ b/bsondoc/bson_doc_util.go @@ -4,6 +4,7 @@ import ( "go.mongodb.org/mongo-driver/bson" ) +// GetFieldValueFromRootDoc returns the value of the provided field (nil if not found) from the root of a bson doc. func GetFieldValueFromRootDoc(doc *bson.D, field string) interface{} { if doc == nil { return nil diff --git a/bsondoc/build_bson_doc.go b/bsondoc/build_bson_doc.go index 45041d3..d1da554 100644 --- a/bsondoc/build_bson_doc.go +++ b/bsondoc/build_bson_doc.go @@ -1,3 +1,4 @@ +// Package bsondoc builds on an existing bson doc according to the provided entity model schema. package bsondoc import ( @@ -12,13 +13,15 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) +// TranslateToEnum is the enum for the type of translation to be done. type TranslateToEnum string const ( - TranslateToEnumMongo TranslateToEnum = "mongo" - TranslateToEnumEntityModel TranslateToEnum = "entity_model" + TranslateToEnumMongo TranslateToEnum = "mongo" // translate to mongo doc + TranslateToEnumEntityModel TranslateToEnum = "entity_model" // translate to entity model ) +// Build builds on the given bson doc based on the provided [schema.EntityModelSchema]. func Build( ctx context.Context, bsonDoc *bson.D, @@ -122,7 +125,7 @@ func build( (*bsonElem)[arrIdx] = convertedValue } - // default case handles all primitive types i.e. all leaf nodes of schema tree or all bson doc + // Default case handles all primitive types i.e. all leaf nodes of schema tree or all bson doc // elements which are not of type bson.D or bson.A. default: // Transformations related logic starts here @@ -138,8 +141,8 @@ func build( var elemVal interface{} if _, ok := bsonDocRef.(*interface{}); !ok { - // this case handles only elements of array which are passed as reference from the above *bson.A case. - // hence, reject any other type. + // This case handles only elements of array which are passed as reference from the above *bson.A case. + // Hence, reject any other type. return nil } else { elemVal = *(bsonDocRef.(*interface{})) @@ -178,14 +181,14 @@ func getConvertedValueForNode( var modifiedVal interface{} var err error - // if nodeVal is nil, then there is no need to do any conversion. + // If nodeVal is nil, then there is no need to do any conversion. if nodeVal == nil { //nolint:nilnil // this is a valid case return nil, nil } - // this switch case provides type support for bson.D and bson.A type of elements. - // without this, *interface{} type of bsonDoc would be passed in the recursive call, + // This switch case provides type support for bson.D and bson.A type of elements. + // Without this, *interface{} type of bsonDoc would be passed in the recursive call, // which will then go to the default case and will not be able to handle any nested type. switch typedValue := nodeVal.(type) { case bson.D: diff --git a/dateformatter/date_formatter.go b/dateformatter/date_formatter.go index 30f3093..01e43bb 100644 --- a/dateformatter/date_formatter.go +++ b/dateformatter/date_formatter.go @@ -1,3 +1,4 @@ +// Package dateformatter provides utilities to get date time in different formats. package dateformatter import ( @@ -7,6 +8,7 @@ import ( ) // DateFormatter provides utilities to get date time in different formats. +// // NOTE: DateFormatter processes the provided date time string in UTC format. type DateFormatter struct { t time.Time @@ -16,6 +18,7 @@ func New(t time.Time) *DateFormatter { return &DateFormatter{t: t.UTC()} } +// GetISOString returns the date time in ISO 8601 format. func (d *DateFormatter) GetISOString() (string, error) { // ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ @@ -47,6 +50,7 @@ func (d *DateFormatter) GetISOString() (string, error) { return "", errors.New("invalid time format") } +// formatMilliSecondsString adds zeros to milliseconds if not present. func formatMilliSecondsString(dateTimeStr string) string { parts := strings.Split(dateTimeStr, ".") partsAfterMillisSplit := 2 diff --git a/entity_mongo_model.go b/entity_mongo_model.go index cfa6d05..92203d4 100644 --- a/entity_mongo_model.go +++ b/entity_mongo_model.go @@ -1,13 +1,12 @@ +// Package mgod implements ODM logic for MongoDB in Go. package mgod import ( "context" "fmt" - "github.com/Lyearn/mgod/bsondoc" "github.com/Lyearn/mgod/errors" "github.com/Lyearn/mgod/schema" - "github.com/Lyearn/mgod/schema/metafield" "github.com/samber/lo" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -15,19 +14,50 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +// EntityMongoModel is a generic interface of available wrapper functions on MongoDB collection. type EntityMongoModel[T any] interface { + // GetDocToInsert returns the bson.D doc to be inserted in the collection for the provided struct object. + // This function is mainly used while creating a doc to be inserted for Union Type models because the underlying type of a union + // type model is interface{}, so it's not possible to identify the underlying concrete type to validate and insert the doc. GetDocToInsert(ctx context.Context, model T) (bson.D, error) + + // InsertOne inserts a single document in the collection. + // Model is kept as interface{} to support Union Type models i.e. accept both bson.D (generated using GetDocToInsert()) and struct object. InsertOne(ctx context.Context, model interface{}, opts ...*options.InsertOneOptions) (T, error) + + // InsertMany inserts multiple documents in the collection. + // Docs is kept as interface{} to support Union Type models i.e. accept both []bson.D (generated using GetDocToInsert()) and []struct objects. InsertMany(ctx context.Context, docs interface{}, opts ...*options.InsertManyOptions) ([]T, error) + + // UpdateMany updates multiple filtered documents in the collection based on the provided update query. UpdateMany(ctx context.Context, filter, update interface{}, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) + + // BulkWrite performs multiple write operations on the collection at once. + // Currently, only InsertOne, UpdateOne, and UpdateMany operations are supported. BulkWrite(ctx context.Context, bulkWrites []mongo.WriteModel, opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, error) + + // Find returns all documents in the collection matching the provided filter. Find(ctx context.Context, filter interface{}, opts ...*options.FindOptions) ([]T, error) + + // FindOne returns a single document from the collection matching the provided filter. FindOne(ctx context.Context, filter interface{}, opts ...*options.FindOneOptions) (*T, error) + + // FindOneAndUpdate returns a single document from the collection based on the provided filter and updates it. FindOneAndUpdate(ctx context.Context, filter, update interface{}, opts ...*options.FindOneAndUpdateOptions) (T, error) + + // DeleteOne deletes a single document in the collection based on the provided filter. DeleteOne(ctx context.Context, filter interface{}, opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) + + // DeleteMany deletes multiple documents in the collection based on the provided filter. DeleteMany(ctx context.Context, filter interface{}, opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) + + // CountDocuments returns the number of documents in the collection for the provided filter. CountDocuments(ctx context.Context, filter interface{}, opts ...*options.CountOptions) (int64, error) + + // Distinct returns the distinct values for the provided field name in the collection for the provided filter. Distinct(ctx context.Context, fieldName string, filter interface{}, opts ...*options.DistinctOptions) ([]interface{}, error) + + // Aggregate performs an aggregation operation on the collection and returns the results. Aggregate(ctx context.Context, pipeline interface{}, opts ...*options.AggregateOptions) ([]bson.D, error) } @@ -42,6 +72,7 @@ type entityMongoModel[T any] struct { discriminatorKey string } +// NewEntityMongoModel returns a new instance of EntityMongoModel for the provided model type and options. func NewEntityMongoModel[T any](modelType T, opts EntityMongoOptions) (EntityMongoModel[T], error) { dbConnection := opts.dbConnection if dbConnection == nil { @@ -83,153 +114,6 @@ func NewEntityMongoModel[T any](modelType T, opts EntityMongoOptions) (EntityMon }, nil } -func (m entityMongoModel[T]) getEntityModel() T { - return m.modelType -} - -func (m entityMongoModel[T]) getMongoDocFromEntityModel(ctx context.Context, model T) (bson.D, error) { - marshalledDoc, err := bson.Marshal(model) - if err != nil { - return nil, err - } - - var bsonDoc bson.D - err = bson.Unmarshal(marshalledDoc, &bsonDoc) - if err != nil { - return nil, err - } - - if bsonDoc == nil { - // empty bson doc - return bsonDoc, nil - } - - if err = metafield.AddMetaFields(&bsonDoc, m.opts.schemaOptions); err != nil { - return nil, err - } - - err = bsondoc.Build(ctx, &bsonDoc, m.schema, bsondoc.TranslateToEnumMongo) - if err != nil { - return nil, err - } - - if m.isUnionType { - discriminatorVal := bsondoc.GetFieldValueFromRootDoc(&bsonDoc, m.discriminatorKey) - if discriminatorVal == nil { - discriminatorVal = schema.GetSchemaNameForModel(m.modelType) - bsonDoc = append(bsonDoc, primitive.E{ - Key: m.discriminatorKey, - Value: discriminatorVal, - }) - } - - cacheKey := GetSchemaCacheKey(m.coll.Name(), discriminatorVal.(string)) - if _, err := schema.EntityModelSchemaCacheInstance.GetSchema(cacheKey); err != nil { - schema.EntityModelSchemaCacheInstance.SetSchema(cacheKey, m.schema) - } - } - - return bsonDoc, nil -} - -func (m entityMongoModel[T]) getEntityModelFromMongoDoc(ctx context.Context, bsonDoc bson.D) (T, error) { - model := m.getEntityModel() - - if bsonDoc == nil { - // empty bson doc - return model, nil - } - - entityModelSchema := m.schema - - if m.isUnionType { - discriminatorVal := bsondoc.GetFieldValueFromRootDoc(&bsonDoc, m.discriminatorKey) - if discriminatorVal != nil { - cacheKey := GetSchemaCacheKey(m.coll.Name(), discriminatorVal.(string)) - if unionElemSchema, err := schema.EntityModelSchemaCacheInstance.GetSchema(cacheKey); err == nil { - entityModelSchema = unionElemSchema - } - } - } - - err := bsondoc.Build(ctx, &bsonDoc, entityModelSchema, bsondoc.TranslateToEnumEntityModel) - if err != nil { - return model, err - } - - marshalledDoc, err := bson.Marshal(bsonDoc) - if err != nil { - return model, err - } - - err = bson.Unmarshal(marshalledDoc, &model) - if err != nil { - return model, err - } - - return model, nil -} - -func (m entityMongoModel[T]) handleTimestampsForUpdateQuery(update interface{}, funcName string) (interface{}, error) { - updateQuery, ok := update.(bson.D) - if !ok { - return nil, errors.NewBadRequestError(errors.BadRequestError{ - Underlying: "update query", - Got: fmt.Sprintf("%T", update), - Expected: "bson.D", - }) - } - - if m.opts.schemaOptions.Timestamps { - updatedAtCommand := bson.E{ - Key: "$currentDate", - Value: bson.D{{ - Key: "updatedAt", - Value: true, - }}, - } - - updateQuery = append(updateQuery, updatedAtCommand) - } - - return updateQuery, nil -} - -// Converts bulkWrite entity models to mongo models. -func (m entityMongoModel[T]) transformToBulkWriteBSONDocs(ctx context.Context, bulkWrites []mongo.WriteModel) error { - for _, bulkWrite := range bulkWrites { - switch bulkWriteType := bulkWrite.(type) { - case *mongo.InsertOneModel: - doc := bulkWriteType.Document - if doc == nil { - continue - } - - bsonDoc, err := m.getMongoDocFromEntityModel(ctx, doc.(T)) - if err != nil { - return err - } - - bulkWriteType.Document = bsonDoc - case *mongo.UpdateOneModel: - updateQuery, err := m.handleTimestampsForUpdateQuery(bulkWriteType.Update, "BulkWrite") - if err != nil { - return err - } - - bulkWriteType.Update = updateQuery - case *mongo.UpdateManyModel: - updateQuery, err := m.handleTimestampsForUpdateQuery(bulkWriteType.Update, "BulkWrite") - if err != nil { - return err - } - - bulkWriteType.Update = updateQuery - } - } - return nil -} - func (m entityMongoModel[T]) GetDocToInsert(ctx context.Context, doc T) (bson.D, error) { bsonDoc, err := m.getMongoDocFromEntityModel(ctx, doc) if err != nil { diff --git a/entity_mongo_model_test.go b/entity_mongo_model_test.go index 44b8289..e02328e 100644 --- a/entity_mongo_model_test.go +++ b/entity_mongo_model_test.go @@ -97,8 +97,7 @@ func (s *EntityMongoModelSuite) TestFind() { mt.AddMockResponses(first, second, killCursors) - opts := mgod.NewEntityMongoOptions(mt.DB). - SetSchemaOptions(schemaopt.SchemaOptions{Collection: s.collName}) + opts := mgod.NewEntityMongoOptions(mt.DB, schemaopt.SchemaOptions{Collection: s.collName}) entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, *opts) s.Nil(err) @@ -134,8 +133,7 @@ func (s *EntityMongoModelSuite) TestFindOne() { {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, })) - opts := mgod.NewEntityMongoOptions(mt.DB). - SetSchemaOptions(schemaopt.SchemaOptions{Collection: s.collName}) + opts := mgod.NewEntityMongoOptions(mt.DB, schemaopt.SchemaOptions{Collection: s.collName}) entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, *opts) s.Nil(err) @@ -170,8 +168,7 @@ func (s *EntityMongoModelSuite) TestInsertOne() { {Key: "age", Value: 18}, })) - opts := mgod.NewEntityMongoOptions(mt.DB). - SetSchemaOptions(schemaopt.SchemaOptions{Collection: s.collName}) + opts := mgod.NewEntityMongoOptions(mt.DB, schemaopt.SchemaOptions{Collection: s.collName}) entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, *opts) s.Nil(err) @@ -196,8 +193,7 @@ func (s *EntityMongoModelSuite) TestInsertOne() { Message: "duplicate key error", })) - opts := mgod.NewEntityMongoOptions(mt.DB). - SetSchemaOptions(schemaopt.SchemaOptions{Collection: s.collName}) + opts := mgod.NewEntityMongoOptions(mt.DB, schemaopt.SchemaOptions{Collection: s.collName}) entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, *opts) s.Nil(err) diff --git a/entity_mongo_model_util.go b/entity_mongo_model_util.go index b7a8191..d664107 100644 --- a/entity_mongo_model_util.go +++ b/entity_mongo_model_util.go @@ -1,6 +1,18 @@ package mgod -import "strings" +import ( + "context" + "fmt" + "strings" + + "github.com/Lyearn/mgod/bsondoc" + "github.com/Lyearn/mgod/errors" + "github.com/Lyearn/mgod/schema" + "github.com/Lyearn/mgod/schema/metafield" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) // GetSchemaCacheKey returns the cache key for the schema of a model. // The cache key is in the format of _. @@ -12,3 +24,153 @@ func GetSchemaCacheKey(collName, modelNameOrVal string) string { return key } + +func (m entityMongoModel[T]) getEntityModel() T { + return m.modelType +} + +// getMongoDocFromEntityModel converts the provided entity model to a bson.D doc. +func (m entityMongoModel[T]) getMongoDocFromEntityModel(ctx context.Context, model T) (bson.D, error) { + marshalledDoc, err := bson.Marshal(model) + if err != nil { + return nil, err + } + + var bsonDoc bson.D + err = bson.Unmarshal(marshalledDoc, &bsonDoc) + if err != nil { + return nil, err + } + + if bsonDoc == nil { + // empty bson doc + return bsonDoc, nil + } + + if err = metafield.AddMetaFields(&bsonDoc, m.opts.schemaOptions); err != nil { + return nil, err + } + + err = bsondoc.Build(ctx, &bsonDoc, m.schema, bsondoc.TranslateToEnumMongo) + if err != nil { + return nil, err + } + + if m.isUnionType { + discriminatorVal := bsondoc.GetFieldValueFromRootDoc(&bsonDoc, m.discriminatorKey) + if discriminatorVal == nil { + discriminatorVal = schema.GetSchemaNameForModel(m.modelType) + bsonDoc = append(bsonDoc, primitive.E{ + Key: m.discriminatorKey, + Value: discriminatorVal, + }) + } + + cacheKey := GetSchemaCacheKey(m.coll.Name(), discriminatorVal.(string)) + if _, err := schema.EntityModelSchemaCacheInstance.GetSchema(cacheKey); err != nil { + schema.EntityModelSchemaCacheInstance.SetSchema(cacheKey, m.schema) + } + } + + return bsonDoc, nil +} + +// getEntityModelFromMongoDoc converts the provided bson.D doc to an entity model. +func (m entityMongoModel[T]) getEntityModelFromMongoDoc(ctx context.Context, bsonDoc bson.D) (T, error) { + model := m.getEntityModel() + + if bsonDoc == nil { + // empty bson doc + return model, nil + } + + entityModelSchema := m.schema + + if m.isUnionType { + discriminatorVal := bsondoc.GetFieldValueFromRootDoc(&bsonDoc, m.discriminatorKey) + if discriminatorVal != nil { + cacheKey := GetSchemaCacheKey(m.coll.Name(), discriminatorVal.(string)) + if unionElemSchema, err := schema.EntityModelSchemaCacheInstance.GetSchema(cacheKey); err == nil { + entityModelSchema = unionElemSchema + } + } + } + + err := bsondoc.Build(ctx, &bsonDoc, entityModelSchema, bsondoc.TranslateToEnumEntityModel) + if err != nil { + return model, err + } + + marshalledDoc, err := bson.Marshal(bsonDoc) + if err != nil { + return model, err + } + + err = bson.Unmarshal(marshalledDoc, &model) + if err != nil { + return model, err + } + + return model, nil +} + +// handleTimestampsForUpdateQuery adds updatedAt field to the update query if the schema options has timestamps enabled. +func (m entityMongoModel[T]) handleTimestampsForUpdateQuery(update interface{}, funcName string) (interface{}, error) { + updateQuery, ok := update.(bson.D) + if !ok { + return nil, errors.NewBadRequestError(errors.BadRequestError{ + Underlying: "update query", + Got: fmt.Sprintf("%T", update), + Expected: "bson.D", + }) + } + + if m.opts.schemaOptions.Timestamps { + updatedAtCommand := bson.E{ + Key: "$currentDate", + Value: bson.D{{ + Key: "updatedAt", + Value: true, + }}, + } + + updateQuery = append(updateQuery, updatedAtCommand) + } + + return updateQuery, nil +} + +// transformToBulkWriteBSONDocs converts bulkWrite entity models to mongo models. +func (m entityMongoModel[T]) transformToBulkWriteBSONDocs(ctx context.Context, bulkWrites []mongo.WriteModel) error { + for _, bulkWrite := range bulkWrites { + switch bulkWriteType := bulkWrite.(type) { + case *mongo.InsertOneModel: + doc := bulkWriteType.Document + if doc == nil { + continue + } + + bsonDoc, err := m.getMongoDocFromEntityModel(ctx, doc.(T)) + if err != nil { + return err + } + + bulkWriteType.Document = bsonDoc + case *mongo.UpdateOneModel: + updateQuery, err := m.handleTimestampsForUpdateQuery(bulkWriteType.Update, "BulkWrite") + if err != nil { + return err + } + + bulkWriteType.Update = updateQuery + case *mongo.UpdateManyModel: + updateQuery, err := m.handleTimestampsForUpdateQuery(bulkWriteType.Update, "BulkWrite") + if err != nil { + return err + } + + bulkWriteType.Update = updateQuery + } + } + return nil +} diff --git a/entity_mongo_options.go b/entity_mongo_options.go index c125741..d9748c9 100644 --- a/entity_mongo_options.go +++ b/entity_mongo_options.go @@ -5,18 +5,16 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) +// EntityMongoOptions is the options to be configured/provided when creating a new [EntityMongoModel]. type EntityMongoOptions struct { - schemaOptions schemaopt.SchemaOptions dbConnection *mongo.Database + schemaOptions schemaopt.SchemaOptions } -func NewEntityMongoOptions(db *mongo.Database) *EntityMongoOptions { +// NewEntityMongoOptions creates a new instance of [EntityMongoOptions]. +func NewEntityMongoOptions(dbConn *mongo.Database, schemaOpts schemaopt.SchemaOptions) *EntityMongoOptions { return &EntityMongoOptions{ - dbConnection: db, + dbConnection: dbConn, + schemaOptions: schemaOpts, } } - -func (o *EntityMongoOptions) SetSchemaOptions(schemaOptions schemaopt.SchemaOptions) *EntityMongoOptions { - o.schemaOptions = schemaOptions - return o -} diff --git a/errors/errors.go b/errors/errors.go index b0a2d16..cea1f4f 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -2,6 +2,7 @@ package errors import "fmt" +// Error is the custom error type. type Error string func (e Error) Error() string { diff --git a/schema/entity_model_schema.go b/schema/entity_model_schema.go index 6b72f57..c773ee0 100644 --- a/schema/entity_model_schema.go +++ b/schema/entity_model_schema.go @@ -1,3 +1,4 @@ +// Package schema provides utilities to build the schema tree for the entity model. package schema import ( @@ -10,32 +11,44 @@ import ( "github.com/samber/lo" ) +// EntityModelSchema holds the schema tree for the entity model. type EntityModelSchema struct { - // root node is a dummy node, it's not a real field in the model. - // actual doc parsing starts from the children of root node. + // Root is the root node of the schema tree. + // Root node is a dummy node, it's not a real field in the model. Actual doc parsing starts from the children of root node. Root TreeNode - // nodes map is used to quickly access the schema tree node by path. + // Nodes is a map used to quickly access the [TreeNode] by path. Nodes map[string]*TreeNode } +// TreeNode is a node in the schema tree. type TreeNode struct { - Path string // path will be used to identify the ancestor chain. used for debugging purposes. - BSONKey string // translated bson key. - Key string // struct key. used for debugging purposes. - Props SchemaFieldProps - // array is used instead of map to preserve the order of fields. - // fields in bson doc should always match with the schema tree order. + // Path is used to identify the ancestor chain. Used for debugging purposes. + Path string + // BSONKey is the translated bson key. + BSONKey string + // Key is the struct field name. Used for debugging purposes. + Key string + // Props contains the field properties. + Props SchemaFieldProps + // Children contains the child nodes. + // Array is used instead of map to preserve the order of fields. Fields in bson doc should always match with the schema tree order. Children []TreeNode } +// SchemaFieldProps are the possible field properties. type SchemaFieldProps struct { - Type reflect.Kind // contains struct field type or the underlying type in case of pointer. - IsPointer bool // will be used to identify pointer type of fields. - Transformers []transformer.Transformer // reference to id, date, etc. transformers - Options fieldopt.SchemaFieldOptions + // Type holds the struct field type or the underlying type in case of pointer. + Type reflect.Kind + // IsPointer is used to identify pointer type of fields. + IsPointer bool + // Transformers are the transformations that needs to be applied on the field while building the bson doc. + Transformers []transformer.Transformer + // Options are the schema options for the field. + Options fieldopt.SchemaFieldOptions } +// BuildSchemaForModel builds the schema tree for the given model. func BuildSchemaForModel[T any](model T, schemaOpts schemaopt.SchemaOptions) (*EntityModelSchema, error) { schemaTree := make([]TreeNode, 0) rootNode := GetDefaultSchemaTreeRootNode() @@ -61,6 +74,7 @@ func BuildSchemaForModel[T any](model T, schemaOpts schemaopt.SchemaOptions) (*E return schema, nil } +// buildSchema is a recursive function that actually builds the schema tree for the given model. func buildSchema[T any](model T, treeRef *[]TreeNode, nodes map[string]*TreeNode, parent string, opts EntityModelSchemaOptions) error { v := reflect.ValueOf(model) @@ -267,7 +281,7 @@ func addMetaFields[T any](model T, schemaOptions schemaopt.SchemaOptions, treeRe rootStructFields := getCurrentLevelBSONFields(v) - for _, metaField := range metafield.AvailableMetaFields { + for _, metaField := range metafield.GetAvailableMetaFields() { if !metaField.IsApplicable(schemaOptions) { continue } diff --git a/schema/entity_model_schema_cache.go b/schema/entity_model_schema_cache.go index f994301..43768ab 100644 --- a/schema/entity_model_schema_cache.go +++ b/schema/entity_model_schema_cache.go @@ -5,6 +5,11 @@ import ( "sync" ) +// EntityModelSchemaCache is the cache implementation than can hold [EntityModelSchema]. +// +// It has the following to main use cases - +// 1. Avoid re-computing the schema for the same entity model. +// 2. Fetch the relevant schema based on the discriminator key in case of union type models to validate the bson doc fetched from MongoDB. type EntityModelSchemaCache interface { GetSchema(schemaName string) (*EntityModelSchema, error) SetSchema(schemaName string, schema *EntityModelSchema) @@ -36,4 +41,5 @@ func (c *entityModelSchemaCache) SetSchema(schemaName string, schema *EntityMode c.cache[schemaName] = schema } +// EntityModelSchemaCacheInstance is the singleton instance of [EntityModelSchemaCache]. var EntityModelSchemaCacheInstance = newEntityModelSchemaCache() diff --git a/schema/entity_model_schema_options.go b/schema/entity_model_schema_options.go index dc1a2fd..475bb4a 100644 --- a/schema/entity_model_schema_options.go +++ b/schema/entity_model_schema_options.go @@ -24,24 +24,3 @@ func (o *EntityModelSchemaOptions) SetParentBSONFields(parentBSONFields []string o.parentBSONFields = parentBSONFields return o } - -type buildSchemaForModelOptions struct { - cache bool - schemaName string -} - -func NewBuildSchemaForModelOptions() *buildSchemaForModelOptions { - return &buildSchemaForModelOptions{ - cache: true, - } -} - -func (o *buildSchemaForModelOptions) SetCache(cache bool) *buildSchemaForModelOptions { - o.cache = cache - return o -} - -func (o *buildSchemaForModelOptions) SetSchemaName(schemaName string) *buildSchemaForModelOptions { - o.schemaName = schemaName - return o -} diff --git a/schema/entity_model_schema_test.go b/schema/entity_model_schema_test.go index 65d6764..2b51ea3 100644 --- a/schema/entity_model_schema_test.go +++ b/schema/entity_model_schema_test.go @@ -82,7 +82,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Key: "ID", Props: schema.SchemaFieldProps{ Type: reflect.String, - Transformers: []transformer.Transformer{transformer.IDTransformerInstance}, + Transformers: []transformer.Transformer{transformer.IDTransformer}, Options: fieldopt.SchemaFieldOptions{ Required: true, }, @@ -134,7 +134,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Key: "JoinedOn", Props: schema.SchemaFieldProps{ Type: reflect.String, - Transformers: []transformer.Transformer{transformer.DateTransformerInstance}, + Transformers: []transformer.Transformer{transformer.DateTransformer}, Options: fieldopt.SchemaFieldOptions{ Required: true, }, @@ -158,7 +158,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Key: "$", // to identify slice element Props: schema.SchemaFieldProps{ Type: reflect.String, - Transformers: []transformer.Transformer{transformer.IDTransformerInstance}, + Transformers: []transformer.Transformer{transformer.IDTransformer}, }, }, }, @@ -191,7 +191,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Key: "ProjectID", Props: schema.SchemaFieldProps{ Type: reflect.String, - Transformers: []transformer.Transformer{transformer.IDTransformerInstance}, + Transformers: []transformer.Transformer{transformer.IDTransformer}, Options: fieldopt.SchemaFieldOptions{ Required: true, }, @@ -203,7 +203,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Key: "CompletedAt", Props: schema.SchemaFieldProps{ Type: reflect.String, - Transformers: []transformer.Transformer{transformer.DateTransformerInstance}, + Transformers: []transformer.Transformer{transformer.DateTransformer}, Options: fieldopt.SchemaFieldOptions{ Required: true, }, @@ -219,7 +219,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Key: "XID", Props: schema.SchemaFieldProps{ Type: reflect.String, - Transformers: []transformer.Transformer{transformer.IDTransformerInstance}, + Transformers: []transformer.Transformer{transformer.IDTransformer}, Options: fieldopt.SchemaFieldOptions{ Required: true, }, @@ -298,7 +298,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Props: schema.SchemaFieldProps{ Type: reflect.String, Options: fieldopt.SchemaFieldOptions{Required: false}, - Transformers: []transformer.Transformer{transformer.DateTransformerInstance}, + Transformers: []transformer.Transformer{transformer.DateTransformer}, }, }) @@ -309,7 +309,7 @@ func (s *EntityModelSchemaSuite) TestBuildSchemaForModel() { Props: schema.SchemaFieldProps{ Type: reflect.String, Options: fieldopt.SchemaFieldOptions{Required: false}, - Transformers: []transformer.Transformer{transformer.DateTransformerInstance}, + Transformers: []transformer.Transformer{transformer.DateTransformer}, }, }) diff --git a/schema/entity_model_schema_utils.go b/schema/entity_model_schema_utils.go index 808e524..ab7b940 100644 --- a/schema/entity_model_schema_utils.go +++ b/schema/entity_model_schema_utils.go @@ -6,10 +6,12 @@ import ( "github.com/Lyearn/mgod/schema/fieldopt" ) +// GetSchemaNameForModel returns the default schema name for the model. func GetSchemaNameForModel[T any](model T) string { return reflect.TypeOf(model).Name() } +// GetDefaultSchemaTreeRootNode returns the default root node of a schema tree. func GetDefaultSchemaTreeRootNode() TreeNode { rootNode := TreeNode{ Path: "$root", @@ -27,6 +29,7 @@ func GetDefaultSchemaTreeRootNode() TreeNode { return rootNode } +// GetPathForField returns the schema tree path for the field. func GetPathForField(field, parent string) string { path := field if parent != "" { diff --git a/schema/fieldopt/default_value_opt.go b/schema/fieldopt/default_value_opt.go index 4746643..55a5e06 100644 --- a/schema/fieldopt/default_value_opt.go +++ b/schema/fieldopt/default_value_opt.go @@ -8,21 +8,27 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -type DefaultValueOption struct{} +type defaultValueOption struct{} func newDefaultValueOption() FieldOption { - return &DefaultValueOption{} + return &defaultValueOption{} } -func (o DefaultValueOption) GetOptName() string { +// DefaultValueOption provides the default value for a field. +// This value of this option is used when the field is not present in the input document. +// This option is applicable only for fields that are not of type struct. +// Defaults to nil for all fields. +var DefaultValueOption = newDefaultValueOption() + +func (o defaultValueOption) GetOptName() string { return "Default" } -func (o DefaultValueOption) GetBSONTagName() string { - return "mgoDefault" +func (o defaultValueOption) GetBSONTagName() string { + return string(FieldOptionTagDefault) } -func (o DefaultValueOption) IsApplicable(field reflect.StructField) bool { +func (o defaultValueOption) IsApplicable(field reflect.StructField) bool { // not available on struct fields if field.Type.Kind() == reflect.Struct { return false @@ -34,11 +40,11 @@ func (o DefaultValueOption) IsApplicable(field reflect.StructField) bool { return tagVal != "" } -func (o DefaultValueOption) GetDefaultValue(field reflect.StructField) interface{} { +func (o defaultValueOption) GetDefaultValue(field reflect.StructField) interface{} { return nil } -func (o DefaultValueOption) GetValue(field reflect.StructField) (interface{}, error) { +func (o defaultValueOption) GetValue(field reflect.StructField) (interface{}, error) { tagVal := field.Tag.Get(o.GetBSONTagName()) fieldType := field.Type.Kind() @@ -70,5 +76,3 @@ func (o DefaultValueOption) GetValue(field reflect.StructField) (interface{}, er return nil, fmt.Errorf("unsupported type %v", fieldType) } } - -var defaultValueOptionInstance = newDefaultValueOption() diff --git a/schema/fieldopt/field_opt_tags.go b/schema/fieldopt/field_opt_tags.go new file mode 100644 index 0000000..4823965 --- /dev/null +++ b/schema/fieldopt/field_opt_tags.go @@ -0,0 +1,10 @@ +package fieldopt + +// FieldOptionTag is the BSON tag by which the properties of a field option is configured. +type FieldOptionTag string + +const ( + FieldOptionTagRequired FieldOptionTag = "bson" + FieldOptionTagXID FieldOptionTag = "mgoID" + FieldOptionTagDefault FieldOptionTag = "mgoDefault" +) diff --git a/schema/fieldopt/field_opts.go b/schema/fieldopt/field_opts.go index 7ba20cd..849eabd 100644 --- a/schema/fieldopt/field_opts.go +++ b/schema/fieldopt/field_opts.go @@ -1,3 +1,4 @@ +// Package fieldopt provides custom options for schema fields. package fieldopt import ( @@ -6,34 +7,48 @@ import ( "github.com/samber/lo" ) +// FieldOption is the interface that needs to be implemented by all [SchemaFieldOptions]. type FieldOption interface { // GetOptName returns the name of the schema option. This name is used to identify the unique option. - // NOTE: Make sure to return the same name as the name of the struct field. + // NOTE: Make sure to return the same name as the name of the field in [SchemaFieldOptions] struct. GetOptName() string - // bson tag name for the option. This is used to identify the option and its flags in the bson tag. + // GetBSONTagName returns the bson tag name for the option. This is used to identify the option and its flags in the bson tag. GetBSONTagName() string + // IsApplicable returns true if the option is applicable for the field. IsApplicable(field reflect.StructField) bool + // GetDefaultValue returns the default value for the field (if available). GetDefaultValue(field reflect.StructField) interface{} + // GetValue returns the provided value for the field. GetValue(field reflect.StructField) (interface{}, error) } +// SchemaFieldOptions are custom schema options available for struct fields. +// These options either modifies the schema based on the field or adds validations to the field. type SchemaFieldOptions struct { - Required bool // defaults to true. can be identified using omitempty bson flag. [FIELD_LEVEL] - XID bool // defaults to true wherever applicable. [STRUCT_LEVEL] - Default interface{} // will be populated using reflect. will be of same type as Type in SchemaFieldProps. [FIELD_LEVEL] - Select bool + // Required suggests whether the field is required or not. [FIELD_LEVEL] + // Defaults to true. Can be identified using omitempty bson flag. + Required bool + // XID suggests whether "_id" field needs to be added in the bson doc for the following object type field. [STRUCT_LEVEL] + // Defaults to true. This option is applicable for fields holding structs only. + XID bool + // Default is the default value for the field. [FIELD_LEVEL] + // Defaults to nil. Will be populated using reflect and will be of the same type as Type in SchemaFieldProps. + Default interface{} + // not implemented yet + Select bool } var availableSchemaOptions = []FieldOption{ - requiredOptionInstance, - xidOptionInstance, - defaultValueOptionInstance, + RequiredOption, + XIDOption, + DefaultValueOption, } var optNameToSchemaOptionMap = lo.KeyBy(availableSchemaOptions, func(opt FieldOption) string { return opt.GetOptName() }) +// GetSchemaOptionsForField returns all the applicable schema field options for the provided field. func GetSchemaOptionsForField(field reflect.StructField) (SchemaFieldOptions, error) { options := SchemaFieldOptions{} diff --git a/schema/fieldopt/required_opt.go b/schema/fieldopt/required_opt.go index 0d19916..1d195aa 100644 --- a/schema/fieldopt/required_opt.go +++ b/schema/fieldopt/required_opt.go @@ -7,29 +7,34 @@ import ( "github.com/samber/lo" ) -type RequiredOption struct{} +type requiredOption struct{} func newRequiredOption() FieldOption { - return &RequiredOption{} + return &requiredOption{} } -func (o RequiredOption) GetOptName() string { +// RequiredOption defines if a field is required or not. +// The option can be invalidated using `omitempty` property of `bson` tag. +// Defaults to true for all fields. +var RequiredOption = newRequiredOption() + +func (o requiredOption) GetOptName() string { return "Required" } -func (o RequiredOption) GetBSONTagName() string { - return "bson" +func (o requiredOption) GetBSONTagName() string { + return string(FieldOptionTagRequired) } -func (o RequiredOption) IsApplicable(field reflect.StructField) bool { +func (o requiredOption) IsApplicable(field reflect.StructField) bool { return true } -func (o RequiredOption) GetDefaultValue(field reflect.StructField) interface{} { +func (o requiredOption) GetDefaultValue(field reflect.StructField) interface{} { return true } -func (o RequiredOption) GetValue(field reflect.StructField) (interface{}, error) { +func (o requiredOption) GetValue(field reflect.StructField) (interface{}, error) { tagVal := field.Tag.Get(o.GetBSONTagName()) splittedTagValues := strings.Split(tagVal, ",") @@ -40,5 +45,3 @@ func (o RequiredOption) GetValue(field reflect.StructField) (interface{}, error) // if omitempty is found => required is false and vice versa return !found, nil } - -var requiredOptionInstance = newRequiredOption() diff --git a/schema/fieldopt/xid_opt.go b/schema/fieldopt/xid_opt.go index 69c43cd..a3b7740 100644 --- a/schema/fieldopt/xid_opt.go +++ b/schema/fieldopt/xid_opt.go @@ -2,25 +2,30 @@ package fieldopt import "reflect" -type XIDOption struct{} +type xidOption struct{} func newXIDOption() FieldOption { - return &XIDOption{} + return &xidOption{} } -func (o XIDOption) GetOptName() string { +// XIDOption defines if `_id` field needs to be added in a object. +// This option is applicable for fields holding structs only. +// Defaults to true for struct fields. +var XIDOption = newXIDOption() + +func (o xidOption) GetOptName() string { return "XID" } -func (o XIDOption) GetBSONTagName() string { - return "mgoID" +func (o xidOption) GetBSONTagName() string { + return string(FieldOptionTagXID) } -func (o XIDOption) IsApplicable(field reflect.StructField) bool { +func (o xidOption) IsApplicable(field reflect.StructField) bool { return field.Type.Kind() == reflect.Struct } -func (o XIDOption) GetDefaultValue(field reflect.StructField) interface{} { +func (o xidOption) GetDefaultValue(field reflect.StructField) interface{} { // if the field is not applicable, then the default value should be false defaultValue := true @@ -31,7 +36,7 @@ func (o XIDOption) GetDefaultValue(field reflect.StructField) interface{} { return defaultValue } -func (o XIDOption) GetValue(field reflect.StructField) (interface{}, error) { +func (o xidOption) GetValue(field reflect.StructField) (interface{}, error) { tagVal := field.Tag.Get(o.GetBSONTagName()) isXIDRequired := true @@ -41,5 +46,3 @@ func (o XIDOption) GetValue(field reflect.StructField) (interface{}, error) { return isXIDRequired, nil } - -var xidOptionInstance = newXIDOption() diff --git a/schema/metafield/created_at.go b/schema/metafield/created_at.go index c63c3b1..fb2c063 100644 --- a/schema/metafield/created_at.go +++ b/schema/metafield/created_at.go @@ -10,31 +10,34 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -type CreatedAtMetaField struct{} +type createdAtMetaField struct{} func newCreatedAtMetaField() MetaField { - return &CreatedAtMetaField{} + return &createdAtMetaField{} } -var createdAtMetaFieldInstance = newCreatedAtMetaField() +// CreatedAtField is the meta field that stores the timestamp of the document creation. +// This field is automatically added (if not present in the input) to the schema if the [schemaopt.SchemaOptions.Timestamps] is set to true. +// The value of this field is set to the current timestamp in ISO format. +var CreatedAtField = newCreatedAtMetaField() -func (m CreatedAtMetaField) GetKey() MetaFieldKey { +func (m createdAtMetaField) GetKey() MetaFieldKey { return MetaFieldKeyCreatedAt } -func (m CreatedAtMetaField) GetReflectKind() reflect.Kind { +func (m createdAtMetaField) GetReflectKind() reflect.Kind { return reflect.String } -func (m CreatedAtMetaField) GetApplicableTransformers() []transformer.Transformer { - return []transformer.Transformer{transformer.DateTransformerInstance} +func (m createdAtMetaField) GetApplicableTransformers() []transformer.Transformer { + return []transformer.Transformer{transformer.DateTransformer} } -func (m CreatedAtMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) bool { +func (m createdAtMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) bool { return schemaOptions.Timestamps } -func (m CreatedAtMetaField) CheckIfValidValue(val interface{}) bool { +func (m createdAtMetaField) CheckIfValidValue(val interface{}) bool { if val, ok := val.(string); ok && val != "" { return true } @@ -42,11 +45,11 @@ func (m CreatedAtMetaField) CheckIfValidValue(val interface{}) bool { return false } -func (m CreatedAtMetaField) FieldAlreadyPresent(doc *bson.D, index int) { +func (m createdAtMetaField) FieldAlreadyPresent(doc *bson.D, index int) { // do nothing. } -func (m CreatedAtMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) error { +func (m createdAtMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) error { isoString, err := dateformatter.New(time.Now().UTC()).GetISOString() if err != nil { return err @@ -57,7 +60,7 @@ func (m CreatedAtMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) return nil } -func (m CreatedAtMetaField) FieldNotPresent(doc *bson.D) { +func (m createdAtMetaField) FieldNotPresent(doc *bson.D) { isoString, _ := dateformatter.New(time.Now().UTC()).GetISOString() *doc = append(*doc, bson.E{ Key: string(m.GetKey()), diff --git a/schema/metafield/doc_version.go b/schema/metafield/doc_version.go index 7b52419..489ea75 100644 --- a/schema/metafield/doc_version.go +++ b/schema/metafield/doc_version.go @@ -8,23 +8,26 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -type DocVersionMetaField struct{} +type docVersionMetaField struct{} func newDocVersionMetaField() MetaField { - return &DocVersionMetaField{} + return &docVersionMetaField{} } -var docVersionMetaFieldInstance = newDocVersionMetaField() +// DocVersionField is the meta field that stores the version of the document. +// This field is automatically added (if not present in the input) to the schema if the [schemaopt.SchemaOptions.VersionKey] is set to true. +// This field starts with a default value of 0. +var DocVersionField = newDocVersionMetaField() -func (m DocVersionMetaField) GetKey() MetaFieldKey { +func (m docVersionMetaField) GetKey() MetaFieldKey { return MetaFieldKeyDocVersion } -func (m DocVersionMetaField) GetReflectKind() reflect.Kind { +func (m docVersionMetaField) GetReflectKind() reflect.Kind { return reflect.Int } -func (m DocVersionMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) bool { +func (m docVersionMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) bool { if schemaOptions.VersionKey == nil { // doc versioning is enabled by default. return true @@ -33,27 +36,27 @@ func (m DocVersionMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) return *schemaOptions.VersionKey } -func (m DocVersionMetaField) GetApplicableTransformers() []transformer.Transformer { +func (m docVersionMetaField) GetApplicableTransformers() []transformer.Transformer { return []transformer.Transformer{} } -func (m DocVersionMetaField) CheckIfValidValue(val interface{}) bool { +func (m docVersionMetaField) CheckIfValidValue(val interface{}) bool { _, ok := val.(int) return ok } -func (m DocVersionMetaField) FieldAlreadyPresent(doc *bson.D, index int) { +func (m docVersionMetaField) FieldAlreadyPresent(doc *bson.D, index int) { // field is already present. hence, incrementing the value. (*doc)[index].Value = (*doc)[index].Value.(int) + 1 } -func (m DocVersionMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) error { +func (m docVersionMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) error { (*doc)[index].Value = 0 return nil } -func (m DocVersionMetaField) FieldNotPresent(doc *bson.D) { +func (m docVersionMetaField) FieldNotPresent(doc *bson.D) { *doc = append(*doc, bson.E{ Key: string(m.GetKey()), Value: 0, diff --git a/schema/metafield/meta_field.go b/schema/metafield/meta_field.go index 84eb911..e599771 100644 --- a/schema/metafield/meta_field.go +++ b/schema/metafield/meta_field.go @@ -1,3 +1,6 @@ +// Package metafield defines and provide functions on custom meta fields for the schema. +// +// Meta fields are those fields that tracks extra information about the document which can be helpful to determine the state of a document. package metafield import ( @@ -9,8 +12,10 @@ import ( ) type MetaField interface { + // GetKey returns the unique key of the meta field. GetKey() MetaFieldKey + // GetReflectKind returns the reflect kind of the meta field. GetReflectKind() reflect.Kind // GetApplicableTransformers returns the list of transformers applicable for the meta field. @@ -36,19 +41,25 @@ type MetaField interface { FieldNotPresent(doc *bson.D) } -var AvailableMetaFields = []MetaField{ - createdAtMetaFieldInstance, - updatedAtMetaFieldInstance, - docVersionMetaFieldInstance, +var availableMetaFields = []MetaField{ + CreatedAtField, + UpdatedAtField, + DocVersionField, } +// GetAvailableMetaFields returns the list of available meta fields. +func GetAvailableMetaFields() []MetaField { + return availableMetaFields +} + +// AddMetaFields adds all applicable meta fields to the bson doc based on the provided schema options. func AddMetaFields(bsonDoc *bson.D, schemaOptions schemaopt.SchemaOptions) error { - for _, metaField := range AvailableMetaFields { + for _, metaField := range availableMetaFields { if !metaField.IsApplicable(schemaOptions) { continue } - if err := ValidatedAndAddFieldValue(bsonDoc, metaField); err != nil { + if err := validatedAndAddFieldValue(bsonDoc, metaField); err != nil { return err } } diff --git a/schema/metafield/meta_field_keys.go b/schema/metafield/meta_field_keys.go index ae16a81..59f6bf4 100644 --- a/schema/metafield/meta_field_keys.go +++ b/schema/metafield/meta_field_keys.go @@ -1,5 +1,6 @@ package metafield +// MetaFieldKey is the unique field name of a meta field. type MetaFieldKey string const ( diff --git a/schema/metafield/updated_at.go b/schema/metafield/updated_at.go index 42924dd..61149fc 100644 --- a/schema/metafield/updated_at.go +++ b/schema/metafield/updated_at.go @@ -10,31 +10,34 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -type UpdatedAtMetaField struct{} +type updatedAtMetaField struct{} func newUpdatedAtMetaField() MetaField { - return &UpdatedAtMetaField{} + return &updatedAtMetaField{} } -var updatedAtMetaFieldInstance = newUpdatedAtMetaField() +// UpdatedAtField is the meta field that stores the timestamp of the document updation. +// This field is automatically added (if not present in the input) to the schema if the [schemaopt.SchemaOptions.Timestamps] is set to true. +// The value of this field is set to the current timestamp in ISO format and is updated every time the document is updated. +var UpdatedAtField = newUpdatedAtMetaField() -func (m UpdatedAtMetaField) GetKey() MetaFieldKey { +func (m updatedAtMetaField) GetKey() MetaFieldKey { return MetaFieldKeyUpdatedAt } -func (m UpdatedAtMetaField) GetReflectKind() reflect.Kind { +func (m updatedAtMetaField) GetReflectKind() reflect.Kind { return reflect.String } -func (m UpdatedAtMetaField) GetApplicableTransformers() []transformer.Transformer { - return []transformer.Transformer{transformer.DateTransformerInstance} +func (m updatedAtMetaField) GetApplicableTransformers() []transformer.Transformer { + return []transformer.Transformer{transformer.DateTransformer} } -func (m UpdatedAtMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) bool { +func (m updatedAtMetaField) IsApplicable(schemaOptions schemaopt.SchemaOptions) bool { return schemaOptions.Timestamps } -func (m UpdatedAtMetaField) CheckIfValidValue(val interface{}) bool { +func (m updatedAtMetaField) CheckIfValidValue(val interface{}) bool { if val, ok := val.(string); ok && val != "" { return true } @@ -42,13 +45,13 @@ func (m UpdatedAtMetaField) CheckIfValidValue(val interface{}) bool { return false } -func (m UpdatedAtMetaField) FieldAlreadyPresent(doc *bson.D, index int) { +func (m updatedAtMetaField) FieldAlreadyPresent(doc *bson.D, index int) { // field is already present. hence, updating the value. isoString, _ := dateformatter.New(time.Now().UTC()).GetISOString() (*doc)[index].Value = isoString } -func (m UpdatedAtMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) error { +func (m updatedAtMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) error { isoString, err := dateformatter.New(time.Now().UTC()).GetISOString() if err != nil { return err @@ -59,7 +62,7 @@ func (m UpdatedAtMetaField) FieldPresentWithIncorrectVal(doc *bson.D, index int) return nil } -func (m UpdatedAtMetaField) FieldNotPresent(doc *bson.D) { +func (m updatedAtMetaField) FieldNotPresent(doc *bson.D) { isoString, _ := dateformatter.New(time.Now().UTC()).GetISOString() *doc = append(*doc, bson.E{ Key: string(m.GetKey()), diff --git a/schema/metafield/validate_and_add_field_util.go b/schema/metafield/validate_and_add_field_util.go index 2fd13c9..8d5ca49 100644 --- a/schema/metafield/validate_and_add_field_util.go +++ b/schema/metafield/validate_and_add_field_util.go @@ -4,7 +4,8 @@ import ( "go.mongodb.org/mongo-driver/bson" ) -func ValidatedAndAddFieldValue(doc *bson.D, metaField MetaField) error { +// validateAndAddField validates if the provided meta field exists in the bson doc with proper type, else adds it. +func validatedAndAddFieldValue(doc *bson.D, metaField MetaField) error { field := string(metaField.GetKey()) for index, elem := range *doc { diff --git a/schema/schemaopt/schema_options.go b/schema/schemaopt/schema_options.go index edea1f1..110a76c 100644 --- a/schema/schemaopt/schema_options.go +++ b/schema/schemaopt/schema_options.go @@ -1,5 +1,8 @@ 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 diff --git a/schema/transformer/convert_field_utils.go b/schema/transformer/convert_field_utils.go index 5c1c463..01bb776 100644 --- a/schema/transformer/convert_field_utils.go +++ b/schema/transformer/convert_field_utils.go @@ -7,6 +7,7 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) +// convertStringToDateTime converts the provided string dates to primitive.DateTime. func convertStringToDateTime(dates ...string) ([]primitive.DateTime, error) { dateTimes := []primitive.DateTime{} @@ -24,6 +25,7 @@ func convertStringToDateTime(dates ...string) ([]primitive.DateTime, error) { return dateTimes, nil } +// convertStringToObjectID converts the provided string ids to primitive.ObjectID. func convertStringToObjectID(ids ...string) ([]primitive.ObjectID, error) { objectIDS := []primitive.ObjectID{} @@ -39,6 +41,7 @@ func convertStringToObjectID(ids ...string) ([]primitive.ObjectID, error) { return objectIDS, nil } +// convertDateTimeToString converts the provided primitive.DateTime to string. func convertDateTimeToString(dateTimes ...primitive.DateTime) ([]string, error) { dates := []string{} @@ -54,6 +57,8 @@ func convertDateTimeToString(dateTimes ...primitive.DateTime) ([]string, error) return dates, nil } +// convertObjectIDToString converts the provided primitive.ObjectID to string. +// //nolint:unparam // error field added to keep the signature of convert field functions consistent func convertObjectIDToString(objectIDs ...primitive.ObjectID) ([]string, error) { ids := []string{} diff --git a/schema/transformer/date_transformer.go b/schema/transformer/date_transformer.go index b70d4cb..1bedc46 100644 --- a/schema/transformer/date_transformer.go +++ b/schema/transformer/date_transformer.go @@ -6,17 +6,19 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) -type DateTransformer struct{} +type dateTransformer struct{} func newDateTransformer() Transformer { - return &DateTransformer{} + return &dateTransformer{} } -func (t DateTransformer) isTransformationRequired(field reflect.StructField) bool { +var DateTransformer = newDateTransformer() + +func (t dateTransformer) IsTransformationRequired(field reflect.StructField) bool { return field.Tag.Get("mgoType") == "date" } -func (t DateTransformer) TransformForMongoDoc(value interface{}) (interface{}, error) { +func (t dateTransformer) TransformForMongoDoc(value interface{}) (interface{}, error) { primitiveDates, err := convertStringToDateTime(value.(string)) if err != nil { return nil, err @@ -25,7 +27,7 @@ func (t DateTransformer) TransformForMongoDoc(value interface{}) (interface{}, e return primitiveDates[0], nil } -func (t DateTransformer) TransformForEntityModelDoc(value interface{}) (interface{}, error) { +func (t dateTransformer) TransformForEntityModelDoc(value interface{}) (interface{}, error) { dates, err := convertDateTimeToString(value.(primitive.DateTime)) if err != nil { return nil, err @@ -33,5 +35,3 @@ func (t DateTransformer) TransformForEntityModelDoc(value interface{}) (interfac return dates[0], nil } - -var DateTransformerInstance = newDateTransformer() diff --git a/schema/transformer/id_transformer.go b/schema/transformer/id_transformer.go index fb32564..90357c2 100644 --- a/schema/transformer/id_transformer.go +++ b/schema/transformer/id_transformer.go @@ -6,17 +6,19 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) -type IDTransformer struct{} +type idTransformer struct{} func newIDTransformer() Transformer { - return &IDTransformer{} + return &idTransformer{} } -func (t IDTransformer) isTransformationRequired(field reflect.StructField) bool { +var IDTransformer = newIDTransformer() + +func (t idTransformer) IsTransformationRequired(field reflect.StructField) bool { return field.Tag.Get("mgoType") == "id" } -func (t IDTransformer) TransformForMongoDoc(value interface{}) (interface{}, error) { +func (t idTransformer) TransformForMongoDoc(value interface{}) (interface{}, error) { objectIDs, err := convertStringToObjectID(value.(string)) if err != nil { return nil, err @@ -25,7 +27,7 @@ func (t IDTransformer) TransformForMongoDoc(value interface{}) (interface{}, err return objectIDs[0], nil } -func (t IDTransformer) TransformForEntityModelDoc(value interface{}) (interface{}, error) { +func (t idTransformer) TransformForEntityModelDoc(value interface{}) (interface{}, error) { ids, err := convertObjectIDToString(value.(primitive.ObjectID)) if err != nil { return nil, err @@ -33,5 +35,3 @@ func (t IDTransformer) TransformForEntityModelDoc(value interface{}) (interface{ return ids[0], nil } - -var IDTransformerInstance = newIDTransformer() diff --git a/schema/transformer/transformer.go b/schema/transformer/transformer.go index 1086adb..72979a9 100644 --- a/schema/transformer/transformer.go +++ b/schema/transformer/transformer.go @@ -1,26 +1,29 @@ +// Package transformer provides custom transformers for schema fields. package transformer import "reflect" -// transformers can transform fields in both directions i.e. from entity model to mongo doc and vice versa. +// Transformer can transform fields in both directions i.e. from entity model to mongo doc and vice versa. type Transformer interface { - isTransformationRequired(field reflect.StructField) bool - // TransformForMongoDoc transforms the incoming value according to mongo requirements + // IsTransformationRequired reports whether the transformer is required for the given field. + IsTransformationRequired(field reflect.StructField) bool + // TransformForMongoDoc transforms the incoming value according to mongo requirements. TransformForMongoDoc(value interface{}) (interface{}, error) - // TransformForEntityModelDoc transforms the incoming value according to entity model requirements + // TransformForEntityModelDoc transforms the incoming value according to entity model requirements. TransformForEntityModelDoc(value interface{}) (interface{}, error) } var availableTransformers = []Transformer{ - IDTransformerInstance, - DateTransformerInstance, + IDTransformer, + DateTransformer, } +// GetRequiredTransformersForField returns the transformers required for the given field. func GetRequiredTransformersForField(field reflect.StructField) []Transformer { transformers := []Transformer{} for _, transformer := range availableTransformers { - if transformer.isTransformationRequired(field) { + if transformer.IsTransformationRequired(field) { transformers = append(transformers, transformer) } }