Additional Firestore Query methods (#369)

* Extend orderBy to be used multiple times.

* Implement startAt/After and endAt/Before

* Implement `limit` method

* Formatting fix

* Fix missing parameter list and related tests

* Formatting fixes

* Pass start/end methods an array, not a list

* Update CHANGELOG.md

* Update pubspec.yaml
diff --git a/packages/cloud_firestore/CHANGELOG.md b/packages/cloud_firestore/CHANGELOG.md
index 7c86cf0..da47363 100644
--- a/packages/cloud_firestore/CHANGELOG.md
+++ b/packages/cloud_firestore/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.2.5
+
+* Query can now have more than one orderBy field.
+* startAt, startAfter, endAt, and endBefore support
+* limit support
+
 ## 0.2.4
 
 * Support for DocumentReference.documentID
diff --git a/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java b/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java
index 072c1ef..9057362 100644
--- a/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java
+++ b/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java
@@ -86,12 +86,31 @@
       }
     }
     @SuppressWarnings("unchecked")
-    List<Object> orderBy = (List<Object>) parameters.get("orderBy");
+    Number limit = (Number) parameters.get("limit");
+    if (limit != null) query = query.limit(limit.longValue());
+    @SuppressWarnings("unchecked")
+    List<List<Object>> orderBy = (List<List<Object>>) parameters.get("orderBy");
     if (orderBy == null) return query;
-    String orderByFieldName = (String) orderBy.get(0);
-    Boolean descending = (Boolean) orderBy.get(1);
-    Query.Direction direction = descending ? Query.Direction.DESCENDING : Query.Direction.ASCENDING;
-    return query.orderBy(orderByFieldName, direction);
+    for (List<Object> order : orderBy) {
+      String orderByFieldName = (String) order.get(0);
+      Boolean descending = (Boolean) order.get(1);
+      Query.Direction direction =
+          descending ? Query.Direction.DESCENDING : Query.Direction.ASCENDING;
+      query = query.orderBy(orderByFieldName, direction);
+    }
+    @SuppressWarnings("unchecked")
+    List<Object> startAt = (List<Object>) parameters.get("startAt");
+    if (startAt != null) query = query.startAt(startAt.toArray());
+    @SuppressWarnings("unchecked")
+    List<Object> startAfter = (List<Object>) parameters.get("startAfter");
+    if (startAfter != null) query = query.startAfter(startAfter.toArray());
+    @SuppressWarnings("unchecked")
+    List<Object> endAt = (List<Object>) parameters.get("endAt");
+    if (endAt != null) query = query.endAt(endAt.toArray());
+    @SuppressWarnings("unchecked")
+    List<Object> endBefore = (List<Object>) parameters.get("endBefore");
+    if (endBefore != null) query = query.endBefore(endBefore.toArray());
+    return query;
   }
 
   private class DocumentObserver implements EventListener<DocumentSnapshot> {
diff --git a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m
index bf7d356..f6e289f 100644
--- a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m
+++ b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m
@@ -41,13 +41,38 @@
       // Unsupported operator
     }
   }
-  id orderBy = parameters[@"orderBy"];
-  if (orderBy) {
+  id limit = parameters[@"limit"];
+  if (limit) {
+    NSNumber *length = limit;
+    query = [query queryLimitedTo:[length intValue]];
+  }
+  NSArray orderBy = parameters[@"orderBy"];
+  for (id item in orderBy) {
     NSArray *orderByParameters = orderBy;
     NSString *fieldName = orderByParameters[0];
     NSNumber *descending = orderByParameters[1];
     query = [query queryOrderedByField:fieldName descending:[descending boolValue]];
   }
+  id startAt = parameters[@"startAt"];
+  if (startAt) {
+    NSArray *startAtValues = startAt;
+    query = [query queryStartingAtValues:startAtValues];
+  }
+  id startAfter = parameters[@"startAfter"];
+  if (startAfter) {
+    NSArray *startAfterValues = startAfter;
+    query = [query queryStartingAfterValues:startAfterValues];
+  }
+  id startAt = parameters[@"endAt"];
+  if (endAt) {
+    NSArray *endAtValues = endAt;
+    query = [query queryEndingAtValues:endAtValues];
+  }
+  id endBefore = parameters[@"endBefore"];
+  if (endBefore) {
+    NSArray *endBeforeValues = endBefore;
+    query = [query queryEndingBeforeValues:endBeforeValues];
+  }
   return query;
 }
 
diff --git a/packages/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/lib/src/query.dart
index 017940d..128a432 100644
--- a/packages/cloud_firestore/lib/src/query.dart
+++ b/packages/cloud_firestore/lib/src/query.dart
@@ -15,6 +15,8 @@
         _parameters = parameters ??
             new Map<String, dynamic>.unmodifiable(<String, dynamic>{
               'where': new List<List<dynamic>>.unmodifiable(<List<dynamic>>[]),
+              'orderBy':
+                  new List<List<dynamic>>.unmodifiable(<List<dynamic>>[]),
             }),
         assert(firestore != null),
         assert(pathComponents != null);
@@ -130,9 +132,68 @@
   /// Creates and returns a new [Query] that's additionally sorted by the specified
   /// [field].
   Query orderBy(String field, {bool descending: false}) {
-    assert(!_parameters.containsKey('orderBy'));
-    return _copyWithParameters(<String, dynamic>{
-      'orderBy': <dynamic>[field, descending]
-    });
+    final List<List<dynamic>> orders =
+        new List<List<dynamic>>.from(_parameters['orderBy']);
+
+    final List<dynamic> order = <dynamic>[field, descending];
+    assert(orders.where((List<dynamic> item) => field == item[0]).isEmpty,
+        'OrderBy $field already exists in this query');
+    orders.add(order);
+    return _copyWithParameters(<String, dynamic>{'orderBy': orders});
+  }
+
+  /// Takes a list of [values], creates and returns a new [Query] that starts after
+  /// the provided fields relative to the order of the query.
+  ///
+  /// The [values] must be in order of [orderBy] filters.
+  ///
+  /// Cannot be used in combination with [startAt].
+  Query startAfter(List<dynamic> values) {
+    assert(!_parameters.containsKey('startAfter'));
+    assert(!_parameters.containsKey('startAt'));
+    return _copyWithParameters(<String, dynamic>{'startAfter': values});
+  }
+
+  /// Takes a list of [values], creates and returns a new [Query] that starts at
+  /// the provided fields relative to the order of the query.
+  ///
+  /// The [values] must be in order of [orderBy] filters.
+  ///
+  /// Cannot be used in combination with [startAfter].
+  Query startAt(List<dynamic> values) {
+    assert(!_parameters.containsKey('startAfter'));
+    assert(!_parameters.containsKey('startAt'));
+    return _copyWithParameters(<String, dynamic>{'startAt': values});
+  }
+
+  /// Takes a list of [values], creates and returns a new [Query] that ends at the
+  /// provided fields relative to the order of the query.
+  ///
+  /// The [values] must be in order of [orderBy] filters.
+  ///
+  /// Cannot be used in combination with [endBefore].
+  Query endAt(List<dynamic> values) {
+    assert(!_parameters.containsKey('endBefore'));
+    assert(!_parameters.containsKey('endAt'));
+    return _copyWithParameters(<String, dynamic>{'endAt': values});
+  }
+
+  /// Takes a list of [values], creates and returns a new [Query] that ends before
+  /// the provided fields relative to the order of the query.
+  ///
+  /// The [values] must be in order of [orderBy] filters.
+  ///
+  /// Cannot be used in combination with [endAt].
+  Query endBefore(List<dynamic> values) {
+    assert(!_parameters.containsKey('endBefore'));
+    assert(!_parameters.containsKey('endAt'));
+    return _copyWithParameters(<String, dynamic>{'endBefore': values});
+  }
+
+  /// Creates and returns a new Query that's additionally limited to only return up
+  /// to the specified number of documents.
+  Query limit(int length) {
+    assert(!_parameters.containsKey('limit'));
+    return _copyWithParameters(<String, dynamic>{'limit': length});
   }
 }
diff --git a/packages/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/pubspec.yaml
index f5abe0d..69f883b 100755
--- a/packages/cloud_firestore/pubspec.yaml
+++ b/packages/cloud_firestore/pubspec.yaml
@@ -3,7 +3,7 @@
   live synchronization and offline support on Android and iOS.
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/cloud_firestore
-version: 0.2.4
+version: 0.2.5
 
 flutter:
   plugin:
diff --git a/packages/cloud_firestore/test/cloud_firestore_test.dart b/packages/cloud_firestore/test/cloud_firestore_test.dart
index 1d906b2..cfd2856 100755
--- a/packages/cloud_firestore/test/cloud_firestore_test.dart
+++ b/packages/cloud_firestore/test/cloud_firestore_test.dart
@@ -96,6 +96,7 @@
               'path': 'foo',
               'parameters': <String, dynamic>{
                 'where': <List<dynamic>>[],
+                'orderBy': <List<dynamic>>[],
               }
             },
           ),
@@ -124,6 +125,7 @@
                   'where': <List<dynamic>>[
                     <dynamic>['createdAt', '<', 100],
                   ],
+                  'orderBy': <List<dynamic>>[],
                 }
               },
             ),
@@ -153,6 +155,7 @@
                   'where': <List<dynamic>>[
                     <dynamic>['profile', '==', null],
                   ],
+                  'orderBy': <List<dynamic>>[],
                 }
               },
             ),
@@ -180,7 +183,9 @@
                 'path': 'foo',
                 'parameters': <String, dynamic>{
                   'where': <List<dynamic>>[],
-                  'orderBy': <dynamic>['createdAt', false],
+                  'orderBy': <List<dynamic>>[
+                    <dynamic>['createdAt', false]
+                  ],
                 }
               },
             ),