github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/mango/query.go (about) 1 package mango 2 3 import ( 4 "encoding/json" 5 "unicode" 6 ) 7 8 // This package provides utility structures to build mango queries 9 10 //////////////////////////////////////////////////////////////// 11 // Filter 12 /////////////////////////////////////////////////////////////// 13 14 // ValueOperator is an operator between a field and a value 15 type ValueOperator string 16 17 // ne ($ne) checks that field != value 18 const ne ValueOperator = "$ne" 19 20 // gt ($gt) checks that field > value 21 const gt ValueOperator = "$gt" 22 23 // gte ($gte) checks that field >= value 24 const gte ValueOperator = "$gte" 25 26 // lt ($lt) checks that field < value 27 const lt ValueOperator = "$lt" 28 29 // lte ($lte) checks that field <= value 30 const lte ValueOperator = "$lte" 31 32 // exists ($exists) checks that the field exists (or is missing) 33 const exists ValueOperator = "$exists" 34 35 // in ($in) checks that the field value equals one of the values 36 const in ValueOperator = "$in" 37 38 // LogicOperator is an operator between two filters 39 type LogicOperator string 40 41 // and ($and) checks that filter && filter2 42 const and LogicOperator = "$and" 43 44 // not ($not) checks that !filter 45 const not LogicOperator = "$not" 46 47 // or ($or) checks that filter1 || filter2 || ... 48 const or LogicOperator = "$or" 49 50 // nor ($nor) checks that !(filter1 || filter2 || ...) 51 const nor LogicOperator = "$nor" 52 53 // A Filter is a filter on documents, to be passed 54 // as the selector of a couchdb.FindRequest 55 // In the future, we might add go-side validation 56 // but we will need to duplicate the couchdb UCA algorithm 57 type Filter interface { 58 json.Marshaler 59 ToMango() Map 60 } 61 62 // Map is an alias for map[string]interface{} 63 type Map map[string]interface{} 64 65 // ToMango implements the Filter interface on Map 66 // it returns the map itself 67 func (m Map) ToMango() Map { 68 return m 69 } 70 71 // MarshalJSON returns a byte json representation of the map 72 func (m Map) MarshalJSON() ([]byte, error) { 73 return json.Marshal(map[string]interface{}(m)) 74 } 75 76 // valueFilter is a filter on a single field 77 type valueFilter struct { 78 field string 79 op ValueOperator 80 value interface{} 81 } 82 83 // ToMango implements the Filter interface on valueFilter 84 // it returns a map, either `{field: value}` or `{field: {$op: value}}` 85 func (vf valueFilter) ToMango() Map { 86 return makeMap(vf.field, makeMap(string(vf.op), vf.value)) 87 } 88 89 func (vf valueFilter) MarshalJSON() ([]byte, error) { 90 return json.Marshal(vf.ToMango()) 91 } 92 93 // logicFilter is a combination of filters with logic operator 94 type logicFilter struct { 95 op LogicOperator 96 filters []Filter 97 } 98 99 // ToMango implements the Filter interface on logicFilter 100 // We could add some logic to make $and queries more readable 101 // For instance 102 // {"$and": [{"field": {"$lt":6}}, {"field": {"$gt":3}}] 103 // ---> {"field": {"$lt":6, "$gt":3} 104 // but it doesnt improve performances. 105 func (lf logicFilter) ToMango() Map { 106 // special case, $not has an arity of one 107 if lf.op == not { 108 return makeMap(string(lf.op), lf.filters[0].ToMango()) 109 } 110 111 // all other LogicOperator works on arrays 112 filters := make([]Map, len(lf.filters)) 113 for i, v := range lf.filters { 114 filters[i] = v.ToMango() 115 } 116 return makeMap(string(lf.op), filters) 117 } 118 119 func (lf logicFilter) MarshalJSON() ([]byte, error) { 120 return json.Marshal(lf.ToMango()) 121 } 122 123 // ensure ValueFilter & LogicFilter match FilterInterface 124 var _ Filter = (*valueFilter)(nil) 125 var _ Filter = (*logicFilter)(nil) 126 127 // Some Filter creation function 128 129 // And returns a filter combining several filters 130 func And(filters ...Filter) Filter { return logicFilter{and, filters} } 131 132 // Or returns a filter combining several filters 133 func Or(filters ...Filter) Filter { return logicFilter{or, filters} } 134 135 // Nor returns a filter combining several filters 136 func Nor(filters ...Filter) Filter { return logicFilter{nor, filters} } 137 138 // In returns a filter that checks if the field is equal to one of the values 139 func In(field string, values []interface{}) Filter { return &valueFilter{field, in, values} } 140 141 // Not returns a filter inversing another filter 142 func Not(filter Filter) Filter { return logicFilter{not, []Filter{filter}} } 143 144 // Exists returns a filter that check that the document has this field 145 func Exists(field string) Filter { return &valueFilter{field, exists, true} } 146 147 // NotExists returns a filter that check that the document does not have this field 148 func NotExists(field string) Filter { return &valueFilter{field, exists, false} } 149 150 // Equal returns a filter that check if a field == value 151 func Equal(field string, value interface{}) Filter { return makeMap(field, value) } 152 153 // NotEqual returns a filter that check if a field != value 154 func NotEqual(field string, value interface{}) Filter { return &valueFilter{field, ne, value} } 155 156 // Gt returns a filter that check if a field > value 157 func Gt(field string, value interface{}) Filter { return &valueFilter{field, gt, value} } 158 159 // Gte returns a filter that check if a field >= value 160 func Gte(field string, value interface{}) Filter { return &valueFilter{field, gte, value} } 161 162 // Lt returns a filter that check if a field < value 163 func Lt(field string, value interface{}) Filter { return &valueFilter{field, lt, value} } 164 165 // Lte returns a filter that check if a field <= value 166 func Lte(field string, value interface{}) Filter { return &valueFilter{field, lte, value} } 167 168 // Between returns a filter that check if v1 <= field < v2 169 func Between(field string, v1 interface{}, v2 interface{}) Filter { 170 return &logicFilter{op: and, filters: []Filter{ 171 &valueFilter{field, gte, v1}, 172 &valueFilter{field, lt, v2}, 173 }} 174 } 175 176 // MaxString is the unicode character \uFFFF, useful as an upperbound for 177 // queryies 178 const MaxString = string(unicode.MaxRune) 179 180 // StartWith returns a filter that check if field's string value start with prefix 181 func StartWith(field string, prefix string) Filter { 182 return Between(field, prefix, prefix+MaxString) 183 } 184 185 //////////////////////////////////////////////////////////////// 186 // Sort 187 /////////////////////////////////////////////////////////////// 188 189 // SortDirection can be either ASC or DESC 190 type SortDirection string 191 192 // Asc is the ascending sorting order 193 const Asc SortDirection = "asc" 194 195 // Desc is the descending sorting order 196 const Desc SortDirection = "desc" 197 198 // SortBy is a sorting rule to be used as the sort of a couchdb.FindRequest 199 // a list of (field, direction) combination. 200 type SortBy []SortByField 201 202 // SortByField is a sorting rule to be used as the sort for a pair of (field, 203 // direction). 204 type SortByField struct { 205 Field string 206 Direction SortDirection 207 } 208 209 // MarshalJSON implements json.Marshaller on SortBy 210 // it will returns a json array [field, direction] 211 func (s SortBy) MarshalJSON() ([]byte, error) { 212 asSlice := make([]Map, len(s)) 213 for i, f := range s { 214 asSlice[i] = makeMap(f.Field, string(f.Direction)) 215 } 216 return json.Marshal(asSlice) 217 } 218 219 // utility function to create a map with a single key 220 func makeMap(key string, value interface{}) Map { 221 out := make(Map) 222 out[key] = value 223 return out 224 }