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  }