diff --git a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart index aef386e77d26..ac1e82df64b4 100755 --- a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'dart:ui' show hashValues, hashList; +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; @@ -17,6 +18,9 @@ import 'package:meta/meta.dart'; import 'src/utils/auto_id_generator.dart'; +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart' + show Source; + part 'src/blob.dart'; part 'src/collection_reference.dart'; part 'src/document_change.dart'; @@ -33,4 +37,3 @@ part 'src/snapshot_metadata.dart'; part 'src/timestamp.dart'; part 'src/transaction.dart'; part 'src/write_batch.dart'; -part 'src/source.dart'; diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart index 413cf41be0be..ec2ee9d467fa 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart @@ -48,15 +48,12 @@ class DocumentReference { /// If [merge] is true, the provided data will be merged into an /// existing document instead of overwriting. Future setData(Map data, {bool merge = false}) { - return Firestore.channel.invokeMethod( - 'DocumentReference#setData', - { - 'app': firestore.app.name, - 'path': path, - 'data': data, - 'options': {'merge': merge}, - }, - ); + return Firestore.platform.documentReference.set( + firestore.app.name, + path: path, + data: data, + options: PlatformSetOptions(merge: merge), + ); } /// Updates fields in the document referred to by this [DocumentReference]. @@ -66,44 +63,38 @@ class DocumentReference { /// /// If no document exists yet, the update will fail. Future updateData(Map data) { - return Firestore.channel.invokeMethod( - 'DocumentReference#updateData', - { - 'app': firestore.app.name, - 'path': path, - 'data': data, - }, - ); + return Firestore.platform.documentReference.update( + firestore.app.name, + path: path, + data: data, + ); } /// Reads the document referenced by this [DocumentReference]. /// /// If no document exists, the read will return null. Future get({Source source = Source.serverAndCache}) async { - final Map data = - await Firestore.channel.invokeMapMethod( - 'DocumentReference#get', - { - 'app': firestore.app.name, - 'path': path, - 'source': _getSourceString(source), - }, - ); + final PlatformDocumentSnapshot snapshot = await Firestore.platform.documentReference.get( + firestore.app.name, + path: path, + source: source, + ); + return DocumentSnapshot._( - data['path'], - _asStringKeyedMap(data['data']), - SnapshotMetadata._(data['metadata']['hasPendingWrites'], - data['metadata']['isFromCache']), + snapshot.path, + _asStringKeyedMap(snapshot.data), + SnapshotMetadata._(snapshot.metadata.hasPendingWrites, + snapshot.metadata.isFromCache), firestore, ); } /// Deletes the document referred to by this [DocumentReference]. Future delete() { - return Firestore.channel.invokeMethod( - 'DocumentReference#delete', - {'app': firestore.app.name, 'path': path}, - ); + return Firestore.platform.documentReference.delete( + firestore.app.name, + path: path, + ); } /// Returns the reference of a collection contained inside of this @@ -118,34 +109,19 @@ class DocumentReference { // TODO(jackson): Reduce code duplication with [Query] Stream snapshots({bool includeMetadataChanges = false}) { assert(includeMetadataChanges != null); - Future _handle; - // It's fine to let the StreamController be garbage collected once all the - // subscribers have cancelled; this analyzer warning is safe to ignore. - StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( - onListen: () { - _handle = Firestore.channel.invokeMethod( - 'DocumentReference#addSnapshotListener', - { - 'app': firestore.app.name, - 'path': path, - 'includeMetadataChanges': includeMetadataChanges, - }, - ).then((dynamic result) => result); - _handle.then((int handle) { - Firestore._documentObservers[handle] = controller; - }); - }, - onCancel: () { - _handle.then((int handle) async { - await Firestore.channel.invokeMethod( - 'removeListener', - {'handle': handle}, - ); - Firestore._documentObservers.remove(handle); - }); - }, - ); - return controller.stream; + + return Firestore.platform.documentReference.snapshots( + firestore.app.name, + path: path, + includeMetadataChanges: includeMetadataChanges, + ).map((PlatformDocumentSnapshot snapshot) { + return DocumentSnapshot._( + snapshot.path, + _asStringKeyedMap(snapshot.data), + SnapshotMetadata._(snapshot.metadata.hasPendingWrites, + snapshot.metadata.isFromCache), + firestore, + ); + }); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index 5c164ed1e122..7f43ce6a806c 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -8,32 +8,11 @@ part of cloud_firestore; /// /// You can get an instance by calling [Firestore.instance]. class Firestore { - Firestore({FirebaseApp app}) : app = app ?? FirebaseApp.instance { - if (_initialized) return; - channel.setMethodCallHandler((MethodCall call) async { - if (call.method == 'QuerySnapshot') { - final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); - _queryObservers[call.arguments['handle']].add(snapshot); - } else if (call.method == 'DocumentSnapshot') { - final DocumentSnapshot snapshot = DocumentSnapshot._( - call.arguments['path'], - _asStringKeyedMap(call.arguments['data']), - SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], - call.arguments['metadata']['isFromCache']), - this, - ); - _documentObservers[call.arguments['handle']].add(snapshot); - } else if (call.method == 'DoTransaction') { - final int transactionId = call.arguments['transactionId']; - final Transaction transaction = Transaction(transactionId, this); - final dynamic result = - await _transactionHandlers[transactionId](transaction); - await transaction._finish(); - return result; - } - }); - _initialized = true; - } + Firestore({FirebaseApp app}) : app = app ?? FirebaseApp.instance; + + /// The platform instance that talks to the native side of the plugin. + @visibleForTesting + static final FirestorePlatform platform = FirestorePlatform.instance ?? (FirestorePlatform.instance = MethodChannelFirestore(FirestoreMessageCodec())); /// Gets the instance of Firestore for the default Firebase app. static final Firestore instance = Firestore(); @@ -43,24 +22,6 @@ class Firestore { /// If null, the default [FirebaseApp] is used. final FirebaseApp app; - static bool _initialized = false; - - @visibleForTesting - static const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/cloud_firestore', - StandardMethodCodec(FirestoreMessageCodec()), - ); - - static final Map> _queryObservers = - >{}; - - static final Map> _documentObservers = - >{}; - - static final Map _transactionHandlers = - {}; - static int _transactionHandlerId = 0; - @override bool operator ==(dynamic o) => o is Firestore && o.app == app; @@ -123,39 +84,41 @@ class Firestore { {Duration timeout = const Duration(seconds: 5)}) async { assert(timeout.inMilliseconds > 0, 'Transaction timeout must be more than 0 milliseconds'); - final int transactionId = _transactionHandlerId++; - _transactionHandlers[transactionId] = transactionHandler; - final Map result = await channel - .invokeMapMethod( - 'Firestore#runTransaction', { - 'app': app.name, - 'transactionId': transactionId, - 'transactionTimeout': timeout.inMilliseconds - }); + + // Wrap the user-supplied [TransactionHandler] into something that can be passed to the Platform implementation. + final PlatformTransactionHandler handler = (PlatformTransaction platformTransaction) async { + Transaction transaction = Transaction(platformTransaction.transactionId, this); + final dynamic result = await transactionHandler(transaction); + await transaction._finish(); + return result; + }; + + final Map result = await Firestore.platform.transaction.run( + app.name, + updateFunction: handler, + transactionTimeout: timeout.inMilliseconds, + ); + return result ?? {}; } @deprecated - Future enablePersistence(bool enable) async { + Future enablePersistence(bool enable) { assert(enable != null); - await channel - .invokeMethod('Firestore#enablePersistence', { - 'app': app.name, - 'enable': enable, - }); + return Firestore.platform.enablePersistence(app.name, enable: enable); } Future settings( {bool persistenceEnabled, String host, bool sslEnabled, - int cacheSizeBytes}) async { - await channel.invokeMethod('Firestore#settings', { - 'app': app.name, - 'persistenceEnabled': persistenceEnabled, - 'host': host, - 'sslEnabled': sslEnabled, - 'cacheSizeBytes': cacheSizeBytes, - }); + int cacheSizeBytes}) { + + return Firestore.platform.settings(app.name, + persistenceEnabled: persistenceEnabled, + host: host, + sslEnabled: sslEnabled, + cacheSizeBytes: cacheSizeBytes, + ); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart index 2da85859ee5a..2fb33b7575f5 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart @@ -52,55 +52,30 @@ class Query { // TODO(jackson): Reduce code duplication with [DocumentReference] Stream snapshots({bool includeMetadataChanges = false}) { assert(includeMetadataChanges != null); - Future _handle; - // It's fine to let the StreamController be garbage collected once all the - // subscribers have cancelled; this analyzer warning is safe to ignore. - StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( - onListen: () { - _handle = Firestore.channel.invokeMethod( - 'Query#addSnapshotListener', - { - 'app': firestore.app.name, - 'path': _path, - 'isCollectionGroup': _isCollectionGroup, - 'parameters': _parameters, - 'includeMetadataChanges': includeMetadataChanges, - }, - ).then((dynamic result) => result); - _handle.then((int handle) { - Firestore._queryObservers[handle] = controller; - }); - }, - onCancel: () { - _handle.then((int handle) async { - await Firestore.channel.invokeMethod( - 'removeListener', - {'handle': handle}, - ); - Firestore._queryObservers.remove(handle); - }); - }, - ); - return controller.stream; + + return Firestore.platform.query.snapshots( + firestore.app.name, + path: _path, + isCollectionGroup: _isCollectionGroup, + parameters: _parameters, + includeMetadataChanges: includeMetadataChanges, + ).map((PlatformQuerySnapshot snapshot) => QuerySnapshot._(snapshot.asMap(), firestore)); } /// Fetch the documents for this query Future getDocuments( {Source source = Source.serverAndCache}) async { assert(source != null); - final Map data = - await Firestore.channel.invokeMapMethod( - 'Query#getDocuments', - { - 'app': firestore.app.name, - 'path': _path, - 'isCollectionGroup': _isCollectionGroup, - 'parameters': _parameters, - 'source': _getSourceString(source), - }, - ); - return QuerySnapshot._(data, firestore); + + final PlatformQuerySnapshot snapshot = await Firestore.platform.query.getDocuments( + firestore.app.name, + path: _path, + isCollectionGroup: _isCollectionGroup, + parameters: _parameters, + source: source, + ); + + return QuerySnapshot._(snapshot.asMap(), firestore); } /// Obtains a CollectionReference corresponding to this query's location. diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart b/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart index d13e3f19f9b2..757523f5af46 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart @@ -23,18 +23,18 @@ class Transaction { } Future _get(DocumentReference documentReference) async { - final Map result = await Firestore.channel - .invokeMapMethod('Transaction#get', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - }); + final PlatformDocumentSnapshot result = await Firestore.platform.transaction.get( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + ); + if (result != null) { return DocumentSnapshot._( documentReference.path, - result['data']?.cast(), - SnapshotMetadata._(result['metadata']['hasPendingWrites'], - result['metadata']['isFromCache']), + result.data, + SnapshotMetadata._(result.metadata.hasPendingWrites, + result.metadata.isFromCache), _firestore); } else { return null; @@ -51,13 +51,12 @@ class Transaction { return result; } - Future _delete(DocumentReference documentReference) async { - return Firestore.channel - .invokeMethod('Transaction#delete', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - }); + Future _delete(DocumentReference documentReference) { + return Firestore.platform.transaction.delete( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + ); } /// Updates fields in the document referred to by [documentReference]. @@ -73,14 +72,13 @@ class Transaction { } Future _update( - DocumentReference documentReference, Map data) async { - return Firestore.channel - .invokeMethod('Transaction#update', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - 'data': data, - }); + DocumentReference documentReference, Map data) { + return Firestore.platform.transaction.update( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + data: data, + ); } /// Writes to the document referred to by the provided [DocumentReference]. @@ -97,13 +95,12 @@ class Transaction { } Future _set( - DocumentReference documentReference, Map data) async { - return Firestore.channel - .invokeMethod('Transaction#set', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - 'data': data, - }); + DocumentReference documentReference, Map data) { + return Firestore.platform.transaction.set( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + data: data + ); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart b/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart index 6a41ee7bfec3..212a72dc32f5 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart @@ -12,49 +12,47 @@ part of cloud_firestore; /// nor can it be committed again. class WriteBatch { WriteBatch._(this._firestore) - : _handle = Firestore.channel.invokeMethod( - 'WriteBatch#create', {'app': _firestore.app.name}); + : _handle = Firestore.platform.writeBatch.create(_firestore.app.name); final Firestore _firestore; - Future _handle; + Future _handle; final List> _actions = >[]; /// Indicator to whether or not this [WriteBatch] has been committed. bool _committed = false; + // This method throws a [StateError] if this [WriteBatch] has already been committed. + void _ensureNotCommitted() { + if (_committed) { + throw StateError( + "This batch has been committed and can no longer be changed."); + } + } + /// Commits all of the writes in this write batch as a single atomic unit. /// /// Calling this method prevents any future operations from being added. Future commit() async { - if (!_committed) { - _committed = true; - await Future.wait(_actions); - await Firestore.channel.invokeMethod( - 'WriteBatch#commit', {'handle': await _handle}); - } else { - throw StateError("This batch has already been committed."); - } + _ensureNotCommitted(); + + _committed = true; + await Future.wait(_actions); + await Firestore.platform.writeBatch.commit(handle: await _handle); } /// Deletes the document referred to by [document]. void delete(DocumentReference document) { - if (!_committed) { - _handle.then((dynamic handle) { - _actions.add( - Firestore.channel.invokeMethod( - 'WriteBatch#delete', - { - 'app': _firestore.app.name, - 'handle': handle, - 'path': document.path, - }, - ), - ); - }); - } else { - throw StateError( - "This batch has been committed and can no longer be changed."); - } + _ensureNotCommitted(); + + _handle.then((PlatformWriteBatchHandle handle) { + _actions.add( + Firestore.platform.writeBatch.delete( + _firestore.app.name, + handle: handle, + path: document.path, + ) + ); + }); } /// Writes to the document referred to by [document]. @@ -65,48 +63,36 @@ class WriteBatch { /// existing document instead of overwriting. void setData(DocumentReference document, Map data, {bool merge = false}) { - if (!_committed) { - _handle.then((dynamic handle) { - _actions.add( - Firestore.channel.invokeMethod( - 'WriteBatch#setData', - { - 'app': _firestore.app.name, - 'handle': handle, - 'path': document.path, - 'data': data, - 'options': {'merge': merge}, - }, - ), - ); - }); - } else { - throw StateError( - "This batch has been committed and can no longer be changed."); - } + _ensureNotCommitted(); + + _handle.then((PlatformWriteBatchHandle handle) { + _actions.add( + Firestore.platform.writeBatch.set( + _firestore.app.name, + handle: handle, + path: document.path, + data: data, + options: PlatformSetOptions(merge: merge), + ) + ); + }); } /// Updates fields in the document referred to by [document]. /// /// If the document does not exist, the operation will fail. void updateData(DocumentReference document, Map data) { - if (!_committed) { - _handle.then((dynamic handle) { - _actions.add( - Firestore.channel.invokeMethod( - 'WriteBatch#updateData', - { - 'app': _firestore.app.name, - 'handle': handle, - 'path': document.path, - 'data': data, - }, - ), - ); - }); - } else { - throw StateError( - "This batch has been committed and can no longer be changed."); - } + _ensureNotCommitted(); + + _handle.then((PlatformWriteBatchHandle handle) { + _actions.add( + Firestore.platform.writeBatch.update( + _firestore.app.name, + handle: handle, + path: document.path, + data: data, + ) + ); + }); } } diff --git a/packages/cloud_firestore/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/cloud_firestore/pubspec.yaml index 1bef85841f13..b044f357ed6e 100755 --- a/packages/cloud_firestore/cloud_firestore/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore/pubspec.yaml @@ -15,6 +15,8 @@ flutter: pluginClass: FLTCloudFirestorePlugin dependencies: + cloud_firestore_platform_interface: + path: ../cloud_firestore_platform_interface flutter: sdk: flutter meta: "^1.0.5" diff --git a/packages/cloud_firestore/cloud_firestore/test/cloud_firestore_test.dart b/packages/cloud_firestore/cloud_firestore/test/cloud_firestore_test.dart index 328427f46e04..92b807b30fc9 100755 --- a/packages/cloud_firestore/cloud_firestore/test/cloud_firestore_test.dart +++ b/packages/cloud_firestore/cloud_firestore/test/cloud_firestore_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -48,7 +49,8 @@ void main() { collectionReference = firestore.collection('foo'); collectionGroupQuery = firestore.collectionGroup('bar'); transaction = Transaction(0, firestore); - Firestore.channel.setMockMethodCallHandler((MethodCall methodCall) async { + + Function methodCallHandler = (MethodCall methodCall) async { log.add(methodCall); switch (methodCall.method) { case 'Query#addSnapshotListener': @@ -60,8 +62,8 @@ void main() { // https://github.com/flutter/flutter/issues/33446 // ignore: deprecated_member_use BinaryMessages.handlePlatformMessage( - Firestore.channel.name, - Firestore.channel.codec.encodeMethodCall( + MethodChannelFirestore.channel.name, + MethodChannelFirestore.channel.codec.encodeMethodCall( MethodCall('QuerySnapshot', { 'app': app.name, 'handle': handle, @@ -93,8 +95,8 @@ void main() { // https://github.com/flutter/flutter/issues/33446 // ignore: deprecated_member_use BinaryMessages.handlePlatformMessage( - Firestore.channel.name, - Firestore.channel.codec.encodeMethodCall( + MethodChannelFirestore.channel.name, + MethodChannelFirestore.channel.codec.encodeMethodCall( MethodCall('DocumentSnapshot', { 'handle': handle, 'path': methodCall.arguments['path'], @@ -167,7 +169,14 @@ void main() { default: return null; } - }); + }; + + // Ensure the MethodChannel platforms are initialized + Firestore.platform; + + // All the MethodChannel implementation share the same channel + MethodChannelFirestore.channel.setMockMethodCallHandler(methodCallHandler); + log.clear(); }); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md b/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..6fadda91b380 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial open-source release. diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE b/packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE new file mode 100644 index 000000000000..000b4618d2bd --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/README.md b/packages/cloud_firestore/cloud_firestore_platform_interface/README.md new file mode 100644 index 000000000000..1af6a2d43467 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/README.md @@ -0,0 +1,26 @@ +# cloud_firestore_platform_interface + +A common platform interface for the [`cloud_firestore`][1] plugin. + +This interface allows platform-specific implementations of the `cloud_firestore` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `cloud_firestore`, extend +[`CloudFirestorePlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`CloudFirestorePlatform` by calling +`CloudFirestorePlatform.instance = MyCloudFirestore()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../cloud_firestore +[2]: lib/cloud_firestore_platform_interface.dart diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart new file mode 100644 index 000000000000..7fc4e7eabba9 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -0,0 +1,7 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/implementations.dart'; +export 'src/interfaces.dart'; +export 'src/types.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations.dart new file mode 100644 index 000000000000..bec4a06b131e --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations.dart @@ -0,0 +1,9 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'implementations/method_channel_document_reference.dart'; +export 'implementations/method_channel_firestore.dart'; +export 'implementations/method_channel_query.dart'; +export 'implementations/method_channel_transaction.dart'; +export 'implementations/method_channel_write_batch.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_document_reference.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_document_reference.dart new file mode 100644 index 000000000000..e978275d7447 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_document_reference.dart @@ -0,0 +1,172 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show required, visibleForTesting; + +import './multi_method_channel.dart'; +import '../interfaces/document_reference_platform.dart'; +import '../types.dart'; + +class MethodChannelDocumentReference extends DocumentReferencePlatform { + MethodChannelDocumentReference(MultiMethodChannel channel) { + MethodChannelDocumentReference._channel = channel; + MethodChannelDocumentReference._channel.addMethodCallHandler('DocumentSnapshot', this._handleDocumentSnapshot); + } + + @visibleForTesting + static MultiMethodChannel get channel => MethodChannelDocumentReference._channel; + static MultiMethodChannel _channel; + + void _handleDocumentSnapshot(MethodCall call) { + final int handle = call.arguments['handle']; + _documentSnapshotStreamControllers[handle].add( + PlatformDocumentSnapshot( + path: call.arguments['path'], + data: call.arguments['data']?.cast(), + metadata: PlatformSnapshotMetadata( + hasPendingWrites: call.arguments['metadata']['hasPendingWrites'], + isFromCache: call.arguments['metadata']['isFromCache'], + ), + ) + ); + } + + /// Reads the document referred to by this [path]. + /// By default, get() attempts to provide up-to-date data when possible by waiting for + /// data from the server, but it may return cached data or fail if you are offline and + /// the server cannot be reached. + /// This behavior can be altered via the [source] parameter. + Future get( + String app, { + @required String path, + @required Source source, + }) { + return channel.invokeMapMethod( + 'DocumentReference#get', + { + 'app': app, + 'path': path, + 'source': getSourceString(source), + }, + ).then((Map data) { + return PlatformDocumentSnapshot( + path: data['path'], + data: data['data']?.cast(), + metadata: PlatformSnapshotMetadata( + hasPendingWrites: data['metadata']['hasPendingWrites'], + isFromCache: data['metadata']['isFromCache'], + ), + ); + }); + } + + /// Deletes the document referred to by this [path]. + Future delete( + String app, { + @required String path, + }) { + return channel.invokeMethod( + 'DocumentReference#delete', + {'app': app, 'path': path}, + ); + } + + /// Writes [data] to the document referred to by this [path]. + /// If the document does not yet exist, it will be created. + /// If you pass [options], the provided data can be merged into an existing document. + Future set( + String app, { + @required String path, + Map data, + PlatformSetOptions options, + }) { + return channel.invokeMethod( + 'DocumentReference#setData', + { + 'app': app, + 'path': path, + 'data': data, + 'options': options.asMap(), + }, + ); + } + + /// Updates [data] in the document referred to by this [path]. + /// The update will fail if applied to a document that does not exist. + Future update( + String app, { + @required String path, + Map data, + }) { + return channel.invokeMethod( + 'DocumentReference#updateData', + { + 'app': app, + 'path': path, + 'data': data, + }, + ); + } + + // Snapshots + + static final Map> _documentSnapshotStreamControllers = >{}; + + /// A Stream of QuerySnapshots. + /// The snapshot stream is never-ending. + Stream snapshots( + String app, { + @required String path, + bool includeMetadataChanges, + }) { + assert(includeMetadataChanges != null); + Future _handle; + // It's fine to let the StreamController be garbage collected once all the + // subscribers have cancelled; this analyzer warning is safe to ignore. + StreamController controller; // ignore: close_sinks + controller = StreamController.broadcast( + onListen: () { + _handle = _addDocumentReferenceSnapshotListener( + app, + path: path, + includeMetadataChanges: includeMetadataChanges, + ).then((dynamic result) => result); + _handle.then((int handle) { + _documentSnapshotStreamControllers[handle] = controller; + }); + }, + onCancel: () { + _handle.then((int handle) async { + await _removeListener(handle); + _documentSnapshotStreamControllers.remove(handle); + }); + }, + ); + return controller.stream; + } + + Future _addDocumentReferenceSnapshotListener( + String app, { + @required String path, + bool includeMetadataChanges, + }) { + return channel.invokeMethod( + 'DocumentReference#addSnapshotListener', + { + 'app': app, + 'path': path, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); + } + + Future _removeListener(int handle) { + return channel.invokeMethod( + 'removeListener', + {'handle': handle}, + ); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_firestore.dart new file mode 100644 index 000000000000..8e43e54b019e --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_firestore.dart @@ -0,0 +1,85 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show visibleForTesting; + +import './multi_method_channel.dart'; +import './method_channel_document_reference.dart'; +import './method_channel_query.dart'; +import './method_channel_transaction.dart'; +import './method_channel_write_batch.dart'; + +import '../interfaces.dart'; + +/// A method channel implementation of the Firestore platform. +class MethodChannelFirestore extends FirestorePlatform { + /// Constructor. Requires a MethodCodec [codec] that can live in userland and have business logic there. + /// This class shouldn't be instantiated more than once per lifetime of the app, so we don't end up with + /// multiple subscriptions to the different channels! + MethodChannelFirestore(StandardMessageCodec codec) { + // Register all other instances... + MultiMethodChannel channel = MultiMethodChannel( + 'plugins.flutter.io/cloud_firestore', + StandardMethodCodec(codec), + ); + + MethodChannelFirestore._channel = channel; + DocumentReferencePlatform.instance = MethodChannelDocumentReference(channel); + QueryPlatform.instance = MethodChannelQuery(channel); + TransactionPlatform.instance = MethodChannelTransaction(channel); + WriteBatchPlatform.instance = MethodChannelWriteBatch(channel); + } + + /// The MethodChannel used to pass messages to the native side. + @visibleForTesting + static MethodChannel get channel => MethodChannelFirestore._channel; + static MethodChannel _channel; + + /// DocumentReference delegate + @override + DocumentReferencePlatform get documentReference => + DocumentReferencePlatform.instance; + + /// Query delegate + @override + QueryPlatform get query => QueryPlatform.instance; + + /// Transaction delegate + @override + TransactionPlatform get transaction => TransactionPlatform.instance; + + /// WriteBatch delegate + @override + WriteBatchPlatform get writeBatch => WriteBatchPlatform.instance; + + @override + Future enablePersistence(String app, {bool enable = true}) { + return channel + .invokeMethod('Firestore#enablePersistence', { + 'app': app, + 'enable': enable, + }); + } + + @override + Future settings( + String app, { + bool persistenceEnabled, + String host, + bool sslEnabled, + int cacheSizeBytes, + }) { + return channel.invokeMethod('Firestore#settings', { + 'app': app, + 'persistenceEnabled': persistenceEnabled, + 'host': host, + 'sslEnabled': sslEnabled, + 'cacheSizeBytes': cacheSizeBytes, + }); + } + + // Find runTransaction in transaction.run +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_query.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_query.dart new file mode 100644 index 000000000000..d861ba4fe9a0 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_query.dart @@ -0,0 +1,117 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show visibleForTesting, required; + +import './multi_method_channel.dart'; + +import '../interfaces/query_platform.dart'; +import '../types.dart'; + +class MethodChannelQuery extends QueryPlatform { + MethodChannelQuery(MultiMethodChannel channel) { + MethodChannelQuery._channel = channel; + MethodChannelQuery._channel.addMethodCallHandler('QuerySnapshot', this._handleQuerySnapshot); + } + + @visibleForTesting + static MultiMethodChannel get channel => MethodChannelQuery._channel; + static MultiMethodChannel _channel; + + void _handleQuerySnapshot(MethodCall call) { + final int handle = call.arguments['handle']; + _querySnapshotStreamControllers[handle].add(PlatformQuerySnapshot(data: call.arguments)); + } + + /// What does this method correspond to in the Firebase API? + Future getDocuments( + String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + Source source, + }) { + return channel.invokeMapMethod( + 'Query#getDocuments', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'source': getSourceString(source), + }, + ).then((Map response) { + return PlatformQuerySnapshot(data: response); + }); + } + + // Snapshots + + static final Map> _querySnapshotStreamControllers = >{}; + + /// A Stream of QuerySnapshots. + /// The snapshot stream is never-ending. + Stream snapshots( + String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + bool includeMetadataChanges, + }) { + assert(includeMetadataChanges != null); + Future _handle; + // It's fine to let the StreamController be garbage collected once all the + // subscribers have cancelled; this analyzer warning is safe to ignore. + StreamController controller; // ignore: close_sinks + controller = StreamController.broadcast( + onListen: () { + _handle = _addQuerySnapshotListener( + app, + path: path, + isCollectionGroup: isCollectionGroup, + parameters: parameters, + includeMetadataChanges: includeMetadataChanges, + ).then((dynamic result) => result); + _handle.then((int handle) { + _querySnapshotStreamControllers[handle] = controller; + }); + }, + onCancel: () { + _handle.then((int handle) async { + await _removeListener(handle); + _querySnapshotStreamControllers.remove(handle); + }); + }, + ); + return controller.stream; + } + + Future _addQuerySnapshotListener( + String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + bool includeMetadataChanges, + }) { + return channel.invokeMethod( + 'Query#addSnapshotListener', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); + } + + Future _removeListener(int handle) { + return channel.invokeMethod( + 'removeListener', + {'handle': handle}, + ); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_transaction.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_transaction.dart new file mode 100644 index 000000000000..6828800cde47 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_transaction.dart @@ -0,0 +1,118 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show required, visibleForTesting; + +import './multi_method_channel.dart'; + +import '../interfaces/transaction_platform.dart'; +import '../types.dart'; + +class MethodChannelTransaction extends TransactionPlatform { + MethodChannelTransaction(MultiMethodChannel channel) { + MethodChannelTransaction._channel = channel; + MethodChannelTransaction._channel.addMethodCallHandler('DoTransaction', this._handleDoTransaction); + } + + @visibleForTesting + static MultiMethodChannel get channel => MethodChannelTransaction._channel; + static MultiMethodChannel _channel; + + static final Map _transactionHandlers = + {}; + static int _transactionHandlerId = 0; + + Future _handleDoTransaction(MethodCall call) async { + final int transactionId = call.arguments['transactionId']; + // Do the transaction + final PlatformTransactionHandler handler = _transactionHandlers[transactionId]; + return await handler(PlatformTransaction(transactionId: transactionId)); + } + + @override + Future> run( + String app, { + @required PlatformTransactionHandler updateFunction, + int transactionTimeout, + }) async { + // The [updateFunction] will be used by the [_handleDoTransaction] method later + final int transactionId = _transactionHandlerId++; + _transactionHandlers[transactionId] = updateFunction; + + return channel.invokeMapMethod( + 'Firestore#runTransaction', { + 'app': app, + 'transactionId': transactionId, + 'transactionTimeout': transactionTimeout + }); + } + + @override + Future get( + String app, { + @required String path, + @required int transactionId, + }) { + return channel + .invokeMapMethod('Transaction#get', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }).then((data) { + return PlatformDocumentSnapshot( + path: data['path'], + data: data['data']?.cast(), + metadata: PlatformSnapshotMetadata( + hasPendingWrites: data['metadata']['hasPendingWrites'], + isFromCache: data['metadata']['isFromCache'], + ), + ); + }); + } + + @override + Future delete( + String app, { + @required String path, + @required int transactionId, + }) { + return channel.invokeMethod('Transaction#delete', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); + } + + @override + Future update( + String app, { + @required String path, + @required int transactionId, + Map data, + }) async { + return channel.invokeMethod('Transaction#update', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); + } + + @override + Future set( + String app, { + @required String path, + @required int transactionId, + Map data, + }) async { + return channel.invokeMethod('Transaction#set', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_write_batch.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_write_batch.dart new file mode 100644 index 000000000000..bf7d1ef246b8 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/method_channel_write_batch.dart @@ -0,0 +1,102 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:meta/meta.dart' show required, visibleForTesting; + +import './multi_method_channel.dart'; + +import '../types.dart'; +import '../interfaces/write_batch_platform.dart'; + +class MethodChannelWriteBatch extends WriteBatchPlatform { + /// Constructor + MethodChannelWriteBatch(MultiMethodChannel channel) { + MethodChannelWriteBatch._channel = channel; + } + + @visibleForTesting + static MultiMethodChannel get channel => MethodChannelWriteBatch._channel; + static MultiMethodChannel _channel; + + /// Creates a new Write Batch + // Android returns a WriteBatch id int + @override + Future create(String app) { + return channel.invokeMethod( + 'WriteBatch#create', {'app': app}).then((id) => PlatformWriteBatchHandle(writeBatchId: id)); + } + + /// Commits all of the writes in this write batch as a single atomic unit. + /// Returns a Promise resolved once all of the writes in the batch have been + /// successfully written to the backend as an atomic unit. + /// Note that it won't resolve while you're offline. + @override + Future commit({ + @required PlatformWriteBatchHandle handle, + }) async { + return channel.invokeMethod( + 'WriteBatch#commit', {'handle': handle.writeBatchId}); + } + + /// Deletes the document referred to by the provided [handle] and [path]. + @override + Future delete( + String app, { + @required PlatformWriteBatchHandle handle, + @required String path, + }) async { + return channel.invokeMethod( + 'WriteBatch#delete', + { + 'app': app, + 'handle': handle.writeBatchId, + 'path': path, + }, + ); + } + + /// Writes to the document referred to by the provided [handle] and [path]. + /// If the document does not exist yet, it will be created. + /// If you pass [options], the provided data can be merged into the existing document. + @override + Future set( + String app, { + @required PlatformWriteBatchHandle handle, + @required String path, + Map data, + PlatformSetOptions options, + }) async { + return channel.invokeMethod( + 'WriteBatch#setData', + { + 'app': app, + 'handle': handle.writeBatchId, + 'path': path, + 'data': data, + 'options': options.asMap(), + }, + ); + } + + /// Updates [data] in the document referred to by the provided [handle] and [path]. + /// The update will fail if applied to a document that does not exist. + @override + Future update( + String app, { + @required PlatformWriteBatchHandle handle, + @required String path, + Map data, + }) async { + return channel.invokeMethod( + 'WriteBatch#updateData', + { + 'app': app, + 'handle': handle.writeBatchId, + 'path': path, + 'data': data, + }, + ); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/multi_method_channel.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/multi_method_channel.dart new file mode 100644 index 000000000000..09c8a3480fb1 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/implementations/multi_method_channel.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +typedef _MultiMethodChannelHandler(MethodCall call); + +class MultiMethodChannel extends MethodChannel { + MultiMethodChannel(name, [codec = const StandardMethodCodec(), BinaryMessenger binaryMessenger ]) + : super(name, codec, binaryMessenger) { + // Register this as handler + super.setMethodCallHandler(_methodCallHandler); + } + + Map _multiMethodChannelHandlers = {}; + + Future _methodCallHandler(MethodCall call) { + _MultiMethodChannelHandler handler = _multiMethodChannelHandlers[call.method]; + return (handler != null) ? handler(call) : null; + } + + void addMethodCallHandler(String method, _MultiMethodChannelHandler handler) { + assert(!_multiMethodChannelHandlers.containsKey(method), 'A handler for method $method has already been registered!'); + _multiMethodChannelHandlers[method] = handler; + } + + void removeMethodCallHandler(String method) { + _multiMethodChannelHandlers.remove(method); + } + + @override + @Deprecated('MultiMethodChannel doesn\'t support setMethodCallHandler, and will throw at runtime. Use addMethodCallHandler (or a base MethodChannel instead).') + void setMethodCallHandler(Future handler(MethodCall call)) { + throw UnsupportedError('MultiMethodChannel doesn\'t support setMethodCallHandler. Use addMethodCallHandler (or use a normal MethodChannel instead).'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces.dart new file mode 100644 index 000000000000..b5869a274fbe --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces.dart @@ -0,0 +1,9 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'interfaces/document_reference_platform.dart'; +export 'interfaces/firestore_platform.dart'; +export 'interfaces/query_platform.dart'; +export 'interfaces/transaction_platform.dart'; +export 'interfaces/write_batch_platform.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/document_reference_platform.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/document_reference_platform.dart new file mode 100644 index 000000000000..4583c763e07c --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/document_reference_platform.dart @@ -0,0 +1,92 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:meta/meta.dart' show required; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../types.dart'; + +/// The Document Reference platform interface. +abstract class DocumentReferencePlatform extends PlatformInterface { + /// Constructor + DocumentReferencePlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The default instance of [DocumentReferencePlatform] to use. + /// + /// Platform-specific plugins should override this with their own class + /// that extends [DocumentReferencePlatform] when they register themselves. + static DocumentReferencePlatform get instance => _instance; + + static DocumentReferencePlatform _instance; + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(DocumentReferencePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // Actual interface + + /// A Stream of Document Snapshots. + /// The snapshot stream is never-ending. + Stream snapshots( + String app, { + @required String path, + bool includeMetadataChanges, + }) { + throw UnimplementedError( + 'DocumentReferencePlatform::snapshots() is not implemented'); + } + + /// Reads the document referred to by this [path]. + /// By default, get() attempts to provide up-to-date data when possible by waiting for + /// data from the server, but it may return cached data or fail if you are offline and + /// the server cannot be reached. + /// This behavior can be altered via the [source] parameter. + Future get( + String app, { + @required String path, + @required Source source, + }) async { + throw UnimplementedError( + 'DocumentReferencePlatform::get() is not implemented'); + } + + /// Deletes the document referred to by this [path]. + Future delete( + String app, { + @required String path, + }) async { + throw UnimplementedError( + 'DocumentReferencePlatform::delete() is not implemented'); + } + + /// Writes [data] to the document referred to by this [path]. + /// If the document does not yet exist, it will be created. + /// If you pass [options], the provided data can be merged into an existing document. + Future set( + String app, { + @required String path, + Map data, + PlatformSetOptions options, + }) async { + throw UnimplementedError( + 'DocumentReferencePlatform::set() is not implemented'); + } + + /// Updates [data] in the document referred to by this [path]. + /// The update will fail if applied to a document that does not exist. + Future update( + String app, { + @required String path, + Map data, + }) async { + throw UnimplementedError( + 'DocumentReferencePlatform::update() is not implemented'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/firestore_platform.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/firestore_platform.dart new file mode 100644 index 000000000000..d81c994535ee --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/firestore_platform.dart @@ -0,0 +1,80 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:meta/meta.dart' show required; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import './document_reference_platform.dart'; +import './query_platform.dart'; +import './transaction_platform.dart'; +import './write_batch_platform.dart'; + +/// The Firestore platform interface. +abstract class FirestorePlatform extends PlatformInterface { + /// Constructor + FirestorePlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The default instance of [FirestorePlatform] to use. + /// + /// Platform-specific plugins should override this with their own class + /// that extends [FirestorePlatform] when they register themselves. + static FirestorePlatform get instance => _instance; + + static FirestorePlatform _instance; + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(FirestorePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // Delegate platforms that need to be initialized at construction time + /// DocumentReference delegate + DocumentReferencePlatform get documentReference { + throw UnimplementedError( + 'Provide a DocumentReferencePlatform on your implementation.'); + } + + /// Query delegate + QueryPlatform get query { + throw UnimplementedError('Provide a QueryPlatform on your implementation.'); + } + + /// Transaction delegate + TransactionPlatform get transaction { + throw UnimplementedError( + 'Provide a TransactionPlatform on your implementation.'); + } + + /// WriteBatch delegate + WriteBatchPlatform get writeBatch { + throw UnimplementedError( + 'Provide a WriteBatchPlatform on your implementation.'); + } + + /// Specifies custom settings to be used to configure the Firestore instance. + /// Must be set before invoking any other methods. + Future settings( + String app, { + bool persistenceEnabled, + String host, + bool sslEnabled, + int cacheSizeBytes, + }) async { + throw UnimplementedError( + 'FirestorePlatform::settings() is not implemented'); + } + + // Actual interface + /// Attempts to enable persistent storage, if possible. + /// Must be called before any other methods (other than [settings]). + Future enablePersistence(String app, {@required bool enable}) async { + throw UnimplementedError( + 'FirestorePlatform::enablePersistence() is not implemented'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/query_platform.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/query_platform.dart new file mode 100644 index 000000000000..0346c5b6992d --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/query_platform.dart @@ -0,0 +1,60 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:meta/meta.dart' show required; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../types.dart'; + +/// The Query platform interface. +abstract class QueryPlatform extends PlatformInterface { + /// Constructor + QueryPlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The default instance of [QueryPlatform] to use. + /// + /// Platform-specific plugins should override this with their own class + /// that extends [QueryPlatform] when they register themselves. + static QueryPlatform get instance => _instance; + + static QueryPlatform _instance; + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(QueryPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // Actual interface + + /// A Stream of QuerySnapshots. + /// The snapshot stream is never-ending. + // TODO(ditman): Type the return of this Stream (PlatformQuerySnapshot?) + Stream snapshots( + String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + bool includeMetadataChanges, + }) { + throw UnimplementedError('QueryPlatform::snapshots() is not implemented'); + } + + /// What does this method correspond to in the Firebase API? + // TODO(ditman): Type this return (PlatformQueryDocument?) + Future getDocuments( + String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + Source source, + }) async { + throw UnimplementedError( + 'QueryPlatform::getDocuments() is not implemented'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/transaction_platform.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/transaction_platform.dart new file mode 100644 index 000000000000..9f5aa639bfcc --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/transaction_platform.dart @@ -0,0 +1,90 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:meta/meta.dart' show required; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../types.dart'; + +/// The Transaction platform interface. +abstract class TransactionPlatform extends PlatformInterface { + /// Constructor + TransactionPlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The default instance of [TransactionPlatform] to use. + /// + /// Platform-specific plugins should override this with their own class + /// that extends [TransactionPlatform] when they register themselves. + static TransactionPlatform get instance => _instance; + + static TransactionPlatform _instance; + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(TransactionPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // Actual interface + + /// Executes the given [updateFunction] and then attempts to commit the changes applied within the transaction. + /// If any document read within the transaction has changed, Firestore retries the updateFunction. + /// If it fails to commit after 5 attempts, the transaction fails. + /// + /// If the transaction completed successfully or was explicitly aborted (the updateFunction returned a failed promise), + /// the promise returned by the updateFunction is returned here. + /// Else, if the transaction failed, a rejected promise with the corresponding failure error will be returned. + // TODO(ditman): What's the type of this return? + Future> run( + String app, { + @required PlatformTransactionHandler updateFunction, + int transactionTimeout, + }) async { + throw UnimplementedError('TransactionPlatform::run() is not implemented'); + } + + /// Reads the transaction referenced by the provided [transactionId]. + Future get( + String app, { + @required String path, + @required int transactionId, + }) async { + throw UnimplementedError('TransactionPlatform::get() is not implemented'); + } + + /// Deletes the transaction referred to by the provided [transactionId]. + Future delete( + String app, { + @required String path, + @required int transactionId, + }) async { + throw UnimplementedError( + 'TransactionPlatform::delete() is not implemented'); + } + + /// Updates fields with [data] in the transaction referred to by the provided [transactionId]. + Future update( + String app, { + @required String path, + @required int transactionId, + Map data, + }) async { + throw UnimplementedError( + 'TransactionPlatform::update() is not implemented'); + } + + /// Writes to the transaction referred to by the provided [transactionId]. + Future set( + String app, { + @required String path, + @required int transactionId, + Map data, + }) async { + throw UnimplementedError('TransactionPlatform::set() is not implemented'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/write_batch_platform.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/write_batch_platform.dart new file mode 100644 index 000000000000..4b362d5a8fac --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/interfaces/write_batch_platform.dart @@ -0,0 +1,82 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +import 'package:meta/meta.dart' show required; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../types.dart'; + +/// The WriteBatch platform interface. +abstract class WriteBatchPlatform extends PlatformInterface { + /// Constructor + WriteBatchPlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The default instance of [WriteBatchPlatform] to use. + /// + /// Platform-specific plugins should override this with their own class + /// that extends [WriteBatchPlatform] when they register themselves. + static WriteBatchPlatform get instance => _instance; + + static WriteBatchPlatform _instance; + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(WriteBatchPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // Actual interface + /// Creates a new Write Batch + // Android returns a WriteBatch id int + Future create(String app) async { + throw UnimplementedError('WriteBatchPlatform::create() is not implemented'); + } + + /// Commits all of the writes in this write batch as a single atomic unit. + /// Returns a Promise resolved once all of the writes in the batch have been + /// successfully written to the backend as an atomic unit. + /// Note that it won't resolve while you're offline. + Future commit({ + @required PlatformWriteBatchHandle handle, + }) async { + throw UnimplementedError('WriteBatchPlatform::commit() is not implemented'); + } + + /// Deletes the document referred to by the provided [handle] and [path]. + Future delete( + String app, { + @required PlatformWriteBatchHandle handle, + @required String path, + }) async { + throw UnimplementedError('WriteBatchPlatform::delete() is not implemented'); + } + + /// Writes to the document referred to by the provided [handle] and [path]. + /// If the document does not exist yet, it will be created. + /// If you pass [options], the provided data can be merged into the existing document. + Future set( + String app, { + @required PlatformWriteBatchHandle handle, + @required String path, + Map data, + PlatformSetOptions options, + }) async { + throw UnimplementedError('WriteBatchPlatform::set() is not implemented'); + } + + /// Updates [data] in the document referred to by the provided [handle] and [path]. + /// The update will fail if applied to a document that does not exist. + Future update( + String app, { + @required PlatformWriteBatchHandle handle, + @required String path, + Map data, + }) async { + throw UnimplementedError('WriteBatchPlatform::update() is not implemented'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart new file mode 100644 index 000000000000..9dcd6b8b082c --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart @@ -0,0 +1,10 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'types/document.dart'; +export 'types/query.dart'; +export 'types/set_options.dart'; +export 'types/source.dart'; +export 'types/transaction.dart'; +export 'types/write_batch.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/document.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/document.dart new file mode 100644 index 000000000000..c43d38569474 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/document.dart @@ -0,0 +1,30 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +class PlatformDocumentSnapshot { + PlatformDocumentSnapshot({this.path, this.data, this.metadata}); + String path; + Map data; + PlatformSnapshotMetadata metadata; + + Map asMap() { + return { + 'data': data, + 'metadata': metadata.asMap(), + }; + } +} + +class PlatformSnapshotMetadata { + PlatformSnapshotMetadata({this.hasPendingWrites, this.isFromCache}); + bool hasPendingWrites; + bool isFromCache; + + Map asMap() { + return { + 'hasPendingWrites': hasPendingWrites, + 'isFromCache': isFromCache, + }; + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/query.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/query.dart new file mode 100644 index 000000000000..b4db8182971c --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/query.dart @@ -0,0 +1,11 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO: this needs to be more precise +class PlatformQuerySnapshot { + PlatformQuerySnapshot({this.data}); + Map data; + + Map asMap() => data; +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/set_options.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/set_options.dart new file mode 100644 index 000000000000..f2fab77b343d --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/set_options.dart @@ -0,0 +1,21 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// An options object that configures the behavior of set() calls in DocumentReference, WriteBatch and Transaction. +/// These calls can be configured to perform granular merges instead of overwriting the target documents in their +/// entirety by providing a SetOptions with merge: true. +class PlatformSetOptions { + /// Constructor + PlatformSetOptions({this.merge}); + + /// Changes the behavior of a set() call to only replace the values specified in its data argument. + /// Fields omitted from the set() call remain untouched. + bool merge; + + Map asMap() { + return { + 'merge': merge, + }; + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/source.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/source.dart similarity index 81% rename from packages/cloud_firestore/cloud_firestore/lib/src/source.dart rename to packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/source.dart index 0a0a43eef8f3..9542dd25fb3f 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/source.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/source.dart @@ -1,8 +1,6 @@ -// 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; +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. /// An enumeration of firestore source types. enum Source { @@ -25,7 +23,7 @@ enum Source { } /// Converts [Source] to [String] -String _getSourceString(Source source) { +String getSourceString(Source source) { assert(source != null); if (source == Source.server) { return 'server'; @@ -35,3 +33,4 @@ String _getSourceString(Source source) { } return 'default'; } + diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/transaction.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/transaction.dart new file mode 100644 index 000000000000..9deebceb7ecf --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/transaction.dart @@ -0,0 +1,10 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +class PlatformTransaction { + PlatformTransaction({this.transactionId}); + int transactionId; +} + +typedef Future PlatformTransactionHandler(PlatformTransaction transaction); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/write_batch.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/write_batch.dart new file mode 100644 index 000000000000..96728a824109 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types/write_batch.dart @@ -0,0 +1,8 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +class PlatformWriteBatchHandle { + PlatformWriteBatchHandle({this.writeBatchId}); + int writeBatchId; +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml b/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..dbffc9681458 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml @@ -0,0 +1,21 @@ +name: cloud_firestore_platform_interface +description: A common platform interface for the cloud_firestore plugin. +homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/cloud_firestore/cloud_firestore_platform_interface +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.1.2 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + plugin_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^4.1.1 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.12.13+hotfix.4 <2.0.0"