├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── debug.log ├── lib ├── mongo_aggregation.dart ├── mongo_dart_query.dart └── src │ ├── geometry_obj.dart │ ├── modifier_builder.dart │ ├── mongo_aggregation │ ├── aggregation_base.dart │ ├── aggregation_pipeline_builder.dart │ ├── aggregation_stages.dart │ ├── arithmetic_operators.dart │ ├── array_object_operators.dart │ ├── common.dart │ ├── comparison_operators.dart │ ├── date_time_operators.dart │ ├── group_stage_accumulators.dart │ ├── logic_operators.dart │ ├── pipeline_builder.dart │ ├── string_operators.dart │ ├── support_classes │ │ └── output.dart │ ├── type_expression_operators.dart │ └── uncategorized_operators.dart │ └── selector_builder.dart ├── pubspec.yaml └── test ├── .gitignore ├── mongo_aggregation ├── aggregation_base_test.dart ├── aggregation_stages_test.dart ├── arithmetic_operators_test.dart ├── array_object_operators_test.dart ├── comparison_operators_test.dart ├── date_time_operators_test.dart ├── group_stage_accumulators_test.dart ├── logic_operators_test.dart ├── string_operators_test.dart ├── type_expressions_operators_test.dart └── uncategorized_operators_test.dart └── selector_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | ### Dart template 2 | # Don’t commit the following directories created by pub. 3 | .buildlog 4 | .pub/ 5 | build/ 6 | packages 7 | .packages 8 | 9 | # Or the files created by dart2js. 10 | *.dart.js 11 | *.js_ 12 | *.js.deps 13 | *.js.map 14 | 15 | # Include when developing application packages. 16 | pubspec.lock 17 | 18 | .dart_tool 19 | 20 | # Ide folders 21 | .idea/ 22 | .vscode/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart: 3 | - dev 4 | - stable 5 | sudo: false 6 | cache: 7 | directories: 8 | - $HOME/.pub-cache 9 | script: pub run test 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.0.2 4 | 5 | - Sample stage 6 | 7 | ## 5.0.1 8 | 9 | - Fixed match operator, the case insensitive flag was inverted (true instead of false and viceversa) 10 | 11 | ## 5.0.0 12 | 13 | Changed dependency on Bson package. This have the following consequenses: 14 | 15 | - BSON classes are mainly used for internal use. See the Bson github site for more details 16 | - BsonRegexp now it is not normally needed, use RegExp instead. 17 | - BsonNull is not needed, you can use null directly. 18 | - A new JsCode class has been created, it is no more needed the use of BsonCode. 19 | - Uuid dependecy has been updated and this means that the UuidValue class has been slightly changed. The .fromString constructure must be used mainly instead of the default one. Check the Uuid package Pub site for details. 20 | 21 | ## 4.0.5 22 | 23 | - Added `escapePatern` parameter to `match` method in `SelectorBuilder`. This parameter allows to escape the pattern passed to the method. Usefule when, for example you have to search for some string conatining a RegExp special character like points (ex '') 24 | 25 | ## 4.0.4 26 | 27 | - Missing Export (Fix) 28 | 29 | ## 4.0.3 30 | 31 | - SetWindowFields() (aggregation). Fix. 32 | 33 | ## 4.0.2 34 | 35 | - SetWindowFields() (aggregation) 36 | 37 | ## 4.0.1 38 | 39 | - addEachToSet method 40 | 41 | ## 4.0.0 42 | 43 | - Inherited Bson 4.0.0 that introduces **breaking changes** 44 | 45 | ## 3.0.0 46 | 47 | - Inherited Decimal 2.3.0 that can introduce **breaking changes** 48 | 49 | ## 2.0.1 50 | 51 | - Fix - Geometry class was not exported 52 | 53 | ## 2.0.0 54 | 55 | - Lint fixes 56 | - Corrected "and" references in selectorBuilder.or(…) 57 | 58 | ## 2.0.0-1.0.beta 59 | 60 | - New UnionWith Stage 61 | - Removed Pedantic -> Moved to Lints 62 | 63 | ### Breaking changes 64 | 65 | - Moved to Bson 2.0.0 that uses Decimal instead of Rational 66 | 67 | ## 1.0.2 68 | 69 | - added geoNear aggregation stage 70 | - SelectorBuilder clone() 71 | 72 | ## 1.0.1 73 | 74 | - $mul operator 75 | 76 | ## 1.0.0 77 | 78 | - Update dependencies for final version 79 | 80 | ## 1.0.0-nullsafety.2 81 | 82 | - updated dependencies 83 | 84 | ## 1.0.0-nullsafety 85 | 86 | ### Potential breaking changes 87 | 88 | - `AEList` and `AEObject` constructors do not accept null parameters 89 | - The `AEList` iterator `current` getter now throws instead of returning null if `current` is undefined (`moveNext` not called or end of Iterable) 90 | - In `SelectorBuilder` the `paramFields` map cannot be set directly any more, but the related method must be used (fields, excludeFields and metaTextScore) 91 | 92 | ## 0.4.2 93 | 94 | - Fixed problem in SelectorBuilder: 95 | if you set a raw map, then you couldn't add any new query expression or you loose the inital raw map "query" section. 96 | - Lint clean-up 97 | - Updated sdk constraint to 2.5.2 98 | 99 | ## 0.4.1 100 | 101 | - class `Set` of aggregation stages moved to class `SetStage` to resolve conflict with `dart:core` library 102 | 103 | ## 0.4.0 - September 30th, 2019 (@alexd1971, @vadimtsushko) 104 | 105 | - Aggregation pipeline builder [PR#14](https://github.com/mongo-dart/mongo_dart_query/pull/14) 106 | 107 | ## 0.3.1 - June 30th, 2018 (@thosakwe, @aemino) 108 | 109 | - Merged [PR #11](https://github.com/mongo-dart/mongo_dart_query/pull/11) from @aemino, 110 | which adds Dart 2 fixes for this package, namely coercin of generic types avoiding implicit casts 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2022 Giorgio Franceschetti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongo_dart_query 2 | 3 | ## ================ 4 | 5 | Query builder for mongo_dart 6 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 4 | # Uncomment to specify additional rules. 5 | # linter: 6 | # rules: 7 | # - camel_case_types 8 | 9 | 10 | ## analyzer: 11 | ### exclude: 12 | ### - path/to/excluded/files/** -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [0813/122800.230:ERROR:registration_protocol_win.cc(107)] CreateFile: Impossibile trovare il file specificato. (0x2) 2 | -------------------------------------------------------------------------------- /lib/mongo_aggregation.dart: -------------------------------------------------------------------------------- 1 | /// Mongodb aggregation library 2 | /// 3 | /// Simplifies to create aggregation pipelines using Dart code. 4 | /// 5 | /// Basic usage: 6 | /// ``` 7 | /// final db = Db('mongodb://127.0.0.1/testdb'); 8 | /// final pipeline = AggregationPipelineBuilder() 9 | /// .addStage( 10 | /// Match(where.eq('status', 'A').map['\$query'])) 11 | /// .addStage( 12 | /// Group( 13 | /// id: Field('cust_id'), 14 | /// fields: { 15 | /// 'total': Sum(Field('amount')) 16 | /// } 17 | /// )).build(); 18 | /// final result = 19 | /// await DbCollection(db, 'orders') 20 | /// .aggregateToStream(pipeline).toList(); 21 | /// ``` 22 | /// Full mongoDB documentation on aggregation queries: 23 | /// https://docs.mongodb.com/manual/aggregation/ 24 | library mongo_aggregation; 25 | 26 | export 'src/mongo_aggregation/aggregation_base.dart'; 27 | export 'src/mongo_aggregation/aggregation_pipeline_builder.dart'; 28 | export 'src/mongo_aggregation/aggregation_stages.dart'; 29 | export 'src/mongo_aggregation/arithmetic_operators.dart'; 30 | export 'src/mongo_aggregation/array_object_operators.dart'; 31 | export 'src/mongo_aggregation/comparison_operators.dart'; 32 | export 'src/mongo_aggregation/date_time_operators.dart'; 33 | export 'src/mongo_aggregation/group_stage_accumulators.dart'; 34 | export 'src/mongo_aggregation/logic_operators.dart'; 35 | export 'src/mongo_aggregation/string_operators.dart'; 36 | export 'src/mongo_aggregation/type_expression_operators.dart'; 37 | export 'src/mongo_aggregation/uncategorized_operators.dart'; 38 | export 'src/geometry_obj.dart'; 39 | export 'src/mongo_aggregation/support_classes/output.dart'; 40 | -------------------------------------------------------------------------------- /lib/mongo_dart_query.dart: -------------------------------------------------------------------------------- 1 | library mongo_dart_query; 2 | 3 | import 'dart:convert' show json; 4 | import 'package:bson/bson.dart'; 5 | import 'src/geometry_obj.dart'; 6 | 7 | export 'src/geometry_obj.dart'; 8 | 9 | part 'src/selector_builder.dart'; 10 | part 'src/modifier_builder.dart'; 11 | -------------------------------------------------------------------------------- /lib/src/geometry_obj.dart: -------------------------------------------------------------------------------- 1 | import '../mongo_aggregation.dart'; 2 | 3 | /// There is 4 | abstract class ShapeOperator extends Operator { 5 | ShapeOperator(super.name, super.args); 6 | } 7 | 8 | enum GeometryObjectType { 9 | // ignore: constant_identifier_names 10 | Point, 11 | // ignore: constant_identifier_names 12 | LineString, 13 | // ignore: constant_identifier_names 14 | Polygon, 15 | // ignore: constant_identifier_names 16 | MultiPoint, 17 | // ignore: constant_identifier_names 18 | MultiLineString, 19 | // ignore: constant_identifier_names 20 | MultiPolygon 21 | } 22 | 23 | /// https://docs.mongodb.com/manual/reference/operator/query/geometry/#mongodb-query-op.-geometry 24 | class Geometry extends ShapeOperator { 25 | Geometry( 26 | {required this.type, 27 | required List coordinates, 28 | Map? crs}) 29 | : super('geometry', { 30 | 'type': type.toString().split('.').last, 31 | 'coordinates': coordinates, 32 | if (crs != null) 'crs': crs 33 | }); 34 | 35 | Geometry.point(List point, {Map? crs}) 36 | : type = GeometryObjectType.Point, 37 | super('geometry', { 38 | 'type': GeometryObjectType.Point.toString().split('.').last, 39 | 'coordinates': point, 40 | if (crs != null) 'crs': crs 41 | }); 42 | 43 | GeometryObjectType type; 44 | } 45 | 46 | /// https://docs.mongodb.com/manual/reference/operator/query/box/#mongodb-query-op.-box 47 | class Box extends ShapeOperator { 48 | Box({required List bottomLeft, required List upperRight}) 49 | : super('box', [bottomLeft, upperRight]); 50 | } 51 | 52 | /// https://docs.mongodb.com/manual/reference/operator/query/box/#mongodb-query-op.-box 53 | class Center extends ShapeOperator { 54 | Center({required List center, required num radius}) 55 | : super('center', [center, radius]); 56 | } 57 | 58 | /// https://docs.mongodb.com/manual/reference/operator/query/box/#mongodb-query-op.-box 59 | class CenterSphere extends ShapeOperator { 60 | CenterSphere({required List center, required num radius}) 61 | : super('centerSphere', [center, radius]); 62 | } 63 | 64 | // TODO missing Polygon 65 | -------------------------------------------------------------------------------- /lib/src/modifier_builder.dart: -------------------------------------------------------------------------------- 1 | part of '../mongo_dart_query.dart'; 2 | 3 | ModifierBuilder get modify => ModifierBuilder(); 4 | 5 | class ModifierBuilder { 6 | Map map = {}; 7 | 8 | @override 9 | String toString() => 'ModifierBuilder($map)'; 10 | 11 | void _updateOperation(String operator, String fieldName, value) { 12 | var opMap = map[operator] as Map?; 13 | if (opMap == null) { 14 | opMap = {}; 15 | map[operator] = opMap; 16 | } 17 | opMap[fieldName] = value; 18 | } 19 | 20 | // ************************ 21 | // *** Field operators 22 | 23 | // Todo 24 | // currentDate -> we need bson to manage dates 25 | 26 | /// Increments the value of the field by the specified amount. 27 | ModifierBuilder inc(String fieldName, value) { 28 | _updateOperation('\$inc', fieldName, value); 29 | return this; 30 | } 31 | 32 | /// Only updates the field if the specified value is less than 33 | /// the existing field value 34 | ModifierBuilder min(String fieldName, value) { 35 | _updateOperation('\$min', fieldName, value); 36 | return this; 37 | } 38 | 39 | /// Only updates the field if the specified value is greater than the 40 | /// existing field value. 41 | ModifierBuilder max(String fieldName, value) { 42 | _updateOperation('\$max', fieldName, value); 43 | return this; 44 | } 45 | 46 | /// Multiplies the value of the field by the specified amount 47 | ModifierBuilder mul(String fieldName, value) { 48 | _updateOperation(r'$mul', fieldName, value); 49 | return this; 50 | } 51 | 52 | ModifierBuilder rename(String oldName, String newName) { 53 | _updateOperation('\$rename', oldName, newName); 54 | return this; 55 | } 56 | 57 | ModifierBuilder set(String fieldName, value) { 58 | _updateOperation('\$set', fieldName, value); 59 | return this; 60 | } 61 | 62 | ModifierBuilder setOnInsert(String fieldName, value) { 63 | _updateOperation('\$setOnInsert', fieldName, value); 64 | return this; 65 | } 66 | 67 | ModifierBuilder unset(String fieldName) { 68 | _updateOperation('\$unset', fieldName, 1); 69 | return this; 70 | } 71 | 72 | // ************************ 73 | // *** Array operators 74 | 75 | /// Adds elements to an array only if they do not already exist in the set. 76 | ModifierBuilder addToSet(String fieldName, value) { 77 | _updateOperation('\$addToSet', fieldName, value); 78 | return this; 79 | } 80 | 81 | ModifierBuilder addEachToSet(String fieldName, List value) { 82 | _updateOperation(r'$addToSet', fieldName, {r'$each': value}); 83 | return this; 84 | } 85 | 86 | /// The popFirst operator removes the first element of an array. 87 | ModifierBuilder popFirst(String fieldName) { 88 | _updateOperation('\$pop', fieldName, -1); 89 | return this; 90 | } 91 | 92 | /// The popLast operator removes the last element of an array. 93 | ModifierBuilder popLast(String fieldName) { 94 | _updateOperation('\$pop', fieldName, 1); 95 | return this; 96 | } 97 | 98 | /// The pull operator removes from an existing array all instances 99 | /// of a value that match a specified condition. 100 | ModifierBuilder pull(String fieldName, value) { 101 | _updateOperation('\$pull', fieldName, value); 102 | return this; 103 | } 104 | 105 | /// The pull operator removes from an existing array all instances 106 | /// of values that match a specified condition. 107 | ModifierBuilder pullAll(String fieldName, List values) { 108 | _updateOperation('\$pullAll', fieldName, values); 109 | return this; 110 | } 111 | 112 | /// Adds an item to an array. 113 | ModifierBuilder push(String fieldName, value) { 114 | _updateOperation('\$push', fieldName, value); 115 | return this; 116 | } 117 | 118 | /// Removes all matching values from an array. 119 | ModifierBuilder pushAll(String fieldName, List values) { 120 | _updateOperation('\$pushAll', fieldName, values); 121 | return this; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/aggregation_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Builder interface 4 | abstract class Builder { 5 | dynamic build(); 6 | const Builder(); 7 | } 8 | 9 | /// Aggregation expression 10 | abstract class AggregationExpr implements Builder { 11 | const AggregationExpr(); 12 | } 13 | 14 | /// Basic aggregation operator 15 | abstract class Operator extends AggregationExpr { 16 | final dynamic _args; 17 | final String _name; 18 | Operator(this._name, this._args); 19 | @override 20 | Map build() => 21 | {'\$$_name': _args is AggregationExpr ? _args.build() : _args}; 22 | } 23 | 24 | /// Basic accumulation operator 25 | abstract class Accumulator extends Operator { 26 | Accumulator(super.name, super.expr); 27 | } 28 | 29 | /// Aggregation expression's list 30 | /// 31 | /// The list is used in aggregation expressions and is aggregation expression as well 32 | class AEList extends Iterable implements AggregationExpr { 33 | final Iterable _iterable; 34 | factory AEList(Iterable iterable) { 35 | //if (iterable == null) return null; 36 | return AEList.internal(iterable.where(_valueIsNotNull).map((value) { 37 | if (value is List) return AEList(value); 38 | if (value is Map) return AEObject(value); 39 | return value; 40 | })); 41 | } 42 | @protected 43 | AEList.internal(this._iterable); 44 | 45 | @override 46 | AEIterator get iterator => AEIterator(_iterable); 47 | @override 48 | List build() => _iterable 49 | .map((expr) => expr is AggregationExpr ? expr.build() : expr) 50 | .toList(); 51 | } 52 | 53 | /// Iterator for [AEList] 54 | class AEIterator implements Iterator { 55 | final Iterable _iterable; 56 | int _currentIndex = -1; 57 | T? _current; 58 | 59 | AEIterator(this._iterable); 60 | 61 | @override 62 | bool moveNext() { 63 | if (_currentIndex + 1 == _iterable.length) { 64 | _current = null; 65 | return false; 66 | } 67 | _current = _iterable.elementAt(++_currentIndex); 68 | return true; 69 | } 70 | 71 | @override 72 | T get current { 73 | if (_current == null) { 74 | throw StateError('The current object is unspecified. ' 75 | 'Check NoveNext() return value before calling the "current" getter.'); 76 | } 77 | return _current!; 78 | } 79 | } 80 | 81 | /// Aggregation expression's object 82 | /// 83 | /// The object is used in aggregation expressions and is aggregation expression as well 84 | class AEObject extends Iterable> 85 | implements AggregationExpr { 86 | final Iterable> _iterable; 87 | 88 | factory AEObject(Map map) { 89 | //if (map == null) return null; 90 | return AEObject.internal(map); 91 | } 92 | @protected 93 | AEObject.internal(Map map) 94 | : _iterable = map.entries.where(_valueIsNotNull).map((entry) { 95 | if (entry.value is List) { 96 | return MapEntry(entry.key, AEList(entry.value as List)); 97 | } 98 | if (entry.value is Map) { 99 | return MapEntry( 100 | entry.key, AEObject(entry.value as Map)); 101 | } 102 | return entry; 103 | }); 104 | @override 105 | AEIterator> get iterator => 106 | AEIterator>(_iterable); 107 | @override 108 | Map build() => 109 | Map.fromEntries(_iterable).map((argName, argValue) => MapEntry( 110 | argName, argValue is AggregationExpr ? argValue.build() : argValue)); 111 | } 112 | 113 | /// Returns `true` if value is not null 114 | /// 115 | /// The function is used to filter not null elements in [AEObject] and [AEList] 116 | /// constuctors 117 | bool _valueIsNotNull(value) => 118 | value is MapEntry ? value.value != null : value != null; 119 | 120 | /// Field path expression 121 | /// 122 | class Field extends AggregationExpr { 123 | final String _fieldPath; 124 | 125 | /// Creates a field path expression 126 | /// 127 | /// [fieldPath] - [String] describing a field path. To traverse an 128 | /// hierarchical document use dot notation. For example: `user.name` 129 | /// 130 | /// After build [Field] will look like `$fieldPath` 131 | const Field(String fieldPath) : _fieldPath = fieldPath; 132 | @override 133 | String build() => '\$$_fieldPath'; 134 | } 135 | 136 | /// Constant expression 137 | class Const extends AggregationExpr { 138 | final dynamic _value; 139 | const Const(this._value); 140 | @override 141 | dynamic build() => _value; 142 | } 143 | 144 | /// Literal expression 145 | /// 146 | /// Literals can be of any type. However, MongoDB parses string literals that 147 | /// start with a dollar sign $ as a path to a field and numeric/boolean literals 148 | /// in expression objects as projection flags. To avoid parsing literals, use 149 | /// the [Literal] expression 150 | class Literal extends AggregationExpr { 151 | final dynamic _expr; 152 | 153 | /// Creates a literal expression 154 | const Literal(this._expr); 155 | @override 156 | Map build() => {'\$literal': _expr}; 157 | } 158 | 159 | /// Aggregation expression's variable 160 | /// 161 | /// [Var] can be used to insert user defined an system variables in aggregation 162 | /// expressions 163 | class Var extends AggregationExpr { 164 | /// Current datetime value. 165 | /// 166 | /// [now] has the same value for all members of the deployment and remains the 167 | /// same throughout all stages of the aggregation pipeline. 168 | static const now = Var('NOW'); 169 | 170 | /// Current timestamp value. 171 | /// 172 | /// [clusterTime] is only available on replica sets and sharded clusters. 173 | /// [clusterTime] has the same value for all members of the deployment and 174 | /// remains the same throughout all stages of the pipeline. 175 | static const clusterTime = Var('CLUSTER_TIME'); 176 | 177 | /// The root document. 178 | /// 179 | /// The top-level document, currently being processed in the aggregation 180 | /// pipeline stage. 181 | static const root = Var('ROOT'); 182 | 183 | /// The start of the field path being processed in the aggregation pipeline stage. 184 | /// 185 | /// Unless documented otherwise, all stages start with curren the same as root. 186 | /// Current is modifiable. However, since $ is equivalent to $$CURRENT., 187 | /// rebinding CURRENT changes the meaning of $ accesses. 188 | static const current = Var('CURRENT'); 189 | 190 | /// A variable which evaluates to the missing value. 191 | /// 192 | /// Allows for the conditional exclusion of fields. In a $projection, a field 193 | /// set to the variable REMOVE is excluded from the output. 194 | /// For an example of its usage, see 195 | /// [Conditionally Exclude Fields](https://docs.mongodb.com/manual/reference/operator/aggregation/project/#remove-example). 196 | static const remove = Var('REMOVE'); 197 | 198 | /// One of the allowed results of a 199 | /// [$redact](https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact) expression. 200 | static const discend = Var('DISCEND'); 201 | 202 | /// One of the allowed results of a 203 | /// [$redact](https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact) expression. 204 | static const prune = Var('PRUNE'); 205 | 206 | /// One of the allowed results of a 207 | /// [$redact](https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact) expression. 208 | static const keep = Var('KEEP'); 209 | 210 | final String _name; 211 | 212 | /// Creates a variable expression 213 | /// 214 | /// After build variable will look like `$$name` 215 | const Var(String name) : _name = name; 216 | 217 | @override 218 | String build() => '\$\$$_name'; 219 | } 220 | 221 | /// Aggregation stage base 222 | abstract class AggregationStage implements AggregationExpr { 223 | final String _name; 224 | final Object _content; 225 | AggregationStage(this._name, this._content); 226 | 227 | @override 228 | Map build() => { 229 | '\$$_name': _content is AggregationExpr 230 | ? (_content as AggregationExpr).build() 231 | : _content 232 | }; 233 | } 234 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/aggregation_pipeline_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// Aggregation pipeline builder 6 | class AggregationPipelineBuilder implements Builder { 7 | @protected 8 | final stages = []; 9 | 10 | /// Adds stage to the pipeline 11 | AggregationPipelineBuilder addStage(AggregationStage stage) { 12 | stages.add(stage); 13 | return this; 14 | } 15 | 16 | /// Builds pipeline 17 | /// 18 | /// Returns aggregation pipeline in format suitable for mongodb aggregate 19 | /// query 20 | @override 21 | List> build() => 22 | stages.map((stage) => stage.build()).toList(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/aggregation_stages.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:mongo_dart_query/mongo_dart_query.dart'; 3 | import 'package:mongo_dart_query/src/mongo_aggregation/common.dart'; 4 | 5 | /// `$addFields` aggregation stage 6 | /// 7 | /// ### Stage description. 8 | /// 9 | /// `$addFields` appends new fields to existing documents. You can include one 10 | /// or more `$addFields` stages in an aggregation pipeline. 11 | /// 12 | /// To add field or fields to embedded documents (including documents in arrays) 13 | /// use the dot notation. 14 | /// 15 | /// To add an element to an existing array field with `$addFields`, use with 16 | /// `$concatArrays`([ConcatArrays]). 17 | /// 18 | /// Example: 19 | /// 20 | /// Dart code 21 | /// ``` 22 | /// AddFields({ 23 | /// 'totalHomework': Sum(Field('homework')), 24 | /// 'totalQuiz': Sum(Field('quiz')) 25 | /// }).build() 26 | /// ``` 27 | /// Equivalent mongoDB aggregation stage: 28 | /// ``` 29 | /// { 30 | /// $addFields: { 31 | /// totalHomework: { $sum: "$homework" } , 32 | /// totalQuiz: { $sum: "$quiz" } 33 | /// } 34 | /// } 35 | /// ``` 36 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/ 37 | class AddFields extends AggregationStage { 38 | /// Creates `$addFields` aggregation stage 39 | AddFields(Map fields) : super('addFields', AEObject(fields)); 40 | } 41 | 42 | /// `$sample` aggregation stage 43 | /// 44 | /// ### Stage description. 45 | /// 46 | /// `$sample` randomly selects a specified number of documents from the aggregation pipeline. 47 | /// This stage can be useful for sampling a subset of documents from a larger dataset. 48 | /// 49 | /// You can include only one `$sample` stage in an aggregation pipeline. 50 | /// 51 | /// Example: 52 | /// 53 | /// Dart code 54 | /// ``` 55 | /// Sample(10).build() 56 | /// ``` 57 | /// Equivalent mongoDB aggregation stage: 58 | /// ``` 59 | /// { 60 | /// $sample: { 61 | /// size: 10 62 | /// } 63 | /// } 64 | /// ``` 65 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/sample/ 66 | class Sample extends AggregationStage { 67 | /// Creates `$sample` aggregation stage 68 | Sample(int size) : super('sample', {'size': size}); 69 | } 70 | 71 | /// `$set` aggregation stage 72 | /// 73 | /// ### Stage description 74 | /// 75 | /// Available since MongoDB version 4.2 76 | /// 77 | /// Adds new fields to documents. `$set` outputs documents that contain all 78 | /// existing fields from the input documents and newly added fields. 79 | /// 80 | /// The `$set` stage is an alias for `$addFields`. 81 | /// 82 | /// `$set` appends new fields to existing documents. You can include one or 83 | /// more $set stages in an aggregation operation. 84 | /// 85 | /// To add field or fields to embedded documents (including documents in 86 | /// arrays) use the dot notation. 87 | /// 88 | /// To add an element to an existing array field with `$set`, use with 89 | /// $concatArrays ([ConcatArrays]). 90 | /// 91 | /// Dart code: 92 | /// ``` 93 | /// SetStage({ 94 | /// 'totalHomework': Sum(Field('homework')), 95 | /// 'totalQuiz': Sum(Field('quiz')) 96 | /// }).build() 97 | /// ``` 98 | /// Equivalent mongoDB aggregation stage: 99 | /// ``` 100 | /// { 101 | /// $set: { 102 | /// totalHomework: { $sum: "$homework" }, 103 | /// totalQuiz: { $sum: "$quiz" } 104 | /// } 105 | /// } 106 | /// ``` 107 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/set/ 108 | class SetStage extends AggregationStage { 109 | /// Creates `$set` aggregation stage 110 | SetStage(Map fields) : super('set', AEObject(fields)); 111 | } 112 | 113 | /// `$setWindowFields` aggregation stage 114 | /// 115 | /// ### Stage description 116 | /// 117 | /// Available since MongoDB version 5.0 118 | /// 119 | /// Performs operations on a specified span of documents in a collection, 120 | /// known as a window, and returns the results based on the chosen 121 | /// window operator. 122 | /// 123 | /// For example, you can use the $setWindowFields stage to output the: 124 | /// - Difference in sales between two documents in a collection. 125 | /// - Sales rankings. 126 | /// - Cumulative sales totals. 127 | /// - Analysis of complex time series information without exporting the data 128 | /// to an external database. 129 | /// 130 | /// Example: 131 | /// 132 | /// Dart code: 133 | /// ``` 134 | /// SetWindowFields( 135 | /// partitionBy: {r'$year': r"$orderDate"}, 136 | /// sortBy: {'orderDate': 1}, 137 | /// output: Output('cumulativeQuantityForYear', Sum(r'$quantity'), 138 | /// documents: ["unbounded", "current"])).build(), 139 | /// ``` 140 | /// Equivalent mongoDB aggregation stage: 141 | /// ``` 142 | /// { 143 | /// r'$setWindowFields': { 144 | /// 'partitionBy': {r'$year': r"$orderDate"}, 145 | /// 'sortBy': {'orderDate': 1}, 146 | /// 'output': { 147 | /// 'cumulativeQuantityForYear': { 148 | /// r'$sum': r"$quantity", 149 | /// 'window': { 150 | /// 'documents': ["unbounded", "current"] 151 | /// } 152 | /// } 153 | /// } 154 | /// } 155 | /// } 156 | /// ``` 157 | /// https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/ 158 | class SetWindowFields extends AggregationStage { 159 | /// Creates `$setWindowFields` aggregation stage 160 | /// 161 | /// * [partitionBy] Optional - Specifies an expression to group the documents. 162 | /// In the $setWindowFields stage, the group of documents is known as a 163 | /// partition. Default is one partition for the entire collection. 164 | /// * [sortBy] Required for some operators 165 | /// Specifies the field(s) to sort the documents by in the partition. 166 | /// Uses the same syntax as the $sort stage. 167 | /// Default is no sorting. 168 | /// * [output] - Specifies the field(s) an related parameters to append to 169 | /// the documents in the output returned by the $setWindowFields stage. 170 | /// Each field is set to the result returned by the window operator. 171 | /// The field can either an Output object, a list of Output Objects or a 172 | /// document containing the explicit description of the output required 173 | SetWindowFields({ 174 | partitionBy, 175 | Map? sortBy, 176 | defaultId, 177 | required dynamic output, 178 | }) : super( 179 | stSetWindowFields, 180 | AEObject({ 181 | if (partitionBy != null) spPartitionBy: partitionBy, 182 | if (sortBy != null) spSortBy: AEObject(sortBy), 183 | 'output': _getOutputDocument(output), 184 | })); 185 | 186 | static AEObject _getOutputDocument(output) { 187 | if (output is Output) { 188 | return AEObject(output.build()); 189 | } else if (output is List) { 190 | return AEObject({for (Output element in output) ...element.build()}); 191 | } else if (output is Map) { 192 | return AEObject(output); 193 | } else { 194 | throw Exception( 195 | 'output parm must be Map, Output or List'); 196 | } 197 | } 198 | } 199 | 200 | /// `$unset` aggregation stage 201 | /// 202 | /// ### Stage description 203 | /// 204 | /// Available since MongoDB version 4.2 205 | /// 206 | /// Removes/excludes fields from documents. 207 | /// 208 | /// Example: 209 | /// 210 | /// Dart code: 211 | /// ``` 212 | /// Unset([ "isbn", "author.first", "copies.warehouse" ]).build() 213 | /// ``` 214 | /// Equivalent mongoDB aggregation stage: 215 | /// ``` 216 | /// { $unset: [ "isbn", "author.first", "copies.warehouse" ] } 217 | /// ``` 218 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/unset/ 219 | class Unset extends AggregationStage { 220 | /// Creates `$unset` aggreagation stage 221 | Unset(List fields) : super('unset', fields); 222 | } 223 | 224 | /// `$bucket` aggregation stage 225 | /// 226 | /// ### Stage description 227 | /// 228 | /// Categorizes incoming documents into groups, called buckets, based on a 229 | /// specified expression and bucket boundaries. 230 | /// 231 | /// Each bucket is represented as a document in the output. The document for 232 | /// each bucket contains an `_id` field, whose value specifies the inclusive 233 | /// lower bound of the bucket and a count field that contains the number of 234 | /// documents in the bucket. The count field is included by default when the 235 | /// output is not specified. 236 | /// 237 | /// `$bucket` only produces output documents for buckets that contain at least 238 | /// one input document. 239 | /// 240 | /// Example: 241 | /// 242 | /// Dart code: 243 | /// ``` 244 | /// Bucket( 245 | /// groupBy: Field('price'), 246 | /// boundaries: [0, 200, 400], 247 | /// defaultId: "Other", 248 | /// output: { 249 | /// 'count': Sum(1), 250 | /// 'titles': Push(Field('title')) 251 | /// } 252 | /// ).build() 253 | /// ``` 254 | /// Equivalent mongoDB aggregation stage: 255 | /// ``` 256 | /// { 257 | /// $bucket: { 258 | /// groupBy: "$price", 259 | /// boundaries: [ 0, 200, 400 ], 260 | /// default: "Other", 261 | /// output: { 262 | /// "count": { $sum: 1 }, 263 | /// "titles" : { $push: "$title" } 264 | /// } 265 | /// } 266 | /// } 267 | /// ``` 268 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/ 269 | class Bucket extends AggregationStage { 270 | /// Creates `$bucket` aggregation stage 271 | /// 272 | /// * [groupBy] - An expression to group documents by. To specify a field 273 | /// path use [Field] object. Unless `$bucket` includes a default 274 | /// specification, each input document must resolve the groupBy field path 275 | /// or expression to a value that falls within one of the ranges specified 276 | /// by the boundaries. 277 | /// * [boundaries] - An array of values based on the [groupBy] expression that 278 | /// specify the boundaries for each bucket. Each adjacent pair of values acts 279 | /// as the inclusive lower boundary and the exclusive upper boundary for the 280 | /// bucket. You must specify at least two boundaries. 281 | /// 282 | /// Example: 283 | /// 284 | /// An array of `[ 0, 5, 10 ]` creates two buckets: 285 | /// 286 | /// * [0, 5) with inclusive lower bound 0 and exclusive upper bound 5. 287 | /// * [5, 10) with inclusive lower bound 5 and exclusive upper bound 10. 288 | /// 289 | /// 290 | /// * [defaultId] - Optional. A literal that specifies the `_id` of an 291 | /// additional bucket that contains all documents whose groupBy expression 292 | /// result does not fall into a bucket specified by boundaries. If 293 | /// unspecified, each input document must resolve the groupBy expression to 294 | /// a value within one of the bucket ranges specified by boundaries or the 295 | /// operation throws an error. The default value must be less than the lowest 296 | /// boundaries value, or greater than or equal to the highest boundaries 297 | /// value. The default value can be of a different type than the entries in 298 | /// boundaries. 299 | /// * [output] - Optional. A document that specifies the fields to include in 300 | /// the output documents in addition to the _id field. To specify the field 301 | /// to include, you must use accumulator expressions. 302 | Bucket( 303 | {required AggregationExpr groupBy, 304 | required List boundaries, 305 | defaultId, 306 | Map? output}) 307 | : super( 308 | 'bucket', 309 | AEObject({ 310 | 'groupBy': groupBy, 311 | 'boundaries': AEList(boundaries), 312 | if (defaultId != null) 'default': defaultId, 313 | if (output != null) 'output': AEObject(output) 314 | })); 315 | } 316 | 317 | /// `$bucketAuto` aggregation stage 318 | /// 319 | /// ### Stage description 320 | /// 321 | /// Categorizes incoming documents into a specific number of groups, called 322 | /// buckets, based on a specified expression. Bucket boundaries are 323 | /// automatically determined in an attempt to evenly distribute the documents 324 | /// into the specified number of buckets. 325 | /// 326 | /// Each bucket is represented as a document in the output. The document for 327 | /// each bucket contains an _id field, whose value specifies the inclusive 328 | /// lower bound and the exclusive upper bound for the bucket, and a count 329 | /// field that contains the number of documents in the bucket. The count 330 | /// field is included by default when the output is not specified. 331 | /// 332 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/ 333 | class BucketAuto extends AggregationStage { 334 | /// Creates `$bucketAuto` aggregation stage 335 | /// 336 | /// * [groupBy] - An expression to group documents by. To specify a field path 337 | /// use [Field] object. 338 | /// * [buckets] - A positive integer that specifies the number of buckets 339 | /// into which input documents are grouped. 340 | /// * [output] - Optional. A document that specifies the fields to include in 341 | /// the output documents in addition to the `_id` field. To specify the field 342 | /// to include, you must use accumulator expressions 343 | /// * [granularity] - Optional. A [Granularity] that specifies the preferred 344 | /// number series to use to ensure that the calculated boundary edges end on 345 | /// preferred round numbers or their powers of 10. 346 | BucketAuto( 347 | {required AggregationExpr groupBy, 348 | required int buckets, 349 | Map? output, 350 | Granularity? granularity}) 351 | : super( 352 | 'bucketAuto', 353 | AEObject({ 354 | 'groupBy': groupBy, 355 | 'buckets': buckets, 356 | if (output != null) 'output': AEObject(output), 357 | if (granularity != null) 'granularity': granularity 358 | })); 359 | } 360 | 361 | /// Granularity for [BucketAuto] 362 | /// 363 | /// Granularity ensures that the boundaries of all buckets adhere to a specified 364 | /// preferred number series. Using a preferred number series provides more 365 | /// control on where the bucket boundaries are set among the range of values in 366 | /// the `groupBy` expression. They may also be used to help logarithmically and 367 | /// evenly set bucket boundaries when the range of the `groupBy` expression 368 | /// scales exponentially. 369 | /// 370 | /// ### Renard Series 371 | /// 372 | /// The Renard number series are sets of numbers derived by taking either the 373 | /// 5th, 10 th, 20 th, 40 th, or 80 th root of 10, then including various powers 374 | /// of the root that equate to values between 1.0 to 10.0 (10.3 in the case of 375 | /// R80). 376 | /// 377 | /// Set granularity to R5, R10, R20, R40, or R80 to restrict bucket boundaries 378 | /// to values in the series. The values of the series are multiplied by a power 379 | /// of 10 when the groupBy values are outside of the 1.0 to 10.0 (10.3 for R80) 380 | /// range. 381 | /// 382 | /// Example: 383 | /// 384 | /// The R5 series is based off of the fifth root of 10, which is 1.58, and 385 | /// includes various powers of this root (rounded) until 10 is reached. The R5 386 | /// series is derived as follows: 387 | /// 388 | /// * 10 0/5 = 1 389 | /// * 10 1/5 = 1.584 ~ 1.6 390 | /// * 10 2/5 = 2.511 ~ 2.5 391 | /// * 10 3/5 = 3.981 ~ 4.0 392 | /// * 10 4/5 = 6.309 ~ 6.3 393 | /// * 10 5/5 = 10 394 | /// 395 | /// The same approach is applied to the other Renard series to offer finer 396 | /// granularity, i.e., more intervals between 1.0 and 10.0 (10.3 for R80). 397 | /// 398 | /// ### E Series 399 | /// 400 | /// The E number series are similar to the Renard series in that they subdivide 401 | /// the interval from 1.0 to 10.0 by the 6 th, 12 th, 24 th, 48 th, 96 th, or 402 | /// 192 nd root of ten with a particular relative error. 403 | /// 404 | /// Set granularity to E6, E12, E24, E48, E96, or E192 to restrict bucket 405 | /// boundaries to values in the series. The values of the series are multiplied 406 | /// by a power of 10 when the groupBy values are outside of the 1.0 to 10.0 407 | /// range. To learn more about the E-series and their respective relative 408 | /// errors, see preferred number series. 409 | /// 410 | /// ### 1-2-5 Series 411 | /// 412 | /// The 1-2-5 series behaves like a three-value Renard series, if such a series 413 | /// existed. 414 | /// 415 | /// Set granularity to 1-2-5 to restrict bucket boundaries to various powers of 416 | /// the third root of 10, rounded to one significant digit. 417 | /// 418 | /// Example: 419 | /// 420 | /// The following values are part of the 1-2-5 series: 0.1, 0.2, 0.5, 1, 2, 5, 421 | /// 10, 20, 50, 100, 200, 500, 1000, and so on… 422 | /// 423 | /// ### Powers of Two Series 424 | /// Set granularity to POWERSOF2 to restrict bucket boundaries to numbers that 425 | /// are a power of two. 426 | /// 427 | /// Example: 428 | /// 429 | /// The following numbers adhere to the power of two Series: 430 | /// 431 | /// * 2^0 = 1 432 | /// * 2^1 = 2 433 | /// * 2^2 = 4 434 | /// * 2^3 = 8 435 | /// * 2^4 = 16 436 | /// * 2^5 = 32 437 | /// * and so on… 438 | /// 439 | /// A common implementation is how various computer components, like memory, 440 | /// often adhere to the POWERSOF2 set of preferred numbers: 441 | /// 442 | /// 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, and so on…. 443 | /// 444 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity 445 | class Granularity extends Const { 446 | static final r5 = Granularity._('R5'); 447 | static final r10 = Granularity._('R10'); 448 | static final r20 = Granularity._('R20'); 449 | static final r40 = Granularity._('R40'); 450 | static final r80 = Granularity._('R80'); 451 | static final g125 = Granularity._('1-2-5'); 452 | static final e6 = Granularity._('E6'); 453 | static final e12 = Granularity._('E12'); 454 | static final e24 = Granularity._('E24'); 455 | static final e48 = Granularity._('E48'); 456 | static final e96 = Granularity._('E96'); 457 | static final e192 = Granularity._('E192'); 458 | static final powersof2 = Granularity._('POWERSOF2'); 459 | 460 | Granularity._(String super.value); 461 | } 462 | 463 | /// `$count` aggregation stage 464 | /// 465 | /// ### Stage description 466 | /// 467 | /// Passes a document to the next stage that contains a count of the number of 468 | /// documents input to the stage. 469 | /// 470 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/count/ 471 | class Count extends AggregationStage { 472 | /// Creates `$count` aggregation stage 473 | /// 474 | /// [fieldName] - is the name of the output field which has the count as its 475 | /// value. [fieldName] must be a non-empty string and must not contain the `.` 476 | /// character. 477 | Count(String fieldName) : super('count', fieldName); 478 | } 479 | 480 | /// `$facet` aggregation stage 481 | /// 482 | /// ### Stage description 483 | /// 484 | /// Processes multiple aggregation pipelines within a single stage on the same 485 | /// set of input documents. Each sub-pipeline has its own field in the output 486 | /// document where its results are stored as an array of documents. 487 | /// 488 | /// The `$facet` stage allows you to create multi-faceted aggregations which 489 | /// characterize data across multiple dimensions, or facets, within a single 490 | /// aggregation stage. Multi-faceted aggregations provide multiple filters and 491 | /// categorizations to guide data browsing and analysis. Retailers commonly 492 | /// use faceting to narrow search results by creating filters on product 493 | /// price, manufacturer, size, etc. 494 | /// 495 | /// Input documents are passed to the `$facet` stage only once. `$facet` enables 496 | /// various aggregations on the same set of input documents, without needing 497 | /// to retrieve the input documents multiple times. 498 | /// 499 | /// Example: 500 | /// 501 | /// Dart code: 502 | /// ``` 503 | /// Facet({ 504 | /// 'categorizedByTags': [ 505 | /// Unwind(Field('tags')), 506 | /// SortByCount(Field('tags')) 507 | /// ], 508 | /// 'categorizedByPrice': [ 509 | /// Match(where.exists('price').map['\$query']), 510 | /// Bucket( 511 | /// groupBy: Field('price'), 512 | /// boundaries: [0, 150, 200, 300, 400], 513 | /// defaultId: 'Other', 514 | /// output: { 515 | /// 'count': Sum(1), 516 | /// 'titles': Push(Field('title')) 517 | /// } 518 | /// ) 519 | /// ], 520 | /// 'categorizedByYears(Auto)': [ 521 | /// BucketAuto( 522 | /// groupBy: Field('year'), 523 | /// buckets: 4 524 | /// ) 525 | /// ] 526 | /// }).build() 527 | /// ``` 528 | /// Equivalent aggreagtion stage: 529 | /// ``` 530 | /// { 531 | /// $facet: { 532 | /// "categorizedByTags": [ 533 | /// { $unwind: "$tags" }, 534 | /// { $sortByCount: "$tags" } 535 | /// ], 536 | /// "categorizedByPrice": [ 537 | /// { $match: { price: { $exists: true } } }, 538 | /// { 539 | /// $bucket: { 540 | /// groupBy: "$price", 541 | /// boundaries: [ 0, 150, 200, 300, 400 ], 542 | /// default: "Other", 543 | /// output: { 544 | /// "count": { $sum: 1 }, 545 | /// "titles": { $push: "$title" } 546 | /// } 547 | /// } 548 | /// } 549 | /// ], 550 | /// "categorizedByYears(Auto)": [ 551 | /// { 552 | /// $bucketAuto: { 553 | /// groupBy: "$year", 554 | /// buckets: 4 555 | /// } 556 | /// } 557 | /// ] 558 | /// } 559 | /// } 560 | /// ``` 561 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/facet/ 562 | class Facet extends AggregationStage { 563 | /// Creates `$facet` aggregation stage 564 | Facet(Map> pipelines) 565 | : super( 566 | 'facet', 567 | AEObject(pipelines 568 | .map((field, stages) => MapEntry(field, AEList(stages))))); 569 | } 570 | 571 | /// `$replaceRoot` aggregation stage 572 | /// 573 | /// ### Stage description 574 | /// 575 | /// Replaces the input document with the specified document. The operation 576 | /// replaces all existing fields in the input document, including the `_id` 577 | /// field. You can promote an existing embedded document to the top level, 578 | /// or create a new document for promotion. 579 | /// 580 | /// Examples: 581 | /// 582 | /// 1. 583 | /// 584 | /// Dart code: 585 | /// 586 | /// ``` 587 | /// ReplaceRoot(Field('name')).build() 588 | /// ``` 589 | /// 590 | /// Equivalent mongoDB aggregation stage: 591 | /// 592 | /// ``` 593 | /// { $replaceRoot: { newRoot: "$name" } } 594 | /// ``` 595 | /// 596 | /// 2. 597 | /// 598 | /// Dart code: 599 | /// 600 | /// ``` 601 | /// ReplaceRoot(MergeObjects([ 602 | /// { 603 | /// '_id': Field('_id'), 604 | /// 'first': '', 605 | /// 'last': '' 606 | /// }, 607 | /// Field('name') 608 | /// ])).build() 609 | /// ``` 610 | /// 611 | /// Equivalent mongoDB aggregation stage: 612 | /// 613 | /// ``` 614 | /// { $replaceRoot: { 615 | /// newRoot: { 616 | /// $mergeObjects: [ { _id: "$_id", first: "", last: "" }, "$name" ] 617 | /// } 618 | /// }} 619 | /// ``` 620 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/ 621 | class ReplaceRoot extends AggregationStage { 622 | /// Creates `$replaceRoot` aggrregation stage 623 | /// 624 | /// The [replacement] document can be any valid expression that resolves to 625 | /// a document. The stage errors and fails if [replacement] is not 626 | /// a document. 627 | ReplaceRoot(replacement) 628 | : super('replaceRoot', AEObject({'newRoot': replacement})); 629 | } 630 | 631 | /// `$replaceWith` aggregation stage 632 | /// 633 | /// ### Stage description 634 | /// 635 | /// Available since MongoDB version 4.2 636 | /// 637 | /// Replaces the input document with the specified document. The operation 638 | /// replaces all existing fields in the input document, including the `_id` 639 | /// field. With `$replaceWith`, you can promote an embedded document to the 640 | /// top-level. You can also specify a new document as the replacement. 641 | /// 642 | /// The `$replaceWith` is an alias for $replaceRoot. 643 | /// 644 | /// Examples: 645 | /// 646 | /// 1. 647 | /// 648 | /// Dart code: 649 | /// 650 | /// ``` 651 | /// ReplaceWith(Field('name')).build() 652 | /// ``` 653 | /// 654 | /// Equivalent mongoDB aggregation stage: 655 | /// 656 | /// ``` 657 | /// { $replaceWith: { newRoot: "$name" } } 658 | /// ``` 659 | /// 660 | /// 2. 661 | /// 662 | /// Dart code: 663 | /// 664 | /// ``` 665 | /// ReplaceWith(MergeObjects([ 666 | /// { 667 | /// '_id': Field('_id'), 668 | /// 'first': '', 669 | /// 'last': '' 670 | /// }, 671 | /// Field('name') 672 | /// ])).build() 673 | /// ``` 674 | /// 675 | /// Equivalent mongoDB aggregation stage: 676 | /// 677 | /// ``` 678 | /// { $replaceWith: { 679 | /// newRoot: { 680 | /// $mergeObjects: [ { _id: "$_id", first: "", last: "" }, "$name" ] 681 | /// } 682 | /// }} 683 | /// ``` 684 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/replaceWith/ 685 | class ReplaceWith extends AggregationStage { 686 | /// Creates `$replaceWith` aggregation stage 687 | /// 688 | /// The [replacement] document can be any valid expression that resolves to a 689 | /// document. 690 | ReplaceWith(replacement) : super('replaceWith', replacement); 691 | } 692 | 693 | /// `$group` aggregation stage 694 | /// 695 | /// ### Stage description 696 | /// 697 | /// Groups documents by some specified expression and outputs to the next 698 | /// stage a document for each distinct grouping. The output documents contain 699 | /// an `_id` field which contains the distinct group by key. The output 700 | /// documents can also contain computed fields that hold the values of some 701 | /// accumulator expression grouped by the `$group`’s `_id` field. `$group` 702 | /// does not order its output documents. 703 | /// 704 | /// Examples: 705 | /// 706 | /// #### Group by Month, Day, and Year 707 | /// 708 | /// The following aggregation operation uses the `$group` stage to group the 709 | /// documents by the month, day, and year and calculates the total price and 710 | /// the average quantity as well as counts the documents per each group: 711 | /// 712 | /// Dart code: 713 | /// ``` 714 | /// Group( 715 | /// id: { 716 | /// 'month': Month(Field('date')), 717 | /// 'day': DayOfMonth(Field('date')), 718 | /// 'year': Year(Field('date')) 719 | /// }, 720 | /// fields: { 721 | /// 'totalPrice': Sum(Multiply([Field('price'), Field('quantity')])), 722 | /// 'averageQuantity': Avg(Field('quantity')), 723 | /// 'count': Sum(1) 724 | /// } 725 | /// ).build() 726 | /// ``` 727 | /// Equivalent mongoDB aggregation stage: 728 | /// ``` 729 | /// { 730 | /// $group : { 731 | /// _id : { 732 | /// month: { $month: { date: "$date" }}, 733 | /// day: { $dayOfMonth: { date: "$date" }}, 734 | /// year: { $year: { date: "$date" }} 735 | /// }, 736 | /// totalPrice: { $sum: { $multiply: [ "$price", "$quantity" ] } }, 737 | /// averageQuantity: { $avg: "$quantity" }, 738 | /// count: { $sum: 1 } 739 | /// } 740 | /// } 741 | /// ``` 742 | /// 743 | /// #### Group by null 744 | /// 745 | /// The following aggregation operation specifies a group `_id` of `null`, 746 | /// calculating the total price and the average quantity as well as counts 747 | /// for all documents in the collection: 748 | /// 749 | /// Dart code: 750 | /// ``` 751 | /// Group( 752 | /// id: BsonNull(), 753 | /// fields: { 754 | /// 'totalPrice': Sum(Multiply([Field('price'), Field('quantity')])), 755 | /// 'averageQuantity': Avg(Field('quantity')), 756 | /// 'count': Sum(1) 757 | /// } 758 | /// ).build() 759 | /// ``` 760 | /// Equivalent mongoDB aggregation stage: 761 | /// ``` 762 | /// { 763 | /// $group : { 764 | /// _id : null, 765 | /// totalPrice: { $sum: { $multiply: [ "$price", "$quantity" ] } }, 766 | /// averageQuantity: { $avg: "$quantity" }, 767 | /// count: { $sum: 1 } 768 | /// } 769 | /// } 770 | /// ``` 771 | /// 772 | /// #### Retrieve Distinct Values 773 | /// 774 | /// The following aggregation operation uses the `$group` stage to group the 775 | /// documents by the item to retrieve the distinct item values: 776 | /// 777 | /// Dart code: 778 | /// ``` 779 | /// Group(id: Field('item')).build() 780 | /// ``` 781 | /// Equivalent mongoDB aggregation stage: 782 | /// ``` 783 | /// { $group : { _id : "$item" } } 784 | /// ``` 785 | /// 786 | /// #### Group title by author 787 | /// 788 | /// The following aggregation operation pivots the data in the books 789 | /// collection to have titles grouped by authors. 790 | /// 791 | /// Dart code: 792 | /// ``` 793 | /// Group( 794 | /// id: Field('author'), 795 | /// fields: {'books': Push(Field('title'))} 796 | /// ).build() 797 | /// ``` 798 | /// Equivalent mongoDB aggregation stage: 799 | /// ``` 800 | /// { $group : { _id : "$author", books: { $push: "$title" } } } 801 | /// ``` 802 | /// 803 | /// #### Group Documents by author 804 | /// 805 | /// The following aggregation operation uses the $$ROOT system variable to 806 | /// group the documents by authors. The resulting documents must not exceed 807 | /// the BSON Document Size limit. 808 | /// 809 | /// Dart code: 810 | /// ``` 811 | /// Group(id: Field('author'), fields: {'books': Push(Var.root)}).build() 812 | /// ``` 813 | /// Equivalent mongoDB aggregation stage: 814 | /// ``` 815 | /// { $group : { _id : "$author", books: { $push: "$$ROOT" } } } 816 | /// ``` 817 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/group/ 818 | class Group extends AggregationStage { 819 | /// Creates `$group` aggregation stage 820 | Group({required id, Map fields = const {}}) 821 | : super( 822 | 'group', 823 | AEObject({'_id': id is Map ? AEObject(id) : id} 824 | ..addAll(fields))); 825 | } 826 | 827 | /// `$match` aggregation stage 828 | /// 829 | /// ### Stage description 830 | /// 831 | /// Filters the documents to pass only the documents that match the specified 832 | /// condition(s) to the next pipeline stage. 833 | /// 834 | /// Examples: 835 | /// 836 | /// 1. Using [SelectorBuilder] query 837 | /// 838 | /// Dart code: 839 | /// ``` 840 | /// Match(where.eq('author', 'dave').map['\$query']).build() 841 | /// ``` 842 | /// Equivalent mongoDB aggregation stage: 843 | /// ``` 844 | /// {$match: {author: "dave"}} 845 | /// ``` 846 | /// 847 | /// 2. Using aggregation expression: 848 | /// 849 | /// Dart code: 850 | /// ``` 851 | /// Match(Expr(Eq(Field('author'), 'dave'))).build() 852 | /// ``` 853 | /// Equivalent mongoDB aggregation stage: 854 | /// ``` 855 | /// {$match: {$expr: {$eq: ['$author', 'dave']}}} 856 | /// ``` 857 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/match/ 858 | class Match extends AggregationStage { 859 | /// Creates `$match` aggreagtion stage 860 | /// 861 | /// [query] can be either a [SelectorBuilder] query part 862 | /// (`selectorBuilder.map['\$query']`) or an aggregation expression wrapped 863 | /// in [Expr] 864 | Match(query) : super('match', query); 865 | } 866 | 867 | /// `$lookup` aggregation stage 868 | /// 869 | /// ### Stage description 870 | /// 871 | /// Performs a left outer join to an unsharded collection in the same database 872 | /// to filter in documents from the “joined” collection for processing. To each 873 | /// input document, the $lookup stage adds a new array field whose elements are 874 | /// the matching documents from the “joined” collection. The $lookup stage 875 | /// passes these reshaped documents to the next stage. 876 | /// 877 | /// Examples: 878 | /// 879 | /// 1. Single Equality Join 880 | /// 881 | /// Dart code: 882 | /// ``` 883 | /// Lookup( 884 | /// from: 'inventory', 885 | /// localField: 'item', 886 | /// foreignField: 'sku', 887 | /// as: 'inventory_docs' 888 | /// ).build() 889 | /// ``` 890 | /// Equivalent mongoDB aggregation stage: 891 | /// ``` 892 | /// { 893 | /// $lookup: { 894 | /// from: "inventory", 895 | /// localField: "item", 896 | /// foreignField: "sku", 897 | /// as: "inventory_docs" 898 | /// } 899 | /// } 900 | /// ``` 901 | /// 902 | /// 2. Specify Multiple Join Conditions: 903 | /// 904 | /// Dart code: 905 | /// ``` 906 | /// Lookup.withPipeline( 907 | /// from: 'warehouses', 908 | /// let: { 909 | /// 'order_item': Field('item'), 910 | /// 'order_qty': Field('ordered') 911 | /// }, 912 | /// pipeline: [ 913 | /// Match(Expr(And([ 914 | /// Eq(Field('stock_item'), Var('order_item')), 915 | /// Gte(Field('instock'), Var('order_qty')) 916 | /// ]))), 917 | /// Project({ 918 | /// 'stock_item': 0, 919 | /// '_id': 0 920 | /// }) 921 | /// ], 922 | /// as: 'stockdata' 923 | /// ).build() 924 | /// ``` 925 | /// Equivalent mongoDB aggregation stage: 926 | /// ``` 927 | /// { 928 | /// $lookup: { 929 | /// from: "warehouses", 930 | /// let: { order_item: "$item", order_qty: "$ordered" }, 931 | /// pipeline: [ 932 | /// { $match: 933 | /// { $expr: 934 | /// { $and: 935 | /// [ 936 | /// { $eq: [ "$stock_item", "$$order_item" ] }, 937 | /// { $gte: [ "$instock", "$$order_qty" ] } 938 | /// ] 939 | /// } 940 | /// } 941 | /// }, 942 | /// { $project: { stock_item: 0, _id: 0 } } 943 | /// ], 944 | /// as: "stockdata" 945 | /// } 946 | /// } 947 | /// ``` 948 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/ 949 | class Lookup extends AggregationStage { 950 | /// Creates ordinary `$lookup` stage 951 | /// 952 | /// * [from] - Specifies the collection in the same database to perform the join 953 | /// with. The from collection cannot be sharded. 954 | /// * [localField] - Specifies the field from the documents input to the 955 | /// `$lookup` stage. `$lookup` performs an equality match on the [localField] to 956 | /// the [foreignField] from the documents of the from collection. If an input 957 | /// document does not contain the [localField], the `$lookup` treats the field as 958 | /// having a value of `null` for matching purposes. 959 | /// * [foreignField] - Specifies the field from the documents in the from 960 | /// collection. `$lookup` performs an equality match on the [foreignField] to 961 | /// the [localField] from the input documents. If a document in the from 962 | /// collection does not contain the [foreignField], the `$lookup` treats the 963 | /// value as `null` for matching purposes. 964 | /// * [as] - Specifies the name of the new array field to add to the input 965 | /// documents. The new array field contains the matching documents from the 966 | /// from collection. If the specified name already exists in the input 967 | /// document, the existing field is overwritten. 968 | Lookup( 969 | {required String from, 970 | required String localField, 971 | required String foreignField, 972 | required String as}) 973 | : super( 974 | 'lookup', 975 | AEObject({ 976 | 'from': from, 977 | 'localField': localField, 978 | 'foreignField': foreignField, 979 | 'as': as 980 | })); 981 | 982 | /// Creates `$lookup` stage with it's own pipeline 983 | /// 984 | /// * [from] - Specifies the collection in the same database to perform the join 985 | /// with. The from collection cannot be sharded. 986 | /// * [let] - Optional. Specifies variables to use in the pipeline field 987 | /// stages. Use the variable expressions to access the fields from the 988 | /// documents input to the $lookup stage. The pipeline cannot directly access 989 | /// the input document fields. Instead, first define the variables for the 990 | /// input document fields, and then reference the variables in the stages in 991 | /// the pipeline. To access the let variables in the pipeline, use the 992 | /// `$expr` ([Expr]) operator. 993 | /// 994 | /// NOTE: 995 | /// 996 | /// The let variables are accessible by the stages in the pipeline, including 997 | /// additional `$lookup` stages nested in the pipeline. 998 | /// * [pipeline] - Specifies the pipeline to run on the joined collection. 999 | /// The pipeline determines the resulting documents from the joined 1000 | /// collection. To return all documents, specify an empty pipeline `[]`. 1001 | /// 1002 | /// The pipeline cannot include the `$out` stage or the `$merge` stage. 1003 | /// 1004 | /// The pipeline cannot directly access the input document fields. Instead, 1005 | /// first define the variables for the input document fields, and then 1006 | /// reference the variables in the stages in the pipeline. 1007 | /// * [as] - Specifies the name of the new array field to add to the input 1008 | /// documents. The new array field contains the matching documents from the 1009 | /// from collection. If the specified name already exists in the input 1010 | /// document, the existing field is overwritten. 1011 | Lookup.withPipeline( 1012 | {required String from, 1013 | required Map let, 1014 | required List pipeline, 1015 | required String as}) 1016 | : super( 1017 | 'lookup', 1018 | AEObject({ 1019 | 'from': from, 1020 | 'let': AEObject(let), 1021 | 'pipeline': AEList(pipeline), 1022 | 'as': as 1023 | })); 1024 | } 1025 | 1026 | /// `$graphLookup` 1027 | /// 1028 | /// ### Stage description 1029 | /// Performs a recursive search on a collection, with options for restricting the search by recursion depth and query filter. 1030 | /// [see mongo db documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/graphLookup/) 1031 | /// 1032 | /// 1033 | /// Dart code: 1034 | /// 1035 | /// ``` 1036 | /// GraphLookup( 1037 | /// from: "follows", 1038 | /// startWith: "user_id", 1039 | /// connectFromField: "my_follows", 1040 | /// connectToField: "followed", 1041 | /// as: "same_follows", 1042 | /// depthField: "depth", 1043 | /// maxDepth: 3, 1044 | /// restrictSearchWithMatch: where.ne("user_id", "user1")); 1045 | /// ``` 1046 | /// 1047 | /// Equivalent mongoDB aggregation stage: 1048 | /// 1049 | /// ``` 1050 | /// $graphLookup: { 1051 | /// from: "follows", 1052 | /// startWith: "$user_id", 1053 | /// connectFromField: "my_follows", 1054 | /// connectToField: "followed", 1055 | /// as: "same_follows", 1056 | /// depthField: "depth", 1057 | /// maxDepth : 3, 1058 | /// restrictSearchWithMatch: { 1059 | /// follower: {$ne: "user1"} 1060 | /// } 1061 | /// } 1062 | /// ``` 1063 | /// 1064 | /// 1065 | class GraphLookup extends AggregationStage { 1066 | GraphLookup( 1067 | {required String from, 1068 | required String startWith, 1069 | required String connectFromField, 1070 | required String connectToField, 1071 | required String as, 1072 | int? maxDepth, 1073 | String? depthField, 1074 | restrictSearchWithMatch}) 1075 | : super( 1076 | 'graphLookup', 1077 | AEObject({ 1078 | 'from': from, 1079 | 'startWith': '\$$startWith', 1080 | 'connectFromField': connectFromField, 1081 | 'connectToField': connectToField, 1082 | 'as': as, 1083 | if (maxDepth != null) 'maxDepth': maxDepth, 1084 | if (depthField != null) 'depthField': depthField, 1085 | if (restrictSearchWithMatch != null) 1086 | 'restrictSearchWithMatch': 1087 | _getRestrictSearchWithMatch(restrictSearchWithMatch) 1088 | })); 1089 | 1090 | static AEObject _getRestrictSearchWithMatch(restrictSearchWithMatch) { 1091 | if (restrictSearchWithMatch is SelectorBuilder) { 1092 | return AEObject(restrictSearchWithMatch.map['\$query']); 1093 | } else if (restrictSearchWithMatch is Map) { 1094 | return AEObject(restrictSearchWithMatch); 1095 | } else { 1096 | throw Exception( 1097 | 'restrictSearchWithMatch must be Map or SelectorBuilder'); 1098 | } 1099 | } 1100 | } 1101 | 1102 | /// `$unwind` aggregation stage 1103 | /// 1104 | /// ### Stage description 1105 | /// 1106 | /// Deconstructs an array field from the input documents to output a document 1107 | /// for each element. Each output document is the input document with the value 1108 | /// of the array field replaced by the element. 1109 | /// 1110 | /// Examples: 1111 | /// 1112 | /// 1. 1113 | /// 1114 | /// Dart code: 1115 | /// ``` 1116 | /// Unwind(Field('sizes')).build() 1117 | /// ``` 1118 | /// Equivalent mongoDB aggregation stage: 1119 | /// ``` 1120 | /// {$unwind : {path: "$sizes"}} 1121 | /// ``` 1122 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/ 1123 | class Unwind extends AggregationStage { 1124 | /// Creates `$unwind` aggregation stage 1125 | /// 1126 | /// * [field] - Field path to an array field. 1127 | /// * [includeArrayIndex] - Optional. The name of a new field to hold the 1128 | /// array index of the element. 1129 | /// * [preserveNullAndEmptyArrays] - Optional. If `true`, if the path is 1130 | /// `null`, missing, or an empty array, `$unwind` outputs the document. If 1131 | /// `false`, `$unwind` does not output a document if the path is `null`, 1132 | /// missing, or an empty array. The default value is `false`. 1133 | Unwind(Field field, 1134 | {String? includeArrayIndex, bool? preserveNullAndEmptyArrays}) 1135 | : super( 1136 | 'unwind', 1137 | AEObject({ 1138 | 'path': field, 1139 | if (includeArrayIndex != null) 1140 | 'includeArrayIndex': includeArrayIndex, 1141 | if (preserveNullAndEmptyArrays != null) 1142 | 'preserveNullAndEmptyArrays': preserveNullAndEmptyArrays 1143 | })); 1144 | } 1145 | 1146 | /// `$project` aggregation stage 1147 | /// 1148 | /// ### Stage description 1149 | /// 1150 | /// Passes along the documents with the requested fields to the next stage in 1151 | /// the pipeline. The specified fields can be existing fields from the input 1152 | /// documents or newly computed fields. 1153 | class Project extends AggregationStage { 1154 | /// Creates `$project` aggreagtion stage 1155 | /// 1156 | /// [specification] have the following forms: 1157 | /// 1158 | /// * ``: `1` or `true` - Specifies the inclusion of a field. 1159 | /// * ``: `0` or `false` - Specifies the exclusion of a field. To 1160 | /// exclude a field conditionally, use the [Var].remove (`REMOVE`) variable 1161 | /// instead. If you specify the exclusion of a field other than `_id`, you 1162 | /// cannot employ any other `$project` specification forms. This restriction 1163 | /// does not apply to conditionally exclusion of a field using the 1164 | /// [Var].remove (`REMOVE`) variable. By default, the `_id` field is included 1165 | /// in the output documents. If you do not need the `_id` field, you have 1166 | /// to exclude it explicitly. 1167 | /// * ``: `` - Adds a new field or resets the value of 1168 | /// an existing field. If the the expression evaluates to [Var].remove 1169 | /// (`$$REMOVE`), the field is excluded in the output. 1170 | /// 1171 | /// Example: 1172 | /// 1173 | /// Dart code: 1174 | /// ``` 1175 | /// Project({ 1176 | /// '_id': 0, 1177 | /// 'title': 1, 1178 | /// 'author': 1 1179 | /// }).build() 1180 | /// ``` 1181 | /// Equivalent mongoDB aggregation stage: 1182 | /// ``` 1183 | /// { $project : { _id: 0, title : 1 , author : 1 } } 1184 | /// ``` 1185 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/project/ 1186 | Project(Map specification) 1187 | : super('project', AEObject(specification)); 1188 | } 1189 | 1190 | /// `$skip` aggregation stage 1191 | /// 1192 | /// ### Stage description 1193 | /// 1194 | /// Skips over the specified number of documents that pass into the stage and 1195 | /// passes the remaining documents to the next stage in the pipeline. 1196 | /// 1197 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/skip/ 1198 | class Skip extends AggregationStage { 1199 | /// Creates `$skip` aggregation stage 1200 | /// 1201 | /// [count] - positive integer that specifies the maximum number of documents 1202 | /// to skip. 1203 | Skip(int count) : super('skip', count); 1204 | } 1205 | 1206 | /// `$limit` aggregation stage 1207 | /// 1208 | /// ### Stage description 1209 | /// 1210 | /// Limits the number of documents passed to the next stage in the pipeline. 1211 | /// 1212 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/limit/ 1213 | class Limit extends AggregationStage { 1214 | /// Creates `$limit` aggregation stage 1215 | /// 1216 | /// [count] - a positive integer that specifies the maximum number of 1217 | /// documents to pass along. 1218 | Limit(int count) : super('limit', count); 1219 | } 1220 | 1221 | /// `$sort` aggregation stage 1222 | /// 1223 | /// ### Stage description 1224 | /// 1225 | /// Sorts all input documents and returns them to the pipeline in sorted order. 1226 | /// 1227 | /// Example: 1228 | /// 1229 | /// Dart code: 1230 | /// ``` 1231 | /// Sort({ 1232 | /// 'age': -1, 1233 | /// 'posts': 1 1234 | /// }).build() 1235 | /// ``` 1236 | /// Equivalent mongoDB aggregation stage: 1237 | /// ``` 1238 | /// { $sort : { age : -1, posts: 1 } } 1239 | /// ``` 1240 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/sort/ 1241 | class Sort extends AggregationStage { 1242 | /// Creates `$sort` aggregation stage 1243 | /// 1244 | /// [specification] - a document that specifies the field(s) to sort by and 1245 | /// the respective sort order. Sort order can have one of the following 1246 | /// values: 1247 | /// 1248 | /// * 1 to specify ascending order. 1249 | /// * -1 to specify descending order. 1250 | Sort(Map specification) 1251 | : super('sort', AEObject(specification)); 1252 | } 1253 | 1254 | /// `$sortByCount` 1255 | /// 1256 | /// ### Stage description 1257 | /// 1258 | /// Groups incoming documents based on the value of a specified expression, 1259 | /// then computes the count of documents in each distinct group. 1260 | /// 1261 | /// Each output document contains two fields: an _id field containing the 1262 | /// distinct grouping value, and a count field containing the number of 1263 | /// documents belonging to that grouping or category. 1264 | /// 1265 | /// The documents are sorted by count in descending order. 1266 | /// 1267 | /// https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/ 1268 | class SortByCount extends AggregationStage { 1269 | /// Creates `$sortByCount` aggregation stage 1270 | /// 1271 | /// [expression] - expression to group by. You can specify any expression 1272 | /// except for a document literal. 1273 | /// 1274 | /// To specify a field path, use [Field]. For example: 1275 | /// 1276 | /// Dart code: 1277 | /// ``` 1278 | /// SortByCount(Field('employee')).build() 1279 | /// ``` 1280 | /// Equivalent mongoDB aggregation stage: 1281 | /// ``` 1282 | /// { $sortByCount: "$employee" } 1283 | /// ``` 1284 | /// 1285 | /// Although you cannot specify a document literal for the group by 1286 | /// expression, you can, however, specify a field or an expression that 1287 | /// evaluates to a document. For example, if employee and business fields are 1288 | /// document fields, then the following is a valid argument to 1289 | /// `$sortByCounts`: 1290 | /// 1291 | /// Dart code: 1292 | /// ``` 1293 | /// SortByCount(MergeObjects([Field('employee'), Field('business')])).build() 1294 | /// ``` 1295 | /// Equivalent mongoDB aggregation stage: 1296 | /// ``` 1297 | /// { $sortByCount: { $mergeObjects: [ "$employee", "$business" ] } } 1298 | /// ``` 1299 | SortByCount(expression) : super('sortByCount', expression); 1300 | } 1301 | 1302 | /// `$geoNear` 1303 | /// 1304 | /// ### Stage description 1305 | /// Outputs documents in order of nearest to farthest from a specified point. 1306 | /// [see mongo db documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#std-label-pipeline-geoNear-key-param-example) 1307 | /// 1308 | /// 1309 | /// Dart code: 1310 | /// 1311 | /// ``` 1312 | /// GeoNear( 1313 | /// near: Geometry.point([-73.99279, 40.719296]), 1314 | /// distanceField: 'dist.calculated', 1315 | /// maxDistance: 2, 1316 | /// query: where.eq('category', 'Parks').map['\$query'], 1317 | /// includeLocs: 'dist.location', 1318 | /// spherical: true 1319 | /// ).build() 1320 | /// ``` 1321 | /// 1322 | /// Equivalent mongoDB aggregation stage: 1323 | /// ``` 1324 | /// { 1325 | /// '$geoNear': { 1326 | /// 'near': { 1327 | /// 'type': 'Point', 1328 | /// 'coordinates': [-73.99279, 40.719296] 1329 | /// }, 1330 | /// 'distanceField': 'dist.calculated', 1331 | /// 'maxDistance': 2, 1332 | /// 'query': {'category': 'Parks'}, 1333 | /// 'includeLocs': 'dist.location', 1334 | /// 'spherical': true 1335 | /// } 1336 | /// } 1337 | /// ``` 1338 | /// 1339 | /// 1340 | class GeoNear extends AggregationStage { 1341 | GeoNear( 1342 | {required Geometry near, 1343 | required String distanceField, 1344 | num? maxDistance, 1345 | num? minDistance, 1346 | bool? spherical, 1347 | dynamic query, 1348 | num? distanceMultiplier, 1349 | String? includeLocs, 1350 | String? key}) 1351 | : assert(near.type == GeometryObjectType.Point, 1352 | '\$geoNear \'near\' field must be Point'), 1353 | super( 1354 | 'geoNear', 1355 | AEObject({ 1356 | 'near': near.build()[r'$geometry'], 1357 | 'distanceField': distanceField, 1358 | if (maxDistance != null) 'maxDistance': maxDistance, 1359 | if (minDistance != null) 'minDistance': minDistance, 1360 | if (spherical != null) 'spherical': spherical, 1361 | if (query != null) 'query': _getQuery(query), 1362 | if (distanceMultiplier != distanceMultiplier) 1363 | 'distanceMultiplier': distanceMultiplier, 1364 | if (includeLocs != null) 'includeLocs': includeLocs, 1365 | if (key != null) 'key': key 1366 | })); 1367 | 1368 | static AEObject _getQuery(query) { 1369 | if (query is SelectorBuilder) { 1370 | return AEObject(query.map['\$query']); 1371 | } else if (query is Map) { 1372 | return AEObject(query); 1373 | } else { 1374 | throw Exception( 1375 | 'restrictSearchWithMatch must be Map or SelectorBuilder'); 1376 | } 1377 | } 1378 | } 1379 | 1380 | /// `$unionWith` aggregation stage 1381 | /// 1382 | /// ### Stage description 1383 | /// 1384 | /// New in version 4.4. 1385 | /// 1386 | /// Performs a union of two collections. $unionWith combines pipeline results 1387 | /// from two collections into a single result set. The stage outputs the 1388 | /// combined result set (including duplicates) to the next stage. 1389 | /// 1390 | /// The order in which the combined result set documents are output 1391 | /// is unspecified. 1392 | /// 1393 | /// Examples: 1394 | /// 1395 | /// Dart code: 1396 | /// ``` 1397 | /// UnionWith( 1398 | /// coll: 'warehouses', 1399 | /// pipeline: [ 1400 | /// Project({ 1401 | /// 'state': 1, 1402 | /// '_id': 0 1403 | /// }) 1404 | /// ], 1405 | /// ).build() 1406 | /// ``` 1407 | /// Equivalent mongoDB aggregation stage: 1408 | /// ``` 1409 | /// { 1410 | /// $unionWith: { 1411 | /// coll: "warehouses", 1412 | /// pipeline: [ 1413 | /// { $project: 1414 | /// { state: 1, _id: 0 } 1415 | /// } 1416 | /// ] 1417 | /// } 1418 | /// } 1419 | /// ``` 1420 | /// https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/ 1421 | class UnionWith extends AggregationStage { 1422 | /// Creates `$UnionWith` stage with it's own pipeline 1423 | /// 1424 | /// * [coll] - The collection or view whose pipeline results you 1425 | /// wish to include in the result set. 1426 | /// * [pipeline] - Optional. An aggregation pipeline to apply to the 1427 | /// specified coll. 1428 | /// [ , , ...] 1429 | /// The pipeline cannot include the $out and $merge stages. 1430 | /// 1431 | /// The combined results from the previous stage and the $unionWith stage 1432 | /// can include duplicates. 1433 | /// 1434 | /// NOTE: 1435 | /// 1436 | /// The $unionWith operation would correspond to the following SQL statement: 1437 | /// ``` 1438 | /// SELECT * 1439 | /// FROM Collection1 1440 | /// WHERE ... 1441 | /// UNION ALL 1442 | /// SELECT * 1443 | /// FROM Collection2 1444 | /// WHERE ... 1445 | /// ``` 1446 | /// The pipeline cannot directly access the input document fields. Instead, 1447 | /// first define the variables for the input document fields, and then 1448 | /// reference the variables in the stages in the pipeline. 1449 | /// * [as] - Specifies the name of the new array field to add to the input 1450 | /// documents. The new array field contains the matching documents from the 1451 | /// from collection. If the specified name already exists in the input 1452 | /// document, the existing field is overwritten. 1453 | UnionWith({required String coll, List? pipeline}) 1454 | : super( 1455 | 'unionWith', 1456 | AEObject({ 1457 | 'coll': coll, 1458 | if (pipeline != null) 'pipeline': AEList(pipeline), 1459 | })); 1460 | } 1461 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/arithmetic_operators.dart: -------------------------------------------------------------------------------- 1 | import 'aggregation_base.dart'; 2 | 3 | /// `$abs` operator 4 | /// 5 | /// Returns the absolute value of an [expr] 6 | class Abs extends Operator { 7 | /// Creates an `$abs` operator expression 8 | Abs(expr) : super('abs', expr); 9 | } 10 | 11 | /// `$add` operator 12 | /// 13 | /// Adds numbers together or adds numbers and a date. If one of the arguments is 14 | /// a date, `$add` treats the other arguments as milliseconds to add to the date. 15 | class Add extends Operator { 16 | /// Creates an `$add` operator expression 17 | Add(List args) : super('add', AEList(args)); 18 | } 19 | 20 | /// `$ceil` operator 21 | /// 22 | /// Returns the smallest integer greater than or equal to the specified number. 23 | class Ceil extends Operator { 24 | /// Creates `$ceil` operator expression 25 | Ceil(expr) : super('ceil', expr); 26 | } 27 | 28 | /// `$divide` operator 29 | /// 30 | /// Divides one number by another and returns the result. 31 | class Divide extends Operator { 32 | /// Creates `$divide` operator expression 33 | Divide(dividend, divisor) : super('divide', AEList([dividend, divisor])); 34 | } 35 | 36 | /// `$exp` operator 37 | /// 38 | /// Raises Euler’s number (i.e. `e` ) to the specified exponent and returns the 39 | /// result. 40 | class Exp extends Operator { 41 | /// Creates `$exp` operator expression 42 | Exp(expr) : super('exp', expr); 43 | } 44 | 45 | /// `$floor` operator 46 | /// 47 | /// Returns the largest integer less than or equal to the specified number. 48 | class Floor extends Operator { 49 | /// Creates `$floor` operator expression 50 | Floor(expr) : super('floor', expr); 51 | } 52 | 53 | /// `$ln` operator 54 | /// 55 | /// Calculates the natural logarithm `ln` of a number and returns the result as 56 | /// a double. 57 | class Ln extends Operator { 58 | /// Creates `$ln` operator expression 59 | Ln(expr) : super('ln', expr); 60 | } 61 | 62 | /// `$log` operator 63 | /// 64 | /// Calculates the `log` of an [expr] in the specified [base] and returns the 65 | /// result as a double. 66 | class Log extends Operator { 67 | /// Creates `$log` operator expression 68 | Log(expr, base) : super('log', AEList([expr, base])); 69 | } 70 | 71 | /// `$log10` operator 72 | /// 73 | /// Calculates the `log` base 10 of an [expr] and returns the result as a 74 | /// double. 75 | class Log10 extends Operator { 76 | /// Creates `$log10` operator expression 77 | Log10(expr) : super('log10', expr); 78 | } 79 | 80 | /// `$mod` operator 81 | /// 82 | /// Divides one number by another and returns the remainder. 83 | class Mod extends Operator { 84 | /// Creates `$mod` operator expression 85 | Mod(dividend, divisor) : super('mod', AEList([dividend, divisor])); 86 | } 87 | 88 | /// `$multiply` operator 89 | /// 90 | /// Multiplies numbers together and returns the result. Pass the arguments to 91 | /// `$multiply` in an array. 92 | class Multiply extends Operator { 93 | /// Creates `$multiply` operator expression 94 | Multiply(List args) : super('multiply', AEList(args)); 95 | } 96 | 97 | /// `$pow` operator 98 | /// 99 | /// Raises an [expr] to the specified [exponent] and returns the result. 100 | class Pow extends Operator { 101 | /// Creates `$pow` operator expression 102 | Pow(expr, exponent) : super('pow', AEList([expr, exponent])); 103 | } 104 | 105 | /// `$round` operator 106 | /// 107 | /// Rounds an [expr] to to a whole integer or to a specified decimal [place]. 108 | class Round extends Operator { 109 | /// Creates `$round` operator expression 110 | Round(expr, [place]) : super('round', AEList([expr, place])); 111 | } 112 | 113 | /// `$sqrt` operator 114 | /// 115 | /// Calculates the square root of a positive number and returns the result as a 116 | /// double. 117 | class Sqrt extends Operator { 118 | /// Creates `$sqrt` operator expression 119 | Sqrt(expr) : super('sqrt', expr); 120 | } 121 | 122 | /// `$subtract` operator 123 | /// 124 | /// Subtracts two numbers to return the difference, or two dates to return the 125 | /// difference in milliseconds, or a date and a number in milliseconds to return 126 | /// the resulting date. 127 | class Subtract extends Operator { 128 | /// Creates `$subtract` operator expression 129 | Subtract(minuend, subtrahend) 130 | : super('subtract', AEList([minuend, subtrahend])); 131 | } 132 | 133 | /// `$trunc` operator 134 | /// 135 | /// Truncates an [expr] to a whole integer or to a specified decimal [place]. 136 | class Trunc extends Operator { 137 | /// Creates `$trunc` operator expression 138 | Trunc(expr, [place]) : super('trunc', AEList([expr, place])); 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/array_object_operators.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// `$arrayElemAt` operator 6 | /// 7 | /// Returns the element at the specified [array] [index]. 8 | class ArrayElemAt extends Operator { 9 | /// Creates `$arrayToObject` operator expression 10 | ArrayElemAt(array, index) : super('arrayElemAt', AEList([array, index])); 11 | } 12 | 13 | /// `$arrayToObject` operator 14 | /// 15 | /// Converts an array into a single document; the array must be either: 16 | /// 17 | /// an array of two-element arrays where the first element is the field name, 18 | /// and the second element is the field value: 19 | /// ``` 20 | /// [ [ "item", "abc123"], [ "qty", 25 ] ] 21 | /// ``` 22 | /// or 23 | /// 24 | /// an array of documents that contains two fields, `k` and `v` where: 25 | /// 26 | /// * The `k` field contains the field name. 27 | /// * The `v` field contains the value of the field. 28 | class ArrayToObject extends Operator { 29 | /// Creates `$arrayToObject` operator expression 30 | ArrayToObject(array) 31 | : super('arrayToObject', array is List ? AEList(array) : array); 32 | } 33 | 34 | /// `$concatArrays` operator 35 | /// 36 | /// Concatenates [arrays] to return the concatenated array. 37 | class ConcatArrays extends Operator { 38 | /// Creates `$concatArrays` operator expression 39 | ConcatArrays(List arrays) 40 | : super('concatArrays', 41 | AEList(arrays.map((elem) => elem is List ? AEList(elem) : elem))); 42 | } 43 | 44 | /// `$filter` operator 45 | /// 46 | /// Selects a subset of an array to return based on the specified condition. 47 | /// Returns an array with only those elements that match the condition. The 48 | /// returned elements are in the original order. 49 | class Filter extends Operator { 50 | /// Creates `$filter` operator expression 51 | /// 52 | /// * [input] - an expression that resolves to an array. 53 | /// * [as] - Optional. A name for the variable that represents each individual 54 | /// element of the input array. If no name is specified, the variable name 55 | /// defaults to `this`. 56 | /// * [cond] - An expression that resolves to a boolean value used to determine 57 | /// if an element should be included in the output array. The expression 58 | /// references each element of the input array individually with the variable 59 | /// name specified in [as]. 60 | Filter({required input, String? as, required cond}) 61 | : super('filter', 62 | AEObject({'input': input, if (as != null) 'as': as, 'cond': cond})); 63 | } 64 | 65 | /// `$in` operator 66 | /// 67 | /// Returns a boolean indicating whether a specified [value] is in an [array]. 68 | class In extends Operator { 69 | /// Creates `$in` operator expression 70 | In(value, array) 71 | : super('in', AEList([value, array is List ? AEList(array) : array])); 72 | } 73 | 74 | /// `$indexOfArray` operator 75 | /// 76 | /// Searches an [array] for an occurence of a specified [value] and returns the 77 | /// array index (zero-based) of the first occurence. If the value is not found, 78 | /// returns `-1`. 79 | class IndexOfArray extends Operator { 80 | /// Creates `$indexOfArray` operator expression 81 | /// 82 | /// * [array] - Can be any valid expression as long as it resolves to an array. 83 | /// * [value] - Can be any valid expression. 84 | /// * [start] - Optional. An integer, or a number that can be represented as 85 | /// integers (such as 2.0), that specifies the starting index position for the 86 | /// search. Can be any valid expression that resolves to a non-negative integral 87 | /// number. If unspecified, the starting index position for the search is the 88 | /// first element. 89 | IndexOfArray(array, value, start, end) 90 | : super('indexOfArray', 91 | AEList([array is List ? AEList(array) : array, value, start, end])); 92 | } 93 | 94 | /// `$isArray` operator 95 | /// 96 | /// Determines if the operand is an array. Returns a boolean. 97 | class IsArray extends Operator { 98 | /// Creates `$isArray` operator expression 99 | IsArray(expr) : super('isArray', expr); 100 | } 101 | 102 | /// `$map` operator 103 | /// 104 | /// Applies an expression to each item in an array and returns an array with the 105 | /// applied results. 106 | /// name specified in [as]. 107 | class MapOp extends Operator { 108 | /// Creates `$map` operator expression 109 | /// 110 | /// * [input] - An expression that resolves to an array. 111 | /// * [as] - Optional. A name for the variable that represents each individual 112 | /// element of the input array. If no name is specified, the variable name 113 | /// defaults to `this`. 114 | /// * [inExpr] - An expression that is applied to each element of the input 115 | /// array. The expression references each element individually with the variable 116 | MapOp({@required input, String? as, @required inExpr}) 117 | : super( 118 | 'map', 119 | AEObject({ 120 | 'input': input is List ? AEList(input) : input, 121 | if (as != null) 'as': as, 122 | 'in': inExpr 123 | })); 124 | } 125 | 126 | /// `$objectToArray` 127 | /// 128 | /// Converts a document to an array. The return array contains an element for 129 | /// each field/value pair in the original document. Each element in the return 130 | /// array is a document that contains two fields `k` and `v`: 131 | /// 132 | /// * The `k` field contains the field name in the original document. 133 | /// * The `v` field contains the value of the field in the original document. 134 | class ObjectToArray extends Operator { 135 | /// Creates `$objectToArray` operator expression 136 | ObjectToArray(expr) 137 | : super('objectToArray', 138 | expr is Map ? AEObject(expr) : expr); 139 | } 140 | 141 | /// `$range` operator 142 | /// 143 | /// Returns an array whose elements are a generated sequence of numbers. `$range` 144 | /// generates the sequence from the specified starting number by successively 145 | /// incrementing the starting number by the specified step value up to but not 146 | /// including the end point. 147 | class Range extends Operator { 148 | /// Creates `$range` operator expression 149 | /// 150 | /// * [start] - An integer that specifies the start of the sequence. Can be any 151 | /// valid expression that resolves to an integer. 152 | /// * [end] - An integer that specifies the exclusive upper limit of the sequence. 153 | /// Can be any valid expression that resolves to an integer. 154 | /// * [step] - Optional. An integer that specifies the increment value. Can be 155 | /// any valid expression that resolves to a non-zero integer. Defaults to 1. 156 | Range(start, end, [step]) : super('range', AEList([start, end, step])); 157 | } 158 | 159 | /// `$reduce` operator 160 | /// 161 | /// Applies an expression to each element in an array and combines them into a 162 | /// single value. 163 | class Reduce extends Operator { 164 | /// Creates `$reduce` operator expression 165 | /// 166 | /// * [input] - Can be any valid expression that resolves to an array. If the 167 | /// argument resolves to a value of null or refers to a missing field, $reduce 168 | /// returns null. If the argument does not resolve to an array or null nor refers 169 | /// to a missing field, `$reduce` returns an error. 170 | /// * [initialValue] - The initial cumulative value set before [inExpr] is applied to 171 | /// the first element of the input array. 172 | /// * [inExpr] - A valid expression that $reduce applies to each element in the 173 | /// input array in left-to-right order. Wrap the input value with $reverseArray 174 | /// to yield the equivalent of applying the combining expression from right-to-left. 175 | /// During evaluation of the in expression, two variables will be available: 176 | /// 177 | /// * `value` is the variable that represents the cumulative value of the expression. 178 | /// * `this` is the variable that refers to the element being processed. 179 | Reduce({@required input, @required initialValue, @required inExpr}) 180 | : super( 181 | 'reduce', 182 | AEObject( 183 | {'input': input, 'initialValue': initialValue, 'in': inExpr})); 184 | } 185 | 186 | /// `$reverseArray` operator 187 | /// 188 | /// Accepts an array expression as an argument and returns an array with the 189 | /// elements in reverse order. 190 | class ReverseArray extends Operator { 191 | /// Creates `$reverseArray` operator expression 192 | ReverseArray(array) 193 | : super('reverseArray', array is List ? AEList(array) : array); 194 | } 195 | 196 | /// `$size` operator 197 | /// 198 | /// Counts and returns the total the number of items in an array. 199 | class Size extends Operator { 200 | /// Creates `$size` operator expression 201 | Size(array) : super('size', array is List ? AEList(array) : array); 202 | } 203 | 204 | /// `$slice` operator 205 | /// 206 | /// Returns a subset of an array. 207 | class Slice extends Operator { 208 | /// Creates `$slice` operator expression 209 | /// 210 | /// * [array] - Any valid expression as long as it resolves to an array. 211 | /// * [position] - Optional. Any valid expression as long as it resolves to an 212 | /// integer. 213 | /// 214 | /// * If positive, `$slice` determines the starting position from the start of 215 | /// the array. If [position] is greater than the number of elements, the `$slice` 216 | /// returns an empty array. 217 | /// *If negative, `$slice` determines the starting position from the end of the 218 | /// array. If the absolute value of the [position] is greater than the number of 219 | /// elements, the starting position is the start of the array. 220 | /// * [n] - Any valid expression as long as it resolves to an integer. If 221 | /// [position] is specified, [n] must resolve to a positive integer. 222 | /// 223 | /// * If positive, $slice returns up to the first n elements in the array. If 224 | /// the [position] is specified, `$slice` returns the first n elements starting 225 | /// from the position. 226 | /// * If negative, `$slice` returns up to the last `n` elements in the array. [n] 227 | /// cannot resolve to a negative number if [position] is specified 228 | Slice(array, n, [position]) 229 | : super('slice', 230 | AEList([array is List ? AEList(array) : array, position, n])); 231 | } 232 | 233 | /// `$zip` operator 234 | /// 235 | /// Transposes an array of input arrays so that the first element of the output 236 | /// array would be an array containing, the first element of the first input 237 | /// array, the first element of the second input array, etc. 238 | /// 239 | /// For example, $zip would transform `[ [ 1, 2, 3 ], [ "a", "b", "c" ] ]` into 240 | /// `[ [ 1, "a" ], [ 2, "b" ], [ 3, "c" ] ]`. 241 | class Zip extends Operator { 242 | /// Creates `$zip` operator expression 243 | /// 244 | /// * [inputs] - An array of expressions that resolve to arrays. The elements 245 | /// of these input arrays combine to form the arrays of the output array. 246 | /// If any of the inputs arrays resolves to a value of null or refers to a 247 | /// missing field, $zip returns null. If any of the inputs arrays does not 248 | /// resolve to an array or null nor refers to a missing field, $zip returns an 249 | /// error. 250 | /// * [useLongestLength] - A boolean which specifies whether the length of the 251 | /// longest array determines the number of arrays in the output array. The 252 | /// default value is false: the shortest array length determines the number of 253 | /// arrays in the output array. 254 | /// * [defaults] - An array of default element values to use if the input arrays 255 | /// have different lengths. You must specify useLongestLength: true along with 256 | /// this field, or else `$zip` will return an error. If [useLongestLength]: 257 | /// `true` but [defaults] is empty or not specified, `$zip` uses `null` as the 258 | /// default value. If specifying a non-empty [defaults], you must specify a 259 | /// default for each input array or else `$zip` will return an error. 260 | Zip({required List inputs, bool useLongestLength = false, List? defaults}) 261 | : super( 262 | 'zip', 263 | AEObject({ 264 | 'inputs': AEList( 265 | inputs.map((elem) => elem is List ? AEList(elem) : elem)), 266 | 'useLongestLength': useLongestLength, 267 | if (defaults != null && defaults.isNotEmpty) 268 | 'defaults': AEList(defaults) 269 | })); 270 | } 271 | 272 | /// `$mergeObjects` operator 273 | /// 274 | /// Combines multiple documents into a single document. 275 | /// 276 | /// * When used as a `$group` stage accumulator, `$mergeObjects` has the 277 | /// following form: 278 | /// ``` 279 | /// { $mergeObjects: } 280 | /// ``` 281 | /// * When used in other expressions, including in the $group stage but not as 282 | /// an accumulator: 283 | /// ``` 284 | /// { $mergeObjects: [ , , ... ] } 285 | /// ``` 286 | /// The can be any valid expression that resolves to a document. 287 | class MergeObjects extends Accumulator { 288 | /// Creates `$mergeObjects` operator expression 289 | MergeObjects(objects) 290 | : super( 291 | 'mergeObjects', 292 | objects is List 293 | ? AEList(objects.map( 294 | (obj) => obj is Map ? AEObject(obj) : obj)) 295 | : objects); 296 | } 297 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/common.dart: -------------------------------------------------------------------------------- 1 | // Stages 2 | const stSetWindowFields = 'setWindowFields'; 3 | 4 | // Stage parms 5 | const spOutput = 'output'; 6 | const spPartitionBy = 'partitionBy'; 7 | const spSortBy = 'sortBy'; 8 | const spWindow = 'window'; 9 | const spDocuments = 'documents'; 10 | const spRange = 'range'; 11 | const spUnit = 'unit'; 12 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/comparison_operators.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// `$cmp` operator 6 | /// 7 | /// Compares two values and returns: 8 | /// 9 | /// * -1 if the first value is less than the second. 10 | /// * 1 if the first value is greater than the second. 11 | /// * 0 if the two values are equivalent. 12 | class Cmp extends Operator { 13 | /// Creates `$cmp` operator expression 14 | Cmp(a, b) : super('cmp', AEList([a, b])); 15 | } 16 | 17 | /// `$eq` operator 18 | /// 19 | /// Compares two values and returns: 20 | /// 21 | /// * `true` when the values are equivalent. 22 | /// * `false` when the values are not equivalent. 23 | class Eq extends Operator { 24 | /// Creates `$eq` operator expression 25 | Eq(a, b) : super('eq', AEList([a, b])); 26 | } 27 | 28 | /// `$gt` operator 29 | /// 30 | /// Compares two values and returns: 31 | /// 32 | /// * `true` when the first value is greater than the second value. 33 | /// * `false` when the first value is less than or equivalent to the second 34 | /// value. 35 | class Gt extends Operator { 36 | /// Creates `$gt` operator expression 37 | Gt(a, b) : super('gt', AEList([a, b])); 38 | } 39 | 40 | /// `$gte` operator 41 | /// 42 | /// Compares two values and returns: 43 | /// 44 | /// * `true` when the first value is greater than or equivalent to the second 45 | /// value. 46 | /// * `false` when the first value is less than the second value. 47 | class Gte extends Operator { 48 | /// Creates `$gte` operator expression 49 | Gte(a, b) : super('gte', AEList([a, b])); 50 | } 51 | 52 | /// `$lt` operator 53 | /// 54 | /// Compares two values and returns: 55 | /// 56 | /// * `true` when the first value is less than the second value. 57 | /// * `false` when the first value is greater than or equivalent to the second 58 | /// value. 59 | class Lt extends Operator { 60 | /// Creates `$lt` operator expression 61 | Lt(a, b) : super('lt', AEList([a, b])); 62 | } 63 | 64 | /// `lte` operator 65 | /// 66 | /// Compares two values and returns: 67 | /// 68 | /// * `true` when the first value is less than or equivalent to the second value. 69 | /// * `false` when the first value is greater than the second value. 70 | class Lte extends Operator { 71 | /// Creates `$lte` operator expression 72 | Lte(a, b) : super('lte', AEList([a, b])); 73 | } 74 | 75 | /// `$ne` operator 76 | /// 77 | /// Compares two values and returns: 78 | /// 79 | /// * `true` when the values are not equivalent. 80 | /// * `false` when the values are equivalent. 81 | class Ne extends Operator { 82 | /// Creates `$ne` operator expression 83 | Ne(a, b) : super('ne', AEList([a, b])); 84 | } 85 | 86 | /// `$cond` operator 87 | /// 88 | /// Evaluates a boolean expression to return one of the two specified return 89 | /// expressions. 90 | /// 91 | /// The arguments can be any valid expression. 92 | class Cond extends Operator { 93 | /// Creates `$cond` operator expression 94 | Cond({@required ifExpr, @required thenExpr, @required elseExpr}) 95 | : super('cond', AEList([ifExpr, thenExpr, elseExpr])); 96 | } 97 | 98 | /// `$ifNull` operator 99 | /// 100 | /// Evaluates an [expression] and returns the value of the [expression] if the 101 | /// [expression] evaluates to a non-null value. If the [expression] evaluates 102 | /// to a null value, including instances of undefined values or missing fields, 103 | /// returns the value of the [replacement] expression. 104 | class IfNull extends Operator { 105 | /// Creates `$ifNull` operator expression 106 | IfNull(expression, replacement) 107 | : super('ifNull', AEList([expression, replacement])); 108 | } 109 | 110 | /// `$switch` operator 111 | /// 112 | /// Evaluates a series of case expressions. When it finds an expression which 113 | /// evaluates to true, $switch executes a specified expression and breaks out 114 | /// of the control flow. 115 | class Switch extends Operator { 116 | /// Creates `$switch` operator expression 117 | /// 118 | /// * [branches] - An array of control branch object. Each branch is an 119 | /// instance of [Case] 120 | /// * [defaultExpr] - Optional. The path to take if no branch case expression 121 | /// evaluates to true. Although optional, if default is unspecified and no 122 | /// branch case evaluates to true, $switch returns an error. 123 | Switch({required List branches, defaultExpr}) 124 | : super( 125 | 'switch', 126 | AEObject({ 127 | 'branches': AEList(branches), 128 | if (defaultExpr != null) 'default': defaultExpr 129 | })); 130 | } 131 | 132 | /// Case branch for [Switch] operator 133 | class Case extends AEObject { 134 | /// Creates [Case] branch for Switch operator 135 | /// 136 | /// * [caseExpr] - Can be any valid expression that resolves to a boolean. If 137 | /// the result is not a boolean, it is coerced to a boolean value. 138 | /// * [thenExpr] - Can be any valid expression. 139 | Case({required AggregationExpr caseExpr, @required thenExpr}) 140 | : super.internal({'case': caseExpr, 'then': thenExpr}); 141 | } 142 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/date_time_operators.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// `$dateFromParts` 6 | class DateFromParts extends Operator { 7 | /// Creates `$dateFromParts` operator expression 8 | /// 9 | /// Constructs and returns a Date object given the date’s constituent 10 | /// properties. 11 | DateFromParts( 12 | {@required year, month, day, hour, minute, second, millisecond, timezone}) 13 | : super( 14 | 'dateFromParts', 15 | AEObject({ 16 | 'year': year, 17 | 'month': month, 18 | 'day': day, 19 | 'hour': hour, 20 | 'minute': minute, 21 | 'second': second, 22 | 'millisecond': millisecond, 23 | 'timezone': timezone 24 | })); 25 | } 26 | 27 | /// ISO date from parts 28 | /// 29 | /// Variant of `$dateFromParts` operator 30 | class IsoDateFromParts extends Operator { 31 | /// Creates `$dateFromParts` operator expression 32 | /// 33 | /// Uses ISO Week Date fields to construct Date 34 | IsoDateFromParts( 35 | {@required year, week, day, hour, minute, second, millisecond, timezone}) 36 | : super( 37 | 'dateFromParts', 38 | AEObject({ 39 | 'isoWeekYear': year, 40 | 'isoWeek': week, 41 | 'isoDayOfWeek': day, 42 | 'hour': hour, 43 | 'minute': minute, 44 | 'second': second, 45 | 'millisecond': millisecond, 46 | 'timezone': timezone 47 | })); 48 | } 49 | 50 | /// `$dateFromString` operator 51 | class DateFromString extends Operator { 52 | /// Creates `$dateFromString` operator expression 53 | /// 54 | /// Converts a date/time string to a date object. 55 | /// 56 | /// * [dateString] - The date/time string to convert to a date object. 57 | /// * [format] - Optional. The date format specification of the [dateString]. 58 | /// The format can be any expression that evaluates to a string literal, 59 | /// containing 0 or more format specifiers. 60 | /// * {timezone} - Optional. The time zone to use to format the date. 61 | /// * [onError] - Optional. If $dateFromString encounters an error while 62 | /// parsing the given dateString, it outputs the result value of the provided 63 | /// [onError] expression. This result value can be of any type. If you do not 64 | /// specify [onError], `$dateFromString` throws an error if it cannot parse 65 | /// [dateString]. 66 | /// * [onNull] - Optional. If the [dateString] provided to `$dateFromString` is 67 | /// `null` or missing, it outputs the result value of the provided [onNull] 68 | /// expression. This result value can be of any type. If you do not specify 69 | /// [onNull] and dateString is null or missing, then $dateFromString outputs null. 70 | DateFromString({@required dateString, format, timezone, onError, onNull}) 71 | : super( 72 | 'dateFromString', 73 | AEObject({ 74 | 'dateString': dateString, 75 | 'format': format, 76 | 'timezone': timezone, 77 | 'onError': onError, 78 | 'onNull': onNull 79 | })); 80 | } 81 | 82 | /// `$dateToParts` operator 83 | class DateToParts extends Operator { 84 | /// Creates `$dateToParts` operator expression 85 | /// 86 | /// Returns a document that contains the constituent parts of a given BSON Date 87 | /// value as individual properties. The properties returned are year, month, 88 | /// day, hour, minute, second and millisecond. You can set the iso8601 property 89 | /// to true to return the parts representing an ISO week date instead. This will 90 | /// return a document where the properties are isoWeekYear, isoWeek, 91 | /// isoDayOfWeek, hour, minute, second and millisecond. 92 | DateToParts(date, {timezone, bool iso8601 = false}) 93 | : super('dateToParts', 94 | AEObject({'date': date, 'timezone': timezone, 'iso8601': iso8601})); 95 | } 96 | 97 | /// `$dayOfMonth` operator 98 | class DayOfMonth extends Operator { 99 | /// Creates `$dayOfMonth` operator expression 100 | /// 101 | /// Returns the day of the month for a date as a number between 1 and 31. 102 | /// 103 | /// * [date] - The date to which the operator is applied. [date] must be a 104 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 105 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 106 | /// must be a valid expression that resolves to a string formatted as either 107 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 108 | /// the result is displayed in UTC. 109 | DayOfMonth(date, {timezone}) 110 | : super('dayOfMonth', AEObject({'date': date, 'timezone': timezone})); 111 | } 112 | 113 | /// `$dayOfWeek` operator 114 | class DayOfWeek extends Operator { 115 | /// Creates `$dayOfWeek` operator expression 116 | /// 117 | /// Returns the day of the week for a date as a number between 1 (Sunday) and 118 | /// 7 (Saturday). 119 | /// 120 | /// * [date] - The date to which the operator is applied. [date] must be a 121 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 122 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 123 | /// must be a valid expression that resolves to a string formatted as either 124 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 125 | /// the result is displayed in UTC. 126 | DayOfWeek(date, {timezone}) 127 | : super('dayOfWeek', AEObject({'date': date, 'timezone': timezone})); 128 | } 129 | 130 | /// `$dayOfYear` operator 131 | class DayOfYear extends Operator { 132 | /// Creates `$dayOfYear` operator expression 133 | /// 134 | /// Returns the day of the year for a date as a number between 1 and 366. 135 | /// 136 | /// * [date] - The date to which the operator is applied. [date] must be a 137 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 138 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 139 | /// must be a valid expression that resolves to a string formatted as either 140 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 141 | /// the result is displayed in UTC. 142 | DayOfYear(date, {timezone}) 143 | : super('dayOfYear', AEObject({'date': date, 'timezone': timezone})); 144 | } 145 | 146 | /// `$hour` operator 147 | class Hour extends Operator { 148 | /// Creates `$hour` operator expression 149 | /// 150 | /// Returns the hour portion of a date as a number between 0 and 23. 151 | /// 152 | /// * [date] - The date to which the operator is applied. [date] must be a 153 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 154 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 155 | /// must be a valid expression that resolves to a string formatted as either 156 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 157 | /// the result is displayed in UTC. 158 | Hour(date, {timezone}) 159 | : super('hour', AEObject({'date': date, 'timezone': timezone})); 160 | } 161 | 162 | /// `$isoDayOfWeek` operator 163 | class IsoDayOfWeek extends Operator { 164 | /// Creates `$isoDayOfWeek` operator expression 165 | /// 166 | /// Returns the weekday number in ISO 8601 format, ranging from 1 (for Monday) 167 | /// to 7 (for Sunday). 168 | /// 169 | /// * [date] - The date to which the operator is applied. [date] must be a 170 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 171 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 172 | /// must be a valid expression that resolves to a string formatted as either 173 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 174 | /// the result is displayed in UTC. 175 | IsoDayOfWeek(date, {timezone}) 176 | : super('isoDayOfWeek', AEObject({'date': date, 'timezone': timezone})); 177 | } 178 | 179 | /// `$isoWeek` operator 180 | class IsoWeek extends Operator { 181 | /// Creates `$isoWeek` operator expression 182 | /// 183 | /// Returns the week number in ISO 8601 format, ranging from 1 to 53. Week 184 | /// numbers start at 1 with the week (Monday through Sunday) that contains 185 | /// the year’s first Thursday. 186 | /// 187 | /// * [date] - The date to which the operator is applied. [date] must be a 188 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 189 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 190 | /// must be a valid expression that resolves to a string formatted as either 191 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 192 | /// the result is displayed in UTC. 193 | IsoWeek(date, {timezone}) 194 | : super('isoWeek', AEObject({'date': date, 'timezone': timezone})); 195 | } 196 | 197 | /// `$isoWeekYear` operator 198 | class IsoWeekYear extends Operator { 199 | /// Creates `$isoWeekYear` operator expression 200 | /// 201 | /// Returns the year number in ISO 8601 format. The year starts with the 202 | /// Monday of week 1 and ends with the Sunday of the last week. 203 | /// 204 | /// * [date] - The date to which the operator is applied. [date] must be a 205 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 206 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 207 | /// must be a valid expression that resolves to a string formatted as either 208 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 209 | /// the result is displayed in UTC. 210 | IsoWeekYear(date, {timezone}) 211 | : super('isoWeekYear', AEObject({'date': date, 'timezone': timezone})); 212 | } 213 | 214 | /// `$millisecond` operator 215 | class Millisecond extends Operator { 216 | /// Creates `$millisecond` operator expression 217 | /// 218 | /// Returns the millisecond portion of a date as an integer between 0 and 999. 219 | /// 220 | /// * [date] - The date to which the operator is applied. [date] must be a 221 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 222 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 223 | /// must be a valid expression that resolves to a string formatted as either 224 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 225 | /// the result is displayed in UTC. 226 | Millisecond(date, {timezone}) 227 | : super('millisecond', AEObject({'date': date, 'timezone': timezone})); 228 | } 229 | 230 | /// `$minute` operator 231 | class Minute extends Operator { 232 | /// Creates `$minute` operator expression 233 | /// 234 | /// Returns the minute portion of a date as a number between 0 and 59. 235 | /// 236 | /// 237 | /// * [date] - The date to which the operator is applied. [date] must be a 238 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 239 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 240 | /// must be a valid expression that resolves to a string formatted as either 241 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 242 | /// the result is displayed in UTC. 243 | Minute(date, {timezone}) 244 | : super('minute', AEObject({'date': date, 'timezone': timezone})); 245 | } 246 | 247 | /// `$month` operator 248 | class Month extends Operator { 249 | /// Creates `$month` operator expression 250 | /// 251 | /// Returns the month of a date as a number between 1 and 12. 252 | /// 253 | /// * [date] - The date to which the operator is applied. [date] must be a 254 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 255 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 256 | /// must be a valid expression that resolves to a string formatted as either 257 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 258 | /// the result is displayed in UTC. 259 | Month(date, {timezone}) 260 | : super('month', AEObject({'date': date, 'timezone': timezone})); 261 | } 262 | 263 | /// `$second` operator 264 | class Second extends Operator { 265 | /// Creates `$second` operator expression 266 | /// 267 | /// Returns the second portion of a date as a number between 0 and 59, but can 268 | /// be 60 to account for leap seconds. 269 | /// 270 | /// * [date] - The date to which the operator is applied. [date] must be a 271 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 272 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 273 | /// must be a valid expression that resolves to a string formatted as either 274 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 275 | /// the result is displayed in UTC. 276 | Second(date, {timezone}) 277 | : super('second', AEObject({'date': date, 'timezone': timezone})); 278 | } 279 | 280 | /// `$toDate` operator 281 | class ToDate extends Operator { 282 | /// Creates `$toDate` operator expression 283 | /// 284 | /// Converts a value to a date. If the value cannot be converted to a date, 285 | /// `$toDate` errors. If the value is `null` or missing, `$toDate` returns `null`. 286 | ToDate(expr) : super('toDate', expr); 287 | } 288 | 289 | /// `$week` operator 290 | class Week extends Operator { 291 | /// Creates `$week` operator expression 292 | /// 293 | /// Returns the week of the year for a date as a number between 0 and 53. 294 | /// 295 | /// * [date] - The date to which the operator is applied. [date] must be a 296 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 297 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 298 | /// must be a valid expression that resolves to a string formatted as either 299 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 300 | /// the result is displayed in UTC. 301 | Week(date, {timezone}) 302 | : super('week', AEObject({'date': date, 'timezone': timezone})); 303 | } 304 | 305 | /// `$year` operator 306 | class Year extends Operator { 307 | /// Creates `$year` operator expression 308 | /// 309 | /// Returns the year portion of a date. 310 | /// 311 | /// * [date] - The date to which the operator is applied. [date] must be a 312 | /// valid expression that resolves to a Date, a Timestamp, or an ObjectID. 313 | /// * {timezone} - Optional. The timezone of the operation result. {timezone} 314 | /// must be a valid expression that resolves to a string formatted as either 315 | /// an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, 316 | /// the result is displayed in UTC. 317 | Year(date, {timezone}) 318 | : super('year', AEObject({'date': date, 'timezone': timezone})); 319 | } 320 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/group_stage_accumulators.dart: -------------------------------------------------------------------------------- 1 | import 'aggregation_base.dart'; 2 | 3 | /// `$addToSet` operator 4 | class AddToSet extends Accumulator { 5 | /// Creates `$addToSet` operator expression 6 | /// 7 | /// Returns an array of all unique values that results from applying an 8 | /// expression to each document in a group of documents that share the 9 | /// same group by key. Order of the elements in the output array is 10 | /// unspecified. 11 | AddToSet(expr) : super('addToSet', expr); 12 | } 13 | 14 | /// `$avg` operator 15 | class Avg extends Accumulator { 16 | /// Creates `$avg` operator expression 17 | /// 18 | /// Returns the average value of the numeric values. $avg ignores non-numeric values. 19 | Avg(expr) : super('avg', expr is List ? AEList(expr) : expr); 20 | } 21 | 22 | /// `$first` operator 23 | class First extends Accumulator { 24 | /// Creates `$first` operator expression 25 | /// 26 | /// Returns the value that results from applying an expression to the first 27 | /// document in a group of documents that share the same group by key. Only 28 | /// meaningful when documents are in a defined order. 29 | First(expr) : super('first', expr); 30 | } 31 | 32 | /// `$last` operator 33 | class Last extends Accumulator { 34 | /// Creates `$last` operator expression 35 | /// 36 | /// Returns the value that results from applying an expression to the last 37 | /// document in a group of documents that share the same group by a field. 38 | /// Only meaningful when documents are in a defined order. 39 | Last(expr) : super('last', expr); 40 | } 41 | 42 | /// `$max` operator 43 | class Max extends Accumulator { 44 | /// Creates `$max` operator expression 45 | /// 46 | /// Returns the maximum value. `$max` compares both value and type, using the 47 | /// specified BSON comparison order for values of different types. 48 | Max(expr) : super('max', expr is List ? AEList(expr) : expr); 49 | } 50 | 51 | /// `$min` operator 52 | class Min extends Accumulator { 53 | /// Creates `$min` operator expression 54 | /// 55 | /// Returns the minimum value. `$min` compares both value and type, using the 56 | /// specified BSON comparison order for values of different types. 57 | Min(expr) : super('min', expr is List ? AEList(expr) : expr); 58 | } 59 | 60 | /// `$push` operator 61 | class Push extends Accumulator { 62 | /// Creates `$push` operator expression 63 | /// 64 | /// Returns an array of all values that result from applying an expression to 65 | /// each document in a group of documents that share the same group by key. 66 | Push(expr) : super('push', expr); 67 | 68 | /// Creates `$push` operator expression 69 | /// 70 | /// More readable way to create `Push(AEList([...]))` 71 | Push.list(List list) : super('push', AEList(list)); 72 | 73 | /// Creates `$push` operator expression 74 | /// 75 | /// More readable way to create `Push(AOBject({...}))` 76 | Push.object(Map object) : super('push', AEObject(object)); 77 | } 78 | 79 | /// `$stdDevPop` operator 80 | class StdDevPop extends Accumulator { 81 | /// Creates `$stdDevPop` operator expression 82 | /// 83 | /// Calculates the population standard deviation of the input values. Use if 84 | /// the values encompass the entire population of data you want to represent 85 | /// and do not wish to generalize about a larger population. `$stdDevPop` ignores 86 | /// non-numeric values. 87 | StdDevPop(expr) : super('stdDevPop', expr is List ? AEList(expr) : expr); 88 | } 89 | 90 | /// `$stdDevSamp` operator 91 | class StdDevSamp extends Accumulator { 92 | /// Creates `$stdDevSamp` operator expression 93 | /// 94 | /// Calculates the sample standard deviation of the input values. Use if the 95 | /// values encompass a sample of a population of data from which to generalize 96 | /// about the population. $stdDevSamp ignores non-numeric values. 97 | StdDevSamp(expr) : super('stdDevSamp', expr is List ? AEList(expr) : expr); 98 | } 99 | 100 | /// `$sum` operator 101 | class Sum extends Accumulator { 102 | /// Creates `$sum` operator expression 103 | /// 104 | /// Calculates and returns the sum of numeric values. $sum ignores non-numeric 105 | /// values. 106 | Sum(expr) : super('sum', expr is List ? AEList(expr) : expr); 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/logic_operators.dart: -------------------------------------------------------------------------------- 1 | import 'aggregation_base.dart'; 2 | 3 | /// `$and` operator 4 | class And extends Operator { 5 | /// Creates `$and` operator expression 6 | /// 7 | /// Evaluates one or more expressions and returns `true` if all of the 8 | /// expressions are `true` or if evoked with no argument expressions. Otherwise, 9 | /// `$and` returns `false`. 10 | And(List args) : super('and', AEList(args)); 11 | } 12 | 13 | /// `$or` operator 14 | class Or extends Operator { 15 | /// Creates `$or` operator expression 16 | /// 17 | /// Evaluates one or more expressions and returns `true` if any of the expressions 18 | /// are `true`. Otherwise, `$or` returns `false`. 19 | Or(List args) : super('or', AEList(args)); 20 | } 21 | 22 | /// `$not` operator 23 | class Not extends Operator { 24 | /// Creates `$not` operator expression 25 | /// 26 | /// Evaluates a boolean and returns the opposite boolean value; i.e. when 27 | /// passed an expression that evaluates to `true`, `$not` returns `false`; when 28 | /// passed an expression that evaluates to `false`, $not returns `true`. 29 | Not(expr) : super('not', expr); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/pipeline_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// Aggregation pipeline builder 6 | class AggregationPipelineBuilder implements Builder { 7 | @protected 8 | final stages = []; 9 | 10 | /// Adds stage to the pipeline 11 | AggregationPipelineBuilder addStage(AggregationStage stage) { 12 | stages.add(stage); 13 | return this; 14 | } 15 | 16 | /// Builds pipeline 17 | /// 18 | /// Returns aggregation pipeline in format suitable for mongodb aggregate 19 | /// query 20 | @override 21 | List> build() => 22 | stages.map((stage) => stage.build()).toList(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/string_operators.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// `$concat` operator 6 | class Concat extends Operator { 7 | /// Creates `$concat` operator expression 8 | /// 9 | /// Concatenates strings and returns the concatenated string. 10 | /// 11 | /// The [strings] is a [List] of valid expression as long as they resolve to 12 | /// strings. 13 | Concat(List strings) : super('concat', AEList(strings)); 14 | } 15 | 16 | /// `$indexOfBytes` operator 17 | class IndexOfBytes extends Operator { 18 | /// Creates `$indexOfBytes` operator expression 19 | /// 20 | /// Searches a [string] for an occurence of a [substring] and returns the 21 | /// UTF-8 byte index (zero-based) of the first occurence. If the [substring] 22 | /// is not found, returns -1. 23 | /// 24 | /// * [string] - Can be any valid expression as long as it resolves to a string. 25 | /// If the string expression resolves to a value of `null` or refers to a 26 | /// field that is missing, $indexOfCP returns `null`. If the string expression 27 | /// does not resolve to a string or `null` nor refers to a missing field, 28 | /// `$indexOfBytes` returns an error. 29 | /// * [substring] - Can be any valid expression as long as it resolves to a 30 | /// string. 31 | /// [start] - Optional. An integral number that specifies the starting index 32 | /// position for the search. Can be any valid expression that resolves to a 33 | /// non-negative integral number. 34 | /// [end] - Optional. An integral number that specifies the ending index 35 | /// position for the search. Can be any valid expression that resolves to a 36 | /// non-negative integral number. If you specify a [end] index value, you 37 | /// should also specify a [start] index value; otherwise, `$indexOfBytes` uses 38 | /// the [end] value as the [start] index value instead of the [end] value. 39 | IndexOfBytes(string, substring, [start, end]) 40 | : super('indexOfBytes', AEList([string, substring, start, end])); 41 | } 42 | 43 | /// `$indexOfCP` operator 44 | class IndexOfCP extends Operator { 45 | /// Creates `$indexOfCP` operator expression 46 | /// 47 | /// Searches a [string] for an occurence of a [substring] and returns the 48 | /// UTF-8 code point index (zero-based) of the first occurence. If the 49 | /// [substring] is not found, returns -1. 50 | /// 51 | /// * [string] - Can be any valid expression as long as it resolves to a string. 52 | /// If the string expression resolves to a value of `null` or refers to a 53 | /// field that is missing, $indexOfCP returns `null`. If the string expression 54 | /// does not resolve to a string or `null` nor refers to a missing field, 55 | /// `$indexOfCP` returns an error. 56 | /// * [substring] - Can be any valid expression as long as it resolves to a 57 | /// string. 58 | /// [start] - Optional. An integral number that specifies the starting index 59 | /// position for the search. Can be any valid expression that resolves to a 60 | /// non-negative integral number. 61 | /// [end] - Optional. An integral number that specifies the ending index 62 | /// position for the search. Can be any valid expression that resolves to a 63 | /// non-negative integral number. If you specify a [end] index value, you 64 | /// should also specify a [start] index value; otherwise, `$indexOfCP` uses 65 | /// the [end] value as the [start] index value instead of the [end value. 66 | IndexOfCP(string, substring, [start, end]) 67 | : super('indexOfCP', AEList([string, substring, start, end])); 68 | } 69 | 70 | /// `$ltrim` operator 71 | class Ltrim extends Operator { 72 | /// Creates `$ltrim` operator expression 73 | /// 74 | /// Removes whitespace characters, including null, or the specified characters 75 | /// from the beginning of a string. 76 | /// 77 | /// * [input] - The string to trim. The argument can be any valid expression 78 | /// that resolves to a string. 79 | /// * [chars] - Optional. The character(s) to trim from the beginning of the input. 80 | /// The argument can be any valid expression that resolves to a string. The `$ltrim` 81 | /// operator breaks down the string into individual UTF code point to trim from input. 82 | /// If unspecified, `$ltrim` removes whitespace characters, including the `null` 83 | /// character. 84 | Ltrim({@required input, chars}) 85 | : super('ltrim', AEObject({'input': input, 'chars': chars})); 86 | } 87 | 88 | // TODO: add options validation 89 | 90 | /// `$regexFind` operator 91 | class RegexFind extends Operator { 92 | /// Creates `$regexFind` operator expression 93 | /// 94 | /// Provides regular expression (regex) pattern matching capability in 95 | /// aggregation expressions. If a match is found, returns a document that 96 | /// contains information on the first match. If a match is not found, returns 97 | /// `null`. 98 | /// 99 | /// MongoDB uses Perl compatible regular expressions (i.e. “PCRE” ) version 100 | /// 8.41 with UTF-8 support. 101 | /// 102 | /// * [input] - The string on which you wish to apply the regex pattern. Can 103 | /// be a string or any valid expression that resolves to a string. 104 | /// * [regex] - The regex pattern to apply. Can be any valid expression that 105 | /// resolves to a string. 106 | /// * [options] - Optional. The following are available for use with 107 | /// regular expression: 108 | /// * `i` - Case insensitivity to match both upper and lower cases. You can 109 | /// specify the option in the options field or as part of the regex field. 110 | /// * `m` - For patterns that include anchors (i.e. ^ for the start, $ for 111 | /// the end), match at the beginning or end of each line for strings with 112 | /// multiline values. Without this option, these anchors match at beginning or 113 | /// end of the string. If the pattern contains no anchors or if the string 114 | /// value has no newline characters (e.g. \n), the m option has no effect. 115 | /// * `x` - “Extended” capability to ignore all white space characters in the 116 | /// pattern unless escaped or included in a character class. Additionally, it 117 | /// ignores characters in-between and including an un-escaped hash/pound (#) 118 | /// character and the next new line, so that you may include comments in 119 | /// complicated patterns. This only applies to data characters; white space 120 | /// characters may never appear within special character sequences in a pattern. 121 | /// The `x` option does not affect the handling of the VT character (i.e. code 122 | /// 11). 123 | /// * `s` - Allows the dot character (i.e. .) to match all characters including 124 | /// newline characters. 125 | RegexFind({@required input, @required regex, options}) 126 | : super('regexFind', 127 | AEObject({'input': input, 'regex': regex, 'options': options})); 128 | } 129 | 130 | /// `$regexFindAll` operator 131 | class RegexFindAll extends Operator { 132 | /// Creates `$regexFindAll` operator expression 133 | /// 134 | /// Provides regular expression (regex) pattern matching capability in 135 | /// aggregation expressions. The operator returns an array of documents that 136 | /// contains information on each match. If a match is not found, returns an 137 | /// empty array. 138 | /// 139 | /// MongoDB uses Perl compatible regular expressions (i.e. “PCRE” ) version 140 | /// 8.41 with UTF-8 support. 141 | /// 142 | /// * [input] - The string on which you wish to apply the regex pattern. Can 143 | /// be a string or any valid expression that resolves to a string. 144 | /// * [regex] - The regex pattern to apply. Can be any valid expression that 145 | /// resolves to a string. 146 | /// * [options] - Optional. The following are available for use with 147 | /// regular expression: 148 | /// * `i` - Case insensitivity to match both upper and lower cases. You can 149 | /// specify the option in the options field or as part of the regex field. 150 | /// * `m` - For patterns that include anchors (i.e. ^ for the start, $ for 151 | /// the end), match at the beginning or end of each line for strings with 152 | /// multiline values. Without this option, these anchors match at beginning or 153 | /// end of the string. If the pattern contains no anchors or if the string 154 | /// value has no newline characters (e.g. \n), the m option has no effect. 155 | /// * `x` - “Extended” capability to ignore all white space characters in the 156 | /// pattern unless escaped or included in a character class. Additionally, it 157 | /// ignores characters in-between and including an un-escaped hash/pound (#) 158 | /// character and the next new line, so that you may include comments in 159 | /// complicated patterns. This only applies to data characters; white space 160 | /// characters may never appear within special character sequences in a pattern. 161 | /// The `x` option does not affect the handling of the VT character (i.e. code 162 | /// 11). 163 | /// * `s` - Allows the dot character (i.e. .) to match all characters including 164 | /// newline characters. 165 | RegexFindAll({@required input, @required regex, options}) 166 | : super('regexFindAll', 167 | AEObject({'input': input, 'regex': regex, 'options': options})); 168 | } 169 | 170 | /// `$regexMatch` operator 171 | class RegexMatch extends Operator { 172 | /// Creates `$regexMatch` operator expression 173 | /// 174 | /// Performs a regular expression (regex) pattern matching and returns: 175 | /// 176 | /// * `true` if a match exists. 177 | /// * `false` if a match doesn’t exist. 178 | /// 179 | /// MongoDB uses Perl compatible regular expressions (i.e. “PCRE” ) version 180 | /// 8.41 with UTF-8 support. 181 | /// 182 | /// * [input] - The string on which you wish to apply the regex pattern. Can 183 | /// be a string or any valid expression that resolves to a string. 184 | /// * [regex] - The regex pattern to apply. Can be any valid expression that 185 | /// resolves to a string. 186 | /// * [options] - Optional. The following are available for use with 187 | /// regular expression: 188 | /// * `i` - Case insensitivity to match both upper and lower cases. You can 189 | /// specify the option in the options field or as part of the regex field. 190 | /// * `m` - For patterns that include anchors (i.e. ^ for the start, $ for 191 | /// the end), match at the beginning or end of each line for strings with 192 | /// multiline values. Without this option, these anchors match at beginning or 193 | /// end of the string. If the pattern contains no anchors or if the string 194 | /// value has no newline characters (e.g. \n), the m option has no effect. 195 | /// * `x` - “Extended” capability to ignore all white space characters in the 196 | /// pattern unless escaped or included in a character class. Additionally, it 197 | /// ignores characters in-between and including an un-escaped hash/pound (#) 198 | /// character and the next new line, so that you may include comments in 199 | /// complicated patterns. This only applies to data characters; white space 200 | /// characters may never appear within special character sequences in a pattern. 201 | /// The `x` option does not affect the handling of the VT character (i.e. code 202 | /// 11). 203 | /// * `s` - Allows the dot character (i.e. .) to match all characters including 204 | /// newline characters. 205 | RegexMatch({@required input, @required regex, options}) 206 | : super('regexMatch', 207 | AEObject({'input': input, 'regex': regex, 'options': options})); 208 | } 209 | 210 | /// `$rtrim` operator 211 | class Rtrim extends Operator { 212 | /// Creates `$rtrim` operator expression 213 | /// 214 | /// Removes whitespace characters, including `null`, or the specified 215 | /// characters from the end of a string. 216 | /// 217 | /// * [input] - The string to trim. The argument can be any valid expression 218 | /// that resolves to a string. 219 | /// * [chars] - Optional. The character(s) to trim from the beginning of the input. 220 | /// The argument can be any valid expression that resolves to a string. The `$ltrim` 221 | /// operator breaks down the string into individual UTF code point to trim from input. 222 | /// If unspecified, `$ltrim` removes whitespace characters, including the `null` 223 | /// character. 224 | Rtrim({@required input, chars}) 225 | : super('rtrim', AEObject({'input': input, 'chars': chars})); 226 | } 227 | 228 | /// `$split` operator 229 | class Split extends Operator { 230 | /// Creates `$split` operator expression 231 | /// 232 | /// Divides a [string] into an array of substrings based on a [delimiter]. 233 | /// `$split` removes the [delimiter] and returns the resulting substrings as 234 | /// elements of an array. If the [delimiter] is not found in the [string], 235 | /// `$split` returns the original [string] as the only element of an array. 236 | /// 237 | /// * [string] - The string to be split. string expression can be any valid 238 | /// expression as long as it resolves to a string. 239 | /// * [delimiter] - The delimiter to use when splitting the string expression. 240 | /// delimiter can be any valid expression as long as it resolves to a string. 241 | Split(string, delimiter) : super('split', AEList([string, delimiter])); 242 | } 243 | 244 | /// `$strLenBytes` operator 245 | class StrLenBytes extends Operator { 246 | /// Creates `$strLenBytes` operator expression 247 | /// 248 | /// Returns the number of UTF-8 encoded bytes in the specified string. 249 | StrLenBytes(expr) : super('strLenBytes', expr); 250 | } 251 | 252 | /// `$strLenCP` operator 253 | class StrLenCP extends Operator { 254 | /// Creates `$xtrLenCp` operator expression 255 | /// 256 | /// Returns the number of UTF-8 code points in the specified string. 257 | StrLenCP(expr) : super('strLenCP', expr); 258 | } 259 | 260 | /// `$strcasecmp` operator 261 | class StrCaseCmp extends Operator { 262 | /// Creates `$strcasecmp` operator expression 263 | /// 264 | /// Performs case-insensitive comparison of two strings. Returns 265 | /// 266 | /// * 1 if first string is “greater than” the second string. 267 | /// * 0 if the two strings are equal. 268 | /// * -1 if the first string is “less than” the second string. 269 | StrCaseCmp(a, b) : super('strcasecmp', AEList([a, b])); 270 | } 271 | 272 | /// `$substrBytes` operator 273 | class SubstrBytes extends Operator { 274 | /// Creates `$substrBytes` operator expression 275 | /// 276 | /// Returns the substring of a string. The substring starts with the 277 | /// character at the specified UTF-8 byte index (zero-based) in the string 278 | /// and continues for the number of bytes specified. 279 | /// 280 | /// * [string] - The string from which the substring will be extracted. 281 | /// [string] expression can be any valid expression as long as it resolves 282 | /// to a string. If the argument resolves to a value of `null` or refers to a 283 | /// field that is missing, `$substrBytes` returns an empty string. If the 284 | /// argument does not resolve to a string or `null` nor refers to a missing 285 | /// field, `$substrBytes` returns an error. 286 | /// * [index] - Indicates the starting point of the substring. Byte [index] can 287 | /// be any valid expression as long as it resolves to a non-negative integer 288 | /// or number that can be represented as an integer (such as 2.0). Byte index 289 | /// cannot refer to a starting index located in the middle of a multi-byte 290 | /// UTF-8 character. 291 | /// * [count] - Can be any valid expression as long as it resolves to a 292 | /// non-negative integer or number that can be represented as an integer 293 | /// (such as 2.0). Byte count can not result in an ending index that is in the 294 | /// middle of a UTF-8 character. 295 | SubstrBytes(string, index, count) 296 | : super('substrBytes', AEList([string, index, count])); 297 | } 298 | 299 | /// `$substrCP` operator 300 | class SubstrCP extends Operator { 301 | /// Creates `$substrCP` operator expression 302 | /// 303 | /// Returns the substring of a string. The substring starts with the character 304 | /// at the specified UTF-8 code point (CP) index (zero-based) in the string for 305 | /// the number of code points specified. 306 | /// 307 | /// * [string] - The string from which the substring will be extracted. [string] 308 | /// expression can be any valid expression as long as it resolves to a string. 309 | /// If the argument resolves to a value of `null` or refers to a field that is 310 | /// missing, `$substrCP` returns an empty string. If the argument does not 311 | /// resolve to a string or `null` nor refers to a missing field, `$substrCP` 312 | /// returns an error. 313 | /// * [index] - Indicates the starting point of the substring. code point index 314 | /// can be any valid expression as long as it resolves to a non-negative 315 | /// integer. 316 | /// * [count] - Can be any valid expression as long as it resolves to a 317 | /// non-negative integer or number that can be represented as an integer 318 | /// (such as 2.0). 319 | SubstrCP(string, index, count) 320 | : super('substrCP', AEList([string, index, count])); 321 | } 322 | 323 | /// `$toLower` operator 324 | class ToLower extends Operator { 325 | /// Creates `$toLower` operator expression 326 | /// 327 | /// Converts a string to lowercase, returning the result. 328 | /// 329 | /// The argument can be any expression as long as it resolves to a string. 330 | ToLower(expr) : super('toLower', expr); 331 | } 332 | 333 | /// `$toString` operator 334 | class ToString extends Operator { 335 | /// Creates `$toString` operator expression 336 | /// 337 | /// Converts a value to a string. If the value cannot be converted to a 338 | /// string, $toString errors. If the value is null or missing, $toString 339 | /// returns null. 340 | /// 341 | /// The $toString takes any valid expression. 342 | ToString(expr) : super('toString', expr); 343 | } 344 | 345 | /// `$trim` operator 346 | class Trim extends Operator { 347 | /// Creates `$trim` operator expression 348 | /// 349 | /// Removes whitespace characters, including null, or the specified characters 350 | /// from the beginning and end of a string. 351 | /// 352 | /// * [input] - The string to trim. The argument can be any valid expression 353 | /// that resolves to a string. 354 | /// * [chars] - Optional. The character(s) to trim from the beginning of the input. 355 | /// The argument can be any valid expression that resolves to a string. The `$ltrim` 356 | /// operator breaks down the string into individual UTF code point to trim from input. 357 | /// If unspecified, `$ltrim` removes whitespace characters, including the `null` 358 | /// character. 359 | Trim({@required input, chars}) 360 | : super('trim', AEObject({'input': input, 'chars': chars})); 361 | } 362 | 363 | /// `$toUpper` operator 364 | class ToUpper extends Operator { 365 | /// Creates `$toUpper` operator expression 366 | /// 367 | /// Converts a string to uppercase, returning the result. 368 | /// 369 | /// The argument can be any expression as long as it resolves to a string. 370 | ToUpper(expr) : super('toUpper', expr); 371 | } 372 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/support_classes/output.dart: -------------------------------------------------------------------------------- 1 | import '../aggregation_base.dart'; 2 | import '../common.dart'; 3 | 4 | /// Creates an "output" document for the setWinowsDield stage 5 | /// * [fieldName] -Specifies the field to append to the documents in the 6 | /// output returned by the $setWindowFields stage. Each field is set to 7 | /// the result returned by the window operator. 8 | /// A field can contain dots to specify embedded document fields and array 9 | /// fields. The semantics for the embedded document dotted notation in the 10 | /// $setWindowFields stage are the same as the $addFields and $set stages. 11 | /// See embedded document $addFields example and embedded document 12 | /// $set example. 13 | /// * [operator] - The window operator is the window operator 14 | /// to use in the $setWindowFields stage. 15 | /// 16 | /// The window operator parameters are the parameters to pass to the window 17 | /// operator. Specifies the window boundaries and parameters. Window 18 | /// boundaries are inclusive. Default is an unbounded window, which includes 19 | /// all documents in the partition. 20 | /// Specify either a documents or range window. 21 | /// - [documents] Optional - A window where the lower and upper boundaries 22 | /// are specified relative to the position of the current document read 23 | /// from the collection. 24 | /// The window boundaries are specified using a two element array 25 | /// containing a lower and upper limit string or integer. Use: 26 | /// - The "current" string for the current document position in the output 27 | /// - The "unbounded" string for the first or last document position in 28 | /// the partition. 29 | /// - [range] Optional - A window where the lower and upper boundaries are 30 | /// defined using a range of values based on the sortBy field in the 31 | /// current document. 32 | /// The window boundaries are specified using a two element array 33 | /// containing a lower and upper limit string or number. Use: 34 | /// - The "current" string for the current document position in the output 35 | /// - The "unbounded" string for the first or last document position in 36 | /// the partition. 37 | /// - A number to add to the value of the sortBy field for the current 38 | /// document. A document is in the window if the sortBy field value is 39 | /// inclusively within the lower and upper boundaries. 40 | /// * [unit] Optional -Specifies the units for time range window boundaries. 41 | /// Can be set to one of these strings: 42 | /// - "year" 43 | /// - "quarter" 44 | /// - "month" 45 | /// - "week" 46 | /// - "day" 47 | /// - "hour" 48 | /// - "minute" 49 | /// - "second" 50 | /// - "millisecond" 51 | /// 52 | /// If omitted, default numeric range window boundaries are used. 53 | class Output extends AEObject { 54 | Output(String fieldName, Accumulator operator, 55 | {List? documents, List? range, String? unit}) 56 | : super.internal(({ 57 | fieldName: { 58 | ...operator.build(), 59 | if (documents != null || range != null || unit != null) 60 | spWindow: { 61 | if (documents != null) spDocuments: AEList(documents), 62 | if (range != null) spRange: AEList(range), 63 | if (unit != null) spUnit: unit 64 | } 65 | } 66 | })); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/type_expression_operators.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'aggregation_base.dart'; 4 | 5 | /// `$convert` operator 6 | class Convert extends Operator { 7 | /// Creates `$convert` operator expression 8 | /// 9 | /// The argument can be any expression as long as it resolves to a string. 10 | /// 11 | /// * [input] - The argument can be any valid expression. 12 | /// * [to] - The argument can be any valid expression that resolves to one of 13 | /// the following numeric or string identifiers: 14 | /// * 'double' 15 | /// * 'string' 16 | /// * 'objectId' 17 | /// * 'bool' 18 | /// * 'date' 19 | /// * 'int' 20 | /// * 'long' 21 | /// * 'decimal' 22 | /// * [onError] - Optional. The value to return on encountering an error during 23 | /// conversion, including unsupported type conversions. The arguments can be 24 | /// any valid expression. If unspecified, the operation throws an error upon 25 | /// encountering an error and stops. 26 | /// * [onNull] - Optional. The value to return if the input is null or missing. 27 | /// The arguments can be any valid expression. If unspecified, `$convert` returns 28 | /// `null` if the input is null or missing. 29 | Convert({@required input, @required to, onError, onNull}) 30 | : super( 31 | 'convert', 32 | AEObject({ 33 | 'input': input, 34 | 'to': to, 35 | 'onError': onError, 36 | 'onNull': onNull 37 | })); 38 | } 39 | 40 | /// `$toBool` operator 41 | class ToBool extends Operator { 42 | /// Creates `$toBool` operator expression 43 | /// 44 | /// Converts a value to a boolean. 45 | /// 46 | /// The [ToBool] takes any valid expression. 47 | ToBool(expr) : super('toBool', expr); 48 | } 49 | 50 | /// `$toDecimal` operator 51 | class ToDecimal extends Operator { 52 | /// Creates `$toDecimal` operator expression 53 | /// 54 | /// Converts a value to a decimal. If the value cannot be converted to a 55 | /// decimal, `$toDecimal` errors. If the value is `null` or missing, 56 | /// `$toDecimal` returns `null`. 57 | /// 58 | /// The [ToDecimal] takes any valid expression. 59 | ToDecimal(expr) : super('toDecimal', expr); 60 | } 61 | 62 | /// `$toDouble` operator 63 | class ToDouble extends Operator { 64 | /// Creates `$toDouble` operator expression 65 | /// 66 | /// Converts a value to a double. If the value cannot be converted to an 67 | /// double, `$toDouble` errors. If the value is `null` or missing, `$toDouble` 68 | /// returns `null`. 69 | /// 70 | /// The [ToDouble] takes any valid expression. 71 | ToDouble(expr) : super('toDouble', expr); 72 | } 73 | 74 | /// `$toInt` operator 75 | class ToInt extends Operator { 76 | /// Creates `$toInt` operator expression 77 | /// 78 | /// Converts a value to an integer. If the value cannot be converted to an 79 | /// integer, `$toInt` errors. If the value is `null` or missing, `$toInt` 80 | /// returns `null`. 81 | /// 82 | /// The [ToInt] takes any valid expression. 83 | ToInt(expr) : super('toInt', expr); 84 | } 85 | 86 | /// `$toLong` operator 87 | class ToLong extends Operator { 88 | /// Creates `$toLong` operator expression 89 | /// 90 | /// Converts a value to a long. If the value cannot be converted to a long, 91 | /// `$toLong` errors. If the value is `null` or missing, `$toLong` returns 92 | /// `null`. 93 | /// 94 | /// The [ToLong] takes any valid expression. 95 | ToLong(expr) : super('toLong', expr); 96 | } 97 | 98 | /// `$toObjectId` operator 99 | class ToObjectId extends Operator { 100 | /// Creates `$toObjectId` operator expression 101 | /// 102 | /// Converts a value to an ObjectId. If the value cannot be converted to an 103 | /// `ObjectId`, `$toObjectId` errors. If the value is `null` or missing, 104 | /// `$toObjectId` returns `null`. 105 | /// 106 | /// The [ToObjectId] takes any valid expression. 107 | ToObjectId(expr) : super('toObjectId', expr); 108 | } 109 | 110 | /// `$type` operator 111 | class Type extends Operator { 112 | /// Creates `$type` operator expression 113 | /// 114 | /// Returns a string that specifies the BSON type of the argument. 115 | /// 116 | /// The argument can be any valid expression. 117 | Type(expr) : super('type', expr); 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/mongo_aggregation/uncategorized_operators.dart: -------------------------------------------------------------------------------- 1 | import 'aggregation_base.dart'; 2 | 3 | /// `$expr` operator 4 | class Expr extends Operator { 5 | /// Creates an `$expr` part of `$match` stage 6 | /// 7 | /// The operator is used in `match` aggregation stage to define match expression 8 | /// as aggregation expression. 9 | /// 10 | /// [expr] - aggregation expression which usually resolves into [bool] 11 | Expr(AggregationExpr expr) : super('expr', expr); 12 | } 13 | 14 | /// `$let` operator 15 | class Let extends Operator { 16 | /// Creates `$let` operator expression 17 | /// 18 | /// Binds variables for use in the specified expression, and returns the 19 | /// result of the expression. 20 | /// 21 | /// * [vars] - Assignment block for the variables accessible in the in 22 | /// expression. To assign a variable, specify a string for the variable name 23 | /// and assign a valid expression for the value. The variable assignments 24 | /// have no meaning outside the in expression, not even within the vars block 25 | /// itself. 26 | /// * [inExpr] - The expression to evaluate. 27 | Let({required Map vars, required inExpr}) 28 | : super('let', AEObject({'vars': AEObject(vars), 'in': inExpr})); 29 | } 30 | 31 | // TODO: Trigonometry Expression Operators 32 | // TODO: Text Expression Operator 33 | -------------------------------------------------------------------------------- /lib/src/selector_builder.dart: -------------------------------------------------------------------------------- 1 | part of '../mongo_dart_query.dart'; 2 | 3 | SelectorBuilder get where => SelectorBuilder(); 4 | const keyQuery = r'$query'; 5 | 6 | class SelectorBuilder { 7 | static final RegExp objectIdRegexp = RegExp('.ObjectId...([0-9a-f]{24})....'); 8 | Map map = {}; 9 | 10 | Map get _query { 11 | if (!map.containsKey(keyQuery)) { 12 | map[keyQuery] = {}; 13 | } 14 | return map[keyQuery] as Map; 15 | } 16 | 17 | int paramSkip = 0; 18 | int paramLimit = 0; 19 | Map? _paramFields; 20 | 21 | Map get paramFields => _paramFields ??= {}; 22 | 23 | @override 24 | String toString() => 'SelectorBuilder($map)'; 25 | 26 | /// Copy to new instance 27 | static SelectorBuilder copyWith(SelectorBuilder other) { 28 | return SelectorBuilder() 29 | ..map = other.map 30 | .._paramFields = other._paramFields 31 | ..paramLimit = other.paramLimit 32 | ..paramSkip = other.paramSkip; 33 | } 34 | 35 | /// 36 | SelectorBuilder clone() { 37 | return copyWith(this); 38 | } 39 | 40 | void _addExpression(String fieldName, value) { 41 | var exprMap = {}; 42 | exprMap[fieldName] = value; 43 | if (_query.isEmpty) { 44 | _query[fieldName] = value; 45 | } else { 46 | _addExpressionMap(exprMap); 47 | } 48 | } 49 | 50 | void _addExpressionMap(Map expr) { 51 | if (_query.containsKey('\$and')) { 52 | var expressions = _query['\$and'] as List; 53 | expressions.add(expr); 54 | } else { 55 | var expressions = [_query]; 56 | expressions.add(expr); 57 | map['\$query'] = {'\$and': expressions}; 58 | } 59 | } 60 | 61 | //void _ensureParamFields() => paramFields /* ??= {} */; 62 | 63 | void _ensureOrderBy() { 64 | _query; 65 | if (!map.containsKey('orderby')) { 66 | map['orderby'] = {}; 67 | } 68 | } 69 | 70 | SelectorBuilder eq(String fieldName, value) { 71 | _addExpression(fieldName, value); 72 | return this; 73 | } 74 | 75 | SelectorBuilder id(ObjectId value) { 76 | _addExpression('_id', value); 77 | return this; 78 | } 79 | 80 | SelectorBuilder ne(String fieldName, value) { 81 | _addExpression(fieldName, {'\$ne': value}); 82 | return this; 83 | } 84 | 85 | SelectorBuilder gt(String fieldName, value) { 86 | _addExpression(fieldName, {'\$gt': value}); 87 | return this; 88 | } 89 | 90 | SelectorBuilder lt(String fieldName, value) { 91 | _addExpression(fieldName, {'\$lt': value}); 92 | return this; 93 | } 94 | 95 | SelectorBuilder gte(String fieldName, value) { 96 | _addExpression(fieldName, {'\$gte': value}); 97 | return this; 98 | } 99 | 100 | SelectorBuilder lte(String fieldName, value) { 101 | _addExpression(fieldName, {'\$lte': value}); 102 | return this; 103 | } 104 | 105 | SelectorBuilder all(String fieldName, List values) { 106 | _addExpression(fieldName, {'\$all': values}); 107 | return this; 108 | } 109 | 110 | SelectorBuilder nin(String fieldName, List values) { 111 | _addExpression(fieldName, {'\$nin': values}); 112 | return this; 113 | } 114 | 115 | SelectorBuilder oneFrom(String fieldName, List values) { 116 | _addExpression(fieldName, {'\$in': values}); 117 | return this; 118 | } 119 | 120 | SelectorBuilder exists(String fieldName) { 121 | _addExpression(fieldName, {'\$exists': true}); 122 | return this; 123 | } 124 | 125 | SelectorBuilder notExists(String fieldName) { 126 | _addExpression(fieldName, {'\$exists': false}); 127 | return this; 128 | } 129 | 130 | SelectorBuilder mod(String fieldName, int value) { 131 | _addExpression(fieldName, { 132 | '\$mod': [value, 0] 133 | }); 134 | return this; 135 | } 136 | 137 | SelectorBuilder match(String fieldName, String pattern, 138 | {bool multiLine = false, 139 | bool caseInsensitive = false, 140 | bool dotAll = false, 141 | bool extended = false, 142 | bool escapePattern = false}) { 143 | _addExpression(fieldName, { 144 | '\$regex': RegExp(escapePattern ? RegExp.escape(pattern) : pattern, 145 | multiLine: multiLine, 146 | caseSensitive: caseInsensitive, 147 | dotAll: dotAll, 148 | unicode: extended) 149 | }); 150 | return this; 151 | } 152 | 153 | SelectorBuilder inRange(String fieldName, min, max, 154 | {bool minInclude = true, bool maxInclude = false}) { 155 | var rangeMap = {}; 156 | if (minInclude) { 157 | rangeMap['\$gte'] = min; 158 | } else { 159 | rangeMap['\$gt'] = min; 160 | } 161 | if (maxInclude) { 162 | rangeMap['\$lte'] = max; 163 | } else { 164 | rangeMap['\$lt'] = max; 165 | } 166 | _addExpression(fieldName, rangeMap); 167 | return this; 168 | } 169 | 170 | SelectorBuilder sortBy(String fieldName, {bool descending = false}) { 171 | _ensureOrderBy(); 172 | var order = 1; 173 | if (descending) { 174 | order = -1; 175 | } 176 | map['orderby'][fieldName] = order; 177 | return this; 178 | } 179 | 180 | SelectorBuilder sortByMetaTextScore(String fieldName) { 181 | _ensureOrderBy(); 182 | map['orderby'][fieldName] = {'\$meta': 'textScore'}; 183 | return this; 184 | } 185 | 186 | SelectorBuilder hint(String fieldName, {bool descending = false}) { 187 | _query; 188 | if (!map.containsKey('\$hint')) { 189 | map['\$hint'] = {}; 190 | } 191 | var order = 1; 192 | if (descending) { 193 | order = -1; 194 | } 195 | map['\$hint'][fieldName] = order; 196 | return this; 197 | } 198 | 199 | SelectorBuilder hintIndex(String indexName) { 200 | _query; 201 | map['\$hint'] = indexName; 202 | return this; 203 | } 204 | 205 | SelectorBuilder comment(String commentStr) { 206 | _query; 207 | map['\$comment'] = commentStr; 208 | return this; 209 | } 210 | 211 | SelectorBuilder explain() { 212 | _query; 213 | map['\$explain'] = true; 214 | return this; 215 | } 216 | 217 | SelectorBuilder snapshot() { 218 | _query; 219 | map['\$snapshot'] = true; 220 | return this; 221 | } 222 | 223 | SelectorBuilder showDiskLoc() { 224 | _query; 225 | map['\$showDiskLoc'] = true; 226 | return this; 227 | } 228 | 229 | SelectorBuilder returnKey() { 230 | _query; 231 | map['\$sreturnKey'] = true; 232 | return this; 233 | } 234 | 235 | SelectorBuilder jsQuery(String javaScriptCode) { 236 | _query['\$where'] = JsCode(javaScriptCode); 237 | return this; 238 | } 239 | 240 | SelectorBuilder metaTextScore(String fieldName) { 241 | paramFields[fieldName] = {'\$meta': 'textScore'}; 242 | 243 | return this; 244 | } 245 | 246 | SelectorBuilder fields(List fields) { 247 | for (var field in fields) { 248 | paramFields[field] = 1; 249 | } 250 | return this; 251 | } 252 | 253 | SelectorBuilder excludeFields(List fields) { 254 | for (var field in fields) { 255 | paramFields[field] = 0; 256 | } 257 | return this; 258 | } 259 | 260 | SelectorBuilder limit(int limit) { 261 | paramLimit = limit; 262 | return this; 263 | } 264 | 265 | SelectorBuilder skip(int skip) { 266 | paramSkip = skip; 267 | return this; 268 | } 269 | 270 | SelectorBuilder raw(Map rawSelector) { 271 | map = rawSelector; 272 | return this; 273 | } 274 | 275 | SelectorBuilder within(String fieldName, value) { 276 | _addExpression(fieldName, { 277 | '\$within': {'\$box': value} 278 | }); 279 | return this; 280 | } 281 | 282 | SelectorBuilder near(String fieldName, var value, [double? maxDistance]) { 283 | if (maxDistance == null) { 284 | _addExpression(fieldName, {'\$near': value}); 285 | } else { 286 | _addExpression( 287 | fieldName, {'\$near': value, '\$maxDistance': maxDistance}); 288 | } 289 | return this; 290 | } 291 | 292 | /// Only support $geometry shape operator 293 | /// Available ShapeOperator instances: Box , Center, CenterSphere, Geometry 294 | SelectorBuilder geoWithin(String fieldName, ShapeOperator shape) { 295 | _addExpression(fieldName, {'\$geoWithin': shape.build()}); 296 | return this; 297 | } 298 | 299 | /// Only support geometry of point 300 | SelectorBuilder nearSphere(String fieldName, Geometry point, 301 | {double? maxDistance, double? minDistance}) { 302 | _addExpression(fieldName, { 303 | '\$nearSphere': { 304 | if (minDistance != null) '\$minDistance': minDistance, 305 | if (maxDistance != null) '\$maxDistance': maxDistance 306 | }..addAll(point.build()), 307 | }); 308 | return this; 309 | } 310 | 311 | /// 312 | SelectorBuilder geoIntersects(String fieldName, Geometry coordinate) { 313 | _addExpression(fieldName, {'\$geoIntersects': coordinate.build()}); 314 | return this; 315 | } 316 | 317 | /// Combine current expression with expression in parameter. 318 | /// [See MongoDB doc](http://docs.mongodb.org/manual/reference/operator/and/#op._S_and) 319 | /// [SelectorBuilder] provides implicit `and` operator for chained queries so these two expression will produce 320 | /// identical MongoDB queries 321 | /// 322 | /// where.eq('price', 1.99).lt('qty', 20).eq('sale', true); 323 | /// where.eq('price', 1.99).and(where.lt('qty',20)).and(where.eq('sale', true)) 324 | /// 325 | /// Both these queries would produce json map: 326 | /// 327 | /// {'\$query': {'\$and': [{'price':1.99},{'qty': {'\$lt': 20 }}, {'sale': true }]}} 328 | SelectorBuilder and(SelectorBuilder other) { 329 | if (_query.isEmpty) { 330 | throw StateError('`And` operation is not supported on empty query'); 331 | } 332 | _addExpressionMap(other._query); 333 | return this; 334 | } 335 | 336 | /// Combine current expression with expression in parameter by logical operator **OR**. 337 | /// [See MongoDB doc](https://www.mongodb.com/docs/manual/reference/operator/query/or/) 338 | /// For example 339 | /// inventory.find(where.eq('price', 1.99).and(where.lt('qty',20).or(where.eq('sale', true)))); 340 | /// 341 | /// This query will select all documents in the inventory collection where: 342 | /// * the **price** field value equals 1.99 and 343 | /// * either the **qty** field value is less than 20 or the **sale** field value is true 344 | /// MongoDB json query from this expression would be 345 | /// {'\$query': {'\$and': [{'price':1.99}, {'\$or': [{'qty': {'\$lt': 20 }}, {'sale': true }]}]}} 346 | SelectorBuilder or(SelectorBuilder other) { 347 | if (_query.isEmpty) { 348 | throw StateError('`Or` operation is not supported on empty query'); 349 | } 350 | if (_query.containsKey('\$or')) { 351 | var expressions = _query['\$or'] as List; 352 | expressions.add(other._query); 353 | } else { 354 | var expressions = [_query]; 355 | expressions.add(other._query); 356 | map['\$query'] = {'\$or': expressions}; 357 | } 358 | return this; 359 | } 360 | 361 | String getQueryString() { 362 | var result = json.encode(map); 363 | return result; 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mongo_dart_query 2 | version: 5.0.2 3 | description: Query builder for mongo_dart 4 | homepage: https://github.com/mongo-dart/mongo_dart_query 5 | environment: 6 | sdk: ">=3.0.0 <4.0.0" 7 | 8 | dependencies: 9 | bson: ^5.0.0 10 | meta: ^1.9.0 11 | fixnum: ^1.1.0 12 | 13 | dev_dependencies: 14 | test: ^1.24.0 15 | lints: ^4.0.0 16 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /*.dart.js 2 | /*.dart.js.deps 3 | /*.dart.js.map 4 | 5 | # Files and directories created by pub 6 | .dart_tool/ 7 | .packages 8 | # Remove the following pattern if you wish to check in your lock file 9 | pubspec.lock 10 | 11 | # Conventional directory for build outputs 12 | build/ 13 | 14 | # Directory created by dartdoc 15 | doc/api/ 16 | 17 | # ide directories 18 | .idea/ 19 | .vscode/ -------------------------------------------------------------------------------- /test/mongo_aggregation/aggregation_base_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('field', () { 6 | expect(Field('field').build(), '\$field'); 7 | }); 8 | 9 | test('literal', () { 10 | expect(Literal('\$value').build(), {'\$literal': '\$value'}); 11 | }); 12 | 13 | test('variable', () { 14 | expect(Var('variable').build(), '\$\$variable'); 15 | }); 16 | 17 | test('system variables', () { 18 | expect(Var.now.build(), '\$\$NOW'); 19 | expect(Var.clusterTime.build(), '\$\$CLUSTER_TIME'); 20 | expect(Var.root.build(), '\$\$ROOT'); 21 | expect(Var.current.build(), '\$\$CURRENT'); 22 | expect(Var.remove.build(), '\$\$REMOVE'); 23 | expect(Var.discend.build(), '\$\$DISCEND'); 24 | expect(Var.prune.build(), '\$\$PRUNE'); 25 | expect(Var.keep.build(), '\$\$KEEP'); 26 | }); 27 | 28 | test('AEList filter null elements', () { 29 | expect(AEList([1, 'string', null, 2]).build(), 30 | containsAllInOrder([1, 'string', 2])); 31 | }); 32 | 33 | test('AEObject filter null values', () { 34 | expect( 35 | AEObject({'num': 1, 'string': 'value', 'null': null, 'two': 2}).build(), 36 | {'num': 1, 'string': 'value', 'two': 2}); 37 | }); 38 | 39 | test('AElist#build', () { 40 | expect(AEList([1, TestExpr(), 'string']).build(), 41 | containsAllInOrder([1, 'test', 'string'])); 42 | }); 43 | 44 | test('AEObject#build', () { 45 | expect(AEObject({'num': 1, 'expr': TestExpr(), 'string': 'value'}).build(), 46 | {'num': 1, 'expr': 'test', 'string': 'value'}); 47 | }); 48 | } 49 | 50 | class TestExpr implements AggregationExpr { 51 | @override 52 | String build() => 'test'; 53 | } 54 | -------------------------------------------------------------------------------- /test/mongo_aggregation/aggregation_stages_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:mongo_dart_query/mongo_dart_query.dart'; 3 | import 'package:test/test.dart' hide Skip; 4 | import 'package:bson/src/types/bson_null.dart'; 5 | 6 | void main() { 7 | test('addFields', () { 8 | expect( 9 | AddFields({ 10 | 'totalHomework': Sum(Field('homework')), 11 | 'totalQuiz': Sum(Field('quiz')) 12 | }).build(), 13 | { 14 | '\$addFields': { 15 | 'totalHomework': {'\$sum': '\$homework'}, 16 | 'totalQuiz': {'\$sum': '\$quiz'} 17 | } 18 | }); 19 | }); 20 | 21 | test('sample', () { 22 | expect(Sample(10).build(), { 23 | r'$sample': {'size': 10} 24 | }); 25 | }); 26 | 27 | test('set', () { 28 | expect( 29 | SetStage({ 30 | 'totalHomework': Sum(Field('homework')), 31 | 'totalQuiz': Sum(Field('quiz')) 32 | }).build(), 33 | { 34 | '\$set': { 35 | 'totalHomework': {'\$sum': '\$homework'}, 36 | 'totalQuiz': {'\$sum': '\$quiz'} 37 | } 38 | }); 39 | }); 40 | 41 | test('setWindowFields', () { 42 | expect( 43 | SetWindowFields( 44 | partitionBy: {r'$year': r"$orderDate"}, 45 | sortBy: {'orderDate': 1}, 46 | output: Output('cumulativeQuantityForYear', Sum(r'$quantity'), 47 | documents: ["unbounded", "current"])).build(), 48 | { 49 | r'$setWindowFields': { 50 | 'partitionBy': {r'$year': r"$orderDate"}, 51 | 'sortBy': {'orderDate': 1}, 52 | 'output': { 53 | 'cumulativeQuantityForYear': { 54 | r'$sum': r"$quantity", 55 | 'window': { 56 | 'documents': ["unbounded", "current"] 57 | } 58 | } 59 | } 60 | } 61 | }); 62 | expect( 63 | SetWindowFields( 64 | partitionBy: r'$state', 65 | sortBy: {'orderDate': 1}, 66 | output: Output('recentOrders', Push(r'$orderDate'), 67 | range: ["unbounded", -10], unit: "month")) 68 | .build(), 69 | { 70 | r'$setWindowFields': { 71 | 'partitionBy': r"$state", 72 | 'sortBy': {'orderDate': 1}, 73 | 'output': { 74 | 'recentOrders': { 75 | r'$push': r"$orderDate", 76 | 'window': { 77 | 'range': ["unbounded", -10], 78 | 'unit': "month" 79 | } 80 | } 81 | } 82 | } 83 | }); 84 | expect( 85 | SetWindowFields(output: Output('recentOrders', Avg(r'$orderDate'))) 86 | .build(), 87 | { 88 | r'$setWindowFields': { 89 | 'output': { 90 | 'recentOrders': { 91 | r'$avg': r"$orderDate", 92 | } 93 | } 94 | } 95 | }); 96 | expect( 97 | SetWindowFields(partitionBy: { 98 | r'$year': r'$orderDate' 99 | }, sortBy: { 100 | 'orderDate': 1 101 | }, output: [ 102 | Output('cumulativeQuantityForYear', Sum(r'$quantity'), 103 | documents: ["unbounded", "current"]), 104 | Output('maximumQuantityForYear', Max(r'$quantity'), 105 | documents: ["unbounded", "unbounded"]) 106 | ]).build(), 107 | { 108 | r'$setWindowFields': { 109 | 'partitionBy': {r'$year': r'$orderDate'}, 110 | 'sortBy': {'orderDate': 1}, 111 | 'output': { 112 | 'cumulativeQuantityForYear': { 113 | r'$sum': r'$quantity', 114 | 'window': { 115 | 'documents': ["unbounded", "current"] 116 | } 117 | }, 118 | 'maximumQuantityForYear': { 119 | r'$max': r'$quantity', 120 | 'window': { 121 | 'documents': ["unbounded", "unbounded"] 122 | } 123 | } 124 | } 125 | } 126 | }); 127 | }); 128 | 129 | test('unset', () { 130 | expect(Unset(['isbn', 'author.first', 'copies.warehouse']).build(), { 131 | '\$unset': ['isbn', 'author.first', 'copies.warehouse'] 132 | }); 133 | }); 134 | 135 | test('bucket', () { 136 | expect( 137 | Bucket( 138 | groupBy: Field('price'), 139 | boundaries: [0, 200, 400], 140 | defaultId: 'Other', 141 | output: {'count': Sum(1), 'titles': Push(Field('title'))}).build(), 142 | { 143 | '\$bucket': { 144 | 'groupBy': '\$price', 145 | 'boundaries': [0, 200, 400], 146 | 'default': 'Other', 147 | 'output': { 148 | 'count': {'\$sum': 1}, 149 | 'titles': {'\$push': '\$title'} 150 | } 151 | } 152 | }); 153 | }); 154 | 155 | test('granularity', () { 156 | expect(Granularity.r5.build(), 'R5'); 157 | expect(Granularity.r10.build(), 'R10'); 158 | expect(Granularity.r20.build(), 'R20'); 159 | expect(Granularity.r40.build(), 'R40'); 160 | expect(Granularity.r80.build(), 'R80'); 161 | expect(Granularity.e6.build(), 'E6'); 162 | expect(Granularity.e12.build(), 'E12'); 163 | expect(Granularity.e24.build(), 'E24'); 164 | expect(Granularity.e48.build(), 'E48'); 165 | expect(Granularity.e96.build(), 'E96'); 166 | expect(Granularity.e192.build(), 'E192'); 167 | expect(Granularity.g125.build(), '1-2-5'); 168 | expect(Granularity.powersof2.build(), 'POWERSOF2'); 169 | }); 170 | 171 | test('bucketAuto', () { 172 | expect( 173 | BucketAuto( 174 | groupBy: Field('_id'), 175 | buckets: 5, 176 | granularity: Granularity.r5, 177 | output: {'count': Sum(1)}).build(), 178 | { 179 | '\$bucketAuto': { 180 | 'groupBy': '\$_id', 181 | 'buckets': 5, 182 | 'granularity': 'R5', 183 | 'output': { 184 | 'count': {'\$sum': 1} 185 | } 186 | } 187 | }); 188 | }); 189 | 190 | test('count', () { 191 | expect(Count('myCount').build(), {'\$count': 'myCount'}); 192 | }); 193 | 194 | test('facet', () { 195 | expect( 196 | Facet({ 197 | 'categorizedByTags': [ 198 | Unwind(Field('tags')), 199 | SortByCount(Field('tags')) 200 | ], 201 | 'categorizedByPrice': [ 202 | Match(where.exists('price').map['\$query']), 203 | Bucket( 204 | groupBy: Field('price'), 205 | boundaries: [0, 150, 200, 300, 400], 206 | defaultId: 'Other', 207 | output: {'count': Sum(1), 'titles': Push(Field('title'))}) 208 | ], 209 | 'categorizedByYears(Auto)': [ 210 | BucketAuto(groupBy: Field('year'), buckets: 4) 211 | ] 212 | }).build(), 213 | { 214 | '\$facet': { 215 | 'categorizedByTags': [ 216 | { 217 | '\$unwind': {'path': '\$tags'} 218 | }, 219 | {'\$sortByCount': '\$tags'} 220 | ], 221 | 'categorizedByPrice': [ 222 | { 223 | '\$match': { 224 | 'price': {'\$exists': true} 225 | } 226 | }, 227 | { 228 | '\$bucket': { 229 | 'groupBy': '\$price', 230 | 'boundaries': [0, 150, 200, 300, 400], 231 | 'default': 'Other', 232 | 'output': { 233 | 'count': {'\$sum': 1}, 234 | 'titles': {'\$push': '\$title'} 235 | } 236 | } 237 | } 238 | ], 239 | 'categorizedByYears(Auto)': [ 240 | { 241 | '\$bucketAuto': {'groupBy': '\$year', 'buckets': 4} 242 | } 243 | ] 244 | } 245 | }); 246 | }); 247 | 248 | test('replaceWith', () { 249 | expect(ReplaceWith(Field('name')).build(), {'\$replaceWith': '\$name'}); 250 | expect( 251 | ReplaceWith(MergeObjects([ 252 | {'_id': Field('_id'), 'first': '', 'last': ''}, 253 | Field('name') 254 | ])).build(), 255 | { 256 | '\$replaceWith': { 257 | '\$mergeObjects': [ 258 | {'_id': '\$_id', 'first': '', 'last': ''}, 259 | '\$name' 260 | ] 261 | } 262 | }); 263 | }); 264 | 265 | test('group', () { 266 | expect( 267 | Group(id: { 268 | 'month': Month(Field('date')), 269 | 'day': DayOfMonth(Field('date')), 270 | 'year': Year(Field('date')) 271 | }, fields: { 272 | 'totalPrice': Sum(Multiply([Field('price'), Field('quantity')])), 273 | 'averageQuantity': Avg(Field('quantity')), 274 | 'count': Sum(1) 275 | }).build(), 276 | { 277 | '\$group': { 278 | '_id': { 279 | 'month': { 280 | '\$month': {'date': '\$date'} 281 | }, 282 | 'day': { 283 | '\$dayOfMonth': {'date': '\$date'} 284 | }, 285 | 'year': { 286 | '\$year': {'date': '\$date'} 287 | } 288 | }, 289 | 'totalPrice': { 290 | '\$sum': { 291 | '\$multiply': ['\$price', '\$quantity'] 292 | } 293 | }, 294 | 'averageQuantity': {'\$avg': '\$quantity'}, 295 | 'count': {'\$sum': 1} 296 | } 297 | }); 298 | expect( 299 | Group(id: BsonNull(), fields: { 300 | 'totalPrice': Sum(Multiply([Field('price'), Field('quantity')])), 301 | 'averageQuantity': Avg(Field('quantity')), 302 | 'count': Sum(1) 303 | }).build(), 304 | { 305 | '\$group': { 306 | '_id': BsonNull(), 307 | 'totalPrice': { 308 | '\$sum': { 309 | '\$multiply': ['\$price', '\$quantity'] 310 | } 311 | }, 312 | 'averageQuantity': {'\$avg': '\$quantity'}, 313 | 'count': {'\$sum': 1} 314 | } 315 | }); 316 | expect(Group(id: Field('item')).build(), { 317 | '\$group': {'_id': '\$item'} 318 | }); 319 | expect( 320 | Group(id: Field('author'), fields: {'books': Push(Field('title'))}) 321 | .build(), 322 | { 323 | '\$group': { 324 | '_id': '\$author', 325 | 'books': {'\$push': '\$title'} 326 | } 327 | }); 328 | expect( 329 | Group(id: Field('author'), fields: {'books': Push(Var.root)}).build(), { 330 | '\$group': { 331 | '_id': '\$author', 332 | 'books': {'\$push': '\$\$ROOT'} 333 | } 334 | }); 335 | }); 336 | 337 | test('match', () { 338 | expect(Match(where.eq('author', 'dave').map['\$query']).build(), { 339 | '\$match': {'author': 'dave'} 340 | }); 341 | expect(Match(Expr(Eq(Field('author'), 'dave'))).build(), { 342 | '\$match': { 343 | '\$expr': { 344 | '\$eq': ['\$author', 'dave'] 345 | } 346 | } 347 | }); 348 | }); 349 | 350 | test('lookup', () { 351 | expect( 352 | Lookup( 353 | from: 'inventory', 354 | localField: 'item', 355 | foreignField: 'sku', 356 | as: 'inventory_docs') 357 | .build(), 358 | { 359 | '\$lookup': { 360 | 'from': 'inventory', 361 | 'localField': 'item', 362 | 'foreignField': 'sku', 363 | 'as': 'inventory_docs' 364 | } 365 | }); 366 | expect( 367 | Lookup.withPipeline( 368 | from: 'warehouses', 369 | let: { 370 | 'order_item': Field('item'), 371 | 'order_qty': Field('ordered') 372 | }, 373 | pipeline: [ 374 | Match(Expr(And([ 375 | Eq(Field('stock_item'), Var('order_item')), 376 | Gte(Field('instock'), Var('order_qty')) 377 | ]))), 378 | Project({'stock_item': 0, '_id': 0}) 379 | ], 380 | as: 'stockdata') 381 | .build(), 382 | { 383 | '\$lookup': { 384 | 'from': 'warehouses', 385 | 'let': {'order_item': '\$item', 'order_qty': '\$ordered'}, 386 | 'pipeline': [ 387 | { 388 | '\$match': { 389 | '\$expr': { 390 | '\$and': [ 391 | { 392 | '\$eq': ['\$stock_item', '\$\$order_item'] 393 | }, 394 | { 395 | '\$gte': ['\$instock', '\$\$order_qty'] 396 | } 397 | ] 398 | } 399 | } 400 | }, 401 | { 402 | '\$project': {'stock_item': 0, '_id': 0} 403 | } 404 | ], 405 | 'as': 'stockdata' 406 | } 407 | }); 408 | }); 409 | 410 | test('graphLookup', () { 411 | expect( 412 | GraphLookup( 413 | from: 'employees', 414 | startWith: 'reportsTo', 415 | connectFromField: 'reportsTo', 416 | connectToField: 'name', 417 | as: 'reportingHierarchy', 418 | depthField: 'depth', 419 | maxDepth: 5, 420 | restrictSearchWithMatch: where.eq('field', 'value')) 421 | .build(), 422 | { 423 | r'$graphLookup': { 424 | 'from': 'employees', 425 | 'startWith': r'$reportsTo', 426 | 'connectFromField': 'reportsTo', 427 | 'connectToField': 'name', 428 | 'as': 'reportingHierarchy', 429 | 'depthField': 'depth', 430 | 'maxDepth': 5, 431 | 'restrictSearchWithMatch': {'field': 'value'} 432 | } 433 | }); 434 | }); 435 | test('unwind', () { 436 | expect(Unwind(Field('sizes')).build(), { 437 | '\$unwind': {'path': '\$sizes'} 438 | }); 439 | }); 440 | 441 | test('project', () { 442 | expect(Project({'_id': 0, 'title': 1, 'author': 1}).build(), { 443 | '\$project': {'_id': 0, 'title': 1, 'author': 1} 444 | }); 445 | }); 446 | 447 | test('skip', () { 448 | expect(Skip(5).build(), {'\$skip': 5}); 449 | }); 450 | 451 | test('limit', () { 452 | expect(Limit(5).build(), {'\$limit': 5}); 453 | }); 454 | 455 | test('sort', () { 456 | expect(Sort({'age': -1, 'posts': 1}).build(), { 457 | '\$sort': {'age': -1, 'posts': 1} 458 | }); 459 | }); 460 | 461 | test('sortByCount', () { 462 | expect(SortByCount(Field('employee')).build(), 463 | {'\$sortByCount': '\$employee'}); 464 | expect( 465 | SortByCount(MergeObjects([Field('employee'), Field('business')])) 466 | .build(), 467 | { 468 | '\$sortByCount': { 469 | '\$mergeObjects': ['\$employee', '\$business'] 470 | } 471 | }); 472 | }); 473 | 474 | test('geoNear', () { 475 | expect( 476 | GeoNear( 477 | near: Geometry.point([-73.99279, 40.719296]), 478 | distanceField: 'dist.calculated', 479 | maxDistance: 2, 480 | query: where.eq('category', 'Parks').map['\$query'], 481 | includeLocs: 'dist.location', 482 | spherical: true) 483 | .build(), 484 | { 485 | r'$geoNear': { 486 | 'near': { 487 | 'type': 'Point', 488 | 'coordinates': [-73.99279, 40.719296] 489 | }, 490 | 'distanceField': 'dist.calculated', 491 | 'maxDistance': 2, 492 | 'query': {'category': 'Parks'}, 493 | 'includeLocs': 'dist.location', 494 | 'spherical': true 495 | } 496 | }); 497 | 498 | expect(SortByCount(Field('employee')).build(), 499 | {'\$sortByCount': '\$employee'}); 500 | expect( 501 | SortByCount(MergeObjects([Field('employee'), Field('business')])) 502 | .build(), 503 | { 504 | '\$sortByCount': { 505 | '\$mergeObjects': ['\$employee', '\$business'] 506 | } 507 | }); 508 | }); 509 | test('unionWith', () { 510 | expect( 511 | UnionWith( 512 | coll: 'warehouses', 513 | pipeline: [ 514 | Project({'state': 1, '_id': 0}) 515 | ], 516 | ).build(), 517 | { 518 | r'$unionWith': { 519 | 'coll': 'warehouses', 520 | 'pipeline': [ 521 | { 522 | r'$project': {'state': 1, '_id': 0} 523 | } 524 | ] 525 | } 526 | }); 527 | expect( 528 | UnionWith(coll: 'warehouses', pipeline: [ 529 | Match(Expr(And([ 530 | Eq(Field('stock_item'), Var('order_item')), 531 | Gte(Field('instock'), Var('order_qty')) 532 | ]))), 533 | Project({'stock_item': 0, '_id': 0}) 534 | ]).build(), 535 | { 536 | r'$unionWith': { 537 | 'coll': 'warehouses', 538 | 'pipeline': [ 539 | { 540 | r'$match': { 541 | r'$expr': { 542 | r'$and': [ 543 | { 544 | r'$eq': [r'$stock_item', r'$$order_item'] 545 | }, 546 | { 547 | r'$gte': [r'$instock', r'$$order_qty'] 548 | } 549 | ] 550 | } 551 | } 552 | }, 553 | { 554 | r'$project': {'stock_item': 0, '_id': 0} 555 | } 556 | ] 557 | } 558 | }); 559 | }); 560 | } 561 | -------------------------------------------------------------------------------- /test/mongo_aggregation/arithmetic_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('abs', () { 6 | expect(Abs(TestExpr()).build(), {'\$abs': '\$field'}); 7 | }); 8 | 9 | test('add', () { 10 | expect(Add([TestExpr(), 1]).build(), { 11 | '\$add': ['\$field', 1] 12 | }); 13 | }); 14 | 15 | test('ceil', () { 16 | expect(Ceil(TestExpr()).build(), {'\$ceil': '\$field'}); 17 | }); 18 | 19 | test('divide', () { 20 | expect(Divide(TestExpr(), 2).build(), { 21 | '\$divide': ['\$field', 2] 22 | }); 23 | }); 24 | 25 | test('exp', () { 26 | expect(Exp(TestExpr()).build(), {'\$exp': '\$field'}); 27 | }); 28 | 29 | test('floor', () { 30 | expect(Floor(TestExpr()).build(), {'\$floor': '\$field'}); 31 | }); 32 | 33 | test('ln', () { 34 | expect(Ln(TestExpr()).build(), {'\$ln': '\$field'}); 35 | }); 36 | 37 | test('log', () { 38 | expect(Log(TestExpr(), 2).build(), { 39 | '\$log': ['\$field', 2] 40 | }); 41 | }); 42 | 43 | test('log10', () { 44 | expect(Log10(TestExpr()).build(), {'\$log10': '\$field'}); 45 | }); 46 | 47 | test('mod', () { 48 | expect(Mod(TestExpr(), 2).build(), { 49 | '\$mod': ['\$field', 2] 50 | }); 51 | }); 52 | 53 | test('multiply', () { 54 | expect(Multiply([TestExpr(), 3]).build(), { 55 | '\$multiply': ['\$field', 3] 56 | }); 57 | }); 58 | 59 | test('pow', () { 60 | expect(Pow(TestExpr(), 3).build(), { 61 | '\$pow': ['\$field', 3] 62 | }); 63 | }); 64 | 65 | test('round', () { 66 | expect(Round(TestExpr(), 3).build(), { 67 | '\$round': ['\$field', 3] 68 | }); 69 | }); 70 | 71 | test('sqrt', () { 72 | expect(Sqrt(TestExpr()).build(), {'\$sqrt': '\$field'}); 73 | }); 74 | 75 | test('subtract', () { 76 | expect(Subtract(TestExpr(), 3).build(), { 77 | '\$subtract': ['\$field', 3] 78 | }); 79 | }); 80 | 81 | test('trunc', () { 82 | expect(Trunc(TestExpr(), 3).build(), { 83 | '\$trunc': ['\$field', 3] 84 | }); 85 | }); 86 | } 87 | 88 | class TestExpr implements AggregationExpr { 89 | @override 90 | String build() => '\$field'; 91 | } 92 | -------------------------------------------------------------------------------- /test/mongo_aggregation/array_object_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('arrayToObjects', () { 6 | expect(ArrayToObject(Field('products')).build(), 7 | {'\$arrayToObject': '\$products'}); 8 | expect( 9 | ArrayToObject([ 10 | ['item', 'abc123'], 11 | ['qty', 25] 12 | ]).build(), 13 | { 14 | '\$arrayToObject': [ 15 | ['item', 'abc123'], 16 | ['qty', 25] 17 | ] 18 | }); 19 | expect( 20 | ArrayToObject([ 21 | {'k': 'item', 'v': 'abc123'}, 22 | {'k': 'qty', 'v': 25} 23 | ]).build(), 24 | { 25 | '\$arrayToObject': [ 26 | {'k': 'item', 'v': 'abc123'}, 27 | {'k': 'qty', 'v': 25} 28 | ] 29 | }); 30 | }); 31 | 32 | test('arrayElemAt', () { 33 | expect(ArrayElemAt(TestExpr(), 0).build(), { 34 | '\$arrayElemAt': ['\$field', 0] 35 | }); 36 | expect(ArrayElemAt([1, 2, 3], 1).build(), { 37 | '\$arrayElemAt': [ 38 | [1, 2, 3], 39 | 1 40 | ] 41 | }); 42 | }); 43 | 44 | test('concatArrays', () { 45 | expect( 46 | ConcatArrays([ 47 | TestExpr(), 48 | [1, 2] 49 | ]).build(), 50 | { 51 | '\$concatArrays': [ 52 | '\$field', 53 | [1, 2] 54 | ] 55 | }); 56 | }); 57 | 58 | test('filter', () { 59 | expect(Filter(input: TestExpr(), as: 'test', cond: TestExpr()).build(), { 60 | '\$filter': {'input': '\$field', 'as': 'test', 'cond': '\$field'} 61 | }); 62 | }); 63 | 64 | test('in', () { 65 | expect(In(TestExpr(), TestExpr()).build(), { 66 | '\$in': ['\$field', '\$field'] 67 | }); 68 | expect(In('string', ['string', TestExpr()]).build(), { 69 | '\$in': [ 70 | 'string', 71 | ['string', '\$field'] 72 | ] 73 | }); 74 | }); 75 | 76 | test('indexOfArray', () { 77 | expect(IndexOfArray(TestExpr(), TestExpr(), 2, 3).build(), { 78 | '\$indexOfArray': ['\$field', '\$field', 2, 3] 79 | }); 80 | expect(IndexOfArray([1, 2, TestExpr()], 'value', 2, 3).build(), { 81 | '\$indexOfArray': [ 82 | [1, 2, '\$field'], 83 | 'value', 84 | 2, 85 | 3 86 | ] 87 | }); 88 | }); 89 | 90 | test('isArray', () { 91 | expect(IsArray(TestExpr()).build(), {'\$isArray': '\$field'}); 92 | }); 93 | 94 | test('map', () { 95 | expect(MapOp(input: TestExpr(), as: 'val', inExpr: TestExpr()).build(), { 96 | '\$map': {'input': '\$field', 'as': 'val', 'in': '\$field'} 97 | }); 98 | }); 99 | 100 | test('range', () { 101 | expect(Range(1, TestExpr(), 2).build(), { 102 | '\$range': [1, '\$field', 2] 103 | }); 104 | }); 105 | 106 | test('reduce', () { 107 | expect( 108 | Reduce(input: TestExpr(), initialValue: 0, inExpr: TestExpr()).build(), 109 | { 110 | '\$reduce': {'input': '\$field', 'initialValue': 0, 'in': '\$field'} 111 | }); 112 | }); 113 | 114 | test('reverseArray', () { 115 | expect(ReverseArray(TestExpr()).build(), {'\$reverseArray': '\$field'}); 116 | expect(ReverseArray([1, 2, TestExpr()]).build(), { 117 | '\$reverseArray': [1, 2, '\$field'] 118 | }); 119 | }); 120 | 121 | test('slice', () { 122 | expect(Slice(TestExpr(), 5, 2).build(), { 123 | '\$slice': ['\$field', 2, 5] 124 | }); 125 | expect(Slice([1, TestExpr()], 5, 2).build(), { 126 | '\$slice': [ 127 | [1, '\$field'], 128 | 2, 129 | 5 130 | ] 131 | }); 132 | }); 133 | 134 | test('zip', () { 135 | expect( 136 | Zip( 137 | inputs: [ 138 | TestExpr(), 139 | [1, 2] 140 | ], 141 | useLongestLength: true, 142 | defaults: ['a', 'b']).build(), 143 | { 144 | '\$zip': { 145 | 'inputs': [ 146 | '\$field', 147 | [1, 2] 148 | ], 149 | 'useLongestLength': true, 150 | 'defaults': ['a', 'b'] 151 | } 152 | }); 153 | }); 154 | 155 | test('mergeObjects', () { 156 | expect(MergeObjects(TestExpr()).build(), {'\$mergeObjects': '\$field'}); 157 | expect( 158 | MergeObjects([ 159 | TestExpr(), 160 | {'a': TestExpr(), 'b': 2} 161 | ]).build(), 162 | { 163 | '\$mergeObjects': [ 164 | '\$field', 165 | {'a': '\$field', 'b': 2} 166 | ] 167 | }); 168 | }); 169 | 170 | test('objectToArray', () { 171 | expect( 172 | ObjectToArray(Field('order')).build(), {'\$objectToArray': '\$order'}); 173 | }); 174 | } 175 | 176 | class TestExpr implements AggregationExpr { 177 | @override 178 | String build() => '\$field'; 179 | } 180 | -------------------------------------------------------------------------------- /test/mongo_aggregation/comparison_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('cmp', () { 6 | expect(Cmp(TestExpr(), 5).build(), { 7 | '\$cmp': ['\$field', 5] 8 | }); 9 | }); 10 | 11 | test('eq', () { 12 | expect(Eq(TestExpr(), 5).build(), { 13 | '\$eq': ['\$field', 5] 14 | }); 15 | }); 16 | 17 | test('gt', () { 18 | expect(Gt(TestExpr(), 5).build(), { 19 | '\$gt': ['\$field', 5] 20 | }); 21 | }); 22 | 23 | test('gte', () { 24 | expect(Gte(TestExpr(), 5).build(), { 25 | '\$gte': ['\$field', 5] 26 | }); 27 | }); 28 | 29 | test('lt', () { 30 | expect(Lt(TestExpr(), 5).build(), { 31 | '\$lt': ['\$field', 5] 32 | }); 33 | }); 34 | 35 | test('lte', () { 36 | expect(Lte(TestExpr(), 5).build(), { 37 | '\$lte': ['\$field', 5] 38 | }); 39 | }); 40 | 41 | test('ne', () { 42 | expect(Ne(TestExpr(), 5).build(), { 43 | '\$ne': ['\$field', 5] 44 | }); 45 | }); 46 | 47 | test('cond', () { 48 | expect( 49 | Cond(ifExpr: TestExpr(), thenExpr: TestExpr(), elseExpr: TestExpr()) 50 | .build(), 51 | { 52 | '\$cond': ['\$field', '\$field', '\$field'] 53 | }); 54 | }); 55 | 56 | test('ifNull', () { 57 | expect(IfNull(TestExpr(), 'replacement').build(), { 58 | '\$ifNull': ['\$field', 'replacement'] 59 | }); 60 | }); 61 | 62 | test('switch', () { 63 | expect( 64 | Switch( 65 | branches: [Case(caseExpr: TestExpr(), thenExpr: 'expr')], 66 | defaultExpr: 'default') 67 | .build(), 68 | { 69 | '\$switch': { 70 | 'branches': [ 71 | {'case': '\$field', 'then': 'expr'} 72 | ], 73 | 'default': 'default' 74 | } 75 | }); 76 | }); 77 | } 78 | 79 | class TestExpr implements AggregationExpr { 80 | @override 81 | String build() => '\$field'; 82 | } 83 | -------------------------------------------------------------------------------- /test/mongo_aggregation/date_time_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('dateFromParts', () { 6 | expect( 7 | DateFromParts( 8 | year: 2019, 9 | month: 9, 10 | day: 1, 11 | hour: 0, 12 | minute: 0, 13 | second: 0, 14 | millisecond: 0, 15 | timezone: 'Europe/Moscow') 16 | .build(), 17 | { 18 | '\$dateFromParts': { 19 | 'year': 2019, 20 | 'month': 9, 21 | 'day': 1, 22 | 'hour': 0, 23 | 'minute': 0, 24 | 'second': 0, 25 | 'millisecond': 0, 26 | 'timezone': 'Europe/Moscow' 27 | } 28 | }); 29 | }); 30 | 31 | test('IsoDateFromParts', () { 32 | expect( 33 | IsoDateFromParts( 34 | year: 2019, 35 | week: 9, 36 | day: 1, 37 | hour: 0, 38 | minute: 0, 39 | second: 0, 40 | millisecond: 0, 41 | timezone: 'Europe/Moscow') 42 | .build(), 43 | { 44 | '\$dateFromParts': { 45 | 'isoWeekYear': 2019, 46 | 'isoWeek': 9, 47 | 'isoDayOfWeek': 1, 48 | 'hour': 0, 49 | 'minute': 0, 50 | 'second': 0, 51 | 'millisecond': 0, 52 | 'timezone': 'Europe/Moscow' 53 | } 54 | }); 55 | }); 56 | 57 | test('dateFromString', () { 58 | expect( 59 | DateFromString( 60 | dateString: '06-15-2018', 61 | format: '%m-%d-%Y', 62 | timezone: 'Europe/Moscow', 63 | onError: TestExpr(), 64 | onNull: TestExpr()) 65 | .build(), 66 | { 67 | '\$dateFromString': { 68 | 'dateString': '06-15-2018', 69 | 'format': '%m-%d-%Y', 70 | 'timezone': 'Europe/Moscow', 71 | 'onError': '\$field', 72 | 'onNull': '\$field' 73 | } 74 | }); 75 | }); 76 | 77 | test('dateToParts', () { 78 | expect( 79 | DateToParts(TestExpr(), timezone: 'Europe/Moscow', iso8601: true) 80 | .build(), 81 | { 82 | '\$dateToParts': { 83 | 'date': '\$field', 84 | 'timezone': 'Europe/Moscow', 85 | 'iso8601': true 86 | } 87 | }); 88 | }); 89 | 90 | test('dayOfMonth', () { 91 | expect(DayOfMonth(TestExpr(), timezone: 'Europe/Moscow').build(), { 92 | '\$dayOfMonth': {'date': '\$field', 'timezone': 'Europe/Moscow'} 93 | }); 94 | }); 95 | 96 | test('dayOfWeek', () { 97 | expect(DayOfWeek(TestExpr(), timezone: 'Europe/Moscow').build(), { 98 | '\$dayOfWeek': {'date': '\$field', 'timezone': 'Europe/Moscow'} 99 | }); 100 | }); 101 | 102 | test('dayOfYear', () { 103 | expect(DayOfYear(TestExpr(), timezone: 'Europe/Moscow').build(), { 104 | '\$dayOfYear': {'date': '\$field', 'timezone': 'Europe/Moscow'} 105 | }); 106 | }); 107 | 108 | test('hour', () { 109 | expect(Hour(TestExpr(), timezone: 'Europe/Moscow').build(), { 110 | '\$hour': {'date': '\$field', 'timezone': 'Europe/Moscow'} 111 | }); 112 | }); 113 | 114 | test('isoDayOfWeek', () { 115 | expect(IsoDayOfWeek(TestExpr(), timezone: 'Europe/Moscow').build(), { 116 | '\$isoDayOfWeek': {'date': '\$field', 'timezone': 'Europe/Moscow'} 117 | }); 118 | }); 119 | 120 | test('isoWeek', () { 121 | expect(IsoWeek(TestExpr(), timezone: 'Europe/Moscow').build(), { 122 | '\$isoWeek': {'date': '\$field', 'timezone': 'Europe/Moscow'} 123 | }); 124 | }); 125 | 126 | test('isoWeekYear', () { 127 | expect(IsoWeekYear(TestExpr(), timezone: 'Europe/Moscow').build(), { 128 | '\$isoWeekYear': {'date': '\$field', 'timezone': 'Europe/Moscow'} 129 | }); 130 | }); 131 | 132 | test('millisecond', () { 133 | expect(Millisecond(TestExpr(), timezone: 'Europe/Moscow').build(), { 134 | '\$millisecond': {'date': '\$field', 'timezone': 'Europe/Moscow'} 135 | }); 136 | }); 137 | 138 | test('minute', () { 139 | expect(Minute(TestExpr(), timezone: 'Europe/Moscow').build(), { 140 | '\$minute': {'date': '\$field', 'timezone': 'Europe/Moscow'} 141 | }); 142 | }); 143 | 144 | test('month', () { 145 | expect(Month(TestExpr(), timezone: 'Europe/Moscow').build(), { 146 | '\$month': {'date': '\$field', 'timezone': 'Europe/Moscow'} 147 | }); 148 | }); 149 | 150 | test('second', () { 151 | expect(Second(TestExpr(), timezone: 'Europe/Moscow').build(), { 152 | '\$second': {'date': '\$field', 'timezone': 'Europe/Moscow'} 153 | }); 154 | }); 155 | 156 | test('week', () { 157 | expect(Week(TestExpr(), timezone: 'Europe/Moscow').build(), { 158 | '\$week': {'date': '\$field', 'timezone': 'Europe/Moscow'} 159 | }); 160 | }); 161 | 162 | test('year', () { 163 | expect(Year(TestExpr(), timezone: 'Europe/Moscow').build(), { 164 | '\$year': {'date': '\$field', 'timezone': 'Europe/Moscow'} 165 | }); 166 | }); 167 | 168 | test('toDate', () { 169 | expect(ToDate(TestExpr()).build(), {'\$toDate': '\$field'}); 170 | }); 171 | } 172 | 173 | class TestExpr implements AggregationExpr { 174 | @override 175 | String build() => '\$field'; 176 | } 177 | -------------------------------------------------------------------------------- /test/mongo_aggregation/group_stage_accumulators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('addToSet', () { 6 | expect(AddToSet(TestExpr()).build(), {'\$addToSet': '\$field'}); 7 | }); 8 | 9 | test('avg', () { 10 | expect(Avg(TestExpr()).build(), {'\$avg': '\$field'}); 11 | expect(Avg([TestExpr(), 2]).build(), { 12 | '\$avg': ['\$field', 2] 13 | }); 14 | }); 15 | 16 | test('first', () { 17 | expect(First(TestExpr()).build(), {'\$first': '\$field'}); 18 | }); 19 | 20 | test('last', () { 21 | expect(Last(TestExpr()).build(), {'\$last': '\$field'}); 22 | }); 23 | 24 | test('max', () { 25 | expect(Max(TestExpr()).build(), {'\$max': '\$field'}); 26 | expect(Max([TestExpr(), 2]).build(), { 27 | '\$max': ['\$field', 2] 28 | }); 29 | }); 30 | 31 | test('min', () { 32 | expect(Min(TestExpr()).build(), {'\$min': '\$field'}); 33 | expect(Min([TestExpr(), 2]).build(), { 34 | '\$min': ['\$field', 2] 35 | }); 36 | }); 37 | 38 | test('push', () { 39 | expect(Push(TestExpr()).build(), {'\$push': '\$field'}); 40 | expect(Push.list([TestExpr(), 1]).build(), { 41 | '\$push': ['\$field', 1] 42 | }); 43 | expect(Push.object({'field': TestExpr(), 'num': 1}).build(), { 44 | '\$push': {'field': '\$field', 'num': 1} 45 | }); 46 | }); 47 | 48 | test('stdDevPop', () { 49 | expect(StdDevPop(TestExpr()).build(), {'\$stdDevPop': '\$field'}); 50 | expect(StdDevPop([TestExpr(), 2]).build(), { 51 | '\$stdDevPop': ['\$field', 2] 52 | }); 53 | }); 54 | 55 | test('stdDevSamp', () { 56 | expect(StdDevSamp(TestExpr()).build(), {'\$stdDevSamp': '\$field'}); 57 | expect(StdDevSamp([TestExpr(), 2]).build(), { 58 | '\$stdDevSamp': ['\$field', 2] 59 | }); 60 | }); 61 | 62 | test('sum', () { 63 | expect(Sum(TestExpr()).build(), {'\$sum': '\$field'}); 64 | expect(Sum([TestExpr(), 2]).build(), { 65 | '\$sum': ['\$field', 2] 66 | }); 67 | }); 68 | } 69 | 70 | class TestExpr implements AggregationExpr { 71 | @override 72 | String build() => '\$field'; 73 | } 74 | -------------------------------------------------------------------------------- /test/mongo_aggregation/logic_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('and', () { 6 | expect(And([TestExpr(), false]).build(), { 7 | '\$and': ['\$field', false] 8 | }); 9 | }); 10 | 11 | test('or', () { 12 | expect(Or([TestExpr(), false]).build(), { 13 | '\$or': ['\$field', false] 14 | }); 15 | }); 16 | 17 | test('not', () { 18 | expect(Not(TestExpr()).build(), {'\$not': '\$field'}); 19 | }); 20 | } 21 | 22 | class TestExpr implements AggregationExpr { 23 | @override 24 | String build() => '\$field'; 25 | } 26 | -------------------------------------------------------------------------------- /test/mongo_aggregation/string_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('concat', () { 6 | expect(Concat([TestExpr(), 'string']).build(), { 7 | '\$concat': ['\$field', 'string'] 8 | }); 9 | }); 10 | 11 | test('indexOfBytes', () { 12 | expect(IndexOfBytes(TestExpr(), 'substr', 1, 10).build(), { 13 | '\$indexOfBytes': ['\$field', 'substr', 1, 10] 14 | }); 15 | }); 16 | 17 | test('indexOfCP', () { 18 | expect(IndexOfCP(TestExpr(), 'substr', 1, 10).build(), { 19 | '\$indexOfCP': ['\$field', 'substr', 1, 10] 20 | }); 21 | }); 22 | 23 | test('ltrim', () { 24 | expect(Ltrim(input: TestExpr(), chars: '*').build(), { 25 | '\$ltrim': {'input': '\$field', 'chars': '*'} 26 | }); 27 | }); 28 | 29 | test('regexFind', () { 30 | expect( 31 | RegexFind(input: TestExpr(), regex: 'regex', options: 'is').build(), { 32 | '\$regexFind': {'input': '\$field', 'regex': 'regex', 'options': 'is'} 33 | }); 34 | }); 35 | 36 | test('regexFindAll', () { 37 | expect( 38 | RegexFindAll(input: TestExpr(), regex: 'regex', options: 'is').build(), 39 | { 40 | '\$regexFindAll': { 41 | 'input': '\$field', 42 | 'regex': 'regex', 43 | 'options': 'is' 44 | } 45 | }); 46 | }); 47 | 48 | test('regexMatch', () { 49 | expect( 50 | RegexMatch(input: TestExpr(), regex: 'regex', options: 'is').build(), { 51 | '\$regexMatch': {'input': '\$field', 'regex': 'regex', 'options': 'is'} 52 | }); 53 | }); 54 | 55 | test('rtrim', () { 56 | expect(Rtrim(input: TestExpr(), chars: '*').build(), { 57 | '\$rtrim': {'input': '\$field', 'chars': '*'} 58 | }); 59 | }); 60 | 61 | test('split', () { 62 | expect(Split(TestExpr(), ',').build(), { 63 | '\$split': ['\$field', ','] 64 | }); 65 | }); 66 | 67 | test('strLenBytes', () { 68 | expect(StrLenBytes(TestExpr()).build(), {'\$strLenBytes': '\$field'}); 69 | }); 70 | 71 | test('strLenCP', () { 72 | expect(StrLenCP(TestExpr()).build(), {'\$strLenCP': '\$field'}); 73 | }); 74 | 75 | test('strcasecmp', () { 76 | expect(StrCaseCmp(TestExpr(), TestExpr()).build(), { 77 | '\$strcasecmp': ['\$field', '\$field'] 78 | }); 79 | }); 80 | 81 | test('substrBytes', () { 82 | expect(SubstrBytes(TestExpr(), 5, 3).build(), { 83 | '\$substrBytes': ['\$field', 5, 3] 84 | }); 85 | }); 86 | 87 | test('substrCP', () { 88 | expect(SubstrCP(TestExpr(), 5, 3).build(), { 89 | '\$substrCP': ['\$field', 5, 3] 90 | }); 91 | }); 92 | 93 | test('toLower', () { 94 | expect(ToLower(TestExpr()).build(), {'\$toLower': '\$field'}); 95 | }); 96 | 97 | test('toString', () { 98 | expect(ToString(TestExpr()).build(), {'\$toString': '\$field'}); 99 | }); 100 | 101 | test('trim', () { 102 | expect(Trim(input: TestExpr(), chars: '*').build(), { 103 | '\$trim': {'input': '\$field', 'chars': '*'} 104 | }); 105 | }); 106 | 107 | test('toUpper', () { 108 | expect(ToUpper(TestExpr()).build(), {'\$toUpper': '\$field'}); 109 | }); 110 | } 111 | 112 | class TestExpr implements AggregationExpr { 113 | @override 114 | String build() => '\$field'; 115 | } 116 | -------------------------------------------------------------------------------- /test/mongo_aggregation/type_expressions_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('convert', () { 6 | expect( 7 | Convert( 8 | input: TestExpr(), 9 | to: 'string', 10 | onError: TestExpr(), 11 | onNull: TestExpr()) 12 | .build(), 13 | { 14 | '\$convert': { 15 | 'input': '\$field', 16 | 'to': 'string', 17 | 'onError': '\$field', 18 | 'onNull': '\$field' 19 | } 20 | }); 21 | }); 22 | 23 | test('toBool', () { 24 | expect(ToBool(TestExpr()).build(), {'\$toBool': '\$field'}); 25 | }); 26 | 27 | test('toDecimal', () { 28 | expect(ToDecimal(TestExpr()).build(), {'\$toDecimal': '\$field'}); 29 | }); 30 | 31 | test('toDouble', () { 32 | expect(ToDouble(TestExpr()).build(), {'\$toDouble': '\$field'}); 33 | }); 34 | 35 | test('toInt', () { 36 | expect(ToInt(TestExpr()).build(), {'\$toInt': '\$field'}); 37 | }); 38 | 39 | test('toLong', () { 40 | expect(ToLong(TestExpr()).build(), {'\$toLong': '\$field'}); 41 | }); 42 | 43 | test('toObjectId', () { 44 | expect(ToObjectId(TestExpr()).build(), {'\$toObjectId': '\$field'}); 45 | }); 46 | 47 | test('type', () { 48 | expect(Type(TestExpr()).build(), {'\$type': '\$field'}); 49 | }); 50 | } 51 | 52 | class TestExpr implements AggregationExpr { 53 | @override 54 | String build() => '\$field'; 55 | } 56 | -------------------------------------------------------------------------------- /test/mongo_aggregation/uncategorized_operators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:mongo_dart_query/mongo_aggregation.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('expr', () { 6 | expect(Expr(TestExpr()).build(), {'\$expr': '\$field'}); 7 | }); 8 | 9 | test('let', () { 10 | expect(Let(vars: {'var': TestExpr()}, inExpr: TestExpr()).build(), { 11 | '\$let': { 12 | 'vars': {'var': '\$field'}, 13 | 'in': '\$field' 14 | } 15 | }); 16 | }); 17 | } 18 | 19 | class TestExpr implements AggregationExpr { 20 | @override 21 | String build() => '\$field'; 22 | } 23 | -------------------------------------------------------------------------------- /test/selector_test.dart: -------------------------------------------------------------------------------- 1 | library test_lib; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:bson/bson.dart'; 5 | import 'package:mongo_dart_query/mongo_dart_query.dart'; 6 | 7 | void main() { 8 | test('SelectorBuilder Creation', () { 9 | var selector = where; 10 | //expect(selector.map is Map, isTrue); 11 | expect(selector.map, isEmpty); 12 | }); 13 | 14 | test('testSelectorBuilderOnObjectId', () { 15 | var id = ObjectId(); 16 | var selector = where.id(id); 17 | //expect(selector.map is Map, isTrue); 18 | expect(selector.map.length, greaterThan(0)); 19 | expect( 20 | selector.map, 21 | equals({ 22 | r'$query': {'_id': id} 23 | })); 24 | }); 25 | 26 | test('testSelectorBuilderRawMap', () { 27 | var selector = where.raw({ 28 | r'$query': {'name': 'joe'} 29 | }); 30 | 31 | var id = ObjectId(); 32 | selector.id(id); 33 | //expect(selector.map is Map, isTrue); 34 | expect(selector.map.length, 1); 35 | expect( 36 | selector.map, 37 | equals({ 38 | r'$query': { 39 | r'$and': [ 40 | {'name': 'joe'}, 41 | {'_id': id} 42 | ] 43 | } 44 | })); 45 | }); 46 | 47 | test('testQueries', () { 48 | var selector = where.gt('my_field', 995).sortBy('my_field'); 49 | expect(selector.map, { 50 | r'$query': { 51 | 'my_field': {r'$gt': 995} 52 | }, 53 | 'orderby': {'my_field': 1} 54 | }); 55 | selector = where 56 | .inRange('my_field', 700, 703, minInclude: false) 57 | .sortBy('my_field'); 58 | expect(selector.map, { 59 | r'$query': { 60 | 'my_field': {r'$gt': 700, r'$lt': 703} 61 | }, 62 | 'orderby': {'my_field': 1} 63 | }); 64 | selector = where.eq('my_field', 17).fields(['str_field']); 65 | expect(selector.map, { 66 | r'$query': {'my_field': 17} 67 | }); 68 | expect(selector.paramFields, {'str_field': 1}); 69 | selector = where.sortBy('a').skip(300); 70 | expect( 71 | selector.map, 72 | equals({ 73 | '\$query': {}, 74 | 'orderby': {'a': 1} 75 | })); 76 | selector = where.hint('bar').hint('baz', descending: true).explain(); 77 | expect( 78 | selector.map, 79 | equals({ 80 | '\$query': {}, 81 | '\$hint': {'bar': 1, 'baz': -1}, 82 | '\$explain': true 83 | })); 84 | selector = where.hintIndex('foo'); 85 | expect(selector.map, equals({'\$query': {}, '\$hint': 'foo'})); 86 | }); 87 | 88 | test('testQueryComposition', () { 89 | var selector = where.gt('a', 995).eq('b', 'bbb'); 90 | expect( 91 | selector.map, 92 | equals({ 93 | r'$query': { 94 | '\$and': [ 95 | { 96 | 'a': {r'$gt': 995} 97 | }, 98 | {'b': 'bbb'} 99 | ] 100 | } 101 | })); 102 | selector = where.gt('a', 995).lt('a', 1000); 103 | expect( 104 | selector.map, 105 | equals({ 106 | r'$query': { 107 | '\$and': [ 108 | { 109 | 'a': {r'$gt': 995} 110 | }, 111 | { 112 | 'a': {r'$lt': 1000} 113 | } 114 | ] 115 | } 116 | })); 117 | selector = 118 | where.gt('a', 995).and(where.lt('b', 1000).or(where.gt('c', 2000))); 119 | expect(selector.map, { 120 | '\$query': { 121 | '\$and': [ 122 | { 123 | 'a': {'\$gt': 995} 124 | }, 125 | { 126 | '\$or': [ 127 | { 128 | 'b': {'\$lt': 1000} 129 | }, 130 | { 131 | 'c': {'\$gt': 2000} 132 | } 133 | ] 134 | } 135 | ] 136 | } 137 | }); 138 | selector = 139 | where.lt('b', 1000).or(where.gt('c', 2000)).and(where.gt('a', 995)); 140 | expect(selector.map, { 141 | '\$query': { 142 | '\$and': [ 143 | { 144 | '\$or': [ 145 | { 146 | 'b': {'\$lt': 1000} 147 | }, 148 | { 149 | 'c': {'\$gt': 2000} 150 | } 151 | ] 152 | }, 153 | { 154 | 'a': {'\$gt': 995} 155 | } 156 | ] 157 | } 158 | }); 159 | selector = where.lt('b', 1000).or(where.gt('c', 2000)).gt('a', 995); 160 | expect(selector.map, { 161 | '\$query': { 162 | '\$and': [ 163 | { 164 | '\$or': [ 165 | { 166 | 'b': {'\$lt': 1000} 167 | }, 168 | { 169 | 'c': {'\$gt': 2000} 170 | } 171 | ] 172 | }, 173 | { 174 | 'a': {'\$gt': 995} 175 | } 176 | ] 177 | } 178 | }); 179 | selector = 180 | where.lt('b', 1000).or(where.gt('c', 2000)).or(where.gt('a', 995)); 181 | expect(selector.map, { 182 | '\$query': { 183 | '\$or': [ 184 | { 185 | 'b': {'\$lt': 1000} 186 | }, 187 | { 188 | 'c': {'\$gt': 2000} 189 | }, 190 | { 191 | 'a': {'\$gt': 995} 192 | } 193 | ] 194 | } 195 | }); 196 | selector = where 197 | .eq('price', 1.99) 198 | .and(where.lt('qty', 20).or(where.eq('sale', true))); 199 | expect(selector.map, { 200 | '\$query': { 201 | '\$and': [ 202 | {'price': 1.99}, 203 | { 204 | '\$or': [ 205 | { 206 | 'qty': {'\$lt': 20} 207 | }, 208 | {'sale': true} 209 | ] 210 | } 211 | ] 212 | } 213 | }); 214 | selector = where 215 | .eq('price', 1.99) 216 | .and(where.lt('qty', 20)) 217 | .and(where.eq('sale', true)); 218 | expect(selector.map, { 219 | '\$query': { 220 | '\$and': [ 221 | {'price': 1.99}, 222 | { 223 | 'qty': {'\$lt': 20} 224 | }, 225 | {'sale': true} 226 | ] 227 | } 228 | }); 229 | selector = where.eq('price', 1.99).lt('qty', 20).eq('sale', true); 230 | expect(selector.map, { 231 | '\$query': { 232 | '\$and': [ 233 | {'price': 1.99}, 234 | { 235 | 'qty': {'\$lt': 20} 236 | }, 237 | {'sale': true} 238 | ] 239 | } 240 | }); 241 | selector = 242 | where.eq('foo', 'bar').or(where.eq('foo', null)).eq('name', 'jack'); 243 | expect(selector.map, { 244 | r'$query': { 245 | r'$and': [ 246 | { 247 | r'$or': [ 248 | {'foo': 'bar'}, 249 | {'foo': null} 250 | ] 251 | }, 252 | {'name': 'jack'} 253 | ] 254 | } 255 | }); 256 | }); 257 | 258 | group('Modifier Builder', () { 259 | test('set unset', () { 260 | var modifier = modify.set('a', 995).set('b', 'bbb'); 261 | expect( 262 | modifier.map, 263 | equals({ 264 | r'$set': {'a': 995, 'b': 'bbb'} 265 | })); 266 | modifier = modify.unset('a').unset('b'); 267 | expect( 268 | modifier.map, 269 | equals({ 270 | r'$unset': {'a': 1, 'b': 1} 271 | })); 272 | }); 273 | test('mul', () { 274 | var modifier = modify.set('a', 995).mul('b', 5); 275 | expect( 276 | modifier.map, 277 | equals({ 278 | r'$set': {'a': 995}, 279 | r'$mul': {'b': 5} 280 | })); 281 | modifier = modify.mul('a', 7.0).mul('b', 3); 282 | expect( 283 | modifier.map, 284 | equals({ 285 | r'$mul': {'a': 7.0, 'b': 3} 286 | })); 287 | }); 288 | test('addEachToSet', () { 289 | var modifier = modify.addEachToSet('a', [1, 2, 3]); 290 | expect( 291 | modifier.map, 292 | equals({ 293 | r'$addToSet': { 294 | 'a': { 295 | r'$each': [1, 2, 3] 296 | } 297 | }, 298 | })); 299 | }); 300 | }); 301 | test('testGetQueryString', () { 302 | var selector = where.eq('foo', 'bar'); 303 | expect(selector.getQueryString(), r'{"$query":{"foo":"bar"}}'); 304 | selector = where.lt('foo', 2); 305 | expect(selector.getQueryString(), r'{"$query":{"foo":{"$lt":2}}}'); 306 | var id = ObjectId(); 307 | selector = where.id(id); 308 | expect(selector.getQueryString(), '{"\$query":{"_id":"${id.oid}"}}'); 309 | // var dbPointer = new DbRef('Dummy',id); 310 | // selector = where.eq('foo',dbPointer); 311 | // expect(selector.getQueryString(),'{"\$query":{"foo":$dbPointer}}'); 312 | }); 313 | 314 | test('sortByMetaTextScore', () { 315 | var fieldName = 'fName'; 316 | var searchText = 'sText'; 317 | var selector = where 318 | .sortBy(fieldName) 319 | .eq('\$text', {'\$search': searchText}).metaTextScore('score'); 320 | 321 | expect(selector.getQueryString(), 322 | r'{"$query":{"$text":{"$search":"sText"}},"orderby":{"fName":1}}'); 323 | }); 324 | 325 | test('copyWith_clone', () { 326 | var selector = where 327 | .eq('field', 'value') 328 | .gt('num_field', 5) 329 | .and(where.nearSphere('geo_obj', Geometry.point([35.0, 35.0]))); 330 | 331 | var copied = SelectorBuilder.copyWith(selector); 332 | 333 | expect(selector.getQueryString(), equals(copied.getQueryString())); 334 | }); 335 | 336 | test('nearSphere', () { 337 | var selector = where.nearSphere( 338 | 'geo_field', 339 | Geometry(type: GeometryObjectType.Polygon, coordinates: [ 340 | [0, 0], 341 | [1, 8], 342 | [12, 30], 343 | [0, 0] 344 | ]), 345 | maxDistance: 1000, 346 | minDistance: 500); 347 | 348 | expect( 349 | selector.map, 350 | equals({ 351 | r'$query': { 352 | 'geo_field': { 353 | r'$nearSphere': { 354 | r'$geometry': { 355 | 'type': 'Polygon', 356 | 'coordinates': [ 357 | [0, 0], 358 | [1, 8], 359 | [12, 30], 360 | [0, 0] 361 | ] 362 | }, 363 | r'$minDistance': 500, 364 | r'$maxDistance': 1000 365 | } 366 | } 367 | } 368 | })); 369 | }); 370 | 371 | test('Match', () { 372 | var selector = where.match('testField', 'john.doe@noone.com'); 373 | expect(selector.map[r'$query']['testField'][r'$regex'].pattern, 374 | RegExp('john.doe@noone.com').pattern); 375 | selector = 376 | where.match('testField', 'john.doe@noone.com', escapePattern: true); 377 | expect(selector.map[r'$query']['testField'][r'$regex'].pattern, 378 | RegExp(r'john\.doe@noone\.com').pattern); 379 | selector = 380 | where.match('testField', 'john.doe@noone.com', caseInsensitive: true); 381 | expect(selector.map[r'$query']['testField'][r'$regex'], 382 | RegExp('john.doe@noone.com', caseSensitive: true)); 383 | }); 384 | test('geoIntersects', () { 385 | var selector = where.geoIntersects( 386 | 'geo_field', 387 | Geometry(type: GeometryObjectType.Polygon, coordinates: [ 388 | [0, 0], 389 | [1, 8], 390 | [12, 30], 391 | [0, 0] 392 | ])); 393 | 394 | expect( 395 | selector.map, 396 | equals({ 397 | r'$query': { 398 | 'geo_field': { 399 | r'$geoIntersects': { 400 | r'$geometry': { 401 | 'type': 'Polygon', 402 | 'coordinates': [ 403 | [0, 0], 404 | [1, 8], 405 | [12, 30], 406 | [0, 0] 407 | ] 408 | } 409 | } 410 | } 411 | } 412 | })); 413 | }); 414 | 415 | test('geoWithin_geometry', () { 416 | var selector = where.geoWithin( 417 | 'geo_field', 418 | Geometry(type: GeometryObjectType.Polygon, coordinates: [ 419 | [0, 0], 420 | [1, 8], 421 | [12, 30], 422 | [0, 0] 423 | ])); 424 | 425 | expect( 426 | selector.map, 427 | equals({ 428 | r'$query': { 429 | 'geo_field': { 430 | r'$geoWithin': { 431 | r'$geometry': { 432 | 'type': 'Polygon', 433 | 'coordinates': [ 434 | [0, 0], 435 | [1, 8], 436 | [12, 30], 437 | [0, 0] 438 | ] 439 | } 440 | } 441 | } 442 | } 443 | })); 444 | }); 445 | 446 | test('geoWithin_box', () { 447 | var selector = where.geoWithin( 448 | 'geo_field', Box(bottomLeft: [5, 8], upperRight: [8.8, 10.5])); 449 | 450 | expect( 451 | selector.map, 452 | equals({ 453 | r'$query': { 454 | 'geo_field': { 455 | r'$geoWithin': { 456 | r'$box': [ 457 | [5, 8], 458 | [8.8, 10.5] 459 | ] 460 | } 461 | } 462 | } 463 | })); 464 | }); 465 | 466 | test('geoWithin_center', () { 467 | var selector = 468 | where.geoWithin('geo_field', Center(center: [5, 8], radius: 50.2)); 469 | 470 | expect( 471 | selector.map, 472 | equals({ 473 | r'$query': { 474 | 'geo_field': { 475 | r'$geoWithin': { 476 | r'$center': [ 477 | [5, 8], 478 | 50.2 479 | ] 480 | } 481 | } 482 | } 483 | })); 484 | }); 485 | } 486 | --------------------------------------------------------------------------------