From 4e78f22e4495fd554117ac4666e2a95065e43570 Mon Sep 17 00:00:00 2001 From: Razvan Cristian Lung Date: Tue, 16 Feb 2021 20:22:13 +0200 Subject: [PATCH] [cloud_firestore][wip] migrate to present #2 --- .../firebase/firestore/document_snapshot.dart | 94 +---- .../lib/src/firebase/firestore/firestore.dart | 9 +- .../firestore_multi_db_component.dart | 16 + .../src/firebase/firestore/model/values.dart | 6 +- .../lib/src/firebase/firestore/query.dart | 372 ++++++++++-------- .../firestore/query_document_snapshot.dart | 2 +- .../firebase/firestore/query_snapshot.dart | 2 +- .../src/firebase/firestore/set_options.dart | 10 +- .../src/firebase/firestore/transaction.dart | 2 +- ...a_converter.dart => user_data_reader.dart} | 226 ++++++----- .../firebase/firestore/user_data_writer.dart | 107 +++++ .../src/firebase/firestore/write_batch.dart | 24 +- .../test/util/test_util.dart | 8 +- .../lib/firebase_core_vm.dart | 3 + .../lib/src/heart_beat/heart_beat_info.dart | 4 +- google_sign_in_dart/pubspec.lock | 40 +- 16 files changed, 518 insertions(+), 407 deletions(-) rename cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/{user_data_converter.dart => user_data_reader.dart} (54%) create mode 100644 cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_writer.dart diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/document_snapshot.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/document_snapshot.dart index dc04c1dd..5b7bc8ea 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/document_snapshot.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/document_snapshot.dart @@ -8,14 +8,15 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/document_reference.dar import 'package:cloud_firestore_vm/src/firebase/firestore/field_path.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/firestore.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/geo_point.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/model/database_id.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/document.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/document_key.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/field_path.dart' as model; import 'package:cloud_firestore_vm/src/firebase/firestore/server_timestamp_behavior.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/snapshot_metadata.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/user_data_writer.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/assert.dart'; import 'package:cloud_firestore_vm/src/firebase/timestamp.dart'; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/document.pb.dart' show Value; import 'package:meta/meta.dart'; /// A [DocumentSnapshot] contains data read from a document in your [Firestore] database. The data can be extracted with @@ -186,14 +187,8 @@ class DocumentSnapshot { serverTimestampBehavior ??= ServerTimestampBehavior.none; checkNotNull(field, 'Provided field path must not be null.'); checkNotNull(serverTimestampBehavior, 'Provided serverTimestampBehavior value must not be null.'); - final Object maybeDate = _getInternal( - FieldPath.fromDotSeparatedPath(field).internalPath, - _FieldValueOptions( - serverTimestampBehavior: serverTimestampBehavior, - timestampsInSnapshotsEnabled: false, - ), - ); - return _castTypedValue(maybeDate, field); + final Timestamp timestamp = getTimestamp(field, serverTimestampBehavior); + return timestamp?.toDate(); } /// Returns the value of the field as a [Timestamp]. @@ -208,10 +203,8 @@ class DocumentSnapshot { serverTimestampBehavior ??= ServerTimestampBehavior.none; checkNotNull(field, 'Provided field path must not be null.'); checkNotNull(serverTimestampBehavior, 'Provided serverTimestampBehavior value must not be null.'); - final Object maybeTimestamp = _getInternal( - FieldPath.fromDotSeparatedPath(field).internalPath, - _FieldValueOptions(serverTimestampBehavior: serverTimestampBehavior), - ); + final Object maybeTimestamp = + _getInternal(FieldPath.fromDotSeparatedPath(field).internalPath, serverTimestampBehavior); return _castTypedValue(maybeTimestamp, field); } @@ -258,82 +251,17 @@ class DocumentSnapshot { try { final T result = value; return result; - } on CastError catch (_) { + } on TypeError catch (_) { throw StateError('Field \'$field\' is not a $T, but it is ${value.runtimeType}'); } } - Object _convertValue(FieldValue value, _FieldValueOptions options) { - if (value is ObjectValue) { - return _convertObject(value, options); - } else if (value is ArrayValue) { - return _convertArray(value, options); - } else if (value is ReferenceValue) { - return _convertReference(value); - } else if (value is TimestampValue) { - return _convertTimestamp(value, options); - } else if (value is ServerTimestampValue) { - return _convertServerTimestamp(value, options); - } else { - return value.value; - } - } - - Object _convertServerTimestamp(ServerTimestampValue value, _FieldValueOptions options) { - switch (options.serverTimestampBehavior) { - case ServerTimestampBehavior.previous: - return value.previousValue; - case ServerTimestampBehavior.estimate: - return value.localWriteTime; - default: - return value.value; - } - } - - Object _convertTimestamp(TimestampValue value, _FieldValueOptions options) { - final Timestamp timestamp = value.value; - if (options.timestampsInSnapshotsEnabled) { - return timestamp; - } else { - return timestamp.toDate(); - } - } - - Object _convertReference(ReferenceValue value) { - final DocumentKey key = value.value; - final DatabaseId refDatabase = value.databaseId; - final DatabaseId database = _firestore.databaseId; - if (refDatabase != database) { - // TODO(long1eu): Somehow support foreign references. - Log.w('$DocumentSnapshot', - 'Document ${key.path} contains a document reference within a different database (${refDatabase.projectId}/${refDatabase.databaseId}) which is not supported. It will be treated as a reference in the current database (${database.projectId}/${database.databaseId}) instead.'); - } - return DocumentReference(key, _firestore); - } - - Map _convertObject(ObjectValue objectValue, _FieldValueOptions options) { - final Map result = {}; - for (MapEntry entry in objectValue.internalValue) { - result[entry.key] = _convertValue(entry.value, options); - } - return result; - } - - List _convertArray(ArrayValue arrayValue, _FieldValueOptions options) { - final List result = List(arrayValue.internalValue.length); - int i = 0; - for (FieldValue v in arrayValue.internalValue) { - result[i] = _convertValue(v, options); - i++; - } - return result; - } - - Object _getInternal(model.FieldPath fieldPath, _FieldValueOptions options) { + Object _getInternal(model.FieldPath fieldPath, ServerTimestampBehavior serverTimestampBehavior) { if (document != null) { - final FieldValue val = document.getField(fieldPath); + final Value val = document.getField(fieldPath); if (val != null) { - return _convertValue(val, options); + final UserDataWriter userDataWriter = UserDataWriter(_firestore, serverTimestampBehavior); + return userDataWriter.convertValue(val); } } return null; diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore.dart index 83fcd713..7f2ed95c 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore.dart @@ -23,7 +23,8 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/model/database_id.dart import 'package:cloud_firestore_vm/src/firebase/firestore/model/resource_path.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/query.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/transaction.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/user_data_converter.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/user_data_reader.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/user_data_writer.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/assert.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/async_task.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/database.dart'; @@ -38,13 +39,13 @@ import 'package:meta/meta.dart'; class Firestore { @visibleForTesting Firestore(this.databaseId, this.firebaseApp, this.client, this._scheduler) - : userDataReader = UserDataConverter(databaseId); + : userDataReader = UserDataReader(databaseId); static const String _tag = 'FirebaseFirestore'; final DatabaseId databaseId; final FirebaseApp firebaseApp; - final UserDataConverter userDataReader; + final UserDataReader userDataReader; final FirestoreClient client; final AsyncQueue _scheduler; @@ -212,7 +213,7 @@ class Firestore { return updateFunction(Transaction(internalTransaction, this)); } - return client.transaction(wrappedUpdateFunction, 5); + return client.transaction(wrappedUpdateFunction); } /// Creates a write batch, used for performing multiple writes as a single diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore_multi_db_component.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore_multi_db_component.dart index d3ed7c30..1d4a6c3b 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore_multi_db_component.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/firestore_multi_db_component.dart @@ -33,4 +33,20 @@ class FirestoreMultiDbComponent { settings: settings, ); } + + /// Remove the instance of a given database ID from this component, such that if [FirestoreMultiDbComponent.get] + /// is called again with the same name, a new instance of [Firestore] is created. + /// + ///

It is a no-op if there is no instance associated with the given database name. + Future remove(String databaseId) async { + instances.remove(databaseId); + } + + void onDeleted(String firebaseAppName, FirebaseOptions options) { + // Shuts down all database instances and remove them from registry map when App is deleted. + for (MapEntry entry in instances.entries) { + entry.value.shutdown(); + instances.remove(entry.key); + } + } } diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/model/values.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/model/values.dart index e11a5392..ad6ae479 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/model/values.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/model/values.dart @@ -23,7 +23,7 @@ final Value NULL_VALUE = Value(nullValue: NullValue.NULL_VALUE); /// support server timestamps. const int TYPE_ORDER_NULL = 0; -const int TYPE_ORDER_bool = 1; +const int TYPE_ORDER_BOOL = 1; const int TYPE_ORDER_NUMBER = 2; const int TYPE_ORDER_TIMESTAMP = 3; const int TYPE_ORDER_SERVER_TIMESTAMP = 4; @@ -40,7 +40,7 @@ int typeOrder(Value value) { case Value_ValueType.nullValue: return TYPE_ORDER_NULL; case Value_ValueType.booleanValue: - return TYPE_ORDER_bool; + return TYPE_ORDER_BOOL; case Value_ValueType.integerValue: return TYPE_ORDER_NUMBER; case Value_ValueType.doubleValue: @@ -161,7 +161,7 @@ int compare(Value left, Value right) { switch (leftType) { case TYPE_ORDER_NULL: return 0; - case TYPE_ORDER_bool: + case TYPE_ORDER_BOOL: return compareBools(left.booleanValue, right.booleanValue); case TYPE_ORDER_NUMBER: return _compareNumbers(left, right); diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query.dart index 1ef88bca..a4ae388e 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query.dart @@ -9,8 +9,7 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/core/bound.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/core/event_manager.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/core/filter/filter.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/core/order_by.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/core/query.dart' - as core; +import 'package:cloud_firestore_vm/src/firebase/firestore/core/query.dart' as core; import 'package:cloud_firestore_vm/src/firebase/firestore/core/query_stream.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/core/view_snapshot.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/document_reference.dart'; @@ -21,14 +20,19 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/firestore_error.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/metadata_change.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/document.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/document_key.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/model/field_path.dart' - as core; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/field_path.dart' as core; import 'package:cloud_firestore_vm/src/firebase/firestore/model/resource_path.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/model/value/field_value.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/server_timestamps.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/values.dart' as core; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/values.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/query_snapshot.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/source.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/assert.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/util.dart'; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/index.dart' as pb; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/index.dart' show Value; +import 'package:cloud_firestore_vm/src/proto/google/protobuf/index.dart' as pb; +import 'package:cloud_firestore_vm/src/proto/google/type/index.dart' as pb; import 'package:rxdart/rxdart.dart'; /// An enum for the direction of a sort. @@ -37,7 +41,7 @@ enum Direction { ascending, descending } /// A [Query] which you can read or listen to. You can also construct refined [Query] objects by adding filters and /// ordering. /// -/// **Subclassing Note**: Firestore classes are not meant to be subclassed except for use in test mocks. Subclassing is +/// **Subclassing Note**: Cloud Firestore classes are not meant to be subclassed except for use in test mocks. Subclassing is /// not supported in production code and new SDK releases may break code that does so. class Query { const Query(this.query, this.firestore) @@ -55,8 +59,7 @@ class Query { /// /// Returns the created [Query]. Query whereEqualTo(String field, Object value) { - return _whereHelper( - FieldPath.fromDotSeparatedPath(field), FilterOperator.equal, value); + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.equal, value); } /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field and @@ -70,6 +73,28 @@ class Query { return _whereHelper(fieldPath, FilterOperator.equal, value); } + /// Creates and returns a new [Query] with the additional filter that documents must contain + /// the specified field and the value does not equal the specified value. + /// + /// [field] The name of the field to compare + /// [value] The value for comparison + /// + /// Returns the created [Query]. + Query whereNotEqualTo(String field, Object value) { + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.notEqual, value); + } + + /// Creates and returns a new [Query] with the additional filter that documents must contain + /// the specified field and the value does not equal the specified value. + /// + /// [fieldPath] The path of the field to compare + /// [value] The value for comparison + /// + /// Returns the created [Query]. + Query whereNotEqualToField(FieldPath fieldPath, Object value) { + return _whereHelper(fieldPath, FilterOperator.notEqual, value); + } + /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field and /// the value should be less than the specified value. /// @@ -78,8 +103,7 @@ class Query { /// /// Returns the created [Query]. Query whereLessThan(String field, Object value) { - return _whereHelper( - FieldPath.fromDotSeparatedPath(field), FilterOperator.lessThan, value); + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.lessThan, value); } /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field and @@ -101,8 +125,7 @@ class Query { /// /// Returns the created [Query]. Query whereLessThanOrEqualTo(String field, Object value) { - return _whereHelper(FieldPath.fromDotSeparatedPath(field), - FilterOperator.lessThanOrEqual, value); + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.lessThanOrEqual, value); } /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field and @@ -124,8 +147,7 @@ class Query { /// /// Returns the created [Query]. Query whereGreaterThan(String field, Object value) { - return _whereHelper(FieldPath.fromDotSeparatedPath(field), - FilterOperator.graterThan, value); + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.graterThan, value); } /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field and @@ -147,8 +169,7 @@ class Query { /// /// Returns the created [Query]. Query whereGreaterThanOrEqualTo(String field, Object value) { - return _whereHelper(FieldPath.fromDotSeparatedPath(field), - FilterOperator.graterThanOrEqual, value); + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.graterThanOrEqual, value); } /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field and @@ -173,8 +194,7 @@ class Query { /// /// Returns the created [Query]. Query whereArrayContains(String field, Object value) { - return _whereHelper(FieldPath.fromDotSeparatedPath(field), - FilterOperator.arrayContains, value); + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.arrayContains, value); } /// Creates and returns a new [Query] with the additional filter that documents must contain the specified field, the @@ -197,10 +217,8 @@ class Query { /// /// A Query can have only one [whereArrayContainsAny] filter and it cannot be /// combined with [whereArrayContains] or [whereIn]. - // TODO(in-queries): Expose to public once backend is ready. - Query whereArrayContainsAny(String field, List value) { - return _whereHelper(FieldPath.fromDotSeparatedPath(field), - FilterOperator.arrayContainsAny, value); + Query whereArrayContainsAny(String field, List values) { + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.arrayContainsAny, values); } /// Creates and returns a new Query with the additional filter that documents @@ -209,9 +227,8 @@ class Query { /// /// A Query can have only one [whereArrayContainsAny] filter and it cannot be /// combined with [whereArrayContains] or [whereIn]. - // TODO(in-queries): Expose to public once backend is ready. - Query whereArrayContainsAnyField(FieldPath fieldPath, List value) { - return _whereHelper(fieldPath, FilterOperator.arrayContainsAny, value); + Query whereArrayContainsAnyField(FieldPath fieldPath, List values) { + return _whereHelper(fieldPath, FilterOperator.arrayContainsAny, values); } /// Creates and returns a new Query with the additional filter that documents @@ -220,10 +237,8 @@ class Query { /// /// A Query can have only one [whereIn] filter, and it cannot be combined with /// [whereArrayContainsAny]. - // TODO(in-queries): Expose to public once backend is ready. - Query whereIn(String field, List value) { - return _whereHelper( - FieldPath.fromDotSeparatedPath(field), FilterOperator.IN, value); + Query whereIn(String field, List values) { + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.IN, values); } /// Creates and returns a new Query with the additional filter that documents @@ -232,39 +247,61 @@ class Query { /// /// A Query can have only one [whereIn] filter, and it cannot be combined with /// [whereArrayContainsAny]. - // TODO(in-queries): Expose to public once backend is ready. - Query whereInField(FieldPath fieldPath, List value) { - return _whereHelper(fieldPath, FilterOperator.IN, value); + Query whereInField(FieldPath fieldPath, List values) { + return _whereHelper(fieldPath, FilterOperator.IN, values); + } + + /// Creates and returns a new [Query] with the additional filter that documents must contain + /// the specified field and the value does not equal any of the values from the provided list. + /// + /// One special case is that [whereNotIn] cannot match [null] values. To query for documents + /// where a field exists and is [null], use [whereNotEqualTo], which can + /// handle this special case. + /// + /// A Query can have only one [whereIn] filter, and it cannot be combined with + /// [whereArrayContainsAny]. + Query whereNotIn(String field, List values) { + return _whereHelper(FieldPath.fromDotSeparatedPath(field), FilterOperator.notIn, values); + } + + /// Creates and returns a new [Query] with the additional filter that documents must contain + /// the specified field and the value does not equal any of the values from the provided list. + /// + /// One special case is that [whereNotIn] cannot match [null] values. To query for + /// documents where a field exists and is [null], use [whereNotEqualTo], which can handle this + /// special case. + /// + /// A Query can have only one [whereIn] filter, and it cannot be combined with + /// [whereArrayContainsAny]. + Query whereNotInField(FieldPath fieldPath, List values) { + return _whereHelper(fieldPath, FilterOperator.notIn, values); } /// Creates and returns a new [Query] with the additional filter that /// documents must contain the specified field and the value should satisfy /// the relation constraint provided. - Query _whereHelper(FieldPath fieldPath, FilterOperator op, Object value) { + Query _whereHelper(FieldPath fieldPath, FilterOperator op, dynamic value) { checkNotNull(fieldPath, 'Provided field path must not be null.'); checkNotNull(op, 'Provided op must not be null.'); - FieldValue fieldValue; + Value fieldValue; final core.FieldPath internalPath = fieldPath.internalPath; if (internalPath.isKeyField) { - if (op == FilterOperator.arrayContains || - op == FilterOperator.arrayContainsAny) { - throw ArgumentError( - 'Invalid query. You can\'t perform "$op" queries on FieldPath.documentId().'); - } else if (op == FilterOperator.IN) { + if (op == FilterOperator.arrayContains || op == FilterOperator.arrayContainsAny) { + throw ArgumentError('Invalid query. You can\'t perform "$op" queries on FieldPath.documentId().'); + } else if (op == FilterOperator.IN || op == FilterOperator.notIn) { _validateDisjunctiveFilterElements(value, op); - final List referenceList = []; - for (Object arrayValue in value) { - referenceList.add(_parseDocumentIdValue(arrayValue)); - } - fieldValue = ArrayValue.fromList(referenceList); + fieldValue = Value(arrayValue: pb.ArrayValue(values: value.map(_parseDocumentIdValue).toList())); } else { fieldValue = _parseDocumentIdValue(value); } } else { - if (op == FilterOperator.IN || op == FilterOperator.arrayContainsAny) { + if (op == FilterOperator.IN || op == FilterOperator.notIn || op == FilterOperator.arrayContainsAny) { _validateDisjunctiveFilterElements(value, op); } - fieldValue = firestore.userDataReader.parseQueryValue(value); + fieldValue = firestore.userDataReader.parseQueryValue( + value, + op == FilterOperator.IN || op == FilterOperator.notIn, + ); } final Filter filter = FieldFilter(fieldPath.internalPath, op, fieldValue); _validateNewFilter(filter); @@ -281,7 +318,7 @@ class Query { /// Parses the given documentIdValue into a ReferenceValue, throwing /// appropriate errors if the value is anything other than a DocumentReference /// or String, or if the string is malformed. - FieldValue _parseDocumentIdValue(Object value) { + Value _parseDocumentIdValue(Object value) { if (value is String) { if (value.isEmpty) { throw ArgumentError( @@ -293,19 +330,17 @@ class Query { 'Invalid query. When querying a collection by FieldPath.documentId() you must provide a plain document ID, but "$value" contains a "/" character.'); } - final ResourcePath path = - query.path.appendField(ResourcePath.fromString(value)); + final ResourcePath path = query.path.appendField(ResourcePath.fromString(value)); if (!DocumentKey.isDocumentKey(path)) { throw ArgumentError( 'Invalid query. When querying a collection group by FieldPath.documentId(), the value provided must result in a valid document path, but "$path" is not because it has an odd number of segments (${path.length}).'); } - return ReferenceValue.valueOf( - firestore.databaseId, DocumentKey.fromPath(path)); + + return core.refValue(firestore.databaseId, DocumentKey.fromPath(path)); } else if (value is DocumentReference) { - return ReferenceValue.valueOf(firestore.databaseId, value.key); + return core.refValue(firestore.databaseId, value.key); } else { - throw ArgumentError( - 'Invalid query. When querying with FieldPath.documentId() you must provide a valid String or ' + throw ArgumentError('Invalid query. When querying with FieldPath.documentId() you must provide a valid String or ' 'DocumentReference, but it was of type: ${typeName(value)}'); } } @@ -315,27 +350,16 @@ class Query { void _validateDisjunctiveFilterElements(Object value, FilterOperator op) { if (value is List) { if (value.isEmpty) { - throw ArgumentError( - 'Invalid Query. A non-empty array is required for "$op" filters.'); + throw ArgumentError('Invalid Query. A non-empty array is required for "$op" filters.'); } else if (value.length > 10) { - throw ArgumentError( - 'Invalid Query. "$op" filters support a maximum of 10 elements in the value array.'); - } else if (value.contains(null)) { - throw ArgumentError( - 'Invalid Query. "$op" filters cannot contain "null" in the value array.'); - } else if (value - .any((dynamic element) => element is double && element.isNaN)) { - throw ArgumentError( - 'Invalid Query. "$op" filters cannot contain "NaN" in the value array.'); + throw ArgumentError('Invalid Query. "$op" filters support a maximum of 10 elements in the value array.'); } } else { - throw ArgumentError( - 'Invalid Query. A non-empty array is required for "$op" filters.'); + throw ArgumentError('Invalid Query. A non-empty array is required for "$op" filters.'); } } - void _validateOrderByFieldMatchesInequality( - core.FieldPath orderBy, core.FieldPath inequality) { + void _validateOrderByFieldMatchesInequality(core.FieldPath orderBy, core.FieldPath inequality) { if (orderBy != inequality) { final String inequalityString = inequality.canonicalString; throw ArgumentError('Invalid query. You have an inequality where filter ' @@ -346,13 +370,58 @@ class Query { } } + /// Given an operator, returns the set of operators that cannot be used with it. + /// + /// Operators in a query must adhere to the following set of rules: + /// * Only one array operator is allowed. + /// * Only one disjunctive operator is allowed. + /// * NOT_EQUAL cannot be used with another NOT_EQUAL operator. + /// * NOT_IN cannot be used with array, disjunctive, or NOT_EQUAL operators. + /// + /// Array operators: [FilterOperator.arrayContains], [FilterOperator.arrayContainsAny] + /// Disjunctive operators: [FilterOperator.IN], [FilterOperator.arrayContainsAny], [FilterOperator.notIn] + List _conflictingOps(FilterOperator op) { + switch (op) { + case FilterOperator.notEqual: + return [ + FilterOperator.notEqual, + FilterOperator.notIn, + ]; + case FilterOperator.arrayContains: + return [ + FilterOperator.arrayContains, + FilterOperator.arrayContainsAny, + FilterOperator.notIn, + ]; + case FilterOperator.IN: + return [ + FilterOperator.arrayContainsAny, + FilterOperator.IN, + FilterOperator.notIn, + ]; + case FilterOperator.arrayContainsAny: + return [ + FilterOperator.arrayContains, + FilterOperator.arrayContainsAny, + FilterOperator.IN, + FilterOperator.notIn, + ]; + case FilterOperator.notIn: + return [ + FilterOperator.arrayContains, + FilterOperator.arrayContainsAny, + FilterOperator.IN, + FilterOperator.notIn, + FilterOperator.notEqual, + ]; + default: + return []; + } + } + void _validateNewFilter(Filter filter) { if (filter is FieldFilter) { final FilterOperator filterOp = filter.operator; - final bool isArrayOperator = - FilterOperator.arrayOperators.contains(filterOp); - final bool isDisjunctiveOperator = - FilterOperator.disjunctiveOperators.contains(filterOp); if (filter.isInequality) { final core.FieldPath existingInequality = query.inequalityField; @@ -360,37 +429,24 @@ class Query { if (existingInequality != null && existingInequality != newInequality) { throw ArgumentError( - 'All where filters other than whereEqualTo() must be on the same field. But you have filters on ' + 'All where filters with an inequality (notEqualTo, notIn, lessThan, ' + 'lessThanOrEqualTo, greaterThan, or greaterThanOrEqualTo) must be on the same field. But you have filters on ' '\'${existingInequality.canonicalString}\' and \'${newInequality.canonicalString}\'', ); } final core.FieldPath firstOrderByField = query.firstOrderByField; if (firstOrderByField != null) { - _validateOrderByFieldMatchesInequality( - firstOrderByField, newInequality); + _validateOrderByFieldMatchesInequality(firstOrderByField, newInequality); } - } else if (isDisjunctiveOperator || isArrayOperator) { - // You can have at most 1 disjunctive filter and 1 array filter. Check - // if the new filter conflicts with an existing one. - FilterOperator conflictingOperator; - if (isDisjunctiveOperator) { - conflictingOperator = - query.findFilterOperator(FilterOperator.disjunctiveOperators); - } - if (conflictingOperator == null && isArrayOperator) { - conflictingOperator = - query.findFilterOperator(FilterOperator.arrayOperators); - } - if (conflictingOperator != null) { - // We special case when it's a duplicate op to give a slightly clearer - // error message. - if (conflictingOperator == filterOp) { - throw ArgumentError( - 'Invalid Query. You cannot use more than one "$filterOp" filter.'); - } else { - throw ArgumentError( - 'Invalid Query. You cannot use "$filterOp" filters with "$conflictingOperator" filters.'); - } + } + + final FilterOperator conflictingOp = query.findFilterOperator(_conflictingOps(filterOp)); + if (conflictingOp != null) { + // We special case when it's a duplicate op to give a slightly clearer error message. + if (conflictingOp == filterOp) { + throw ArgumentError("Invalid Query. You cannot use more than one '$filterOp' filter."); + } else { + throw ArgumentError("Invalid Query. You cannot use '$filterOp' filters with '$conflictingOp' filters."); } } } @@ -414,8 +470,7 @@ class Query { /// [direction] the direction to sort. /// /// Returns the created Query. - Query orderByField(FieldPath fieldPath, - [Direction direction = Direction.ascending]) { + Query orderByField(FieldPath fieldPath, [Direction direction = Direction.ascending]) { checkNotNull(fieldPath, 'Provided field path must not be null.'); return _orderBy(fieldPath.internalPath, direction); } @@ -431,24 +486,36 @@ class Query { 'Invalid query. You must not call Query.endAt() or Query.endBefore() before calling Query.orderBy().'); } _validateOrderByField(fieldPath); - final OrderByDirection dir = direction == Direction.ascending - ? OrderByDirection.ascending - : OrderByDirection.descending; + final OrderByDirection dir = + direction == Direction.ascending ? OrderByDirection.ascending : OrderByDirection.descending; return Query(query.orderBy(OrderBy.getInstance(dir, fieldPath)), firestore); } - /// Creates and returns a new [Query] that's additionally limited to only return up to the specified number of - /// documents. + /// Creates and returns a new [Query] that only returns the first matching documents up to the specified number. /// /// [limit] the maximum number of items to return. /// /// Returns the created Query. Query limit(int limit) { if (limit <= 0) { - throw ArgumentError( - 'Invalid Query. Query limit ($limit) is invalid. Limit must be positive.'); + throw ArgumentError('Invalid Query. Query limit ($limit) is invalid. Limit must be positive.'); } - return Query(query.limit(limit), firestore); + return Query(query.limitToFirst(limit), firestore); + } + + /// Creates and returns a new [Query] that only returns the last matching documents up to the specified number. + /// + /// You must specify at least one [orderBy] clause for [limitToLast] queries, otherwise an exception will be thrown + /// during execution. + /// + /// [limit] the maximum number of items to return. + /// + /// Returns the created Query. + Query limitToLast(int limit) { + if (limit <= 0) { + throw ArgumentError('Invalid Query. Query limit ($limit) is invalid. Limit must be positive.'); + } + return Query(query.limitToLast(limit), firestore); } /// Creates and returns a new [Query] that starts at the provided document (inclusive). The starting position is @@ -459,8 +526,7 @@ class Query { /// /// Returns the created Query. Query startAtDocument(DocumentSnapshot snapshot) { - final Bound bound = - _boundFromDocumentSnapshot('startAt', snapshot, /*before:*/ true); + final Bound bound = _boundFromDocumentSnapshot('startAt', snapshot, /*before:*/ true); return Query(query.startAt(bound), firestore); } @@ -471,8 +537,7 @@ class Query { /// /// Returns the created Query. Query startAt(List fieldValues) { - final Bound bound = - _boundFromFields('startAt', fieldValues, /*before:*/ true); + final Bound bound = _boundFromFields('startAt', fieldValues, /*before:*/ true); return Query(query.startAt(bound), firestore); } @@ -484,8 +549,7 @@ class Query { /// /// Returns the created Query. Query startAfterDocument(DocumentSnapshot snapshot) { - final Bound bound = - _boundFromDocumentSnapshot('startAfter', snapshot, /*before:*/ false); + final Bound bound = _boundFromDocumentSnapshot('startAfter', snapshot, /*before:*/ false); return Query(query.startAt(bound), firestore); } @@ -496,8 +560,7 @@ class Query { /// /// Returns the created Query. Query startAfter(List fieldValues) { - final Bound bound = - _boundFromFields('startAfter', fieldValues, /*before:*/ false); + final Bound bound = _boundFromFields('startAfter', fieldValues, /*before:*/ false); return Query(query.startAt(bound), firestore); } @@ -508,8 +571,7 @@ class Query { /// /// Returns the created Query. Query endBeforeDocument(DocumentSnapshot snapshot) { - final Bound bound = - _boundFromDocumentSnapshot('endBefore', snapshot, /*before:*/ true); + final Bound bound = _boundFromDocumentSnapshot('endBefore', snapshot, /*before:*/ true); return Query(query.endAt(bound), firestore); } @@ -520,8 +582,7 @@ class Query { /// /// Returns the created Query. Query endBefore(List fieldValues) { - final Bound bound = - _boundFromFields('endBefore', fieldValues, /*before:*/ true); + final Bound bound = _boundFromFields('endBefore', fieldValues, /*before:*/ true); return Query(query.endAt(bound), firestore); } @@ -532,8 +593,7 @@ class Query { /// /// Returns the created Query. Query endAtDocument(DocumentSnapshot snapshot) { - final Bound bound = - _boundFromDocumentSnapshot('endAt', snapshot, /*before:*/ false); + final Bound bound = _boundFromDocumentSnapshot('endAt', snapshot, /*before:*/ false); return Query(query.endAt(bound), firestore); } @@ -544,8 +604,7 @@ class Query { /// /// Returns the created Query. Query endAt(List fieldValues) { - final Bound bound = - _boundFromFields('endAt', fieldValues, /*before:*/ false); + final Bound bound = _boundFromFields('endAt', fieldValues, /*before:*/ false); return Query(query.endAt(bound), firestore); } @@ -556,16 +615,13 @@ class Query { /// /// Will throw if the document does not contain all fields of the order by of the query or if any of the fields in the /// order by are an uncommitted server timestamp. - Bound _boundFromDocumentSnapshot( - String methodName, DocumentSnapshot snapshot, bool before) { - checkNotNull( - snapshot, 'Provided snapshot must not be null.'); + Bound _boundFromDocumentSnapshot(String methodName, DocumentSnapshot snapshot, bool before) { + checkNotNull(snapshot, 'Provided snapshot must not be null.'); if (!snapshot.exists) { - throw ArgumentError( - 'Can\'t use a DocumentSnapshot for a document that doesn\'t exist for $methodName().'); + throw ArgumentError('Can\'t use a DocumentSnapshot for a document that doesn\'t exist for $methodName().'); } final Document document = snapshot.document; - final List components = []; + final List components = []; // Because people expect to continue/end a query at the exact document provided, we need to use the implicit sort // order rather than the explicit sort order, because it's guaranteed to contain the document key. That way the @@ -573,11 +629,10 @@ class Query { // using the explicit sort orders), multiple documents could match the position, yielding duplicate results. for (OrderBy orderBy in query.orderByConstraints) { if (orderBy.field == core.FieldPath.keyPath) { - components - .add(ReferenceValue.valueOf(firestore.databaseId, document.key)); + components.add(refValue(firestore.databaseId, document.key)); } else { - final FieldValue value = document.getField(orderBy.field); - if (value is ServerTimestampValue) { + final Value value = document.getField(orderBy.field); + if (ServerTimestamps.isServerTimestamp(value)) { throw ArgumentError( 'Invalid query. You are trying to start or end a query using a document for which the field ' '\'${orderBy.field}\' is an uncommitted server timestamp. (Since the value of this field is unknown, you ' @@ -604,14 +659,13 @@ class Query { 'number of orderBy() clauses.'); } - final List components = []; + final List components = []; for (int i = 0; i < values.length; i++) { final Object rawValue = values[i]; final OrderBy orderBy = explicitOrderBy[i]; if (orderBy.field == core.FieldPath.keyPath) { if (rawValue is! String) { - throw ArgumentError( - 'Invalid query. Expected a string for document ID in $methodName(), but got $rawValue.'); + throw ArgumentError('Invalid query. Expected a string for document ID in $methodName(), but got $rawValue.'); } final String documentId = rawValue; @@ -619,17 +673,15 @@ class Query { throw ArgumentError( 'Invalid query. When querying a collection and ordering by FieldPath.documentId(), the value passed to $methodName() must be a plain document ID, but "$documentId" contains a slash.'); } - final ResourcePath path = - query.path.appendField(ResourcePath.fromString(documentId)); + final ResourcePath path = query.path.appendField(ResourcePath.fromString(documentId)); if (!DocumentKey.isDocumentKey(path)) { throw ArgumentError( 'Invalid query. When querying a collection group and ordering by FieldPath.documentId(), the value passed to $methodName() must result in a valid document path, but "$path" is not because it contains an odd number of segments.'); } final DocumentKey key = DocumentKey.fromPath(path); - components.add(ReferenceValue.valueOf(firestore.databaseId, key)); + components.add(core.refValue(firestore.databaseId, key)); } else { - final FieldValue wrapped = - firestore.userDataReader.parseQueryValue(rawValue); + final Value wrapped = firestore.userDataReader.parseQueryValue(rawValue); components.add(wrapped); } } @@ -647,9 +699,10 @@ class Query { /// /// Returns a Future that will be resolved with the results of the [Query]. Future get([Source source = Source.defaultSource]) async { + _validateHasExplicitOrderByForLimitToLast(); + if (source == Source.cache) { - final ViewSnapshot viewSnap = - await firestore.client.getDocumentsFromLocalCache(query); + final ViewSnapshot viewSnap = await firestore.client.getDocumentsFromLocalCache(query); return QuerySnapshot(Query(query, firestore), viewSnap, firestore); } else { @@ -682,44 +735,41 @@ class Query { /// Return cached results first and then queries the server with resume token /// to ensure unnecessary reads are not made. Stream getSnapshots([MetadataChanges changes]) { - final ListenOptions options = - _internalOptions(changes ?? MetadataChanges.exclude); + final ListenOptions options = _internalOptions(changes ?? MetadataChanges.exclude); return _getSnapshotsInternal(options); } Stream _getSnapshotsInternal(ListenOptions options) { - return Stream.fromFuture( - firestore.client.listen(query, options)) + _validateHasExplicitOrderByForLimitToLast(); + return Stream.fromFuture(firestore.client.listen(query, options)) .flatMap((QueryStream it) => it) - .map((ViewSnapshot snapshot) => - QuerySnapshot(this, snapshot, firestore)); + .map((ViewSnapshot snapshot) => QuerySnapshot(this, snapshot, firestore)); } /// Converts the API options object to the internal options object. static ListenOptions _internalOptions(MetadataChanges metadataChanges) { return ListenOptions( - includeDocumentMetadataChanges: - metadataChanges == MetadataChanges.include, + includeDocumentMetadataChanges: metadataChanges == MetadataChanges.include, includeQueryMetadataChanges: metadataChanges == MetadataChanges.include, ); } + void _validateHasExplicitOrderByForLimitToLast() { + if (query.hasLimitToLast && query.explicitSortOrder.isEmpty) { + throw StateError('limitToLast() queries require specifying at least one orderBy() clause'); + } + } + @override bool operator ==(Object other) => identical(this, other) || - other is Query && - runtimeType == other.runtimeType && - query == other.query && - firestore == other.firestore; + other is Query && runtimeType == other.runtimeType && query == other.query && firestore == other.firestore; @override int get hashCode => query.hashCode ^ firestore.hashCode; @override String toString() { - return (ToStringHelper(runtimeType) - ..add('query', query) - ..add('firestore', firestore)) - .toString(); + return (ToStringHelper(runtimeType)..add('query', query)..add('firestore', firestore)).toString(); } } diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_document_snapshot.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_document_snapshot.dart index 3e4c8b57..af8981db 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_document_snapshot.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_document_snapshot.dart @@ -18,7 +18,7 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/util/assert.dart'; /// contain only existing documents, the [exists] method will always return true and [data] will /// never be null. /// -/// **Subclassing Note**: Firestore classes are not meant to be subclassed except for use in test +/// **Subclassing Note**: Cloud Firestore classes are not meant to be subclassed except for use in test /// mocks. Subclassing is not supported in production code and new SDK releases may break code that /// does so. class QueryDocumentSnapshot extends DocumentSnapshot { diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_snapshot.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_snapshot.dart index e1d5dc66..e612ca90 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_snapshot.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/query_snapshot.dart @@ -51,7 +51,7 @@ class QuerySnapshot extends Iterable { /// Returns the list of documents that changed since the last snapshot. If it's the first snapshot all documents will /// be in the list as added changes. /// - /// [metadataChanges] Indicates whether metadata-only changes (i.e. only [Query.metadata] changed) should be included. + /// [metadataChanges] Indicates whether metadata-only changes (i.e. only [DocumentSnapshot.metadata] changed) should be included. /// /// Returns the list of document changes since the last snapshot. List getDocumentChanges(MetadataChanges metadataChanges) { diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/set_options.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/set_options.dart index d9e63046..ca7c8877 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/set_options.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/set_options.dart @@ -24,10 +24,10 @@ class SetOptions { : merge = true, fieldMask = null; - /// Changes the behavior of set() calls to only replace the fields under [fieldPaths]. Any field - /// that is not specified in [fieldPaths] is ignored and remains untouched. + /// Changes the behavior of [set] calls to only replace the given [fields]. Any field + /// that is not specified in [fields] is ignored and remains untouched. /// - /// It is an error to pass a [SetOptions] object to a set() call that is missing a value for any + /// It is an error to pass a [SetOptions] object to a [set] call that is missing a value for any /// of the fields specified here. /// /// [fields] the list of fields to merge. Fields can contain dots to reference nested fields @@ -40,10 +40,10 @@ class SetOptions { return SetOptions._(true, FieldMask(fieldPaths)); } - /// Changes the behavior of set() calls to only replace the fields under [fieldPaths]. Any field + /// Changes the behavior of [set] calls to only replace the given [fields]. Any field /// that is not specified in [fieldPaths] is ignored and remains untouched. /// - /// It is an error to pass a SetOptions object to a set() call that is missing a value for any of + /// It is an error to pass a SetOptions object to a [set] call that is missing a value for any of /// the fields specified here in its to data argument. factory SetOptions.mergeFieldPaths(List fields) { final Set fieldPaths = diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/transaction.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/transaction.dart index d3cbc426..6abe633a 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/transaction.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/transaction.dart @@ -24,7 +24,7 @@ typedef TransactionCallback = Future Function(Transaction); /// A [Transaction] is passed to a [Function] to provide the methods to read and write data within /// the transaction context. /// -/// **Subclassing Note**: Firestore classes are not meant to be subclassed except for use in test +/// **Subclassing Note**: Cloud Firestore classes are not meant to be subclassed except for use in test /// mocks. Subclassing is not supported in production code and new SDK releases may break code that /// does so. class Transaction { diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_converter.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_reader.dart similarity index 54% rename from cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_converter.dart rename to cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_reader.dart index b85bd770..ca36e64b 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_converter.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_reader.dart @@ -5,10 +5,8 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/blob.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/core/user_data.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/document_reference.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/field_path.dart' - as firestore; -import 'package:cloud_firestore_vm/src/firebase/firestore/field_value.dart' - as firestore; +import 'package:cloud_firestore_vm/src/firebase/firestore/field_path.dart' as firestore; +import 'package:cloud_firestore_vm/src/firebase/firestore/field_value.dart' as firestore; import 'package:cloud_firestore_vm/src/firebase/firestore/geo_point.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/database_id.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/field_path.dart'; @@ -16,37 +14,39 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/model/mutation/array_t import 'package:cloud_firestore_vm/src/firebase/firestore/model/mutation/field_mask.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/mutation/numeric_increment_transform_operation.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/model/mutation/server_timestamp_operation.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/model/value/field_value.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/util/assert.dart'; import 'package:cloud_firestore_vm/src/firebase/timestamp.dart'; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/index.dart' show Value; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/index.dart' as pb; +import 'package:cloud_firestore_vm/src/proto/google/protobuf/index.dart' as pb; +import 'package:cloud_firestore_vm/src/proto/google/type/index.dart' as pb; +import 'package:fixnum/fixnum.dart'; + +import 'model/object_value.dart'; /// Helper for parsing raw user input (provided via the API) into internal model classes. -class UserDataConverter { - UserDataConverter(this.databaseId); +class UserDataReader { + UserDataReader(this.databaseId); final DatabaseId databaseId; /// Parse document data from a non-merge set() call. UserDataParsedSetData parseSetData(Map input) { - final UserDataParseAccumulator accumulator = - UserDataParseAccumulator(UserDataSource.set); - final FieldValue updateData = _parseData(input, accumulator.rootContext); + final UserDataParseAccumulator accumulator = UserDataParseAccumulator(UserDataSource.set); + final ObjectValue updateData = _convertAndParseDocumentData(input, accumulator.rootContext); return accumulator.toSetData(updateData); } /// Parse document data from a set() call with SetOptions.merge() set. - UserDataParsedSetData parseMergeData( - Map input, FieldMask fieldMask) { - final UserDataParseAccumulator accumulator = - UserDataParseAccumulator(UserDataSource.mergeSet); - final ObjectValue updateData = _parseData(input, accumulator.rootContext); + UserDataParsedSetData parseMergeData(Map input, FieldMask fieldMask) { + final UserDataParseAccumulator accumulator = UserDataParseAccumulator(UserDataSource.mergeSet); + final ObjectValue updateData = _convertAndParseDocumentData(input, accumulator.rootContext); if (fieldMask != null) { // Verify that all elements specified in the field mask are part of the parsed context. for (FieldPath field in fieldMask.mask) { if (!accumulator.contains(field)) { - throw ArgumentError( - 'Field \'$field\' is specified in your field mask but not in your input data.'); + throw ArgumentError('Field \'$field\' is specified in your field mask but not in your input data.'); } } @@ -59,45 +59,39 @@ class UserDataConverter { /// Parse update data from an update() call. UserDataParsedUpdateData parseUpdateData(Map data) { checkNotNull(data, 'Provided update data must not be null.'); - final UserDataParseAccumulator accumulator = - UserDataParseAccumulator(UserDataSource.update); + final UserDataParseAccumulator accumulator = UserDataParseAccumulator(UserDataSource.update); final UserDataParseContext context = accumulator.rootContext; - ObjectValue updateData = ObjectValue.empty; + final ObjectValueBuilder updateData = ObjectValue.newBuilder(); for (MapEntry entry in data.entries) { - final FieldPath fieldPath = - firestore.FieldPath.fromDotSeparatedPath(entry.key).internalPath; + final FieldPath fieldPath = firestore.FieldPath.fromDotSeparatedPath(entry.key).internalPath; final Object fieldValue = entry.value; if (fieldValue is firestore.FieldValue && fieldValue.isDelete) { // Add it to the field mask, but don't add anything to updateData. context.addToFieldMask(fieldPath); } else { - final FieldValue parsedValue = - _parseData(fieldValue, context.childContextForField(fieldPath)); + final Value parsedValue = _parseData(fieldValue, context.childContextForField(fieldPath)); if (parsedValue != null) { context.addToFieldMask(fieldPath); - updateData = updateData.set(fieldPath, parsedValue); + updateData[fieldPath] = fieldValue; } } } - return accumulator.toUpdateData(updateData); + return accumulator.toUpdateData(updateData.build()); } /// Parses the update data from the update(field, value, field, value...) varargs call, accepting both strings and /// FieldPaths. - UserDataParsedUpdateData parseUpdateDataFromList( - List fieldsAndValues) { + UserDataParsedUpdateData parseUpdateDataFromList(List fieldsAndValues) { // fieldsAndValues.length and alternating types should already be validated by collectUpdateArguments(). final int length = fieldsAndValues.length; - hardAssert(length.remainder(2) == 0, - 'Expected fieldAndValues to contain an even number of elements'); + hardAssert(length.remainder(2) == 0, 'Expected fieldAndValues to contain an even number of elements'); - final UserDataParseAccumulator accumulator = - UserDataParseAccumulator(UserDataSource.update); + final UserDataParseAccumulator accumulator = UserDataParseAccumulator(UserDataSource.update); final UserDataParseContext context = accumulator.rootContext; - ObjectValue updateData = ObjectValue.empty; + final ObjectValueBuilder updateData = ObjectValue.newBuilder(); for (int i = 0; i < length; i += 2) { final Object fieldPath = fieldsAndValues[i]; @@ -109,8 +103,7 @@ class UserDataConverter { FieldPath parsedField; if (fieldPath is String) { - parsedField = - firestore.FieldPath.fromDotSeparatedPath(fieldPath).internalPath; + parsedField = firestore.FieldPath.fromDotSeparatedPath(fieldPath).internalPath; } else { final firestore.FieldPath path = fieldPath; parsedField = path.internalPath; @@ -120,36 +113,55 @@ class UserDataConverter { // Add it to the field mask, but don't add anything to updateData. context.addToFieldMask(parsedField); } else { - final FieldValue parsedValue = - _parseData(fieldValue, context.childContextForField(parsedField)); + final Value parsedValue = _parseData(fieldValue, context.childContextForField(parsedField)); if (parsedValue != null) { context.addToFieldMask(parsedField); - updateData = updateData.set(parsedField, parsedValue); + updateData[parsedField] = parsedValue; } } } - return accumulator.toUpdateData(updateData); + return accumulator.toUpdateData(updateData.build()); } /// Parse a 'query value' (e.g. value in a where filter or a value in a cursor bound). - FieldValue parseQueryValue(Object input) { + /// + /// [allowArrays] Whether the query value is an List that may directly contain additional + /// List (e.g. the operand of a `whereIn` query). + Value parseQueryValue(Object input, [bool allowArrays = false]) { + allowArrays ??= false; + final UserDataParseAccumulator accumulator = - UserDataParseAccumulator(UserDataSource.argument); - final FieldValue parsed = _parseData(input, accumulator.rootContext); + UserDataParseAccumulator(allowArrays ? UserDataSource.arrayArgument : UserDataSource.argument); + final Value parsed = _parseData(input, accumulator.rootContext); hardAssert(parsed != null, 'Parsed data should not be null.'); - hardAssert(accumulator.fieldTransforms.isEmpty, - 'Field transforms should have been disallowed.'); + hardAssert(accumulator.fieldTransforms.isEmpty, 'Field transforms should have been disallowed.'); return parsed; } + ObjectValue _convertAndParseDocumentData(Object input, UserDataParseContext context) { + const String badDocReason = 'Invalid data. Data must be a Map, but it was '; + + // Check Array before calling CustomClassMapper since it'll give you a confusing message + // to use List instead, which also won't work in a set(). + if (input is List) { + throw ArgumentError('$badDocReason a List'); + } + + final Value parsedValue = _parseData(input, context); + if (parsedValue.whichValueType() != pb.Value_ValueType.mapValue) { + throw ArgumentError('$badDocReason of type: ${input.runtimeType}'); + } + return ObjectValue(parsedValue); + } + /// Internal helper for parsing user data. /// /// A [context] object representing the current path being parsed, the source of the data being parsed, etc. Returns /// parsed value, or null if the value was a FieldValue sentinel that should not be included in the resulting parsed /// data. - FieldValue _parseData(Object input, UserDataParseContext context) { + Value _parseData(Object input, UserDataParseContext context) { if (input is Map) { return _parseMap(input.cast(), context); } else if (input is firestore.FieldValue) { @@ -167,7 +179,10 @@ class UserDataConverter { if (input is List) { // TODO(long1eu): Include the path containing the array in the error message. - if (context.arrayElement) { + // In the case of IN queries, the parsed data is an array (representing the set of values + // to be included for the IN query) that may directly contain additional arrays (each + // representing an individual field value), so we disable this validation. + if (context.arrayElement && context.dataSource != UserDataSource.arrayArgument) { throw context.createError('Nested arrays are not supported'); } return _parseList(input, context); @@ -177,57 +192,51 @@ class UserDataConverter { } } - ObjectValue _parseMap(Map map, UserDataParseContext context) { - final Map result = {}; - + Value _parseMap(Map map, UserDataParseContext context) { if (map.isEmpty) { if (context.path != null && context.path.isNotEmpty) { context.addToFieldMask(context.path); } - return ObjectValue.empty; + return Value(mapValue: pb.MapValue()); } else { + final pb.MapValue result = pb.MapValue(); for (MapEntry entry in map.entries) { if (entry.key is! String) { - throw context.createError( - 'Non-String Map key (${entry.value}) is not allowed'); + throw context.createError('Non-String Map key (${entry.value}) is not allowed'); } final String key = entry.key; - final FieldValue parsedValue = - _parseData(entry.value, context.childContextForSegment(key)); + final Value parsedValue = _parseData(entry.value, context.childContextForSegment(key)); if (parsedValue != null) { - result[key] = parsedValue; + result.fields[key] = parsedValue; } } + return Value(mapValue: result); } - return ObjectValue.fromMap(result); } - ArrayValue _parseList(List list, UserDataParseContext context) { - final List result = List(list.length); + Value _parseList(List list, UserDataParseContext context) { + final List result = List(list.length); int entryIndex = 0; for (T entry in list) { - FieldValue parsedEntry = - _parseData(entry, context.childContextForArrayIndex(entryIndex)); + Value parsedEntry = _parseData(entry, context.childContextForArrayIndex(entryIndex)); // Just include nulls in the array for fields being replaced with a sentinel. - parsedEntry ??= NullValue.nullValue(); + parsedEntry ??= Value(nullValue: pb.NullValue.NULL_VALUE); result[entryIndex] = parsedEntry; entryIndex++; } - return ArrayValue.fromList(result); + + return Value(arrayValue: pb.ArrayValue(values: result)); } /// 'Parses' the provided FieldValue, adding any necessary transforms to [context._fieldTransforms]. - void _parseSentinelFieldValue( - firestore.FieldValue value, UserDataParseContext context) { + void _parseSentinelFieldValue(firestore.FieldValue value, UserDataParseContext context) { // Sentinels are only supported with writes, and not within arrays. if (!context.isWrite) { - throw context.createError( - '${value.methodName}() can only be used with set() and update()'); + throw context.createError('${value.methodName}() can only be used with set() and update()'); } if (context.path == null) { - throw context.createError( - '${value.methodName}() is not currently supported inside arrays'); + throw context.createError('${value.methodName}() is not currently supported inside arrays'); } if (value.isDelete) { @@ -236,33 +245,26 @@ class UserDataConverter { // fieldMask so it gets deleted. context.addToFieldMask(context.path); } else if (context.dataSource == UserDataSource.update) { - hardAssert(context.path.isNotEmpty, - 'FieldValue.delete() at the top level should have already been handled.'); - throw context.createError( - 'FieldValue.delete() can only appear at the top level of your update data'); + hardAssert(context.path.isNotEmpty, 'FieldValue.delete() at the top level should have already been handled.'); + throw context.createError('FieldValue.delete() can only appear at the top level of your update data'); } else { // We shouldn't encounter delete sentinels for queries or non-merge [set()] calls. - throw context.createError( - 'FieldValue.delete() can only be used with update() and set() with SetOptions.merge()'); + throw context + .createError('FieldValue.delete() can only be used with update() and set() with SetOptions.merge()'); } } else if (value.isServerTimestamp) { context.addToFieldTransforms(context.path, ServerTimestampOperation()); } else if (value.isArrayUnion) { - final List parsedElements = - _parseArrayTransformElements(value.elements); - final ArrayTransformOperation arrayUnion = - ArrayTransformOperationUnion(parsedElements); + final List parsedElements = _parseArrayTransformElements(value.elements); + final ArrayTransformOperation arrayUnion = ArrayTransformOperationUnion(parsedElements); context.addToFieldTransforms(context.path, arrayUnion); } else if (value.isArrayRemove) { - final List parsedElements = - _parseArrayTransformElements(value.elements); - final ArrayTransformOperation arrayRemove = - ArrayTransformOperationRemove(parsedElements); + final List parsedElements = _parseArrayTransformElements(value.elements); + final ArrayTransformOperation arrayRemove = ArrayTransformOperationRemove(parsedElements); context.addToFieldTransforms(context.path, arrayRemove); } else if (value.isIncrement) { - final FieldValue operand = parseQueryValue(value.elements[0]); - final NumericIncrementTransformOperation incrementOperation = - NumericIncrementTransformOperation(operand); + final Value operand = parseQueryValue(value.elements[0]); + final NumericIncrementTransformOperation incrementOperation = NumericIncrementTransformOperation(operand); context.addToFieldTransforms(context.path, incrementOperation); } else { throw fail('Unknown FieldValue type: ${value.runtimeType}'); @@ -273,51 +275,61 @@ class UserDataConverter { /// /// Returns the parsed value, or null if the value was a FieldValue sentinel that should not be included in the /// resulting parsed data. - FieldValue _parseScalarValue(Object input, UserDataParseContext context) { + Value _parseScalarValue(Object input, UserDataParseContext context) { if (input == null) { - return NullValue.nullValue(); + return Value(nullValue: pb.NullValue.NULL_VALUE); } else if (input is int) { - return IntegerValue.valueOf(input); + return Value(integerValue: Int64(input)); } else if (input is double) { - return DoubleValue.valueOf(input); + return Value(doubleValue: input); } else if (input is bool) { - return BoolValue.valueOf(input); + return Value(booleanValue: input); } else if (input is String) { - return StringValue.valueOf(input); + return Value(stringValue: input); } else if (input is DateTime) { - return TimestampValue.valueOf(Timestamp.fromDate(input)); + return Value(timestampValue: pb.Timestamp.fromDateTime(input)); } else if (input is Timestamp) { - final Timestamp timestamp = input; - final int seconds = timestamp.seconds; - // Firestore backend truncates precision down to microseconds. To ensure offline mode works the same with regards - // to truncation, perform the truncation immediately without waiting for the backend to do that. - final int truncatedNanoseconds = timestamp.nanoseconds ~/ 1000 * 1000; - return TimestampValue.valueOf(Timestamp(seconds, truncatedNanoseconds)); + return _parseTimestamp(input); } else if (input is GeoPoint) { - return GeoPointValue.valueOf(input); + return Value( + geoPointValue: pb.LatLng( + latitude: input.latitude, + longitude: input.longitude, + ), + ); } else if (input is Blob) { - return BlobValue.valueOf(input); + return Value(bytesValue: input.bytes); } else if (input is DocumentReference) { final DocumentReference ref = input; // TODO(long1eu): Rework once pre-converter is ported to Android. if (ref.firestore != null) { final DatabaseId otherDb = ref.firestore.databaseId; if (otherDb != databaseId) { - throw context.createError( - 'Document reference is for database ${otherDb.projectId}/${otherDb.databaseId} but ' + throw context.createError('Document reference is for database ${otherDb.projectId}/${otherDb.databaseId} but ' 'should be for database ${databaseId.projectId}/${databaseId.databaseId}'); } } - return ReferenceValue.valueOf(databaseId, ref.key); + + return Value( + referenceValue: 'projects/${databaseId.projectId}/databases/${databaseId.databaseId}/documents/${input.path}', + ); } else { throw context.createError('Unsupported type: ${input.runtimeType}'); } } - List _parseArrayTransformElements(List elements) { - final UserDataParseAccumulator accumulator = - UserDataParseAccumulator(UserDataSource.argument); - final List result = List(elements.length); + Value _parseTimestamp(Timestamp timestamp) { + // Firestore backend truncates precision down to microseconds. To ensure offline mode works + // the same with regards to truncation, perform the truncation immediately without waiting for + // the backend to do that. + final int truncatedNanoseconds = timestamp.nanoseconds ~/ 1000 * 1000; + + return Value(timestampValue: pb.Timestamp(seconds: Int64(timestamp.seconds), nanos: truncatedNanoseconds)); + } + + List _parseArrayTransformElements(List elements) { + final UserDataParseAccumulator accumulator = UserDataParseAccumulator(UserDataSource.argument); + final List result = List(elements.length); for (int i = 0; i < elements.length; i++) { final Object element = elements[i]; // Although array transforms are used with writes, the actual elements being unioned or removed are not considered diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_writer.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_writer.dart new file mode 100644 index 00000000..55ed1f91 --- /dev/null +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/user_data_writer.dart @@ -0,0 +1,107 @@ +// File created by +// Lung Razvan +// on 20/09/2018 + +import 'dart:typed_data'; + +import 'package:_firebase_internal_vm/_firebase_internal_vm.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/document_reference.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/firestore.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/geo_point.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/database_id.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/document_key.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/server_timestamps.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/model/values.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/server_timestamp_behavior.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/util/assert.dart'; +import 'package:cloud_firestore_vm/src/firebase/timestamp.dart'; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/index.dart' show Value; +import 'package:cloud_firestore_vm/src/proto/google/firestore/v1/index.dart' as pb; +import 'package:cloud_firestore_vm/src/proto/google/protobuf/index.dart' as pb; +import 'package:cloud_firestore_vm/src/proto/google/type/index.dart' as pb; + +/// Helper for parsing raw user input (provided via the API) into internal model classes. +class UserDataWriter { + const UserDataWriter(this.firestore, this.serverTimestampBehavior); + + final Firestore firestore; + final ServerTimestampBehavior serverTimestampBehavior; + + Object convertValue(Value value) { + switch (typeOrder(value)) { + case TYPE_ORDER_MAP: + return convertObject(value.mapValue.fields); + case TYPE_ORDER_ARRAY: + return _convertArray(value.arrayValue); + case TYPE_ORDER_REFERENCE: + return _convertReference(value); + case TYPE_ORDER_TIMESTAMP: + return _convertTimestamp(value.timestampValue); + case TYPE_ORDER_SERVER_TIMESTAMP: + return _convertServerTimestamp(value); + case TYPE_ORDER_NULL: + return null; + case TYPE_ORDER_BOOL: + return value.booleanValue; + case TYPE_ORDER_NUMBER: + return value.whichValueType() == pb.Value_ValueType.integerValue + ? value.integerValue.toInt() + : value.doubleValue; + case TYPE_ORDER_STRING: + return value.stringValue; + case TYPE_ORDER_BLOB: + return Uint8List.fromList(value.bytesValue); + case TYPE_ORDER_GEOPOINT: + return GeoPoint( + value.geoPointValue.latitude, + value.geoPointValue.longitude, + ); + default: + throw fail('Unknown value type: ${value.whichValueType()}'); + } + } + + Map convertObject(Map mapValue) { + return mapValue.map((String key, Value value) => MapEntry(key, convertValue(value))); + } + + Object _convertServerTimestamp(Value serverTimestampValue) { + switch (serverTimestampBehavior) { + case ServerTimestampBehavior.previous: + final Value previousValue = ServerTimestamps.getPreviousValue(serverTimestampValue); + if (previousValue == null) { + return null; + } + return convertValue(previousValue); + case ServerTimestampBehavior.estimate: + return _convertTimestamp(ServerTimestamps.getLocalWriteTime(serverTimestampValue)); + default: + return null; + } + } + + Object _convertTimestamp(pb.Timestamp value) { + return Timestamp(value.seconds.toInt(), value.nanos); + } + + List _convertArray(pb.ArrayValue arrayValue) { + return arrayValue.values.map(convertValue).toList(); + } + + Object _convertReference(Value value) { + final DatabaseId refDatabase = DatabaseId.fromName(value.referenceValue); + final DocumentKey key = DocumentKey.fromName(value.referenceValue); + final DatabaseId database = firestore.databaseId; + if (refDatabase != database) { + // TODO: Somehow support foreign references. + Log.w( + 'DocumentSnapshot', + 'Document ${key.path} contains a document reference within a different database ' + '(${refDatabase.projectId}/${refDatabase.databaseId}) which is not supported. ' + 'It will be treated as a reference in the current database ' + '(${database.projectId}/${database.databaseId}) instead.', + ); + } + return DocumentReference(key, firestore); + } +} diff --git a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/write_batch.dart b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/write_batch.dart index 663ad740..69cc63cc 100644 --- a/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/write_batch.dart +++ b/cloud_firestore/cloud_firestore_vm/lib/src/firebase/firestore/write_batch.dart @@ -27,7 +27,7 @@ typedef BatchCallback = Future Function(WriteBatch); /// Unlike transactions, write batches are persisted offline and therefore are preferable when you /// don't need to condition your writes on read data. /// -/// **Subclassing Note**: Firestore classes are not meant to be subclassed except for use in test +/// **Subclassing Note**: Cloud Firestore classes are not meant to be subclassed except for use in test /// mocks. Subclassing is not supported in production code and new SDK releases may break code that /// does so. class WriteBatch { @@ -46,8 +46,7 @@ class WriteBatch { /// [options] is an object to configure the set behavior. /// /// Returns this [WriteBatch] instance. Used for chaining method calls. - WriteBatch set(DocumentReference documentRef, Map data, - [SetOptions options]) { + WriteBatch set(DocumentReference documentRef, Map data, [SetOptions options]) { options ??= SetOptions.overwrite; _firestore.validateReference(documentRef); checkNotNull(data, 'Provided data must not be null.'); @@ -55,8 +54,7 @@ class WriteBatch { final UserDataParsedSetData parsed = options.merge ? _firestore.userDataReader.parseMergeData(data, options.fieldMask) : _firestore.userDataReader.parseSetData(data); - _mutations - .addAll(parsed.toMutationList(documentRef.key, Precondition.none)); + _mutations.add(parsed.toMutation(documentRef.key, Precondition.none)); return this; } @@ -70,13 +68,12 @@ class WriteBatch { /// /// Returns this [WriteBatch] instance. Used for chaining method calls. WriteBatch updateFromList(DocumentReference documentRef, List data) { - final UserDataParsedUpdateData parsedData = _firestore.userDataReader - .parseUpdateDataFromList(collectUpdateArguments(1, data)); + final UserDataParsedUpdateData parsedData = + _firestore.userDataReader.parseUpdateDataFromList(collectUpdateArguments(1, data)); _firestore.validateReference(documentRef); _verifyNotCommitted(); - _mutations.addAll( - parsedData.toMutationList(documentRef.key, Precondition(exists: true))); + _mutations.add(parsedData.toMutation(documentRef.key, Precondition(exists: true))); return this; } @@ -89,13 +86,11 @@ class WriteBatch { /// /// Returns this [WriteBatch] instance. Used for chaining method calls. WriteBatch update(DocumentReference documentRef, Map data) { - final UserDataParsedUpdateData parsedData = - _firestore.userDataReader.parseUpdateData(data); + final UserDataParsedUpdateData parsedData = _firestore.userDataReader.parseUpdateData(data); _firestore.validateReference(documentRef); _verifyNotCommitted(); - _mutations.addAll( - parsedData.toMutationList(documentRef.key, Precondition(exists: true))); + _mutations.add(parsedData.toMutation(documentRef.key, Precondition(exists: true))); return this; } @@ -122,8 +117,7 @@ class WriteBatch { void _verifyNotCommitted() { if (_committed) { - throw StateError( - 'A write batch can no longer be used after commit() has been called.'); + throw StateError('A write batch can no longer be used after commit() has been called.'); } } } diff --git a/cloud_firestore/cloud_firestore_vm/test/util/test_util.dart b/cloud_firestore/cloud_firestore_vm/test/util/test_util.dart index cbb19156..b2345904 100644 --- a/cloud_firestore/cloud_firestore_vm/test/util/test_util.dart +++ b/cloud_firestore/cloud_firestore_vm/test/util/test_util.dart @@ -39,7 +39,7 @@ import 'package:cloud_firestore_vm/src/firebase/firestore/remote/remote_event.da import 'package:cloud_firestore_vm/src/firebase/firestore/remote/target_change.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/remote/watch_change.dart'; import 'package:cloud_firestore_vm/src/firebase/firestore/remote/watch_change_aggregator.dart'; -import 'package:cloud_firestore_vm/src/firebase/firestore/user_data_converter.dart'; +import 'package:cloud_firestore_vm/src/firebase/firestore/user_data_writer.dart'; import 'package:cloud_firestore_vm/src/firebase/timestamp.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; @@ -73,7 +73,7 @@ final Map emptyMap = {}; FieldValue wrap(Object value) { final DatabaseId databaseId = DatabaseId.forProject('project'); - final UserDataConverter dataConverter = UserDataConverter(databaseId); + final UserDataWriter dataConverter = UserDataWriter(databaseId); // HACK: We use parseQueryValue() since it accepts scalars as well as // arrays / objects, and our tests currently use wrap() pretty generically // so we don't know the intent. @@ -408,8 +408,8 @@ DeleteMutation deleteMutation(String path) { /// use dotted-notation for nested fields (i.e. { 'foo.bar': FieldValue.foo() } and must not contain any non-sentinel /// data. TransformMutation transformMutation(String path, Map data) { - final UserDataConverter dataConverter = - UserDataConverter(DatabaseId.forProject('project')); + final UserDataWriter dataConverter = + UserDataWriter(DatabaseId.forProject('project')); final UserDataParsedUpdateData result = dataConverter.parseUpdateData(data); // The order of the transforms doesn't matter, but we sort them so tests can diff --git a/firebase_core/firebase_core_vm/lib/firebase_core_vm.dart b/firebase_core/firebase_core_vm/lib/firebase_core_vm.dart index be3b520a..52742dd7 100644 --- a/firebase_core/firebase_core_vm/lib/firebase_core_vm.dart +++ b/firebase_core/firebase_core_vm/lib/firebase_core_vm.dart @@ -12,4 +12,7 @@ export 'src/heart_beat/heart_beat_info.dart'; export 'src/heart_beat/heart_beat_info_storage.dart'; export 'src/heart_beat/heart_beat_result.dart'; export 'src/heart_beat/sdk_heart_beat_result.dart'; +export 'src/platform/global_library_version_registrar.dart'; +export 'src/platform/library_version.dart'; +export 'src/platform/user_agent_publiser.dart'; export 'src/platform_js.dart' if (dart.library.io) 'src/platform_io.dart'; diff --git a/firebase_core/firebase_core_vm/lib/src/heart_beat/heart_beat_info.dart b/firebase_core/firebase_core_vm/lib/src/heart_beat/heart_beat_info.dart index 216bc247..34ca5e8a 100644 --- a/firebase_core/firebase_core_vm/lib/src/heart_beat/heart_beat_info.dart +++ b/firebase_core/firebase_core_vm/lib/src/heart_beat/heart_beat_info.dart @@ -14,9 +14,9 @@ import 'heart_beat_result.dart'; /// This also exposes functions to store and retrieve [HeartBeatInfo] in the form of /// [HeartBeatResult]. abstract class HeartBeatInfo { - HeartBeat getHeartBeatCode(String heartBeatTag); + HeartBeat getHeartBeatCode(String heartBeatKey); - Future storeHeartBeatInfo(String heartBeatTag); + Future storeHeartBeatInfo(String heartBeatKey); Future> getAndClearStoredHeartBeatInfo(); } diff --git a/google_sign_in_dart/pubspec.lock b/google_sign_in_dart/pubspec.lock index 2c1c10a0..e202c545 100644 --- a/google_sign_in_dart/pubspec.lock +++ b/google_sign_in_dart/pubspec.lock @@ -7,42 +7,42 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety" + version: "2.5.0-nullsafety.3" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety" + version: "2.1.0-nullsafety.3" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.2" + version: "1.1.0-nullsafety.5" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety" + version: "1.2.0-nullsafety.3" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety" + version: "1.1.0-nullsafety.3" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.2" + version: "1.15.0-nullsafety.5" convert: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety" + version: "1.2.0-nullsafety.3" file: dependency: transitive description: @@ -141,28 +141,28 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.3-nullsafety.3" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety" + version: "0.12.10-nullsafety.3" meta: dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.2" + version: "1.3.0-nullsafety.6" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety" + version: "1.8.0-nullsafety.3" path_provider_linux: dependency: transitive description: @@ -258,49 +258,49 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety" + version: "1.8.0-nullsafety.4" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety" + version: "1.10.0-nullsafety.6" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety" + version: "2.1.0-nullsafety.3" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety" + version: "1.1.0-nullsafety.3" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety" + version: "1.2.0-nullsafety.3" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety" + version: "0.2.19-nullsafety.6" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.2" + version: "1.3.0-nullsafety.5" url_launcher: dependency: "direct main" description: @@ -342,7 +342,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.2" + version: "2.1.0-nullsafety.5" xdg_directories: dependency: transitive description: @@ -351,5 +351,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.0-0.0.dev <2.10.0" + dart: ">=2.12.0-0.0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0"