package dbs import ( "fmt" "reflect" "runtime/debug" "strings" "go.mongodb.org/mongo-driver/bson" ) type Operator int const ( LIKE Operator = iota EXISTS IN GTE LTE LT GT EQUAL NOT ELEMMATCH OR ) var str = [...]string{ "like", "exists", "in", "gte", "lte", "lt", "gt", "equal", "not", "elemMatch", "or", } func (m Operator) String() string { return str[m] } func (m Operator) ToMongoOperator(k string, value interface{}) bson.M { defer func() { if r := recover(); r != nil { fmt.Println("Recovered. Error:\n", r, debug.Stack()) } }() defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}} switch m { case LIKE: return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case EXISTS: return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case IN: return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case GTE: return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case GT: return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case LTE: return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case LT: return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case ELEMMATCH: return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value, false)}} case EQUAL: return bson.M{k: value} case NOT: return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value, false)} case OR: return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value, true)} default: return defaultValue } } func StringToOperator(s string) Operator { for i, v := range str { if v == s { return Operator(i) } } return LIKE } func GetBson(filters *Filters) bson.D { f := bson.D{} orList := bson.A{} andList := bson.A{} if filters != nil { for k, filter := range filters.Or { for _, ff := range filter { orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value)) } } for k, filter := range filters.And { for _, ff := range filter { andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value)) } } if len(orList) > 0 && len(andList) == 0 { f = bson.D{{Key: "$or", Value: orList}} } else { if len(orList) > 0 { andList = append(andList, bson.M{"$or": orList}) } f = bson.D{{Key: "$and", Value: andList}} } } return f } func (m Operator) ToValueOperator(operator Operator, value interface{}, or bool) interface{} { switch value := value.(type) { case *Filters: bson := GetBson(value) if or { for _, b := range bson { if b.Key == "$or" { return b.Value } } } else { return bson } default: if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" { value = "" } if operator == LIKE { return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*" } } return value } type Filters struct { And map[string][]Filter `json:"and"` Or map[string][]Filter `json:"or"` } type Filter struct { Operator string `json:"operator,omitempty"` Value interface{} `json:"value,omitempty"` } // FiltersFromFlatMap builds a *Filters from a map[string]interface{} whose structure // mirrors the JSON form of Filters: // // { // "and": { "name": [{"operator":"like","value":"foo"}] }, // "or": { "source": [{"operator":"equal","value":"bar"}] } // } // // Keys inside "and"/"or" are json tag names; the function resolves each to its // full dotted BSON path using the target struct. Unknown keys are kept as-is. func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Filters { filters := &Filters{ And: make(map[string][]Filter), Or: make(map[string][]Filter), } paths := jsonToBsonPaths(reflect.TypeOf(target), "") resolve := func(jsonKey string) string { if p, ok := paths[jsonKey]; ok { return p } return jsonKey } parseFilters := func(raw interface{}) map[string][]Filter { out := make(map[string][]Filter) m, ok := raw.(map[string]interface{}) if !ok { return out } for jsonKey, val := range m { bsonKey := resolve(jsonKey) items, ok := val.([]interface{}) if !ok { continue } for _, item := range items { entry, ok := item.(map[string]interface{}) if !ok { continue } f := Filter{} if op, ok := entry["operator"].(string); ok { f.Operator = op } if v, ok := entry["value"]; ok { f.Value = v } out[bsonKey] = append(out[bsonKey], f) } } return out } if and, ok := flatMap["and"]; ok { filters.And = parseFilters(and) } if or, ok := flatMap["or"]; ok { filters.Or = parseFilters(or) } return filters } // jsonToBsonPaths recursively walks a struct type and returns a map of // json_name → dotted_bson_path for every field reachable from that type. // // Anonymous embedded fields without any tag follow the BSON convention of this // codebase: they are stored as a nested sub-document whose key is the lowercased // struct type name (e.g. utils.AbstractObject → "abstractobject"). func jsonToBsonPaths(t reflect.Type, prefix string) map[string]string { for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice { t = t.Elem() } result := make(map[string]string) if t.Kind() != reflect.Struct { return result } for i := 0; i < t.NumField(); i++ { field := t.Field(i) jsonTag := field.Tag.Get("json") bsonTag := field.Tag.Get("bson") jsonName := strings.Split(jsonTag, ",")[0] bsonName := strings.Split(bsonTag, ",")[0] // Anonymous embedded struct with no tags: use lowercase type name as BSON prefix. if field.Anonymous && jsonName == "" && bsonName == "" { ft := field.Type for ft.Kind() == reflect.Ptr { ft = ft.Elem() } if ft.Kind() == reflect.Struct { embedPrefix := strings.ToLower(ft.Name()) if prefix != "" { embedPrefix = prefix + "." + embedPrefix } for k, v := range jsonToBsonPaths(ft, embedPrefix) { if _, exists := result[k]; !exists { result[k] = v } } } continue } if jsonName == "" || jsonName == "-" { continue } if bsonName == "" || bsonName == "-" { bsonName = jsonName } fullPath := bsonName if prefix != "" { fullPath = prefix + "." + bsonName } result[jsonName] = fullPath ft := field.Type for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice { ft = ft.Elem() } if ft.Kind() == reflect.Struct { for k, v := range jsonToBsonPaths(ft, fullPath) { if _, exists := result[k]; !exists { result[k] = v } } } } return result } type Input = map[string]interface{} func InputToBson(i Input, isUpdate bool) bson.D { input := bson.D{} for k, v := range i { if k == "id" { input = append(input, bson.E{Key: "_id", Value: v}) } else { input = append(input, bson.E{Key: k, Value: v}) } } if isUpdate { return bson.D{{Key: "$set", Value: input}} } return input }