diff --git a/packages/cloud_firestore/CHANGELOG.md b/packages/cloud_firestore/CHANGELOG.md index 4eafbd61d00d..ed8162e75f90 100644 --- a/packages/cloud_firestore/CHANGELOG.md +++ b/packages/cloud_firestore/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.12.10 + +* Added `FieldPath` class and `FieldPath.documentId` to refer to the document id in queries. +* Added assertions and exceptions that help you building correct queries. + ## 0.12.9+8 * Updated README instructions for contributing for consistency with other Flutterfire plugins. 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 eddabe3aa400..c4e75669d517 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 @@ -129,16 +129,32 @@ private Object[] getDocumentValues( List data = new ArrayList<>(); if (orderBy != null) { for (List order : orderBy) { - String orderByFieldName = (String) order.get(0); - if (orderByFieldName.contains(".")) { - String[] fieldNameParts = orderByFieldName.split("\\."); - Map current = (Map) documentData.get(fieldNameParts[0]); - for (int i = 1; i < fieldNameParts.length - 1; i++) { - current = (Map) current.get(fieldNameParts[i]); + final Object field = order.get(0); + + if (field instanceof FieldPath) { + if (field == FieldPath.documentId()) { + // This is also checked by an assertion on the Dart side. + throw new IllegalArgumentException( + "You cannot order by the document id when using" + + "{start/end}{At/After/Before}Document as the library will order by the document" + + " id implicitly in order to to add other fields to the order clause."); + } else { + // Unsupported type. + } + } else if (field instanceof String) { + String orderByFieldName = (String) field; + if (orderByFieldName.contains(".")) { + String[] fieldNameParts = orderByFieldName.split("\\."); + Map current = (Map) documentData.get(fieldNameParts[0]); + for (int i = 1; i < fieldNameParts.length - 1; i++) { + current = (Map) current.get(fieldNameParts[i]); + } + data.add(current.get(fieldNameParts[fieldNameParts.length - 1])); + } else { + data.add(documentData.get(orderByFieldName)); } - data.add(current.get(fieldNameParts[fieldNameParts.length - 1])); } else { - data.add(documentData.get(orderByFieldName)); + // Invalid type. } } } @@ -213,21 +229,67 @@ private Query getQuery(Map arguments) { @SuppressWarnings("unchecked") List> whereConditions = (List>) parameters.get("where"); for (List condition : whereConditions) { - String fieldName = (String) condition.get(0); + String fieldName = null; + FieldPath fieldPath = null; + final Object field = condition.get(0); + if (field instanceof String) { + fieldName = (String) field; + } else if (field instanceof FieldPath) { + fieldPath = (FieldPath) field; + } else { + // Invalid type. + } + String operator = (String) condition.get(1); Object value = condition.get(2); if ("==".equals(operator)) { - query = query.whereEqualTo(fieldName, value); + if (fieldName != null) { + query = query.whereEqualTo(fieldName, value); + } else if (fieldPath != null) { + query = query.whereEqualTo(fieldPath, value); + } else { + // Invalid type. + } } else if ("<".equals(operator)) { - query = query.whereLessThan(fieldName, value); + if (fieldName != null) { + query = query.whereLessThan(fieldName, value); + } else if (fieldPath != null) { + query = query.whereLessThan(fieldPath, value); + } else { + // Invalid type. + } } else if ("<=".equals(operator)) { - query = query.whereLessThanOrEqualTo(fieldName, value); + if (fieldName != null) { + query = query.whereLessThanOrEqualTo(fieldName, value); + } else if (fieldPath != null) { + query = query.whereLessThanOrEqualTo(fieldPath, value); + } else { + // Invalid type. + } } else if (">".equals(operator)) { - query = query.whereGreaterThan(fieldName, value); + if (fieldName != null) { + query = query.whereGreaterThan(fieldName, value); + } else if (fieldPath != null) { + query = query.whereGreaterThan(fieldPath, value); + } else { + // Invalid type. + } } else if (">=".equals(operator)) { - query = query.whereGreaterThanOrEqualTo(fieldName, value); + if (fieldName != null) { + query = query.whereGreaterThanOrEqualTo(fieldName, value); + } else if (fieldPath != null) { + query = query.whereGreaterThanOrEqualTo(fieldPath, value); + } else { + // Invalid type. + } } else if ("array-contains".equals(operator)) { - query = query.whereArrayContains(fieldName, value); + if (fieldName != null) { + query = query.whereArrayContains(fieldName, value); + } else if (fieldPath != null) { + query = query.whereArrayContains(fieldPath, value); + } else { + // Invalid type. + } } else { // Invalid operator. } @@ -239,11 +301,28 @@ private Query getQuery(Map arguments) { List> orderBy = (List>) parameters.get("orderBy"); if (orderBy == null) return query; for (List order : orderBy) { - String orderByFieldName = (String) order.get(0); + String fieldName = null; + FieldPath fieldPath = null; + final Object field = order.get(0); + if (field instanceof String) { + fieldName = (String) field; + } else if (field instanceof FieldPath) { + fieldPath = (FieldPath) field; + } else { + // Invalid type. + } + boolean descending = (boolean) order.get(1); Query.Direction direction = descending ? Query.Direction.DESCENDING : Query.Direction.ASCENDING; - query = query.orderBy(orderByFieldName, direction); + + if (fieldName != null) { + query = query.orderBy(fieldName, direction); + } else if (fieldPath != null) { + query = query.orderBy(fieldPath, direction); + } else { + // Invalid type. + } } @SuppressWarnings("unchecked") Map startAtDocument = (Map) parameters.get("startAtDocument"); @@ -259,6 +338,11 @@ private Query getQuery(Map arguments) { || startAfterDocument != null || endAtDocument != null || endBeforeDocument != null) { + if (orderBy.isEmpty()) { + throw new IllegalStateException( + "You need to order by at least one field when using " + + "{start/end}{At/After/Before}Document as you need some value to e.g. start after."); + } boolean descending = (boolean) orderBy.get(orderBy.size() - 1).get(1); Query.Direction direction = descending ? Query.Direction.DESCENDING : Query.Direction.ASCENDING; @@ -859,6 +943,7 @@ final class FirestoreMessageCodec extends StandardMessageCodec { private static final byte TIMESTAMP = (byte) 136; private static final byte INCREMENT_DOUBLE = (byte) 137; private static final byte INCREMENT_INTEGER = (byte) 138; + private static final byte DOCUMENT_ID = (byte) 139; @Override protected void writeValue(ByteArrayOutputStream stream, Object value) { @@ -922,6 +1007,8 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { case INCREMENT_DOUBLE: final Number doubleIncrementValue = (Number) readValue(buffer); return FieldValue.increment(doubleIncrementValue.doubleValue()); + case DOCUMENT_ID: + return FieldPath.documentId(); default: return super.readValueOfType(type, buffer); } diff --git a/packages/cloud_firestore/example/test_driver/cloud_firestore.dart b/packages/cloud_firestore/example/test_driver/cloud_firestore.dart index 12460b7ad07f..21d51b538722 100644 --- a/packages/cloud_firestore/example/test_driver/cloud_firestore.dart +++ b/packages/cloud_firestore/example/test_driver/cloud_firestore.dart @@ -294,5 +294,39 @@ void main() { await doc1.delete(); await doc2.delete(); }); + + test('FieldPath.documentId', () async { + // Populate the database with two test documents. + final CollectionReference messages = firestore.collection('messages'); + + // Use document ID as a unique identifier to ensure that we don't + // collide with other tests running against this database. + final DocumentReference doc = messages.document(); + final String documentId = doc.documentID; + + await doc.setData({ + 'message': 'testing field path', + 'created_at': FieldValue.serverTimestamp(), + }); + + // This tests the native implementations of the where and + // orderBy methods handling FieldPath.documentId. + // There is also an error thrown when ordering by document id + // natively, however, that is also covered by assertion + // on the Dart side, which is tested with a unit test. + final QuerySnapshot querySnapshot = await messages + .orderBy(FieldPath.documentId) + .where(FieldPath.documentId, isEqualTo: documentId) + .getDocuments(); + + await doc.delete(); + + final List results = querySnapshot.documents; + final DocumentSnapshot result = results[0]; + + expect(results.length, 1); + expect(result.data['message'], 'testing field path'); + expect(result.documentID, documentId); + }); }); } diff --git a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m index ccb7dee868fc..5a612480180f 100644 --- a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m +++ b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m @@ -31,17 +31,36 @@ if (orderBy) { for (id item in orderBy) { NSArray *orderByParameters = item; - NSString *fieldName = orderByParameters[0]; - if ([fieldName rangeOfString:@"."].location != NSNotFound) { - NSArray *fieldNameParts = [fieldName componentsSeparatedByString:@"."]; - NSDictionary *currentMap = [documentData objectForKey:[fieldNameParts objectAtIndex:0]]; - for (int i = 1; i < [fieldNameParts count] - 1; i++) { - currentMap = [currentMap objectForKey:[fieldNameParts objectAtIndex:i]]; + NSObject *field = orderByParameters[0]; + + if ([field isKindOfClass:[FIRFieldPath class]]) { + if ([field isEqual:FIRFieldPath.documentID]) { + // This is also checked by an assertion on the Dart side. + [NSException + raise:@"Invalid use of FieldValue.documentId" + format: + @"You cannot order by the document id when using " + "{start/end}{At/After/Before}Document as the library will order by the document" + " id implicitly in order to to add other fields to the order clause."]; + } else { + // Unsupported type. + } + } else if ([field isKindOfClass:[NSString class]]) { + NSString *fieldName = orderByParameters[0]; + if ([fieldName rangeOfString:@"."].location != NSNotFound) { + NSArray *fieldNameParts = [fieldName componentsSeparatedByString:@"."]; + NSDictionary *currentMap = [documentData objectForKey:[fieldNameParts objectAtIndex:0]]; + for (int i = 1; i < [fieldNameParts count] - 1; i++) { + currentMap = [currentMap objectForKey:[fieldNameParts objectAtIndex:i]]; + } + [values + addObject:[currentMap objectForKey:[fieldNameParts + objectAtIndex:[fieldNameParts count] - 1]]]; + } else { + [values addObject:[documentData objectForKey:fieldName]]; } - [values addObject:[currentMap objectForKey:[fieldNameParts - objectAtIndex:[fieldNameParts count] - 1]]]; } else { - [values addObject:[documentData objectForKey:fieldName]]; + // Invalid type. } } } @@ -68,21 +87,69 @@ NSArray *whereConditions = parameters[@"where"]; for (id item in whereConditions) { NSArray *condition = item; - NSString *fieldName = condition[0]; + + FIRFieldPath *fieldPath = nil; + NSString *fieldName = nil; + NSObject *field = condition[0]; + + if ([field isKindOfClass:[NSString class]]) { + fieldName = (NSString *)field; + } else if ([field isKindOfClass:[FIRFieldPath class]]) { + fieldPath = (FIRFieldPath *)field; + } else { + // Invalid type. + } + NSString *op = condition[1]; id value = condition[2]; if ([op isEqualToString:@"=="]) { - query = [query queryWhereField:fieldName isEqualTo:value]; + if (fieldName != nil) { + query = [query queryWhereField:fieldName isEqualTo:value]; + } else if (fieldPath != nil) { + query = [query queryWhereFieldPath:fieldPath isEqualTo:value]; + } else { + // Invalid type. + } } else if ([op isEqualToString:@"<"]) { - query = [query queryWhereField:fieldName isLessThan:value]; + if (fieldName != nil) { + query = [query queryWhereField:fieldName isLessThan:value]; + } else if (fieldPath != nil) { + query = [query queryWhereFieldPath:fieldPath isLessThan:value]; + } else { + // Invalid type. + } } else if ([op isEqualToString:@"<="]) { - query = [query queryWhereField:fieldName isLessThanOrEqualTo:value]; + if (fieldName != nil) { + query = [query queryWhereField:fieldName isLessThanOrEqualTo:value]; + } else if (fieldPath != nil) { + query = [query queryWhereFieldPath:fieldPath isLessThanOrEqualTo:value]; + } else { + // Invalid type. + } } else if ([op isEqualToString:@">"]) { - query = [query queryWhereField:fieldName isGreaterThan:value]; + if (fieldName != nil) { + query = [query queryWhereField:fieldName isGreaterThan:value]; + } else if (fieldPath != nil) { + query = [query queryWhereFieldPath:fieldPath isGreaterThan:value]; + } else { + // Invalid type. + } } else if ([op isEqualToString:@">="]) { - query = [query queryWhereField:fieldName isGreaterThanOrEqualTo:value]; + if (fieldName != nil) { + query = [query queryWhereField:fieldName isGreaterThanOrEqualTo:value]; + } else if (fieldPath != nil) { + query = [query queryWhereFieldPath:fieldPath isGreaterThanOrEqualTo:value]; + } else { + // Invalid type. + } } else if ([op isEqualToString:@"array-contains"]) { - query = [query queryWhereField:fieldName arrayContains:value]; + if (fieldName != nil) { + query = [query queryWhereField:fieldName arrayContains:value]; + } else if (fieldPath != nil) { + query = [query queryWhereFieldPath:fieldPath arrayContains:value]; + } else { + // Invalid type. + } } else { // Unsupported operator } @@ -95,9 +162,26 @@ NSArray *orderBy = parameters[@"orderBy"]; if (orderBy) { for (NSArray *orderByParameters in orderBy) { - NSString *fieldName = orderByParameters[0]; + FIRFieldPath *fieldPath = nil; + NSString *fieldName = nil; + NSObject *field = orderByParameters[0]; + if ([field isKindOfClass:[NSString class]]) { + fieldName = (NSString *)field; + } else if ([field isKindOfClass:[FIRFieldPath class]]) { + fieldPath = (FIRFieldPath *)field; + } else { + // Invalid type. + } + NSNumber *descending = orderByParameters[1]; - query = [query queryOrderedByField:fieldName descending:[descending boolValue]]; + + if (fieldName != nil) { + query = [query queryOrderedByField:fieldName descending:[descending boolValue]]; + } else if (fieldPath != nil) { + query = [query queryOrderedByFieldPath:fieldPath descending:[descending boolValue]]; + } else { + // Invalid type. + } } } id startAt = parameters[@"startAt"]; @@ -110,6 +194,11 @@ id endAtDocument = parameters[@"endAtDocument"]; id endBeforeDocument = parameters[@"endBeforeDocument"]; if (startAtDocument || startAfterDocument || endAtDocument || endBeforeDocument) { + if ([orderBy count] == 0) { + [NSException raise:@"No order by clause specified" + format:@"You need to order by at least one field when using {start/end}{At/" + "After/Before}Document as you need some value to e.g. start after."]; + } NSArray *orderByParameters = [orderBy lastObject]; NSNumber *descending = orderByParameters[1]; query = [query queryOrderedByFieldPath:FIRFieldPath.documentID @@ -221,6 +310,7 @@ static FIRFirestoreSource getSource(NSDictionary *arguments) { const UInt8 TIMESTAMP = 136; const UInt8 INCREMENT_DOUBLE = 137; const UInt8 INCREMENT_INTEGER = 138; +const UInt8 DOCUMENT_ID = 139; @interface FirestoreWriter : FlutterStandardWriter - (void)writeValue:(id)value; @@ -323,6 +413,9 @@ - (id)readValueOfType:(UInt8)type { NSNumber *value = [self readValue]; return [FIRFieldValue fieldValueForIntegerIncrement:value.intValue]; } + case DOCUMENT_ID: { + return [FIRFieldPath documentID]; + } default: return [super readValueOfType:type]; } diff --git a/packages/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/lib/cloud_firestore.dart index fba72b049cb6..aef386e77d26 100755 --- a/packages/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/lib/cloud_firestore.dart @@ -22,6 +22,7 @@ part 'src/collection_reference.dart'; part 'src/document_change.dart'; part 'src/document_reference.dart'; part 'src/document_snapshot.dart'; +part 'src/field_path.dart'; part 'src/field_value.dart'; part 'src/firestore.dart'; part 'src/firestore_message_codec.dart'; diff --git a/packages/cloud_firestore/lib/src/field_path.dart b/packages/cloud_firestore/lib/src/field_path.dart new file mode 100644 index 000000000000..64cdde8fad31 --- /dev/null +++ b/packages/cloud_firestore/lib/src/field_path.dart @@ -0,0 +1,21 @@ +// Copyright 2017, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of cloud_firestore; + +enum _FieldPathType { + documentId, +} + +/// A [FieldPath] refers to a field in a document. +class FieldPath { + const FieldPath._(this.type); + + @visibleForTesting + final _FieldPathType type; + + /// The path to the document id, which can be used in queries. + static FieldPath get documentId => + const FieldPath._(_FieldPathType.documentId); +} diff --git a/packages/cloud_firestore/lib/src/firestore_message_codec.dart b/packages/cloud_firestore/lib/src/firestore_message_codec.dart index 14f6e020675c..9f658cd1c84b 100644 --- a/packages/cloud_firestore/lib/src/firestore_message_codec.dart +++ b/packages/cloud_firestore/lib/src/firestore_message_codec.dart @@ -19,6 +19,7 @@ class FirestoreMessageCodec extends StandardMessageCodec { static const int _kTimestamp = 136; static const int _kIncrementDouble = 137; static const int _kIncrementInteger = 138; + static const int _kDocumentId = 139; static const Map _kFieldValueCodes = { @@ -30,6 +31,11 @@ class FirestoreMessageCodec extends StandardMessageCodec { FieldValueType.incrementInteger: _kIncrementInteger, }; + static const Map<_FieldPathType, int> _kFieldPathCodes = + <_FieldPathType, int>{ + _FieldPathType.documentId: _kDocumentId, + }; + @override void writeValue(WriteBuffer buffer, dynamic value) { if (value is DateTime) { @@ -60,6 +66,10 @@ class FirestoreMessageCodec extends StandardMessageCodec { assert(code != null); buffer.putUint8(code); if (value.value != null) writeValue(buffer, value.value); + } else if (value is FieldPath) { + final int code = _kFieldPathCodes[value.type]; + assert(code != null); + buffer.putUint8(code); } else { super.writeValue(buffer, value); } @@ -104,6 +114,8 @@ class FirestoreMessageCodec extends StandardMessageCodec { case _kIncrementInteger: final int value = readValue(buffer); return FieldValue.increment(value); + case _kDocumentId: + return FieldPath.documentId; default: return super.readValueOfType(type, buffer); } diff --git a/packages/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/lib/src/query.dart index 114b48a6df57..9b273a9bb501 100644 --- a/packages/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/lib/src/query.dart @@ -110,14 +110,16 @@ class Query { /// Creates and returns a new [Query] with additional filter on specified /// [field]. [field] refers to a field in a document. /// - /// The [field] may consist of a single field name (referring to a top level - /// field in the document), or a series of field names seperated by dots '.' + /// The [field] may be a [String] consisting of a single field name + /// (referring to a top level field in the document), + /// or a series of field names separated by dots '.' /// (referring to a nested field in the document). + /// Alternatively, the [field] can also be a [FieldPath]. /// /// Only documents satisfying provided condition are included in the result /// set. Query where( - String field, { + dynamic field, { dynamic isEqualTo, dynamic isLessThan, dynamic isLessThanOrEqualTo, @@ -126,11 +128,14 @@ class Query { dynamic arrayContains, bool isNull, }) { + assert(field is String || field is FieldPath, + 'Supported [field] types are [String] and [FieldPath].'); + final ListEquality equality = const ListEquality(); final List> conditions = List>.from(_parameters['where']); - void addCondition(String field, String operator, dynamic value) { + void addCondition(dynamic field, String operator, dynamic value) { final List condition = [field, operator, value]; assert( conditions @@ -162,13 +167,38 @@ class Query { /// Creates and returns a new [Query] that's additionally sorted by the specified /// [field]. - Query orderBy(String field, {bool descending = false}) { + /// The field may be a [String] representing a single field name or a [FieldPath]. + /// + /// After a [FieldPath.documentId] order by call, you cannot add any more [orderBy] + /// calls. + /// Furthermore, you may not use [orderBy] on the [FieldPath.documentId] [field] when + /// using [startAfterDocument], [startAtDocument], [endAfterDocument], + /// or [endAtDocument] because the order by clause on the document id + /// is added by these methods implicitly. + Query orderBy(dynamic field, {bool descending = false}) { + assert(field != null && descending != null); + assert(field is String || field is FieldPath, + 'Supported [field] types are [String] and [FieldPath].'); + final List> orders = List>.from(_parameters['orderBy']); final List order = [field, descending]; assert(orders.where((List item) => field == item[0]).isEmpty, 'OrderBy $field already exists in this query'); + + assert(() { + if (field == FieldPath.documentId) { + return !(_parameters.containsKey('startAfterDocument') || + _parameters.containsKey('startAtDocument') || + _parameters.containsKey('endAfterDocument') || + _parameters.containsKey('endAtDocument')); + } + return true; + }(), + '{start/end}{At/After/Before}Document order by document id themselves. ' + 'Hence, you may not use an order by [FieldPath.documentId] when using any of these methods for a query.'); + orders.add(order); return _copyWithParameters({'orderBy': orders}); } @@ -192,6 +222,12 @@ class Query { assert(!_parameters.containsKey('startAt')); assert(!_parameters.containsKey('startAfterDocument')); assert(!_parameters.containsKey('startAtDocument')); + assert( + List>.from(_parameters['orderBy']) + .where((List item) => item[0] == FieldPath.documentId) + .isEmpty, + '[startAfterDocument] orders by document id itself. ' + 'Hence, you may not use an order by [FieldPath.documentId] when using [startAfterDocument].'); return _copyWithParameters({ 'startAfterDocument': { 'id': documentSnapshot.documentID, @@ -220,6 +256,12 @@ class Query { assert(!_parameters.containsKey('startAt')); assert(!_parameters.containsKey('startAfterDocument')); assert(!_parameters.containsKey('startAtDocument')); + assert( + List>.from(_parameters['orderBy']) + .where((List item) => item[0] == FieldPath.documentId) + .isEmpty, + '[startAtDocument] orders by document id itself. ' + 'Hence, you may not use an order by [FieldPath.documentId] when using [startAtDocument].'); return _copyWithParameters({ 'startAtDocument': { 'id': documentSnapshot.documentID, @@ -282,6 +324,12 @@ class Query { assert(!_parameters.containsKey('endAt')); assert(!_parameters.containsKey('endBeforeDocument')); assert(!_parameters.containsKey('endAtDocument')); + assert( + List>.from(_parameters['orderBy']) + .where((List item) => item[0] == FieldPath.documentId) + .isEmpty, + '[endAtDocument] orders by document id itself. ' + 'Hence, you may not use an order by [FieldPath.documentId] when using [endAtDocument].'); return _copyWithParameters({ 'endAtDocument': { 'id': documentSnapshot.documentID, @@ -327,6 +375,12 @@ class Query { assert(!_parameters.containsKey('endAt')); assert(!_parameters.containsKey('endBeforeDocument')); assert(!_parameters.containsKey('endAtDocument')); + assert( + List>.from(_parameters['orderBy']) + .where((List item) => item[0] == FieldPath.documentId) + .isEmpty, + '[endBeforeDocument] orders by document id itself. ' + 'Hence, you may not use an order by [FieldPath.documentId] when using [endBeforeDocument].'); return _copyWithParameters({ 'endBeforeDocument': { 'id': documentSnapshot.documentID, diff --git a/packages/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/pubspec.yaml index d153fe7f69c8..b244f0e728b4 100755 --- a/packages/cloud_firestore/pubspec.yaml +++ b/packages/cloud_firestore/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Cloud Firestore, a cloud-hosted, noSQL database live synchronization and offline support on Android and iOS. author: Flutter Team homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/cloud_firestore -version: 0.12.9+8 +version: 0.12.10 flutter: plugin: diff --git a/packages/cloud_firestore/test/cloud_firestore_test.dart b/packages/cloud_firestore/test/cloud_firestore_test.dart index 32729d6954d7..967e27b991b1 100755 --- a/packages/cloud_firestore/test/cloud_firestore_test.dart +++ b/packages/cloud_firestore/test/cloud_firestore_test.dart @@ -968,6 +968,80 @@ void main() { ), ); }); + + test('FieldPath', () async { + await collectionReference + .where(FieldPath.documentId, isEqualTo: 'bar') + .getDocuments(); + expect( + log, + equals([ + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'parameters': { + 'where': >[ + [FieldPath.documentId, '==', 'bar'], + ], + 'orderBy': >[], + }, + 'source': 'default', + }, + ), + ]), + ); + }); + test('orderBy assertions', () async { + // Can only order by the same field once. + expect(() { + firestore.collection('foo').orderBy('bar').orderBy('bar'); + }, throwsAssertionError); + // Cannot order by unsupported types. + expect(() { + firestore.collection('foo').orderBy(0); + }, throwsAssertionError); + // Parameters cannot be null. + expect(() { + firestore.collection('foo').orderBy(null); + }, throwsAssertionError); + expect(() { + firestore.collection('foo').orderBy('bar', descending: null); + }, throwsAssertionError); + + // Cannot order by document id when paginating with documents. + final DocumentReference documentReference = + firestore.document('foo/bar'); + final DocumentSnapshot snapshot = await documentReference.get(); + expect(() { + firestore + .collection('foo') + .startAfterDocument(snapshot) + .orderBy(FieldPath.documentId); + }, throwsAssertionError); + }); + test('document pagination FieldPath assertions', () async { + final DocumentReference documentReference = + firestore.document('foo/bar'); + final DocumentSnapshot snapshot = await documentReference.get(); + final Query query = + firestore.collection('foo').orderBy(FieldPath.documentId); + + expect(() { + query.startAfterDocument(snapshot); + }, throwsAssertionError); + expect(() { + query.startAtDocument(snapshot); + }, throwsAssertionError); + expect(() { + query.endAtDocument(snapshot); + }, throwsAssertionError); + expect(() { + query.endBeforeDocument(snapshot); + }, throwsAssertionError); + }); }); group('FirestoreMessageCodec', () { @@ -1004,6 +1078,10 @@ void main() { _checkEncodeDecode(codec, FieldValue.increment(1.0)); _checkEncodeDecode(codec, FieldValue.increment(1)); }); + + test('encode and decode FieldPath', () { + _checkEncodeDecode(codec, FieldPath.documentId); + }); }); group('Timestamp', () { @@ -1234,6 +1312,8 @@ bool _deepEquals(dynamic valueA, dynamic valueB) { if (valueA is FieldValue) { return valueB is FieldValue && _deepEqualsFieldValue(valueA, valueB); } + if (valueA is FieldPath) + return valueB is FieldPath && valueA.type == valueB.type; return valueA == valueB; }