diff --git a/cmd/migrate.go b/cmd/migrate.go index 19fe64c..272bf59 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -12,4 +12,4 @@ var migrateCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { fmt.Println("Running database migrations") }, -} \ No newline at end of file +} diff --git a/cmd/root.go b/cmd/root.go index d18fd72..4984cc9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,7 +10,7 @@ import ( var rootCmd = &cobra.Command{ Use: "elemental", Short: "Your next gen database ODM", - Long: `Elemental is a user database ODM that allows you to interact with your database in a much more user friendly way than standard database drivers`, + Long: `Elemental is a user database ODM that allows you to interact with your database in a much more user friendly way than standard database drivers`, Run: func(cmd *cobra.Command, args []string) { fmt.Println(` @@ -29,7 +29,7 @@ If you encounter any issues, please report them at "https://github.com/go-elemen func init() { rootCmd.AddCommand(migrateCmd) rootCmd.AddCommand(seedCmd) - } +} func Execute() { if err := rootCmd.Execute(); err != nil { diff --git a/cmd/seed.go b/cmd/seed.go index 1bc7cc0..a5b3177 100644 --- a/cmd/seed.go +++ b/cmd/seed.go @@ -12,4 +12,4 @@ var seedCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { fmt.Println("Running seeders") }, -} \ No newline at end of file +} diff --git a/connection/connection.go b/connection/connection.go index a171389..50688d2 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -4,14 +4,14 @@ import ( "context" "elemental/constants" "elemental/utils" - "time" - "golang.org/x/exp/maps" "github.com/samber/lo" "go.mongodb.org/mongo-driver/event" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" + "golang.org/x/exp/maps" + "time" ) const connectionTimeout = 30 * time.Second @@ -89,4 +89,4 @@ func Use(database string, alias ...string) *mongo.Database { // @param alias - The alias of the connection to use func UseDefault(alias ...string) *mongo.Database { return lo.ToPtr(clients[e_utils.Coalesce(e_utils.First(alias), "default")]).Database(e_utils.Coalesce(defaultDatabases[e_utils.Coalesce(e_utils.First(alias), "default")], "test")) -} \ No newline at end of file +} diff --git a/constants/errors.go b/constants/errors.go index a37b31e..2fab6c6 100644 --- a/constants/errors.go +++ b/constants/errors.go @@ -1,6 +1,6 @@ package e_constants const ( - ErrURIRequired = "URI is required" + ErrURIRequired = "URI is required" ErrMustPairSortArguments = "Sort arguments must be in pairs" -) \ No newline at end of file +) diff --git a/core/model.go b/core/model.go index a70ece9..149e6ce 100644 --- a/core/model.go +++ b/core/model.go @@ -85,6 +85,13 @@ func (m Model[T]) InsertMany(docs []T) Model[T] { } func (m Model[T]) Find(query ...primitive.M) Model[T] { + m.executor = func(m Model[T], ctx context.Context) any { + var results []T + e_utils.Must(lo.Must(m.Collection().Aggregate(ctx, m.pipeline)).All(ctx, &results)) + m.middleware.post.find.run(results) + m.checkConditionsAndPanic(results) + return results + } m.pipeline = append(m.pipeline, bson.D{{Key: "$match", Value: e_utils.DefaultQuery(query...)}}) return m } diff --git a/core/model_actions.go b/core/model_actions.go index 4c7c69e..65bf60b 100644 --- a/core/model_actions.go +++ b/core/model_actions.go @@ -3,7 +3,7 @@ package elemental import ( "context" "elemental/connection" - "elemental/utils" + "elemental/utils" "reflect" "go.mongodb.org/mongo-driver/mongo" @@ -52,4 +52,4 @@ func (m Model[T]) SetDatabase(database string) Model[T] { func (m Model[T]) SetCollection(collection string) Model[T] { m.temporaryCollection = &collection return m -} \ No newline at end of file +} diff --git a/core/model_middleware.go b/core/model_middleware.go index 32d53e3..64bd057 100644 --- a/core/model_middleware.go +++ b/core/model_middleware.go @@ -3,6 +3,7 @@ package elemental import ( e_utils "elemental/utils" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" ) @@ -13,13 +14,23 @@ type listener[T any] struct { } type pre[T any] struct { - save listener[T] - updateOne listener[T] + save listener[T] + updateOne listener[T] + deleteOne listener[T] + deleteMany listener[T] + findOneAndUpdate listener[T] + findOneAndDelete listener[T] } type post[T any] struct { - save listener[T] - updateOne listener[T] + save listener[T] + updateOne listener[T] + deleteOne listener[T] + deleteMany listener[T] + find listener[T] + findOneAndUpdate listener[T] + findOneAndDelete listener[T] + findOneAndReplace listener[T] } type middleware[T any] struct { @@ -62,4 +73,64 @@ func (m Model[T]) PostUpdateOne(f func(result *mongo.UpdateResult, err error) bo m.middleware.post.updateOne.functions = append(m.middleware.post.updateOne.functions, func(args ...interface{}) bool { return f(args[0].(*mongo.UpdateResult), e_utils.Cast[error](args[1])) }) -} \ No newline at end of file +} + +func (m Model[T]) PreDeleteOne(f func(filters primitive.M) bool) { + m.middleware.pre.deleteOne.functions = append(m.middleware.pre.deleteOne.functions, func(args ...interface{}) bool { + return f(args[0].(primitive.M)) + }) +} + +func (m Model[T]) PostDeleteOne(f func(result *mongo.DeleteResult, err error) bool) { + m.middleware.post.deleteOne.functions = append(m.middleware.post.deleteOne.functions, func(args ...interface{}) bool { + return f(args[0].(*mongo.DeleteResult), e_utils.Cast[error](args[1])) + }) +} + +func (m Model[T]) PreDeleteMany(f func(filters primitive.M) bool) { + m.middleware.pre.deleteMany.functions = append(m.middleware.pre.deleteMany.functions, func(args ...interface{}) bool { + return f(args[0].(primitive.M)) + }) +} + +func (m Model[T]) PostDeleteMany(f func(result *mongo.DeleteResult, err error) bool) { + m.middleware.post.deleteMany.functions = append(m.middleware.post.deleteMany.functions, func(args ...interface{}) bool { + return f(args[0].(*mongo.DeleteResult), e_utils.Cast[error](args[1])) + }) +} + +func (m Model[T]) PostFind(f func(doc []T) bool) { + m.middleware.post.find.functions = append(m.middleware.post.find.functions, func(args ...interface{}) bool { + return f(args[0].([]T)) + }) +} + +func (m Model[T]) PostFindOneAndUpdate(f func(doc *T) bool) { + m.middleware.post.findOneAndUpdate.functions = append(m.middleware.post.findOneAndUpdate.functions, func(args ...interface{}) bool { + return f(args[0].(*T)) + }) +} + +func (m Model[T]) PreFindOneAndUpdate(f func(filters primitive.M) bool) { + m.middleware.pre.findOneAndUpdate.functions = append(m.middleware.pre.findOneAndUpdate.functions, func(args ...interface{}) bool { + return f(args[0].(primitive.M)) + }) +} + +func (m Model[T]) PreFindOneAndDelete(f func(filters primitive.M) bool) { + m.middleware.pre.findOneAndDelete.functions = append(m.middleware.pre.findOneAndDelete.functions, func(args ...interface{}) bool { + return f(args[0].(primitive.M)) + }) +} + +func (m Model[T]) PostFindOneAndDelete(f func(doc *T) bool) { + m.middleware.post.findOneAndDelete.functions = append(m.middleware.post.findOneAndDelete.functions, func(args ...interface{}) bool { + return f(args[0].(*T)) + }) +} + +func (m Model[T]) PostFindOneAndReplace(f func(doc *T) bool) { + m.middleware.post.findOneAndReplace.functions = append(m.middleware.post.findOneAndReplace.functions, func(args ...interface{}) bool { + return f(args[0].(*T)) + }) +} diff --git a/core/model_ops.go b/core/model_ops.go index 5960e15..e9750b1 100644 --- a/core/model_ops.go +++ b/core/model_ops.go @@ -29,7 +29,7 @@ func (m Model[T]) GreaterThanOrEquals(value any) Model[T] { } func (m Model[T]) Between(min, max any) Model[T] { - return m.addToFilters( "$gte", min).addToFilters("$lte", max) + return m.addToFilters("$gte", min).addToFilters("$lte", max) } func (m Model[T]) Mod(divisor, remainder int) Model[T] { @@ -64,4 +64,3 @@ func (m Model[T]) Or() Model[T] { m.orConditionActive = true return m } - diff --git a/core/model_populate.go b/core/model_populate.go index a192b1b..4d24c3c 100644 --- a/core/model_populate.go +++ b/core/model_populate.go @@ -69,7 +69,7 @@ func (m Model[T]) populate(value any) Model[T] { } func (m Model[T]) Populate(values ...any) Model[T] { - if (len(values) == 1 && reflect.ValueOf(values[0]).Kind() == reflect.String && (strings.Contains(values[0].(string), ",") || strings.Contains(values[0].(string), " "))) { + if len(values) == 1 && reflect.ValueOf(values[0]).Kind() == reflect.String && (strings.Contains(values[0].(string), ",") || strings.Contains(values[0].(string), " ")) { values := strings.FieldsFunc(values[0].(string), func(r rune) bool { return r == ',' || r == ' ' }) @@ -82,4 +82,4 @@ func (m Model[T]) Populate(values ...any) Model[T] { } } return m -} \ No newline at end of file +} diff --git a/core/model_query_delete.go b/core/model_query_delete.go index 4b3d358..c56d9ac 100644 --- a/core/model_query_delete.go +++ b/core/model_query_delete.go @@ -11,7 +11,9 @@ import ( func (m Model[T]) FindOneAndDelete(query ...primitive.M) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { var doc T + m.middleware.pre.findOneAndDelete.run(query[0]) result := m.Collection().FindOneAndDelete(ctx, e_utils.DefaultQuery(query...)) + m.middleware.post.findOneAndDelete.run(&doc) m.checkConditionsAndPanicForSingleResult(result) e_utils.Must(result.Decode(&doc)) return doc @@ -25,7 +27,9 @@ func (m Model[T]) FindByIdAndDelete(id primitive.ObjectID) Model[T] { func (m Model[T]) DeleteOne(query ...primitive.M) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { + m.middleware.pre.deleteOne.run(query[0]) result, err := m.Collection().DeleteOne(ctx, e_utils.DefaultQuery(query...)) + m.middleware.post.deleteOne.run(result, err) m.checkConditionsAndPanicForErr(err) return result } @@ -46,8 +50,10 @@ func (m Model[T]) Delete(doc T) Model[T] { func (m Model[T]) DeleteMany(query ...primitive.M) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { + m.middleware.pre.deleteMany.run(query[0]) result, err := m.Collection().DeleteMany(ctx, e_utils.DefaultQuery(query...)) m.checkConditionsAndPanicForErr(err) + m.middleware.post.deleteMany.run(result, err) return result } return m diff --git a/core/model_query_update.go b/core/model_query_update.go index cb77c64..6e6e383 100644 --- a/core/model_query_update.go +++ b/core/model_query_update.go @@ -12,6 +12,7 @@ import ( func (m Model[T]) FindOneAndUpdate(query *primitive.M, doc any, opts ...*options.FindOneAndUpdateOptions) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { + m.middleware.pre.findOneAndUpdate.run(doc) return (func() any { var resultDoc T filters := lo.FromPtr(query) @@ -19,6 +20,7 @@ func (m Model[T]) FindOneAndUpdate(query *primitive.M, doc any, opts ...*options filters[k] = v } result := m.Collection().FindOneAndUpdate(ctx, filters, primitive.M{"$set": m.parseDocument(doc)}, parseUpdateOptions(m, opts)...) + m.middleware.post.findOneAndUpdate.run(&resultDoc) m.checkConditionsAndPanicForSingleResult(result) e_utils.Must(result.Decode(&resultDoc)) return resultDoc @@ -41,7 +43,7 @@ func (m Model[T]) FindByIDAndUpdate(id primitive.ObjectID, doc any, opts ...*opt func (m Model[T]) UpdateOne(query *primitive.M, doc any, opts ...*options.UpdateOptions) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { filters := make(primitive.M) - if (query != nil) { + if query != nil { filters = lo.FromPtr(query) } for k, v := range m.findMatchStage() { @@ -85,7 +87,7 @@ func (m Model[T]) Save(doc T) Model[T] { func (m Model[T]) UpdateMany(query *primitive.M, doc any, opts ...*options.UpdateOptions) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { filters := make(primitive.M) - if (query != nil) { + if query != nil { filters = lo.FromPtr(query) } for k, v := range m.findMatchStage() { @@ -101,7 +103,7 @@ func (m Model[T]) UpdateMany(query *primitive.M, doc any, opts ...*options.Updat func (m Model[T]) ReplaceOne(query *primitive.M, doc any, opts ...*options.ReplaceOptions) Model[T] { m.executor = func(m Model[T], ctx context.Context) any { filters := make(primitive.M) - if (query != nil) { + if query != nil { filters = lo.FromPtr(query) } for k, v := range m.findMatchStage() { @@ -127,13 +129,14 @@ func (m Model[T]) FindOneAndReplace(query *primitive.M, doc any, opts ...*option m.executor = func(m Model[T], ctx context.Context) any { var resultDoc T filters := make(primitive.M) - if (query != nil) { + if query != nil { filters = lo.FromPtr(query) } for k, v := range m.findMatchStage() { filters[k] = v } res := m.Collection().FindOneAndReplace(ctx, filters, m.parseDocument(doc), opts...) + m.middleware.post.findOneAndReplace.run(&resultDoc) m.checkConditionsAndPanicForSingleResult(res) e_utils.Must(res.Decode(&resultDoc)) return resultDoc diff --git a/core/schema.go b/core/schema.go index e985a2d..f21530b 100644 --- a/core/schema.go +++ b/core/schema.go @@ -4,11 +4,11 @@ import ( "context" "elemental/connection" "elemental/utils" - "reflect" "github.com/creasty/defaults" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "reflect" ) type Schema struct { @@ -20,17 +20,16 @@ func NewSchema(definitions map[string]Field, opts ...SchemaOptions) Schema { schema := Schema{ Definitions: definitions, } - if (len(opts) > 0) { + if len(opts) > 0 { defaults.Set(opts[0]) schema.Options = opts[0] } return schema } - func (s Schema) Field(path string) *Field { definition := s.Definitions[path] - if (definition != (Field{})) { + if definition != (Field{}) { return &definition } return nil @@ -43,7 +42,7 @@ func (s Schema) syncIndexes(reflectedBaseType reflect.Type) { if (definition.Index != options.IndexOptions{}) { reflectedField, _ := reflectedBaseType.FieldByName(field) indexModel := mongo.IndexModel{ - Keys: bson.D{{Key: reflectedField.Tag.Get("bson") , Value: e_utils.Coalesce(definition.IndexOrder, 1)}}, + Keys: bson.D{{Key: reflectedField.Tag.Get("bson"), Value: e_utils.Coalesce(definition.IndexOrder, 1)}}, Options: &definition.Index, } collection.Indexes().CreateOne(context.TODO(), indexModel) diff --git a/core/schema_validator.go b/core/schema_validator.go index 748fb87..8a66579 100644 --- a/core/schema_validator.go +++ b/core/schema_validator.go @@ -15,9 +15,9 @@ import ( func enforceSchema[T any](schema Schema, doc *T, reflectedEntityType *reflect.Type, defaults ...bool) (bson.M, bson.M) { var entityToInsert bson.M - if (reflectedEntityType != nil) { + if reflectedEntityType != nil { entityToInsert = e_utils.Cast[bson.M](doc) - if (entityToInsert == nil) { + if entityToInsert == nil { entityToInsert = make(bson.M) } } else { @@ -28,13 +28,13 @@ func enforceSchema[T any](schema Schema, doc *T, reflectedEntityType *reflect.Ty id, _ := (*reflectedEntityType).FieldByName("ID") createdAt, _ := (*reflectedEntityType).FieldByName("CreatedAt") updatedAt, _ := (*reflectedEntityType).FieldByName("UpdatedAt") - if (id.Type != nil) { + if id.Type != nil { SetDefault(&entityToInsert, id.Tag.Get("bson"), primitive.NewObjectID()) } - if (createdAt.Type != nil) { + if createdAt.Type != nil { SetDefault(&entityToInsert, createdAt.Tag.Get("bson"), time.Now()) } - if (updatedAt.Type != nil) { + if updatedAt.Type != nil { SetDefault(&entityToInsert, updatedAt.Tag.Get("bson"), time.Now()) } } @@ -59,7 +59,7 @@ func enforceSchema[T any](schema Schema, doc *T, reflectedEntityType *reflect.Ty entityToInsert[fieldBsonName], detailedEntity[fieldBsonName] = enforceSchema(*definition.Schema, e_utils.Cast[*bson.M](entityToInsert[fieldBsonName]), &subdocumentField.Type, false) continue } - if (definition.Type == reflect.Struct && (definition.Ref != "" || definition.Collection != "") && entityToInsert[fieldBsonName]!= nil) { + if definition.Type == reflect.Struct && (definition.Ref != "" || definition.Collection != "") && entityToInsert[fieldBsonName] != nil { subdocumentField, _ := (*reflectedEntityType).FieldByName(field) subdocumentIdField, _ := subdocumentField.Type.FieldByName("ID") entityToInsert = lo.Assign( diff --git a/main.go b/main.go index c9b7f24..cc06705 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,9 @@ package main import ( - "elemental/cmd" + "elemental/cmd" ) func main() { - e_cmd.Execute() -} \ No newline at end of file + e_cmd.Execute() +} diff --git a/plugins/queryfilter/index.go b/plugins/queryfilter/index.go new file mode 100644 index 0000000..208e825 --- /dev/null +++ b/plugins/queryfilter/index.go @@ -0,0 +1,61 @@ +package queryfilter + +import ( + "log" + "net/http" + "strings" + "strconv" +) + + +func MongooseFilterQuery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + defer func() { + if r := recover(); r != nil { + log.Println("[ FilterQuery ] - Failed to parse query", r) + } + }() + + filterMap := make(map[string]string) + for key, value := range query { + filterMap[key] = value[0] + } + + reqFilter := MapFilters(filterMap) + _ = MapFilters(filterMap) + + sortValues := query.Get("sort") + if sortValues != "" { + sortMap := make(map[string]interface{}) + sortPairs := strings.Split(sortValues, ",") + for _, pair := range sortPairs { + keyValue := strings.Split(pair, "=") + if len(keyValue) == 2 { + key := keyValue[0] + dir := keyValue[1] + if dir == "1" || dir == "-1" { + dirInt, _ := strconv.Atoi(dir) + sortMap[key] = dirInt + } + } + } + reqFilter["sort"] = sortMap + } + + r.ParseForm() + includeValues := r.Form.Get("include") + if includeValues != "" { + include := strings.Split(includeValues, ",") + reqFilter["include"] = include + } + + selectValues := r.Form.Get("select") + if selectValues != "" { + selectFields := strings.Split(selectValues, ",") + reqFilter["select"] = strings.Join(selectFields, " ") + } + + next.ServeHTTP(w, r) + }) +} \ No newline at end of file diff --git a/plugins/queryfilter/tests/index.test.go b/plugins/queryfilter/tests/index.test.go new file mode 100644 index 0000000..94aedf7 --- /dev/null +++ b/plugins/queryfilter/tests/index.test.go @@ -0,0 +1,53 @@ +package queryfilter + +import ( + "reflect" + "testing" + "time" +) + +var basicFilterReq = map[string]interface{}{ + "query": map[string]interface{}{ + "filter": map[string]interface{}{ + "name": "eq(John)", + "lastName": "ne(Doe)", + "middleName": "reg(.*Nathan.*)", + "age": "gt(20)", + "email": "nin(email1,email2,email3)", + "address": "in(address1,address2,address3)", + "weight": "gte(50)", + "height": "lt(180)", + "birthdate": "lte(2000-01-01)", + "isAlive": "exists(true)", + "isVerified": "eq(true)", + "isDeleted": "false", + }, + }, +} + +var basicFilterResult = map[string]interface{}{ + "name": map[string]interface{}{"$eq": "John"}, + "lastName": map[string]interface{}{"$ne": "Doe"}, + "middleName": map[string]interface{}{"$regex": "/.*Nathan.*/"}, + "age": map[string]interface{}{"$gt": 20}, + "email": map[string]interface{}{"$nin": []string{"email1", "email2", "email3"}}, + "address": map[string]interface{}{"$in": []string{"address1", "address2", "address3"}}, + "weight": map[string]interface{}{"$gte": 50}, + "height": map[string]interface{}{"$lt": 180}, + "birthdate": map[string]interface{}{"$lte": time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)}, + "isAlive": map[string]interface{}{"$exists": true}, + "isVerified": map[string]interface{}{"$eq": true}, + "isDeleted": false, +} + +func TestFilterQuery(t *testing.T) { + basicFilterReq := make(map[string]interface{}) + basicFilterResult := make(map[string]interface{}) + + MongooseFilterQuery(basicFilterReq, basicFilterResult) + + if !reflect.DeepEqual(basicFilterReq["query"], basicFilterResult) { + t.Errorf("Filtering failed. Expected: %v, Got: %v", basicFilterResult, basicFilterReq["query"]) + } +} + diff --git a/plugins/queryfilter/utils.go b/plugins/queryfilter/utils.go new file mode 100644 index 0000000..7b5af3b --- /dev/null +++ b/plugins/queryfilter/utils.go @@ -0,0 +1,104 @@ +package queryfilter + +import ( + "regexp" + "strconv" + "strings" + "time" +) + +var complexOperators = []string{"and", "or"} + +func ReplaceOperator(value, operator string) string { + return strings.TrimSuffix(strings.Replace(value, operator+"(", "", 1), ")") +} + +func ParseOperatorValue(value, operator string) interface{} { + value = ReplaceOperator(value, operator) + if _, err := strconv.ParseFloat(value, 64); err != nil { + if _, err := time.Parse(time.RFC3339, value); err == nil { + return value + } else if matched, _ := regexp.MatchString("^[0-9a-fA-F]{24}$", value); matched { + return struct{ ID string }{value} + } + } else { + if floatValue, err := strconv.ParseFloat(value, 64); err == nil { + return floatValue + } + } + return value +} + +func MapValue(value string) interface{} { + if strings.HasPrefix(value, "eq(") { + value = ParseOperatorValue(value, "eq").(string) + if value == "true" || value == "false" { + parsedvalue, _ := strconv.ParseBool(value) + return parsedvalue + } + return value + } else if strings.HasPrefix(value, "ne(") { + return map[string]interface{}{"$ne": ParseOperatorValue(value, "ne")} + } else if strings.HasPrefix(value, "gt(") { + return map[string]interface{}{"$gt": ParseOperatorValue(value, "gt")} + } else if strings.HasPrefix(value, "gte(") { + return map[string]interface{}{"$gte": ParseOperatorValue(value, "gte")} + } else if strings.HasPrefix(value, "lt(") { + return map[string]interface{}{"$lt": ParseOperatorValue(value, "lt")} + } else if strings.HasPrefix(value, "lte(") { + return map[string]interface{}{"$lte": ParseOperatorValue(value, "lte")} + } else if strings.HasPrefix(value, "in(") { + return map[string]interface{}{"$in": strings.Split(ParseOperatorValue(value, "in").(string), ",")} + } else if strings.HasPrefix(value, "nin(") { + return map[string]interface{}{"$nin": strings.Split(ParseOperatorValue(value, "nin").(string), ",")} + } else if strings.HasPrefix(value, "reg(") { + return map[string]interface{}{"$regex": regexp.MustCompile(ReplaceOperator(value, "reg"))} + } else if strings.HasPrefix(value, "exists(") { + b, _ := strconv.ParseBool(ParseOperatorValue(value, "exists").(string)) + return map[string]interface{}{"$exists": b} + } + if value == "true" || value == "false" { + parsedvalue, _ := strconv.ParseBool(value) + return parsedvalue + } + return value +} + +func MapFilters(filter map[string]string) map[string]interface{} { + result := make(map[string]interface{}) + if filter != nil { + for key, value := range filter { + if contains(complexOperators, key) { + subFilters := make([]map[string]interface{}, 0) + kvPairs := strings.Split(value, ",") + for _, kv := range kvPairs { + subKey, subValue := strings.Split(kv, "=")[0], strings.Split(kv, "=")[1] + subFilters = append(subFilters, map[string]interface{}{subKey: MapValue(subValue)}) + } + result["$"+key] = subFilters + } else { + for _, op := range complexOperators { + if strings.HasPrefix(value, op+"(") { + values := strings.Split(ParseOperatorValue(value, op).(string), ",") + subFilters := make([]map[string]interface{}, 0) + for _, subValue := range values { + subFilters = append(subFilters, map[string]interface{}{key: MapValue(subValue)}) + } + result["$"+op] = subFilters + } else { + result[key] = filter[key]} + } + } + } + } + return result +} + +func contains(arr []string, str string) bool { + for _, a := range arr { + if a == str { + return true + } + } + return false +} diff --git a/tests/base/base.go b/tests/base/base.go index 3e3be63..2152b22 100644 --- a/tests/base/base.go +++ b/tests/base/base.go @@ -133,13 +133,13 @@ var KingdomModel = elemental.NewModel[Kingdom]("Kingdom", elemental.NewSchema(ma var BestiaryModel = elemental.NewModel[Bestiary]("Bestiary", elemental.NewSchema(map[string]elemental.Field{ "Monster": { - Type: reflect.Struct, - Ref: "Monster", + Type: reflect.Struct, + Ref: "Monster", }, "Kingdom": { - Type: reflect.Struct, - Ref: "Kingdom", + Type: reflect.Struct, + Ref: "Kingdom", }, }, elemental.SchemaOptions{ Collection: "bestiary", -})) \ No newline at end of file +})) diff --git a/tests/core_create_test.go b/tests/core_create_test.go index 7ad190a..aa2f11a 100644 --- a/tests/core_create_test.go +++ b/tests/core_create_test.go @@ -54,7 +54,7 @@ func TestCoreCreate(t *testing.T) { }) Convey("Create a monster which has a sub schema with defaults", t, func() { monster := MonsterModel.Create(Monster{ - Name: "Katakan", + Name: "Katakan", Category: "Vampire", }).Exec().(Monster) So(monster.ID, ShouldNotBeNil) diff --git a/tests/core_middleware_test.go b/tests/core_middleware_test.go index 53768be..6bf9f1a 100644 --- a/tests/core_middleware_test.go +++ b/tests/core_middleware_test.go @@ -44,10 +44,76 @@ func TestCoreMiddleware(t *testing.T) { return true }) + CastleModel.PreDeleteOne(func(filters primitive.M) bool { + invokedHooks["preDeleteOne"] = true + return true + }) + + CastleModel.PostDeleteOne(func(result *mongo.DeleteResult, err error) bool { + invokedHooks["postDeleteOne"] = true + return true + }) + + CastleModel.PreDeleteMany(func(filters primitive.M) bool { + invokedHooks["preDeleteMany"] = true + return true + }) + + CastleModel.PostDeleteMany(func(result *mongo.DeleteResult, err error) bool { + invokedHooks["postDeleteMany"] = true + return true + }) + + CastleModel.PostFind(func(castle []Castle) bool { + invokedHooks["postFind"] = true + return true + }) + + CastleModel.PreFindOneAndUpdate(func(filter primitive.M) bool { + invokedHooks["preFindOneAndUpdate"] = true + return true + }) + + CastleModel.PostFindOneAndUpdate(func(castle *Castle) bool { + invokedHooks["postFindOneAndUpdate"] = true + return true + }) + + CastleModel.PreFindOneAndDelete(func(filters primitive.M) bool { + invokedHooks["preFindOneAndDelete"] = true + return true + }) + + CastleModel.PostFindOneAndDelete(func(castle *Castle) bool { + invokedHooks["postFindOneAndDelete"] = true + return true + }) + + CastleModel.PostFindOneAndReplace(func(castle *Castle) bool { + invokedHooks["postFindOneAndReplace"] = true + return true + }) + CastleModel.Create(Castle{Name: "Aretuza"}).Exec() + CastleModel.Create(Castle{Name: "Maverick"}).Exec() + + CastleModel.Create(Castle{Name: "Robert"}).Exec() + + CastleModel.FindOneAndReplace(&primitive.M{"name": "Robert"}, Castle{Name: "Jack"}).Exec() + CastleModel.UpdateOne(&primitive.M{"name": "Aretuza"}, Castle{Name: "Kaer Morhen"}).Exec() + CastleModel.Find().Exec() + + CastleModel.FindOneAndUpdate(&primitive.M{"name": "Maverick"}, primitive.M{"name": "Maverickk"}).Exec() + + CastleModel.DeleteOne(primitive.M{"name": "Kaer Morhen"}).Exec() + + CastleModel.FindOneAndDelete(primitive.M{"name": "Jack"}).Exec() + + CastleModel.DeleteMany(primitive.M{"name": primitive.M{"$in": []string{"Aretuza", "Maverick"}}}).Exec() + Convey("Pre hooks", t, func() { Convey("Save", func() { So(invokedHooks["preSave"], ShouldBeTrue) @@ -55,7 +121,20 @@ func TestCoreMiddleware(t *testing.T) { Convey("UpdateOne", func() { So(invokedHooks["preUpdateOne"], ShouldBeTrue) }) + Convey("DeleteOne", func() { + So(invokedHooks["preDeleteOne"], ShouldBeTrue) + }) + Convey("DeleteMany", func() { + So(invokedHooks["preDeleteMany"], ShouldBeTrue) + }) + Convey("FindOneAndUpdate", func() { + So(invokedHooks["preFindOneAndUpdate"], ShouldBeTrue) + }) + Convey("FindOneAndDelete", func() { + So(invokedHooks["preFindOneAndDelete"], ShouldBeTrue) + }) }) + Convey("Post hooks", t, func() { Convey("Save", func() { So(invokedHooks["postSave"], ShouldBeTrue) @@ -63,5 +142,23 @@ func TestCoreMiddleware(t *testing.T) { Convey("UpdateOne", func() { So(invokedHooks["postUpdateOne"], ShouldBeTrue) }) + Convey("DeleteOne", func() { + So(invokedHooks["postDeleteOne"], ShouldBeTrue) + }) + Convey("DeleteMany", func() { + So(invokedHooks["postDeleteMany"], ShouldBeTrue) + }) + Convey("Find", func() { + So(invokedHooks["postFind"], ShouldBeTrue) + }) + Convey("FindOneAndUpdate", func() { + So(invokedHooks["postFindOneAndUpdate"], ShouldBeTrue) + }) + Convey("FindOneAndDelete", func() { + So(invokedHooks["postFindOneAndDelete"], ShouldBeTrue) + }) + Convey("FindOneAndReplace", func() { + So(invokedHooks["postFindOneAndReplace"], ShouldBeTrue) + }) }) } diff --git a/tests/core_read_select_test.go b/tests/core_read_select_test.go index d265875..e8001ba 100644 --- a/tests/core_read_select_test.go +++ b/tests/core_read_select_test.go @@ -65,8 +65,8 @@ func TestCoreReadSelect(t *testing.T) { Convey("In conjunction with a string input (commas)", func() { users := UserModel.Find().Select("name, -_id").Limit(limit).Exec().([]User) assert(users) - }) - }) + }) + }) Convey(fmt.Sprintf("%d user names and ages", limit), func() { assert := func(users []User) { So(users, ShouldHaveLength, limit) diff --git a/tests/core_read_test.go b/tests/core_read_test.go index 4887de8..f12ba33 100644 --- a/tests/core_read_test.go +++ b/tests/core_read_test.go @@ -50,13 +50,13 @@ func TestCoreRead(t *testing.T) { users := UserModel.Find(primitive.M{"name": "Yarpen Zigrin"}).Exec().([]User) So(users, ShouldHaveLength, 0) Convey("With or fail", func() { - So(func () { + So(func() { UserModel.Find(primitive.M{"name": "Yarpen Zigrin"}).OrFail().Exec() }, ShouldPanicWith, errors.New("no results found matching the given query")) }) Convey("With or fail and custom error", func() { err := errors.New("no user found") - So(func () { + So(func() { UserModel.Find(primitive.M{"name": "Yarpen Zigrin"}).OrFail(err).Exec() }, ShouldPanicWith, err) }) @@ -115,10 +115,10 @@ func TestCoreRead(t *testing.T) { So(users[1].Name, ShouldEqual, e_mocks.Imlerith.Name) So(users[2].Name, ShouldEqual, e_mocks.Caranthir.Name) So(users[3].Name, ShouldEqual, e_mocks.Yennefer.Name) - So(users[4].Name, ShouldEqual, e_mocks.Geralt.Name) + So(users[4].Name, ShouldEqual, e_mocks.Geralt.Name) }) Convey("Must panic when finding with invalid sort arguments", func() { - So(func () { + So(func() { UserModel.Find().Sort("age", 1, "name").Exec() }, ShouldPanicWith, e_constants.ErrMustPairSortArguments) }) diff --git a/tests/core_triggers_test.go b/tests/core_triggers_test.go index 85c5abe..11b6153 100644 --- a/tests/core_triggers_test.go +++ b/tests/core_triggers_test.go @@ -26,7 +26,7 @@ func TestCoreTriggers(t *testing.T) { var insertedCastle Castle var updatedCastle Castle - var replacedCastle Castle + var replacedCastle Castle var deletedCastleID primitive.ObjectID var collectionDropped bool diff --git a/tests/mocks/mocks.go b/tests/mocks/mocks.go index b54810d..c724c8b 100644 --- a/tests/mocks/mocks.go +++ b/tests/mocks/mocks.go @@ -5,19 +5,19 @@ import ( ) const ( - DB_URI = "mongodb+srv://akalankaperera128:pFAnQVXE6vrbcXNk@default.ynr156r.mongodb.net/elemental" - DEFAULT_DB = "elemental" - SECONDARY_DB = "elemental_secondary" - TERTIARY_DB = "elemental_tertiary" + DB_URI = "mongodb+srv://akalankaperera128:pFAnQVXE6vrbcXNk@default.ynr156r.mongodb.net/elemental" + DEFAULT_DB = "elemental" + SECONDARY_DB = "elemental_secondary" + TERTIARY_DB = "elemental_tertiary" TEMPORARY_DB_1 = "elemental_temporary_1" TEMPORARY_DB_2 = "elemental_temporary_2" TEMPORARY_DB_3 = "elemental_temporary_3" ) var ( - WolfSchool = "Wolf" - BearSchool = "Bear" - GriffinSchool = "Griffin" + WolfSchool = "Wolf" + BearSchool = "Bear" + GriffinSchool = "Griffin" ManticoreSchool = "Manticore" ) @@ -58,7 +58,7 @@ var ( Age: 300, Weapons: []string{"Silver sword", "Steel sword", "Crossbow"}, Retired: true, - School: &WolfSchool, + School: &WolfSchool, } ) diff --git a/utils/caster.go b/utils/caster.go index ef3cfb9..ea32327 100644 --- a/utils/caster.go +++ b/utils/caster.go @@ -39,7 +39,6 @@ func ToMap(s any) map[string]interface{} { return m } - // Converts any type to a given type based on their bson representation. It partially fills the target in case they are not directly compatible. func CastBSON[T any](val any) T { return FromBSON[T](ToBSON(val)) @@ -60,10 +59,10 @@ func FromBSON[T any](bytes []byte) T { // Converts an interface to a bson document func ToBSONDoc(v interface{}) (doc *bson.M) { - data, err := bson.Marshal(v) - if err != nil { - return nil - } + data, err := bson.Marshal(v) + if err != nil { + return nil + } bson.Unmarshal(data, &doc) return doc -} \ No newline at end of file +} diff --git a/utils/misc.go b/utils/misc.go index cbe85fa..07abc29 100644 --- a/utils/misc.go +++ b/utils/misc.go @@ -33,6 +33,7 @@ func ProtectWithCallback(f func(), onError func(err interface{})) { }() f() } + // Extracts and returns the context from an optional slice of contexts. If the slice is empty, it returns a new context. func DefaultCTX(slice []context.Context) context.Context { if len(slice) == 0 { diff --git a/utils/slice.go b/utils/slice.go index d8ebfd2..2eda708 100644 --- a/utils/slice.go +++ b/utils/slice.go @@ -33,4 +33,4 @@ func CastBSONSlice[T any](slice []interface{}) []T { return lo.Map(slice, func(doc interface{}, _ int) T { return CastBSON[T](doc) }) -} \ No newline at end of file +} diff --git a/utils/struct.go b/utils/struct.go index 47333ae..3a1756b 100644 --- a/utils/struct.go +++ b/utils/struct.go @@ -2,10 +2,10 @@ package e_utils import ( "fmt" - "reflect" - "strconv" "github.com/samber/lo" "go.mongodb.org/mongo-driver/bson/primitive" + "reflect" + "strconv" ) func setField(field reflect.Value, defaultVal string) error { @@ -23,7 +23,7 @@ func setField(field reflect.Value, defaultVal string) error { return nil } -func SetDefaults(ptr interface{},) error { +func SetDefaults(ptr interface{}) error { if reflect.TypeOf(ptr).Kind() != reflect.Ptr { return fmt.Errorf("Not a pointer") } @@ -40,25 +40,25 @@ func SetDefaults(ptr interface{},) error { return nil } -func IsEmpty (value interface{}) bool { +func IsEmpty(value interface{}) bool { if value == nil { return true } - if (lo.IsEmpty(value)) { + if lo.IsEmpty(value) { return true } reflectedValue := reflect.ValueOf(value) - if (!reflectedValue.IsValid() || reflectedValue.IsZero()) { + if !reflectedValue.IsValid() || reflectedValue.IsZero() { return true } reflectedValueType := reflect.TypeOf(value) var dateTime primitive.DateTime var objectId primitive.ObjectID - if (reflectedValueType == reflect.TypeOf(&dateTime) || reflectedValueType == reflect.TypeOf(dateTime)) { + if reflectedValueType == reflect.TypeOf(&dateTime) || reflectedValueType == reflect.TypeOf(dateTime) { return value.(primitive.DateTime).Time().IsZero() } - if (reflectedValueType == reflect.TypeOf(&objectId) || reflectedValueType == reflect.TypeOf(objectId)) { + if reflectedValueType == reflect.TypeOf(&objectId) || reflectedValueType == reflect.TypeOf(objectId) { return value.(primitive.ObjectID).IsZero() } return false -} \ No newline at end of file +}