diff --git a/.github/workflows/firebase_database.yaml b/.github/workflows/firebase_database.yaml index 98336a179b49..86b00bf625a1 100644 --- a/.github/workflows/firebase_database.yaml +++ b/.github/workflows/firebase_database.yaml @@ -19,8 +19,6 @@ on: env: FLUTTERFIRE_PLUGIN_SCOPE: "*firebase_database*" FLUTTERFIRE_PLUGIN_SCOPE_EXAMPLE: "*firebase_database_example*" - # TODO(Salakar): `flutter_driver` does not yet support NNBD - FLUTTER_COMMAND_FLAGS: "--no-sound-null-safety" jobs: android: @@ -34,9 +32,13 @@ jobs: - name: "Install Flutter" run: ./.github/workflows/scripts/install-flutter.sh stable - name: "Install Tools" - run: ./.github/workflows/scripts/install-tools.sh + run: | + ./.github/workflows/scripts/install-tools.sh + sudo npm i -g firebase-tools - name: "Build Example" run: ./.github/workflows/scripts/build-example.sh android + - name: Start Firebase Emulator + run: cd ./.github/workflows/scripts && ./start-firebase-emulator.sh - name: "Drive Example" uses: reactivecircus/android-emulator-runner@v2 with: @@ -61,8 +63,11 @@ jobs: run: | ./.github/workflows/scripts/install-tools.sh flutter config --enable-macos-desktop + sudo npm i -g firebase-tools - name: "Build iOS Example" run: ./.github/workflows/scripts/build-example.sh ios + - name: Start Firebase Emulator + run: cd ./.github/workflows/scripts && ./start-firebase-emulator.sh - name: "Drive iOS Example" run: ./.github/workflows/scripts/drive-example.sh ios - name: "Build MacOS Example" @@ -82,6 +87,8 @@ jobs: - name: "Install Tools" run: | ./.github/workflows/scripts/install-tools.sh - flutter config --enable-web + sudo npm i -g firebase-tools + - name: Start Firebase Emulator + run: cd ./.github/workflows/scripts && ./start-firebase-emulator.sh - name: "Drive Example" run: ./.github/workflows/scripts/drive-example.sh web diff --git a/.github/workflows/scripts/database.rules.json b/.github/workflows/scripts/database.rules.json new file mode 100644 index 000000000000..411461aeaa87 --- /dev/null +++ b/.github/workflows/scripts/database.rules.json @@ -0,0 +1,33 @@ +{ + "rules": { + ".read": false, + ".write": false, + "denied_read": { + ".read": false, + ".write": false + }, + "messages": { + ".read": true, + ".write": true + }, + "counter": { + ".read": true, + ".write": true + }, + "tests": { + ".read": true, + ".write": true, + ".indexOn": [ + ".value", + "number" + ], + "ordered": { + ".read": true, + ".write": true, + ".indexOn": [ + "value" + ] + } + } + } +} diff --git a/.github/workflows/scripts/firebase.json b/.github/workflows/scripts/firebase.json index 46f71e9de3f8..8cc3a3f8c203 100644 --- a/.github/workflows/scripts/firebase.json +++ b/.github/workflows/scripts/firebase.json @@ -2,6 +2,9 @@ "firestore": { "rules": "firestore.rules" }, + "database": { + "rules": "database.rules.json" + }, "functions": { "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build", "source": "functions" @@ -16,6 +19,9 @@ "storage": { "port": "9199" }, + "database": { + "port": "9000" + }, "ui": { "enabled": true, "port": 4000 diff --git a/.github/workflows/scripts/functions/package.json b/.github/workflows/scripts/functions/package.json index 7ae1c6e585b1..54344a93b862 100644 --- a/.github/workflows/scripts/functions/package.json +++ b/.github/workflows/scripts/functions/package.json @@ -10,7 +10,7 @@ "logs": "firebase functions:log" }, "engines": { - "node": "12" + "node": "16" }, "main": "lib/index.js", "dependencies": { diff --git a/.github/workflows/scripts/start-firebase-emulator.sh b/.github/workflows/scripts/start-firebase-emulator.sh index 23daf88c6b7f..e941c4188feb 100755 --- a/.github/workflows/scripts/start-firebase-emulator.sh +++ b/.github/workflows/scripts/start-firebase-emulator.sh @@ -20,7 +20,7 @@ if [[ ! -d "functions/node_modules" ]]; then fi export STORAGE_EMULATOR_DEBUG=true -EMU_START_COMMAND="firebase emulators:start --only auth,firestore,functions,storage --project react-native-firebase-testing" +EMU_START_COMMAND="firebase emulators:start --only auth,firestore,functions,storage,database --project react-native-firebase-testing" MAX_RETRIES=3 MAX_CHECKATTEMPTS=60 @@ -53,4 +53,4 @@ while [ $RETRIES -le $MAX_RETRIES ]; do done echo "Firebase Emulator Suite did not come online after $MAX_RETRIES attempts." -exit 1 \ No newline at end of file +exit 1 diff --git a/.gitignore b/.gitignore index deefb0cd7f0b..36876c48bea1 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,5 @@ yarn-error.log* firebase-debug.log firestore-debug.log +database-debug.log ui-debug.log diff --git a/docs/database/usage.mdx b/docs/database/usage.mdx index 14a56dd01e8d..b84b09747070 100644 --- a/docs/database/usage.mdx +++ b/docs/database/usage.mdx @@ -3,4 +3,325 @@ title: Realtime Database sidebar_label: Usage --- -Realtime Database usage +To start using the Realtime Database package within your project, import it at the top of your project files: + +```dart +import 'package:firebase_database/firebase_database.dart'; +``` + +Before using Firestore, you must first have ensured you have [initialized FlutterFire](../overview.mdx#initializing-flutterfire). + +To create a new Database instance, call the [`instance`](!firebase_database.FirebaseDatabase.instance) getter on [`FirebaseDatabase`](!firebase_database.FirebaseDatabase): + +```dart +FirebaseDatabase database = FirebaseDatabase.instance; +``` + +By default, this allows you to interact with the Realtime Database using the default Firebase App used whilst installing FlutterFire on your +platform. If however you'd like to use it with a secondary Firebase App, use the [`instanceFor`](!firebase_database.FirebaseDatabase.instanceFor) method: + +```dart +FirebaseApp secondaryApp = Firebase.app('SecondaryApp'); +FirebaseDatabase database = FirebaseDatabase.instanceFor(app: secondaryApp); +``` + +## Creating a reference + +Realtime Database stores data as JSON, however enables you to access *nodes* of the data via a `DatabaseReference`. +For example, if your data is stored as the following: + +```json +{ + "users": { + "123": { + "name": "John" + } + } +} +``` + +You can create a reference to a node by providing a path: + +- `users`: Creates a reference to the entire "users" object +- `users/123`: Creates a reference to the "123" user object +- `users/123/name`: Creates a reference to a property (with the value of "John") + +To create a new `DatabaseReference`, pass the path to the `ref` method: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); +``` + +If you do not provide a path, the reference will point to the root of your database. + +The `DatabaseReference` provides some useful utilities to access sub-nodes, access the parent +(if one exists) and modify data. + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +// Access a child of the current reference +DatabaseReference child = ref.child("name"); + +print(ref.key); // "123" +print(ref.parent()); // "users" +print(ref.root()); // Database root +``` + +### Read data + +The Realtime Database allows you to read data either once, or be notified on any changes to the node +and it's children. + +To read the data once, call the `once` method on a `DatabaseReference`: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +// Get the data once +DatabaseEvent event = await ref.once(); + +// Print the data of the snapshot +print(event.snapshot.value); // { "name": "John" } +``` + +The result of a read is a `DatabaseEvent`. This contains a `DataSnapshot` of the data at the location of the reference, +along with some additional metadata such as the reference key or whether the node exists in your database. + +If you'd instead like to subscribe to realtime updates, the `onValue` method returns a `Stream`: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +// Get the Stream +Stream stream = ref.onValue; + +// Subscribe to the stream! +stream.listen((DatabaseEvent event) { + print('Event Type: ${event.type}'); // DatabaseEventType.value; + print('Snapshot: ${event.snapshot}'); // DataSnapshot +}); +``` + +Anytime data within the `users/123` node changes, the Stream will fire a new event. + +### Querying + +Without specifying any query constraints, reading data will return all data within the node. If you wish +to query your node for a subset of data, the package provides utilities to do so. For example, we could +choose to order our data by an "age" property, and limit the returns returned: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users"); + +Query query = ref.orderByChild("age").limitToFirst(10); + +DataSnapshot snapshot = await query.once(); +``` + +Alternatively we could instead return users between certain ages by using `startAt` and `endAt`: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users"); + +Query query = ref.orderByChild("age").startAt(18).endAt(30); + +DataSnapshot snapshot = await query.once(); +``` + +### Modifying data + +When modifying data we can either `set` the data (overwrite everything which exists at a node) or +`update` specific parts of a node. + +To set data, call the `set` method on a `DatabaseReference`: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +await ref.set({ + "name": "John", + "age": 18, + "address": { + "line1": "100 Mountain View" + } +}); +``` + +To update data, provide a `Map` where the keys point to specific nodes of the parent `DatabaseReference`: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +// Only update the name, leave the age and address! +await ref.update({ + "age": 19, +}); +``` + +The `update` method accepts a sub-path to nodes, allowing you to update multiple nodes on the database at +once: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users"); + +await ref.update({ + "123/age": 19, + "123/address/line1": "1 Mountain View", +}); +``` + +### Removing data + +To remove data from your database, call the `remove` method (or `set` with a `null` value): + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +await ref.remove(); +``` + +Removing a node will delete **all** data, including nested objects. + +### Running transactions + +When working with data that could be corrupted by concurrent modifications, such as incremental +counters, you can use a transaction. A `TransactionHandler` takes the current state of the data as +an argument and returns the new desired state you would like to write. If another client writes to +the location before your new value is successfully written, your update function is called again with the new +current value, and the write is retried. + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123"); + +TransactionResult result = await ref.runTransaction((Object? post) { + // Ensure a post at the ref exists. + if (post == null) { + return Transaction.abort(); + } + + Map _post = post as Map; + _post['likes'] = (_post['likes'] ?? 0) + 1; + + // Return the new data. + return Transaction.success(_post); +}); +``` + +By default, events are raised each time the transaction update function runs. So if it is run +multiple times, you may see intermediate states. You can set `applyLocally` to false to suppress these +intermediate states and instead wait until the transaction has completed before events are raised: + +```dart +await ref.runTransaction((Object? post, applyLocally: false) { + // ... +}); +``` + +The result of a transaction is a `TransactionResult`, which contains information such as whether +the transaction was committed, and the new snapshot: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123"); + +TransactionResult result = await ref.runTransaction((Object? post) { + // ... +}); + +print('Committed? ${result.committed}'); // true / false +print('Snapshot? ${result.snapshot}'); // DataSnapshot +``` + +#### Aborting a transaction + +If you wish to safely abort a transaction from executing, you can throw a `AbortTransactionException`: + +```dart +TransactionResult result = await ref.runTransaction((Object? user) { + if (user !== null) { + return Transaction.abort(); + } + + // ... +}); + +print(result.committed); // false +``` + +### Atomic server-side operations + +Alternatively we're able to execute atomic operations on the server to ensure there is no chance of conflicts +or out-of-sync updates. + +For example, we can increment an age and set a timestamp on the server, rather than the client by using the +`ServerValue` class: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); + +await ref.update({ + // Increment the age on the server + "age": ServerValue.increment(1), + // Add a server generated timestamp + "createdAt": ServerValue.timestamp, +}); +``` + +## Offline Capabilities + +Firebase Database clients provide simple primitives that you can use to write to the database when +a client disconnects from the Firebase Database servers. These updates occur whether the client +disconnects cleanly or not, so you can rely on them to clean up data even if a connection is +dropped or a client crashes. All write operations, including setting, updating, and removing, can +be performed upon a disconnection. + +First, create a new `OnDisconnect` instance on a `DatabaseReference`: + +```dart +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123"); +OnDisconnect onDisconnect = ref.onDisconnect(); +``` + +Next apply any operations which should trigger when the user disconnects. For example, we can easily +create a presence system by storing a boolean value on a user if they are online, and remove it +if they go offline: + +```dart +// Somewhere in your application, trigger that the user is online: +DatabaseReference ref = FirebaseDatabase.instance.ref("users/123/status"); +await ref.set(true); +// And after; +// Create an OnDisconnect instance for your ref and set to false, +// the Firebase backend will then only set the value on your ref to false +// when your client goes offline. +OnDisconnect onDisconnect = ref.onDisconnect(); +await onDisconnect.set(false); +``` + +The `OnDisconnect` instance supports `set`, `setWithPriority`, `remove` and `update` operations. + +You can remove any committed operations at anytime by calling the `cancel` method: + +```dart +// Cancel any previously committed operations +OnDisconnect onDisconnect = ref.onDisconnect(); +await onDisconnect.cancel(); +``` + +## Emulator Usage + +If you are using the local [Firestore emulators](https://firebase.google.com/docs/rules/emulator-setup), +then it is possible to connect to this using the `useDatabaseEmulator` method. Ensure you pass the correct port on which the Firebase emulator is running on. + +Ensure you have enabled network connections to the emulators in your apps following the emulator usage instructions in the general FlutterFire installation notes for each operating system. + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + + // Ideal time to initialize + FirebaseDatabase.instance.useDatabaseEmulator('localhost', 9000); + //... +} +``` diff --git a/melos.yaml b/melos.yaml index 9773e2a71f6d..a82942df5c2b 100644 --- a/melos.yaml +++ b/melos.yaml @@ -15,7 +15,7 @@ ignore: scripts: lint:all: run: melos run analyze && melos run format - description: Run all static analysis checks + description: Run all static analysis checks. analyze: run: | @@ -42,8 +42,11 @@ scripts: build:all: run: | - melos run build:example_ios_pub --no-select && melos run build:example_android_pub --no-select && melos run build:example_macos --no-select && melos bootstrap - description: Build all example apps + melos run build:example_ios_pub --no-select && \ + melos run build:example_android_pub --no-select && \ + melos run build:example_macos --no-select && \ + melos bootstrap + description: Build all example apps. build:example_android: run: | @@ -97,16 +100,21 @@ scripts: "flutter build macos --no-pub" description: | Build a specific example app for macOS. - - Requires `flutter channel stable`. - - Requires `flutter config --enable-macos-desktop` set. select-package: dir-exists: - macos scope: "*example*" test:all: - run: melos run test --no-select && melos run test:mobile_e2e --no-select - description: Run all tests available on stable channel + run: | + melos run test --no-select && \ + melos run test:web --no-select && \ + melos run test:web_e2e --no-select && \ + melos run test:mobile_e2e --no-select + description: | + Run all tests available. + - Requires chromedriver running on port 4444. + - Requires an Android emulator or iOS simulator to be already running. test: run: | @@ -136,7 +144,7 @@ scripts: "flutter drive --no-pub --target=./test_driver/MELOS_PARENT_PACKAGE_NAME_e2e.dart" description: | Run all Android or iOS test driver e2e tests in a specific example app. - - Requires an Android emulator or iOS simulator. + - Requires an Android emulator or iOS simulator to be already running. select-package: dir-exists: - test_driver @@ -151,8 +159,6 @@ scripts: "flutter drive --device-id=web-server --no-pub --target=./test_driver/MELOS_PARENT_PACKAGE_NAME_e2e.dart" description: | Run all Web test driver e2e tests in a specific example app. - - Requires `flutter channel master` (or beta). - - Requires `flutter config --enable-web` set. - Requires chromedriver running on port 4444. select-package: dir-exists: @@ -166,26 +172,30 @@ scripts: "flutter drive -d macos --no-pub --target=./test_driver/MELOS_PARENT_PACKAGE_NAME_e2e.dart" description: | Run all MacOS test driver e2e tests in a specific example app. - - Requires `flutter channel master`. - - Requires `flutter config --enable-macos-desktop` set. select-package: dir-exists: - macos - test_driver scope: "*example*" - # Clean things very deeply, can be used to establish "pristine checkout" status - clean:deep: > - git clean -x -d -f -q + clean:deep: + run: git clean -x -d -f -q + description: Clean things very deeply, can be used to establish "pristine checkout" status. - # Additional cleanup lifecycle script, executed when `melos clean` is ran. + qualitycheck: + run: | + melos run clean:deep && \ + melos clean && \ + melos bootstrap && \ + melos run lint:all && \ + melos run build:all && \ + melos run test:all + description: Run all targets generally expected in CI for a full local quality check. + + # Additional cleanup lifecycle script, executed when `melos clean` is run. postclean: > melos exec -c 6 -- "flutter clean" - # Run all targets generally expected in CI for a full local quality check - qualitycheck: > - melos run clean:deep && melos clean && melos bootstrap && melos run lint:all && melos run build:all && melos run test:all - dev_dependencies: pedantic: 1.8.0 diff --git a/packages/firebase_database/firebase_database/CHANGELOG.md b/packages/firebase_database/firebase_database/CHANGELOG.md index 09e7c3bef4da..0b71959d50df 100644 --- a/packages/firebase_database/firebase_database/CHANGELOG.md +++ b/packages/firebase_database/firebase_database/CHANGELOG.md @@ -1,3 +1,69 @@ +## [UNRELEASED] + +Realtime Database has been fully reworked to bring the plugin inline with the federated plugin +setup, a more familiar API, better documentation and many more unit and end-to-end tests. + +- General + - Fixed an issue where providing a `Map` with `int` keys would crash. + +- `FirebaseDatabase` + - **DEPRECATED**: `FirebaseDatabase()` has now been deprecated in favor of `FirebaseDatabase.instanceFor()`. + - **DEPRECATED**: `reference()` has now been deprecated in favor of `ref()`. + - **NEW**: Added support for `ref()`, which allows you to provide an optional path to any database node rather than calling `child()`. + - **NEW**: Add emulator support via `useDatabaseEmulator()`. + - **NEW**: Add support for `refFromURL()`. + - **BREAKING**: `setPersistenceEnabled()` is now synchronous. + - **BREAKING**: `setPersistenceCacheSizeBytes()` is now synchronous. + - **BREAKING**: `setLoggingEnabled()` is now synchronous. + +- `DatabaseReference` + - **BREAKING**: `parent` is now a getter (inline with the JavaScript API). + - **BREAKING**: `root` is now a getter (inline with the JavaScript API). + - **BREAKING**: `set()` now accepts an `Object?` value (rather than `dynamic`) and no longer accepts a priority. + - **NEW**: Added support for `setWithPriority()`. + - **NEW**: Added support for locally applying transaction results via the `applyLocally` property on `runTransaction`. + +- `Query` + - **NEW**: `once()` now accepts an optional `DatabaseEventType` (rather than just subscribing to the value). + - **BREAKING**: `limitToFirst()` now asserts the value is positive. + - **BREAKING**: `limitToLast()` now asserts the value is positive. + +- `OnDisconnect` + - **BREAKING**: `set()` now accepts an `Object?` value (rather than `dynamic`) and no longer accepts a priority. + - **NEW**: Added support for `setWithPriority()`. + +- `Event` + - **BREAKING**: The `Event` class returned from database queries has been renamed to `DatabaseEvent`. + +- **NEW**: `DatabaseEvent` (old `Event`) + - The `DatabaseEventType` is now returned on the event. + - The `previousChildKey` is now returned on the event (previously called `previousSiblingKey`). + +- **NEW**: `DatabaseEventType` + - A `DatabaseEventType` is now returned from a `DatabaseEvent`. + +- `DataSnapshot` + - **NEW**: Added support for accessing the priority via the `.priority` getter. + - **NEW**: Added support for determining whether the snapshot has a child via `hasChild()`. + - **NEW**: Added support for accessing a snapshot child node via `child()`. + - **NEW**: Added support for iterating the child nodes of the snapshot via the `.children` getter. + - **BREAKING** `snapshot.value` are no longer pre-sorted when using order queries, use `.children` + if you need to itterate over your value keys in order. + +- `TransactionResult` + - **BREAKING**: The result of a transaction no longer returns a `DatabaseError`, instead handle errors of a transaction via a `Future` completion error. + +- **NEW**: `Transaction` + - **NEW**: Added `Transaction.success(value)` return this from inside your `TransactionHandler` to indicate a successful execution. + - **NEW**: Added `Transaction.abort()` return this from inside your `TransactionHandler` to indicate that the transaction should be aborted. + +- `TransactionHandler` + - **BREAKING** Transaction handlers must now always return an instance of `Transaction` either via `Transaction.success()` or `Transaction.abort()`. + +- `DatabaseError` + - **BREAKING**: The `DatabaseError` class has been removed. Errors are now returned as a `FirebaseException` inline with the other plugins. + + ## 8.2.0 - **FEAT**: automatically inject Firebase JS SDKs (#7359). diff --git a/packages/firebase_database/firebase_database/README.md b/packages/firebase_database/firebase_database/README.md index 3628d8dbecab..c24dab42f595 100755 --- a/packages/firebase_database/firebase_database/README.md +++ b/packages/firebase_database/firebase_database/README.md @@ -1,53 +1,25 @@ -# Firebase Realtime Database for Flutter +# Firebase Database Plugin for Flutter -[![pub package](https://img.shields.io/pub/v/firebase_database.svg)](https://pub.dev/packages/firebase_database) - -A Flutter plugin to use the [Firebase Realtime Database API](https://firebase.google.com/products/database/). - -For Flutter plugins for other Firebase products, see [README.md](https://github.com/FirebaseExtended/flutterfire/blob/master/README.md). - -## Usage - -To use this plugin, add `firebase_database` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages). You will also need the `firebase_core` dependency if you do not have it already. +A Flutter plugin to use the [Firebase Database API](https://firebase.google.com/docs/database/). -**Example connecting to the default database:** -```dart -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_database/firebase_database.dart'; +To learn more about Firebase Firebase Database, please visit the [Firebase website](https://firebase.google.com/products/realtime-database) -final FirebaseApp app = FirebaseApp(name: '[DEFAULT]'); -final DatabaseReference db = FirebaseDatabase(app: firebaseApp).reference(); -db.child('your_db_child').once().then((result) => print('result = $result')); -``` +[![pub package](https://img.shields.io/pub/v/firebase_database.svg)](https://pub.dev/packages/firebase_database) ## Getting Started -See the `example` directory for a complete sample app using Firebase Realtime Database. +To get started with Firebase Database for Flutter, please [see the documentation](https://firebase.flutter.dev/docs/database/overview). -You might also consider watching this getting started video. +## Usage -[![The Firebase Realtime Database and Flutter - Firecasts](https://img.youtube.com/vi/sXBJZD0fBa4/0.jpg)](https://www.youtube.com/watch?v=sXBJZD0fBa4) +To use this plugin, please visit the [Firebase Database Usage documentation](https://firebase.flutter.dev/docs/database/usage) ## Issues and feedback Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/FirebaseExtended/flutterfire/issues/new). -Plugin issues that are not specific to Flutterfire can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new). +Plugin issues that are not specific to FlutterFire can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new). To contribute a change to this plugin, please review our [contribution guide](https://github.com/FirebaseExtended/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/FirebaseExtended/flutterfire/pulls). - -### Testing -The unit test is in `test` directory which you can run using `flutter test`. -The integration test is in `example/test_driver/firebase_database_e2e.dart` which you can run on an emulator: -``` -cd example -flutter drive --target=./test_driver/firebase_database_e2e.dart -``` - -To test the web implementation, [download and run ChromeDriver](https://flutter.dev/docs/testing/integration-tests#running-in-a-browser), and then run `flutter_drive`: - -``` -flutter drive --target=./test_driver/firebase_database_e2e.dart -d web-server --release --browser-name=chrome --web-port=8080 -``` diff --git a/packages/firebase_database/firebase_database/analysis_options.yaml b/packages/firebase_database/firebase_database/analysis_options.yaml new file mode 100644 index 000000000000..835470402fcb --- /dev/null +++ b/packages/firebase_database/firebase_database/analysis_options.yaml @@ -0,0 +1,8 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# in the LICENSE file. + +include: ../../../analysis_options.yaml +linter: + rules: + public_member_api_docs: true diff --git a/packages/firebase_database/firebase_database/android/build.gradle b/packages/firebase_database/firebase_database/android/build.gradle index f5547c0f20f2..0498c7bca551 100755 --- a/packages/firebase_database/firebase_database/android/build.gradle +++ b/packages/firebase_database/firebase_database/android/build.gradle @@ -35,10 +35,10 @@ def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) { } android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { - minSdkVersion 16 + minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -48,7 +48,11 @@ android { api firebaseCoreProject implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") implementation 'com.google.firebase:firebase-database' - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:1.3.0' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/ChildEventsProxy.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/ChildEventsProxy.java new file mode 100644 index 000000000000..1c2afa60a78e --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/ChildEventsProxy.java @@ -0,0 +1,41 @@ +package io.flutter.plugins.firebase.database; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import io.flutter.plugin.common.EventChannel.EventSink; + +public class ChildEventsProxy extends EventsProxy implements ChildEventListener { + protected ChildEventsProxy(@NonNull EventSink eventSink, @NonNull String eventType) { + super(eventSink, eventType); + } + + @Override + public void onChildAdded(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) { + sendEvent(Constants.EVENT_TYPE_CHILD_ADDED, snapshot, previousChildName); + } + + @Override + public void onChildChanged(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) { + sendEvent(Constants.EVENT_TYPE_CHILD_CHANGED, snapshot, previousChildName); + } + + @Override + public void onChildRemoved(@NonNull DataSnapshot snapshot) { + sendEvent(Constants.EVENT_TYPE_CHILD_REMOVED, snapshot, null); + } + + @Override + public void onChildMoved(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) { + sendEvent(Constants.EVENT_TYPE_CHILD_MOVED, snapshot, previousChildName); + } + + @Override + public void onCancelled(@NonNull DatabaseError error) { + final FlutterFirebaseDatabaseException e = + FlutterFirebaseDatabaseException.fromDatabaseError(error); + eventSink.error(e.getCode(), e.getMessage(), e.getAdditionalData()); + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/Constants.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/Constants.java new file mode 100644 index 000000000000..e76c84ff84b4 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/Constants.java @@ -0,0 +1,54 @@ +package io.flutter.plugins.firebase.database; + +public class Constants { + public static final String APP_NAME = "appName"; + + // FirebaseDatabase instance options. + public static final String DATABASE_URL = "databaseURL"; + public static final String DATABASE_LOGGING_ENABLED = "loggingEnabled"; + public static final String DATABASE_PERSISTENCE_ENABLED = "persistenceEnabled"; + public static final String DATABASE_EMULATOR_HOST = "emulatorHost"; + public static final String DATABASE_EMULATOR_PORT = "emulatorPort"; + public static final String DATABASE_CACHE_SIZE_BYTES = "cacheSizeBytes"; + + public static final String EVENT_CHANNEL_NAME_PREFIX = "eventChannelNamePrefix"; + + public static final String PATH = "path"; + public static final String KEY = "key"; + public static final String VALUE = "value"; + public static final String PRIORITY = "priority"; + public static final String SNAPSHOT = "snapshot"; + + public static final String COMMITTED = "committed"; + + public static final String MODIFIERS = "modifiers"; + public static final String ORDER_BY = "orderBy"; + public static final String CURSOR = "cursor"; + public static final String LIMIT = "limit"; + public static final String START_AT = "startAt"; + public static final String START_AFTER = "startAfter"; + public static final String END_AT = "endAt"; + public static final String END_BEFORE = "endBefore"; + public static final String LIMIT_TO_FIRST = "limitToFirst"; + public static final String LIMIT_TO_LAST = "limitToLast"; + + public static final String EVENT_TYPE = "eventType"; + + public static final String EVENT_TYPE_CHILD_ADDED = "childAdded"; + public static final String EVENT_TYPE_CHILD_REMOVED = "childRemoved"; + public static final String EVENT_TYPE_CHILD_CHANGED = "childChanged"; + public static final String EVENT_TYPE_CHILD_MOVED = "childMoved"; + public static final String EVENT_TYPE_VALUE = "value"; + + public static final String CHILD_KEYS = "childKeys"; + public static final String PREVIOUS_CHILD_NAME = "previousChildKey"; + + public static final String METHOD_CALL_TRANSACTION_HANDLER = + "FirebaseDatabase#callTransactionHandler"; + public static final String TRANSACTION_KEY = "transactionKey"; + public static final String TRANSACTION_APPLY_LOCALLY = "transactionApplyLocally"; + + public static final String ERROR_CODE = "code"; + public static final String ERROR_MESSAGE = "message"; + public static final String ERROR_DETAILS = "details"; +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/EventStreamHandler.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/EventStreamHandler.java new file mode 100644 index 000000000000..568f13f6b134 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/EventStreamHandler.java @@ -0,0 +1,55 @@ +package io.flutter.plugins.firebase.database; + +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.Query; +import com.google.firebase.database.ValueEventListener; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.StreamHandler; +import java.util.Map; +import java.util.Objects; + +interface OnDispose { + void run(); +} + +public class EventStreamHandler implements StreamHandler { + private final Query query; + private final OnDispose onDispose; + private ValueEventListener valueEventListener; + private ChildEventListener childEventListener; + + public EventStreamHandler(Query query, OnDispose onDispose) { + this.query = query; + this.onDispose = onDispose; + } + + @SuppressWarnings("unchecked") + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + final Map args = (Map) arguments; + final String eventType = (String) Objects.requireNonNull(args.get(Constants.EVENT_TYPE)); + + if (Constants.EVENT_TYPE_VALUE.equals(eventType)) { + valueEventListener = new ValueEventsProxy(events); + query.addValueEventListener(valueEventListener); + } else { + childEventListener = new ChildEventsProxy(events, eventType); + query.addChildEventListener(childEventListener); + } + } + + @Override + public void onCancel(Object arguments) { + this.onDispose.run(); + + if (valueEventListener != null) { + query.removeEventListener(valueEventListener); + valueEventListener = null; + } + + if (childEventListener != null) { + query.removeEventListener(childEventListener); + childEventListener = null; + } + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/EventsProxy.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/EventsProxy.java new file mode 100644 index 000000000000..c92f0722e741 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/EventsProxy.java @@ -0,0 +1,43 @@ +package io.flutter.plugins.firebase.database; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import com.google.firebase.database.DataSnapshot; +import io.flutter.plugin.common.EventChannel; +import java.util.HashMap; +import java.util.Map; + +@RestrictTo(RestrictTo.Scope.LIBRARY) +public abstract class EventsProxy { + protected final EventChannel.EventSink eventSink; + private final String eventType; + + protected EventsProxy(@NonNull EventChannel.EventSink eventSink, @NonNull String eventType) { + this.eventSink = eventSink; + this.eventType = eventType; + } + + Map buildAdditionalParams( + @NonNull String eventType, @Nullable String previousChildName) { + final Map params = new HashMap<>(); + params.put(Constants.EVENT_TYPE, eventType); + + if (previousChildName != null) { + params.put(Constants.PREVIOUS_CHILD_NAME, previousChildName); + } + + return params; + } + + protected void sendEvent( + @NonNull String eventType, DataSnapshot snapshot, @Nullable String previousChildName) { + if (!this.eventType.equals(eventType)) return; + + FlutterDataSnapshotPayload payload = new FlutterDataSnapshotPayload(snapshot); + final Map additionalParams = + buildAdditionalParams(eventType, previousChildName); + + eventSink.success(payload.withAdditionalParams(additionalParams).toMap()); + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.java index 65b07d37fc8c..f77c75cda874 100644 --- a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.java +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.java @@ -4,35 +4,492 @@ package io.flutter.plugins.firebase.database; +import static io.flutter.plugins.firebase.core.FlutterFirebasePluginRegistry.registerPlugin; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseException; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.Logger; +import com.google.firebase.database.OnDisconnect; +import com.google.firebase.database.Query; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.StreamHandler; +import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.firebase.core.FlutterFirebasePlugin; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class FirebaseDatabasePlugin + implements FlutterFirebasePlugin, FlutterPlugin, MethodCallHandler { + protected static final HashMap databaseInstanceCache = new HashMap<>(); + private static final String METHOD_CHANNEL_NAME = "plugins.flutter.io/firebase_database"; + private final Map queryListenersCount = new HashMap<>(); + private final Map streamHandlers = new HashMap<>(); + private MethodChannel methodChannel; + private BinaryMessenger messenger; + + private static FirebaseDatabase getCachedFirebaseDatabaseInstanceForKey(String key) { + synchronized (databaseInstanceCache) { + return databaseInstanceCache.get(key); + } + } + + private static void setCachedFirebaseDatabaseInstanceForKey( + FirebaseDatabase database, String key) { + synchronized (databaseInstanceCache) { + FirebaseDatabase existingInstance = databaseInstanceCache.get(key); + if (existingInstance == null) { + databaseInstanceCache.put(key, database); + } + } + } + + private void initPluginInstance(BinaryMessenger messenger) { + registerPlugin(METHOD_CHANNEL_NAME, this); + this.messenger = messenger; + + methodChannel = new MethodChannel(messenger, METHOD_CHANNEL_NAME); + methodChannel.setMethodCallHandler(this); + } + + FirebaseDatabase getDatabase(Map arguments) { + String appName = (String) arguments.get(Constants.APP_NAME); + if (appName == null) appName = "[DEFAULT]"; + + String databaseURL = (String) arguments.get(Constants.DATABASE_URL); + if (databaseURL == null) databaseURL = ""; + + final String instanceKey = appName.concat(databaseURL); + + // Check for an existing pre-configured instance and return it if it exists. + final FirebaseDatabase existingInstance = getCachedFirebaseDatabaseInstanceForKey(instanceKey); + if (existingInstance != null) { + return existingInstance; + } + + final FirebaseApp app = FirebaseApp.getInstance(appName); + final FirebaseDatabase database; + if (!databaseURL.isEmpty()) { + database = FirebaseDatabase.getInstance(app, databaseURL); + } else { + database = FirebaseDatabase.getInstance(app); + } + + Boolean loggingEnabled = (Boolean) arguments.get(Constants.DATABASE_LOGGING_ENABLED); + Boolean persistenceEnabled = (Boolean) arguments.get(Constants.DATABASE_PERSISTENCE_ENABLED); + String emulatorHost = (String) arguments.get(Constants.DATABASE_EMULATOR_HOST); + Integer emulatorPort = (Integer) arguments.get(Constants.DATABASE_EMULATOR_PORT); + Object cacheSizeBytes = (Object) arguments.get(Constants.DATABASE_CACHE_SIZE_BYTES); + + try { + if (loggingEnabled != null) { + database.setLogLevel(loggingEnabled ? Logger.Level.DEBUG : Logger.Level.NONE); + } + + if (emulatorHost != null && emulatorPort != null) { + database.useEmulator(emulatorHost, emulatorPort); + } + + if (persistenceEnabled != null) { + database.setPersistenceEnabled(persistenceEnabled); + } + + if (cacheSizeBytes != null) { + if (cacheSizeBytes instanceof Long) { + database.setPersistenceCacheSizeBytes((Long) cacheSizeBytes); + } else if (cacheSizeBytes instanceof Integer) { + database.setPersistenceCacheSizeBytes(Long.valueOf((Integer) cacheSizeBytes)); + } + } + } catch (DatabaseException e) { + final String message = e.getMessage(); + if (message == null) throw e; + if (!message.contains("must be made before any other usage of FirebaseDatabase")) { + throw e; + } + } + + setCachedFirebaseDatabaseInstanceForKey(database, instanceKey); + return database; + } + + private DatabaseReference getReference(Map arguments) { + final FirebaseDatabase database = getDatabase(arguments); + final String path = (String) Objects.requireNonNull(arguments.get(Constants.PATH)); + + return database.getReference(path); + } + + @SuppressWarnings("unchecked") + private Query getQuery(Map arguments) { + DatabaseReference ref = getReference(arguments); + final List> modifiers = + (List>) Objects.requireNonNull(arguments.get(Constants.MODIFIERS)); + + return new QueryBuilder(ref, modifiers).build(); + } + + private Task goOnline(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final FirebaseDatabase database = getDatabase(arguments); + database.goOnline(); + return null; + }); + } + + private Task goOffline(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final FirebaseDatabase database = getDatabase(arguments); + database.goOffline(); + return null; + }); + } + + private Task purgeOutstandingWrites(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final FirebaseDatabase database = getDatabase(arguments); + database.purgeOutstandingWrites(); + return null; + }); + } + + private Task setValue(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + final Object value = arguments.get(Constants.VALUE); + Tasks.await(ref.setValue(value)); + return null; + }); + } + + private Task setValueWithPriority(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + final Object value = arguments.get(Constants.VALUE); + final Object priority = arguments.get(Constants.PRIORITY); + Tasks.await(ref.setValue(value, priority)); + return null; + }); + } + + private Task update(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + + @SuppressWarnings("unchecked") + final Map value = + (Map) Objects.requireNonNull(arguments.get(Constants.VALUE)); + Tasks.await(ref.updateChildren(value)); + + return null; + }); + } + + private Task setPriority(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + final Object priority = arguments.get(Constants.PRIORITY); + Tasks.await(ref.setPriority(priority)); + return null; + }); + } + + private Task> runTransaction(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + + final int transactionKey = + (int) Objects.requireNonNull(arguments.get(Constants.TRANSACTION_KEY)); + final boolean transactionApplyLocally = + (boolean) Objects.requireNonNull(arguments.get(Constants.TRANSACTION_APPLY_LOCALLY)); + + final TransactionHandler handler = new TransactionHandler(methodChannel, transactionKey); + + ref.runTransaction(handler, transactionApplyLocally); + + return Tasks.await(handler.getTask()); + }); + } + + private Task> queryGet(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final Query query = getQuery(arguments); + final DataSnapshot snapshot = Tasks.await(query.get()); + final FlutterDataSnapshotPayload payload = new FlutterDataSnapshotPayload(snapshot); + + return payload.toMap(); + }); + } + + private Task queryKeepSynced(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final Query query = getQuery(arguments); + final boolean keepSynced = + (Boolean) Objects.requireNonNull(arguments.get(Constants.VALUE)); + query.keepSynced(keepSynced); + + return null; + }); + } + + private Task observe(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final Query query = getQuery(arguments); + final String eventChannelNamePrefix = + (String) arguments.get(Constants.EVENT_CHANNEL_NAME_PREFIX); + + int newListenersCount; + synchronized (queryListenersCount) { + Integer currentListenersCount = queryListenersCount.get(eventChannelNamePrefix); + newListenersCount = currentListenersCount == null ? 1 : currentListenersCount + 1; + queryListenersCount.put(eventChannelNamePrefix, newListenersCount); + } + + final String eventChannelName = eventChannelNamePrefix + "#" + newListenersCount; + final EventChannel eventChannel = new EventChannel(messenger, eventChannelName); + final EventStreamHandler streamHandler = + new EventStreamHandler( + query, + () -> { + eventChannel.setStreamHandler(null); + synchronized (queryListenersCount) { + Integer currentListenersCount = + queryListenersCount.get(eventChannelNamePrefix); + queryListenersCount.put( + eventChannelNamePrefix, + currentListenersCount == null ? 0 : currentListenersCount - 1); + } + }); + + eventChannel.setStreamHandler(streamHandler); + streamHandlers.put(eventChannel, streamHandler); + return eventChannelName; + }); + } + + private Task setOnDisconnect(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final Object value = arguments.get(Constants.VALUE); + final OnDisconnect onDisconnect = getReference(arguments).onDisconnect(); + Tasks.await(onDisconnect.setValue(value)); + return null; + }); + } + + private Task setWithPriorityOnDisconnect(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final Object value = arguments.get(Constants.VALUE); + final Object priority = arguments.get(Constants.PRIORITY); + final OnDisconnect onDisconnect = getReference(arguments).onDisconnect(); -/** FirebaseDatabasePlugin */ -public class FirebaseDatabasePlugin implements FlutterPlugin { + Task onDisconnectTask; + if (priority instanceof Double) { + onDisconnectTask = onDisconnect.setValue(value, ((Number) priority).doubleValue()); + } else if (priority instanceof String) { + onDisconnectTask = onDisconnect.setValue(value, (String) priority); + } else if (priority == null) { + onDisconnectTask = onDisconnect.setValue(value, (String) null); + } else { + throw new Exception("Invalid priority value for OnDisconnect.setWithPriority"); + } - private MethodChannel channel; - private MethodCallHandlerImpl methodCallHandler; + Tasks.await(onDisconnectTask); + return null; + }); + } + + private Task updateOnDisconnect(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + + @SuppressWarnings("unchecked") + final Map value = + (Map) Objects.requireNonNull(arguments.get(Constants.VALUE)); - private void setupMethodChannel(BinaryMessenger messenger) { - channel = new MethodChannel(messenger, "plugins.flutter.io/firebase_database"); - methodCallHandler = new MethodCallHandlerImpl(channel); - channel.setMethodCallHandler(methodCallHandler); + final Task task = ref.onDisconnect().updateChildren(value); + Tasks.await(task); + return null; + }); + } + + private Task cancelOnDisconnect(Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + final DatabaseReference ref = getReference(arguments); + Tasks.await(ref.onDisconnect().cancel()); + return null; + }); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + final Task methodCallTask; + final Map arguments = call.arguments(); + + switch (call.method) { + case "FirebaseDatabase#goOnline": + methodCallTask = goOnline(arguments); + break; + case "FirebaseDatabase#goOffline": + methodCallTask = goOffline(arguments); + break; + case "FirebaseDatabase#purgeOutstandingWrites": + methodCallTask = purgeOutstandingWrites(arguments); + break; + case "DatabaseReference#set": + methodCallTask = setValue(arguments); + break; + case "DatabaseReference#setWithPriority": + methodCallTask = setValueWithPriority(arguments); + break; + case "DatabaseReference#update": + methodCallTask = update(arguments); + break; + case "DatabaseReference#setPriority": + methodCallTask = setPriority(arguments); + break; + case "DatabaseReference#runTransaction": + methodCallTask = runTransaction(arguments); + break; + case "OnDisconnect#set": + methodCallTask = setOnDisconnect(arguments); + break; + case "OnDisconnect#setWithPriority": + methodCallTask = setWithPriorityOnDisconnect(arguments); + break; + case "OnDisconnect#update": + methodCallTask = updateOnDisconnect(arguments); + break; + case "OnDisconnect#cancel": + methodCallTask = cancelOnDisconnect(arguments); + break; + case "Query#get": + methodCallTask = queryGet(arguments); + break; + case "Query#keepSynced": + methodCallTask = queryKeepSynced(arguments); + break; + case "Query#observe": + methodCallTask = observe(arguments); + break; + default: + result.notImplemented(); + return; + } + + methodCallTask.addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + final Object r = task.getResult(); + result.success(r); + } else { + Exception exception = task.getException(); + + FlutterFirebaseDatabaseException e; + + if (exception instanceof FlutterFirebaseDatabaseException) { + e = (FlutterFirebaseDatabaseException) exception; + } else if (exception instanceof DatabaseException) { + e = + FlutterFirebaseDatabaseException.fromDatabaseException( + (DatabaseException) exception); + } else { + Log.e( + "firebase_database", + "An unknown error occurred handling native method call " + call.method, + exception); + e = FlutterFirebaseDatabaseException.fromException(exception); + } + + result.error(e.getCode(), e.getMessage(), e.getAdditionalData()); + } + }); } @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - setupMethodChannel(binding.getBinaryMessenger()); + initPluginInstance(binding.getBinaryMessenger()); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + methodChannel.setMethodCallHandler(null); cleanup(); } + @Override + public Task> getPluginConstantsForFirebaseApp(FirebaseApp firebaseApp) { + return Tasks.call(cachedThreadPool, HashMap::new); + } + + @Override + public Task didReinitializeFirebaseCore() { + return Tasks.call( + cachedThreadPool, + () -> { + cleanup(); + return null; + }); + } + private void cleanup() { - methodCallHandler.cleanup(); - methodCallHandler = null; - channel.setMethodCallHandler(null); + removeEventStreamHandlers(); + synchronized (queryListenersCount) { + queryListenersCount.clear(); + } + databaseInstanceCache.clear(); + } + + private void removeEventStreamHandlers() { + for (EventChannel eventChannel : streamHandlers.keySet()) { + StreamHandler streamHandler = streamHandlers.get(eventChannel); + if (streamHandler != null) { + streamHandler.onCancel(null); + eventChannel.setStreamHandler(null); + } + } + streamHandlers.clear(); } } diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FlutterDataSnapshotPayload.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FlutterDataSnapshotPayload.java new file mode 100644 index 000000000000..69563ef59c94 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FlutterDataSnapshotPayload.java @@ -0,0 +1,47 @@ +package io.flutter.plugins.firebase.database; + +import com.google.firebase.database.DataSnapshot; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class FlutterDataSnapshotPayload { + private Map payloadMap = new HashMap<>(); + + public FlutterDataSnapshotPayload(DataSnapshot snapshot) { + Map snapshotMap = new HashMap<>(); + + snapshotMap.put(Constants.KEY, snapshot.getKey()); + snapshotMap.put(Constants.VALUE, snapshot.getValue()); + snapshotMap.put(Constants.PRIORITY, snapshot.getPriority()); + + final int childrenCount = (int) snapshot.getChildrenCount(); + if (childrenCount == 0) { + snapshotMap.put(Constants.CHILD_KEYS, new ArrayList<>()); + } else { + final String[] childKeys = new String[childrenCount]; + int i = 0; + final Iterable children = snapshot.getChildren(); + for (DataSnapshot child : children) { + childKeys[i] = child.getKey(); + i++; + } + snapshotMap.put(Constants.CHILD_KEYS, Arrays.asList(childKeys)); + } + + payloadMap.put(Constants.SNAPSHOT, snapshotMap); + } + + FlutterDataSnapshotPayload withAdditionalParams(Map params) { + final Map prevPayloadMap = payloadMap; + payloadMap = new HashMap<>(); + payloadMap.putAll(prevPayloadMap); + payloadMap.putAll(params); + return this; + } + + Map toMap() { + return payloadMap; + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FlutterFirebaseDatabaseException.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FlutterFirebaseDatabaseException.java new file mode 100644 index 000000000000..9bf205736b06 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/FlutterFirebaseDatabaseException.java @@ -0,0 +1,146 @@ +package io.flutter.plugins.firebase.database; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseException; +import java.util.HashMap; +import java.util.Map; + +public class FlutterFirebaseDatabaseException extends Exception { + public static final String UNKNOWN_ERROR_CODE = "unknown"; + public static final String UNKNOWN_ERROR_MESSAGE = "An unknown error occurred"; + private static final String MODULE = "firebase_database"; + private final String code; + private final String message; + private final Map additionalData; + + public FlutterFirebaseDatabaseException( + @NonNull String code, @NonNull String message, @Nullable Map additionalData) { + this.code = code; + this.message = message; + + if (additionalData != null) { + this.additionalData = additionalData; + } else { + this.additionalData = new HashMap<>(); + } + + this.additionalData.put(Constants.ERROR_CODE, code); + this.additionalData.put(Constants.ERROR_MESSAGE, message); + } + + static FlutterFirebaseDatabaseException fromDatabaseError(DatabaseError e) { + final int errorCode = e.getCode(); + + String code = UNKNOWN_ERROR_CODE; + String message = UNKNOWN_ERROR_MESSAGE; + + switch (errorCode) { + case DatabaseError.DATA_STALE: + code = "data-stale"; + message = "The transaction needs to be run again with current data."; + break; + case DatabaseError.OPERATION_FAILED: + code = "failure"; + message = "The server indicated that this operation failed."; + break; + case DatabaseError.PERMISSION_DENIED: + code = "permission-denied"; + message = "Client doesn't have permission to access the desired data."; + break; + case DatabaseError.DISCONNECTED: + code = "disconnected"; + message = "The operation had to be aborted due to a network disconnect."; + break; + case DatabaseError.EXPIRED_TOKEN: + code = "expired-token"; + message = "The supplied auth token has expired."; + break; + case DatabaseError.INVALID_TOKEN: + code = "invalid-token"; + message = "The supplied auth token was invalid."; + break; + case DatabaseError.MAX_RETRIES: + code = "max-retries"; + message = "The transaction had too many retries."; + break; + case DatabaseError.OVERRIDDEN_BY_SET: + code = "overridden-by-set"; + message = "The transaction was overridden by a subsequent set."; + break; + case DatabaseError.UNAVAILABLE: + code = "unavailable"; + message = "The service is unavailable."; + break; + case DatabaseError.NETWORK_ERROR: + code = "network-error"; + message = "The operation could not be performed due to a network error."; + break; + case DatabaseError.WRITE_CANCELED: + code = "write-cancelled"; + message = "The write was canceled by the user."; + break; + } + + if (code.equals(UNKNOWN_ERROR_CODE)) { + return unknown(e.getMessage()); + } + + final Map additionalData = new HashMap<>(); + final String errorDetails = e.getDetails(); + additionalData.put(Constants.ERROR_DETAILS, errorDetails); + return new FlutterFirebaseDatabaseException(code, message, additionalData); + } + + static FlutterFirebaseDatabaseException fromDatabaseException(DatabaseException e) { + final DatabaseError error = DatabaseError.fromException(e); + return fromDatabaseError(error); + } + + static FlutterFirebaseDatabaseException fromException(@Nullable Exception e) { + if (e == null) return unknown(); + return unknown(e.getMessage()); + } + + static FlutterFirebaseDatabaseException unknown() { + return unknown(null); + } + + static FlutterFirebaseDatabaseException unknown(@Nullable String errorMessage) { + final Map details = new HashMap<>(); + String code = UNKNOWN_ERROR_CODE; + + String message = errorMessage; + + if (errorMessage == null) { + message = UNKNOWN_ERROR_MESSAGE; + } + + if (message.contains("Index not defined, add \".indexOn\"")) { + // No known error code for this in DatabaseError, so we manually have to + // detect it. + code = "index-not-defined"; + message = message.replaceFirst("java.lang.Exception: ", ""); + } else if (message.contains("Permission denied")) { + // Permission denied when using Firebase emulator does not correctly come + // through as a DatabaseError. + code = "permission-denied"; + message = "Client doesn't have permission to access the desired data."; + } + + return new FlutterFirebaseDatabaseException(code, message, details); + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public Map getAdditionalData() { + return additionalData; + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/MethodCallHandlerImpl.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/MethodCallHandlerImpl.java deleted file mode 100644 index 44332318b294..000000000000 --- a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/MethodCallHandlerImpl.java +++ /dev/null @@ -1,633 +0,0 @@ -// 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. - -package io.flutter.plugins.firebase.database; - -import android.os.Handler; -import android.util.Log; -import android.util.SparseArray; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.database.ChildEventListener; -import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.DatabaseException; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.Logger; -import com.google.firebase.database.MutableData; -import com.google.firebase.database.Query; -import com.google.firebase.database.Transaction; -import com.google.firebase.database.ValueEventListener; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - - private static final String TAG = "MethodCallHandlerImpl"; - - private MethodChannel channel; - - private final Handler handler = new Handler(); - private static final String EVENT_TYPE_CHILD_ADDED = "EventType.childAdded"; - private static final String EVENT_TYPE_CHILD_REMOVED = "EventType.childRemoved"; - private static final String EVENT_TYPE_CHILD_CHANGED = "EventType.childChanged"; - private static final String EVENT_TYPE_CHILD_MOVED = "EventType.childMoved"; - private static final String EVENT_TYPE_VALUE = "EventType.value"; - - // Handles are ints used as indexes into the sparse array of active observers - private int nextHandle = 0; - private final SparseArray observers = new SparseArray<>(); - - MethodCallHandlerImpl(MethodChannel channel) { - this.channel = channel; - } - - void cleanup() { - final int size = observers.size(); - for (int i = 0; i < size; i++) { - final EventObserver observer = observers.valueAt(i); - final Query query = observer.query; - if (observer.requestedEventType.equals(EVENT_TYPE_VALUE)) { - query.removeEventListener((ValueEventListener) observer); - } else { - query.removeEventListener((ChildEventListener) observer); - } - } - - observers.clear(); - } - - private DatabaseReference getReference(FirebaseDatabase database, Map arguments) { - String path = (String) arguments.get("path"); - DatabaseReference reference = database.getReference(); - if (path != null) { - reference = reference.child(path); - } - return reference; - } - - private Query getQuery(FirebaseDatabase database, Map arguments) { - Query query = getReference(database, arguments); - @SuppressWarnings("unchecked") - Map parameters = (Map) arguments.get("parameters"); - if (parameters == null) { - return query; - } - Object orderBy = parameters.get("orderBy"); - if ("child".equals(orderBy)) { - query = query.orderByChild((String) parameters.get("orderByChildKey")); - } else if ("key".equals(orderBy)) { - query = query.orderByKey(); - } else if ("value".equals(orderBy)) { - query = query.orderByValue(); - } else if ("priority".equals(orderBy)) { - query = query.orderByPriority(); - } - if (parameters.containsKey("startAt")) { - Object startAt = parameters.get("startAt"); - if (parameters.containsKey("startAtKey")) { - String startAtKey = (String) parameters.get("startAtKey"); - if (startAt instanceof Boolean) { - query = query.startAt((Boolean) startAt, startAtKey); - } else if (startAt instanceof Number) { - query = query.startAt(((Number) startAt).doubleValue(), startAtKey); - } else { - query = query.startAt((String) startAt, startAtKey); - } - } else { - if (startAt instanceof Boolean) { - query = query.startAt((Boolean) startAt); - } else if (startAt instanceof Number) { - query = query.startAt(((Number) startAt).doubleValue()); - } else { - query = query.startAt((String) startAt); - } - } - } - if (parameters.containsKey("endAt")) { - Object endAt = parameters.get("endAt"); - if (parameters.containsKey("endAtKey")) { - String endAtKey = (String) parameters.get("endAtKey"); - if (endAt instanceof Boolean) { - query = query.endAt((Boolean) endAt, endAtKey); - } else if (endAt instanceof Number) { - query = query.endAt(((Number) endAt).doubleValue(), endAtKey); - } else { - query = query.endAt((String) endAt, endAtKey); - } - } else { - if (endAt instanceof Boolean) { - query = query.endAt((Boolean) endAt); - } else if (endAt instanceof Number) { - query = query.endAt(((Number) endAt).doubleValue()); - } else { - query = query.endAt((String) endAt); - } - } - } - if (parameters.containsKey("equalTo")) { - Object equalTo = parameters.get("equalTo"); - if (parameters.containsKey("equalToKey")) { - String equalToKey = (String) parameters.get("equalToKey"); - if (equalTo instanceof Boolean) { - query = query.equalTo((Boolean) equalTo, equalToKey); - } else if (equalTo instanceof Number) { - query = query.equalTo(((Number) equalTo).doubleValue(), equalToKey); - } else { - query = query.equalTo((String) equalTo, equalToKey); - } - } else { - if (equalTo instanceof Boolean) { - query = query.equalTo((Boolean) equalTo); - } else if (equalTo instanceof Number) { - query = query.equalTo(((Number) equalTo).doubleValue()); - } else { - query = query.equalTo((String) equalTo); - } - } - } - if (parameters.containsKey("limitToFirst")) { - query = query.limitToFirst((int) parameters.get("limitToFirst")); - } - if (parameters.containsKey("limitToLast")) { - query = query.limitToLast((int) parameters.get("limitToLast")); - } - return query; - } - - private class DefaultCompletionListener implements DatabaseReference.CompletionListener { - - private final MethodChannel.Result result; - - DefaultCompletionListener(MethodChannel.Result result) { - this.result = result; - } - - @Override - public void onComplete(@Nullable DatabaseError error, @NonNull DatabaseReference ref) { - if (error != null) { - result.error(String.valueOf(error.getCode()), error.getMessage(), error.getDetails()); - } else { - result.success(null); - } - } - } - - private static class EventObserver implements ChildEventListener, ValueEventListener { - - private MethodChannel channel; - private String requestedEventType; - private Query query; - private int handle; - - EventObserver(MethodChannel channel, String requestedEventType, Query query, int handle) { - this.channel = channel; - this.requestedEventType = requestedEventType; - this.query = query; - this.handle = handle; - } - - private void sendEvent( - String eventType, @NonNull DataSnapshot snapshot, String previousChildName) { - if (eventType.equals(requestedEventType)) { - Map arguments = new HashMap<>(); - Map snapshotMap = new HashMap<>(); - snapshotMap.put("key", snapshot.getKey()); - snapshotMap.put("value", snapshot.getValue()); - arguments.put("handle", handle); - arguments.put("snapshot", snapshotMap); - arguments.put("previousSiblingKey", previousChildName); - arguments.put("childKeys", getChildKeys(snapshot)); - channel.invokeMethod("Event", arguments); - } - } - - private ArrayList getChildKeys(DataSnapshot snapshot) { - ArrayList childKeys = new ArrayList<>(); - - if (snapshot.hasChildren()) { - for (DataSnapshot child : snapshot.getChildren()) { - childKeys.add(child.getKey()); - } - } - - return childKeys; - } - - @Override - public void onCancelled(@NonNull DatabaseError error) { - Map arguments = new HashMap<>(); - arguments.put("handle", handle); - arguments.put("error", asMap(error)); - channel.invokeMethod("Error", arguments); - } - - @Override - public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChildName) { - sendEvent(EVENT_TYPE_CHILD_ADDED, snapshot, previousChildName); - } - - @Override - public void onChildRemoved(@NonNull DataSnapshot snapshot) { - sendEvent(EVENT_TYPE_CHILD_REMOVED, snapshot, null); - } - - @Override - public void onChildChanged(@NonNull DataSnapshot snapshot, String previousChildName) { - sendEvent(EVENT_TYPE_CHILD_CHANGED, snapshot, previousChildName); - } - - @Override - public void onChildMoved(@NonNull DataSnapshot snapshot, String previousChildName) { - sendEvent(EVENT_TYPE_CHILD_MOVED, snapshot, previousChildName); - } - - @Override - public void onDataChange(@NonNull DataSnapshot snapshot) { - sendEvent(EVENT_TYPE_VALUE, snapshot, null); - } - } - - @Override - public void onMethodCall(final MethodCall call, @NonNull final MethodChannel.Result result) { - final Map arguments = call.arguments(); - final FirebaseDatabase database; - String appName = call.argument("app"); - String databaseURL = call.argument("databaseURL"); - if (appName != null && databaseURL != null) { - database = FirebaseDatabase.getInstance(FirebaseApp.getInstance(appName), databaseURL); - } else if (appName != null) { - database = FirebaseDatabase.getInstance(FirebaseApp.getInstance(appName)); - } else if (databaseURL != null) { - database = FirebaseDatabase.getInstance(databaseURL); - } else { - database = FirebaseDatabase.getInstance(); - } - - switch (call.method) { - case "FirebaseDatabase#goOnline": - { - database.goOnline(); - result.success(null); - break; - } - - case "FirebaseDatabase#goOffline": - { - database.goOffline(); - result.success(null); - break; - } - - case "FirebaseDatabase#purgeOutstandingWrites": - { - database.purgeOutstandingWrites(); - result.success(null); - break; - } - - case "FirebaseDatabase#setPersistenceEnabled": - { - Boolean isEnabled = call.argument("enabled"); - try { - database.setPersistenceEnabled(isEnabled); - result.success(true); - } catch (DatabaseException e) { - // Database is already in use, e.g. after hot reload/restart. - result.success(false); - } - break; - } - - case "FirebaseDatabase#setPersistenceCacheSizeBytes": - { - Object value = call.argument("cacheSize"); - Long cacheSizeBytes = 10485760L; // 10mb default - - if (value instanceof Long) { - cacheSizeBytes = (Long) value; - } else if (value instanceof Integer) { - cacheSizeBytes = Long.valueOf((Integer) value); - } - - try { - database.setPersistenceCacheSizeBytes(cacheSizeBytes); - result.success(true); - } catch (DatabaseException e) { - // Database is already in use, e.g. after hot reload/restart. - result.success(false); - } - break; - } - - case "FirebaseDatabase#setLoggingEnabled": - { - boolean enabled = call.argument("enabled"); - - if (enabled) { - database.setLogLevel(Logger.Level.DEBUG); - } else { - database.setLogLevel(Logger.Level.INFO); - } - result.success(null); - break; - } - - case "DatabaseReference#set": - { - Object value = call.argument("value"); - Object priority = call.argument("priority"); - DatabaseReference reference = getReference(database, arguments); - if (priority != null) { - reference.setValue( - value, priority, new MethodCallHandlerImpl.DefaultCompletionListener(result)); - } else { - reference.setValue(value, new MethodCallHandlerImpl.DefaultCompletionListener(result)); - } - break; - } - - case "DatabaseReference#update": - { - Map value = call.argument("value"); - DatabaseReference reference = getReference(database, arguments); - reference.updateChildren( - value, new MethodCallHandlerImpl.DefaultCompletionListener(result)); - break; - } - - case "DatabaseReference#setPriority": - { - Object priority = call.argument("priority"); - DatabaseReference reference = getReference(database, arguments); - reference.setPriority( - priority, new MethodCallHandlerImpl.DefaultCompletionListener(result)); - break; - } - - case "DatabaseReference#runTransaction": - { - final DatabaseReference reference = getReference(database, arguments); - - // Initiate native transaction. - reference.runTransaction( - new Transaction.Handler() { - @NonNull - @Override - public Transaction.Result doTransaction(@NonNull MutableData mutableData) { - // Tasks are used to allow native execution of doTransaction to wait while - // Snapshot is - // processed by logic on the Dart side. - final TaskCompletionSource> updateMutableDataTCS = - new TaskCompletionSource<>(); - final Task> updateMutableDataTCSTask = - updateMutableDataTCS.getTask(); - - final Map doTransactionMap = new HashMap<>(); - doTransactionMap.put("transactionKey", call.argument("transactionKey")); - - final Map snapshotMap = new HashMap<>(); - snapshotMap.put("key", mutableData.getKey()); - snapshotMap.put("value", mutableData.getValue()); - doTransactionMap.put("snapshot", snapshotMap); - - // Return snapshot to Dart side for update. - handler.post( - new Runnable() { - @Override - public void run() { - channel.invokeMethod( - "DoTransaction", - doTransactionMap, - new MethodChannel.Result() { - @Override - @SuppressWarnings("unchecked") - public void success(Object result) { - updateMutableDataTCS.setResult((Map) result); - } - - @Override - public void error( - String errorCode, String errorMessage, Object errorDetails) { - String exceptionMessage = - "Error code: " - + errorCode - + "\nError message: " - + errorMessage - + "\nError details: " - + errorDetails; - updateMutableDataTCS.setException( - new Exception(exceptionMessage)); - } - - @Override - public void notImplemented() { - updateMutableDataTCS.setException( - new Exception("DoTransaction not implemented on Dart side.")); - } - }); - } - }); - - try { - // Wait for updated snapshot from the Dart side. - final Map updatedSnapshotMap = - Tasks.await( - updateMutableDataTCSTask, - (int) arguments.get("transactionTimeout"), - TimeUnit.MILLISECONDS); - // Set value of MutableData to value returned from the Dart side. - mutableData.setValue(updatedSnapshotMap.get("value")); - } catch (ExecutionException | InterruptedException | TimeoutException e) { - Log.e(TAG, "Unable to commit Snapshot update. Transaction failed.", e); - if (e instanceof TimeoutException) { - Log.e(TAG, "Transaction at " + reference.toString() + " timed out."); - } - return Transaction.abort(); - } - return Transaction.success(mutableData); - } - - @Override - public void onComplete( - DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) { - final Map completionMap = new HashMap<>(); - completionMap.put("transactionKey", call.argument("transactionKey")); - if (databaseError != null) { - completionMap.put("error", asMap(databaseError)); - } - completionMap.put("committed", committed); - if (dataSnapshot != null) { - Map snapshotMap = new HashMap<>(); - snapshotMap.put("key", dataSnapshot.getKey()); - snapshotMap.put("value", dataSnapshot.getValue()); - completionMap.put("snapshot", snapshotMap); - } - - // Invoke transaction completion on the Dart side. - handler.post( - new Runnable() { - public void run() { - result.success(completionMap); - } - }); - } - }); - break; - } - - case "OnDisconnect#set": - { - Object value = call.argument("value"); - Object priority = call.argument("priority"); - DatabaseReference reference = getReference(database, arguments); - if (priority != null) { - if (priority instanceof String) { - reference - .onDisconnect() - .setValue( - value, - (String) priority, - new MethodCallHandlerImpl.DefaultCompletionListener(result)); - } else if (priority instanceof Double) { - reference - .onDisconnect() - .setValue( - value, - (double) priority, - new MethodCallHandlerImpl.DefaultCompletionListener(result)); - } else if (priority instanceof Map) { - reference - .onDisconnect() - .setValue( - value, - (Map) priority, - new MethodCallHandlerImpl.DefaultCompletionListener(result)); - } - } else { - reference - .onDisconnect() - .setValue(value, new MethodCallHandlerImpl.DefaultCompletionListener(result)); - } - break; - } - - case "OnDisconnect#update": - { - Map value = call.argument("value"); - DatabaseReference reference = getReference(database, arguments); - reference - .onDisconnect() - .updateChildren(value, new MethodCallHandlerImpl.DefaultCompletionListener(result)); - break; - } - - case "OnDisconnect#cancel": - { - DatabaseReference reference = getReference(database, arguments); - reference - .onDisconnect() - .cancel(new MethodCallHandlerImpl.DefaultCompletionListener(result)); - break; - } - - case "Query#get": - { - DatabaseReference reference = getReference(database, arguments); - reference - .get() - .addOnCompleteListener( - new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (!task.isSuccessful()) { - Exception exception = task.getException(); - result.error("get-failed", exception.getMessage(), null); - } else { - DataSnapshot dataSnapshot = task.getResult(); - Map snapshotMap = new HashMap<>(); - snapshotMap.put("key", dataSnapshot.getKey()); - snapshotMap.put("value", dataSnapshot.getValue()); - Map resultMap = new HashMap<>(); - resultMap.put("snapshot", snapshotMap); - result.success(resultMap); - } - } - }); - break; - } - - case "Query#keepSynced": - { - Boolean value = call.argument("value"); - getQuery(database, arguments).keepSynced(value); - result.success(null); - break; - } - - case "Query#observe": - { - String eventType = call.argument("eventType"); - int handle = nextHandle++; - Query query = getQuery(database, arguments); - MethodCallHandlerImpl.EventObserver observer = - new MethodCallHandlerImpl.EventObserver(channel, eventType, query, handle); - observers.put(handle, observer); - if (EVENT_TYPE_VALUE.equals(eventType)) { - query.addValueEventListener(observer); - } else { - query.addChildEventListener(observer); - } - result.success(handle); - break; - } - - case "Query#removeObserver": - { - Integer handle = call.argument("handle"); - MethodCallHandlerImpl.EventObserver observer = observers.get(handle); - if (observer != null) { - final Query query = observer.query; - if (observer.requestedEventType.equals(EVENT_TYPE_VALUE)) { - query.removeEventListener((ValueEventListener) observer); - } else { - query.removeEventListener((ChildEventListener) observer); - } - observers.delete(handle); - result.success(null); - break; - } else { - result.error("unknown_handle", "removeObserver called on an unknown handle", null); - break; - } - } - - default: - { - result.notImplemented(); - break; - } - } - } - - private static Map asMap(DatabaseError error) { - Map map = new HashMap<>(); - map.put("code", error.getCode()); - map.put("message", error.getMessage()); - map.put("details", error.getDetails()); - return map; - } -} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/QueryBuilder.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/QueryBuilder.java new file mode 100644 index 000000000000..5f3d73c58021 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/QueryBuilder.java @@ -0,0 +1,192 @@ +package io.flutter.plugins.firebase.database; + +import androidx.annotation.NonNull; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.Query; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class QueryBuilder { + private final List> modifiers; + private Query query; + + public QueryBuilder( + @NonNull DatabaseReference ref, @NonNull List> modifiers) { + this.query = ref; + this.modifiers = modifiers; + } + + public Query build() { + if (modifiers.isEmpty()) return query; + + for (Map modifier : modifiers) { + String type = (String) Objects.requireNonNull(modifier.get("type")); + + switch (type) { + case Constants.LIMIT: + limit(modifier); + break; + case Constants.CURSOR: + cursor(modifier); + break; + case Constants.ORDER_BY: + orderBy(modifier); + break; + } + } + + return query; + } + + private void limit(Map modifier) { + String name = (String) Objects.requireNonNull(modifier.get("name")); + int value = (int) Objects.requireNonNull(modifier.get("limit")); + + if (Constants.LIMIT_TO_FIRST.equals(name)) { + query = query.limitToFirst(value); + } else if (Constants.LIMIT_TO_LAST.equals(name)) { + query = query.limitToLast(value); + } + } + + private void orderBy(Map modifier) { + String name = (String) Objects.requireNonNull(modifier.get("name")); + + switch (name) { + case "orderByKey": + query = query.orderByKey(); + break; + case "orderByValue": + query = query.orderByValue(); + break; + case "orderByPriority": + query = query.orderByPriority(); + break; + case "orderByChild": + { + String path = (String) Objects.requireNonNull(modifier.get("path")); + query = query.orderByChild(path); + } + } + } + + private void cursor(Map modifier) { + String name = (String) Objects.requireNonNull(modifier.get("name")); + + switch (name) { + case Constants.START_AT: + startAt(modifier); + break; + case Constants.START_AFTER: + startAfter(modifier); + break; + case Constants.END_AT: + endAt(modifier); + break; + case Constants.END_BEFORE: + endBefore(modifier); + break; + } + } + + private void startAt(Map modifier) { + final Object value = modifier.get("value"); + final String key = (String) modifier.get("key"); + + if (value instanceof Boolean) { + if (key == null) { + query = query.startAt((Boolean) value); + } else { + query = query.startAt((Boolean) value, key); + } + } else if (value instanceof Number) { + if (key == null) { + query = query.startAt(((Number) value).doubleValue()); + } else { + query = query.startAt(((Number) value).doubleValue(), key); + } + } else { + if (key == null) { + query = query.startAt((String) value); + } else { + query = query.startAt((String) value, key); + } + } + } + + private void startAfter(Map modifier) { + final Object value = modifier.get("value"); + final String key = (String) modifier.get("key"); + + if (value instanceof Boolean) { + if (key == null) { + query = query.startAfter((Boolean) value); + } else { + query = query.startAfter((Boolean) value, key); + } + } else if (value instanceof Number) { + if (key == null) { + query = query.startAfter(((Number) value).doubleValue()); + } else { + query = query.startAfter(((Number) value).doubleValue(), key); + } + } else { + if (key == null) { + query = query.startAfter((String) value); + } else { + query = query.startAfter((String) value, key); + } + } + } + + private void endAt(Map modifier) { + final Object value = modifier.get("value"); + final String key = (String) modifier.get("key"); + + if (value instanceof Boolean) { + if (key == null) { + query = query.endAt((Boolean) value); + } else { + query = query.endAt((Boolean) value, key); + } + } else if (value instanceof Number) { + if (key == null) { + query = query.endAt(((Number) value).doubleValue()); + } else { + query = query.endAt(((Number) value).doubleValue(), key); + } + } else { + if (key == null) { + query = query.endAt((String) value); + } else { + query = query.endAt((String) value, key); + } + } + } + + private void endBefore(Map modifier) { + final Object value = modifier.get("value"); + final String key = (String) modifier.get("key"); + + if (value instanceof Boolean) { + if (key == null) { + query = query.endBefore((Boolean) value); + } else { + query = query.endBefore((Boolean) value, key); + } + } else if (value instanceof Number) { + if (key == null) { + query = query.endBefore(((Number) value).doubleValue()); + } else { + query = query.endBefore(((Number) value).doubleValue(), key); + } + } else { + if (key == null) { + query = query.endBefore((String) value); + } else { + query = query.endBefore((String) value, key); + } + } + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/TransactionExecutor.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/TransactionExecutor.java new file mode 100644 index 000000000000..28add8f264ba --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/TransactionExecutor.java @@ -0,0 +1,68 @@ +package io.flutter.plugins.firebase.database; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class TransactionExecutor { + private final TaskCompletionSource completion; + private final MethodChannel channel; + + protected TransactionExecutor(MethodChannel channel) { + this.completion = new TaskCompletionSource<>(); + this.channel = channel; + } + + protected Object execute(final Map arguments) + throws ExecutionException, InterruptedException { + new Handler(Looper.getMainLooper()) + .post( + () -> + channel.invokeMethod( + Constants.METHOD_CALL_TRANSACTION_HANDLER, + arguments, + new MethodChannel.Result() { + @Override + public void success(@Nullable Object result) { + completion.setResult(result); + } + + @Override + @SuppressWarnings("unchecked") + public void error( + String errorCode, + @Nullable String errorMessage, + @Nullable Object errorDetails) { + String message = errorMessage; + Map additionalData = new HashMap<>(); + + if (message == null) { + message = FlutterFirebaseDatabaseException.UNKNOWN_ERROR_MESSAGE; + } + + if (errorDetails instanceof Map) { + additionalData = (Map) errorDetails; + } + + final FlutterFirebaseDatabaseException e = + new FlutterFirebaseDatabaseException( + errorCode, message, additionalData); + + completion.setException(e); + } + + @Override + public void notImplemented() { + // never called + } + })); + + return Tasks.await(completion.getTask()); + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/TransactionHandler.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/TransactionHandler.java new file mode 100644 index 000000000000..80e469b180e6 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/TransactionHandler.java @@ -0,0 +1,82 @@ +package io.flutter.plugins.firebase.database; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.MutableData; +import com.google.firebase.database.Transaction; +import com.google.firebase.database.Transaction.Handler; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class TransactionHandler implements Handler { + private final MethodChannel channel; + private final TaskCompletionSource> transactionCompletionSource; + private final int transactionKey; + + public TransactionHandler(@NonNull MethodChannel channel, int transactionKey) { + this.channel = channel; + this.transactionKey = transactionKey; + this.transactionCompletionSource = new TaskCompletionSource<>(); + } + + Task> getTask() { + return transactionCompletionSource.getTask(); + } + + @NonNull + @Override + public Transaction.Result doTransaction(@NonNull MutableData currentData) { + final Map snapshotMap = new HashMap<>(); + final Map transactionArgs = new HashMap<>(); + + snapshotMap.put(Constants.KEY, currentData.getKey()); + snapshotMap.put(Constants.VALUE, currentData.getValue()); + + transactionArgs.put(Constants.SNAPSHOT, snapshotMap); + transactionArgs.put(Constants.TRANSACTION_KEY, transactionKey); + + try { + final TransactionExecutor executor = new TransactionExecutor(channel); + final Object updatedData = executor.execute(transactionArgs); + @SuppressWarnings("unchecked") + final Map transactionHandlerResult = + (Map) Objects.requireNonNull(updatedData); + final boolean aborted = + (boolean) Objects.requireNonNull(transactionHandlerResult.get("aborted")); + final boolean exception = + (boolean) Objects.requireNonNull(transactionHandlerResult.get("exception")); + if (aborted || exception) { + return Transaction.abort(); + } else { + currentData.setValue(transactionHandlerResult.get("value")); + return Transaction.success(currentData); + } + } catch (Exception e) { + Log.e("firebase_database", "An unexpected exception occurred for a transaction.", e); + return Transaction.abort(); + } + } + + @Override + public void onComplete( + @Nullable DatabaseError error, boolean committed, @Nullable DataSnapshot currentData) { + if (error != null) { + transactionCompletionSource.setException( + FlutterFirebaseDatabaseException.fromDatabaseError(error)); + } else if (currentData != null) { + final FlutterDataSnapshotPayload payload = new FlutterDataSnapshotPayload(currentData); + + final Map additionalParams = new HashMap<>(); + additionalParams.put(Constants.COMMITTED, committed); + + transactionCompletionSource.setResult(payload.withAdditionalParams(additionalParams).toMap()); + } + } +} diff --git a/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/ValueEventsProxy.java b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/ValueEventsProxy.java new file mode 100644 index 000000000000..e087cb30eaa9 --- /dev/null +++ b/packages/firebase_database/firebase_database/android/src/main/java/io/flutter/plugins/firebase/database/ValueEventsProxy.java @@ -0,0 +1,25 @@ +package io.flutter.plugins.firebase.database; + +import androidx.annotation.NonNull; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.ValueEventListener; +import io.flutter.plugin.common.EventChannel.EventSink; + +public class ValueEventsProxy extends EventsProxy implements ValueEventListener { + protected ValueEventsProxy(@NonNull EventSink eventSink) { + super(eventSink, Constants.EVENT_TYPE_VALUE); + } + + @Override + public void onDataChange(@NonNull DataSnapshot snapshot) { + sendEvent(Constants.EVENT_TYPE_VALUE, snapshot, null); + } + + @Override + public void onCancelled(@NonNull DatabaseError error) { + final FlutterFirebaseDatabaseException e = + FlutterFirebaseDatabaseException.fromDatabaseError(error); + eventSink.error(e.getCode(), e.getMessage(), e.getAdditionalData()); + } +} diff --git a/packages/firebase_database/firebase_database/example/android/app/build.gradle b/packages/firebase_database/firebase_database/example/android/app/build.gradle index 5880242a2646..124a2aa55f60 100755 --- a/packages/firebase_database/firebase_database/example/android/app/build.gradle +++ b/packages/firebase_database/firebase_database/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 lintOptions { disable 'InvalidPackage' @@ -34,8 +34,8 @@ android { defaultConfig { applicationId 'com.invertase.testing' - minSdkVersion 16 - targetSdkVersion 29 + minSdkVersion 21 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/firebase_database/firebase_database/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_database/firebase_database/example/android/gradle/wrapper/gradle-wrapper.properties index 3c46198fce9e..297f2fec363f 100644 --- a/packages/firebase_database/firebase_database/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/firebase_database/firebase_database/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/firebase_database/firebase_database/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_database/firebase_database/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100755 --- a/packages/firebase_database/firebase_database/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/firebase_database/firebase_database/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/firebase_database/firebase_database/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_database/firebase_database/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist b/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist index adc773132365..7a426b2a8926 100755 --- a/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist +++ b/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist @@ -45,5 +45,10 @@ UIViewControllerBasedStatusBarAppearance + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/packages/firebase_database/firebase_database/example/lib/main.dart b/packages/firebase_database/firebase_database/example/lib/main.dart index 9e6869a4034e..99f05f051b85 100755 --- a/packages/firebase_database/firebase_database/example/lib/main.dart +++ b/packages/firebase_database/firebase_database/example/lib/main.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -8,26 +7,39 @@ import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:firebase_database/ui/firebase_animated_list.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb; import 'package:flutter/material.dart'; +// Change to false to use live database instance. +const useEmulator = true; +// The port we've set the Firebase Database emulator to run on via the +// `firebase.json` configuration file. +const emulatorPort = 9000; +// Android device emulators consider localhost of the host machine as 10.0.2.2 +// so let's use that if running on Android. +final emulatorHost = + (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) + ? '10.0.2.2' + : 'localhost'; + Future main() async { WidgetsFlutterBinding.ensureInitialized(); final FirebaseApp app = await Firebase.initializeApp( options: const FirebaseOptions( - apiKey: 'AIzaSyAgUhHU8wSJgO5MVNy95tMT07NEjzMOfz0', - authDomain: 'react-native-firebase-testing.firebaseapp.com', - databaseURL: 'https://react-native-firebase-testing.firebaseio.com', - projectId: 'react-native-firebase-testing', - storageBucket: 'react-native-firebase-testing.appspot.com', - messagingSenderId: '448618578101', - appId: '1:448618578101:web:772d484dc9eb15e9ac3efc', - measurementId: 'G-0N1G9FLDZE'), + apiKey: 'AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0', + appId: '1:448618578101:ios:2bc5c1fe2ec336f8ac3efc', + messagingSenderId: '448618578101', + projectId: 'react-native-firebase-testing', + databaseURL: 'https://react-native-firebase-testing.firebaseio.com', + storageBucket: 'react-native-firebase-testing.appspot.com', + ), + ); + runApp( + MaterialApp( + title: 'Flutter Database Example', + home: MyHomePage(app: app), + ), ); - runApp(MaterialApp( - title: 'Flutter Database Example', - home: MyHomePage(app: app), - )); } class MyHomePage extends StatefulWidget { @@ -43,52 +55,81 @@ class _MyHomePageState extends State { int _counter = 0; late DatabaseReference _counterRef; late DatabaseReference _messagesRef; - late StreamSubscription _counterSubscription; - late StreamSubscription _messagesSubscription; + late StreamSubscription _counterSubscription; + late StreamSubscription _messagesSubscription; bool _anchorToBottom = false; String _kTestKey = 'Hello'; String _kTestValue = 'world!'; - DatabaseError? _error; + FirebaseException? _error; + bool initialized = false; @override void initState() { + init(); super.initState(); - // Demonstrates configuring to the database using a file - _counterRef = FirebaseDatabase.instance.reference().child('counter'); - // Demonstrates configuring the database directly - final FirebaseDatabase database = FirebaseDatabase(app: widget.app); - _messagesRef = database.reference().child('messages'); - database.reference().child('counter').get().then((DataSnapshot? snapshot) { - print( - 'Connected to directly configured database and read ${snapshot!.value}'); - }); + } - database.setLoggingEnabled(true); + Future init() async { + _counterRef = FirebaseDatabase.instance.ref('counter'); + final database = FirebaseDatabase.instanceFor(app: widget.app); + _messagesRef = database.ref('messages'); + + if (useEmulator) { + database.useDatabaseEmulator(emulatorHost, emulatorPort); + } + database.setLoggingEnabled(false); if (!kIsWeb) { database.setPersistenceEnabled(true); database.setPersistenceCacheSizeBytes(10000000); - _counterRef.keepSynced(true); } - _counterSubscription = _counterRef.onValue.listen((Event event) { - setState(() { - _error = null; - _counter = event.snapshot.value ?? 0; - }); - }, onError: (Object o) { - final DatabaseError error = o as DatabaseError; - setState(() { - _error = error; - }); - }); - _messagesSubscription = - _messagesRef.limitToLast(10).onChildAdded.listen((Event event) { - print('Child added: ${event.snapshot.value}'); - }, onError: (Object o) { - final DatabaseError error = o as DatabaseError; - print('Error: ${error.code} ${error.message}'); + + if (!kIsWeb) { + await _counterRef.keepSynced(true); + } + + setState(() { + initialized = true; }); + + try { + final counterSnapshot = await _counterRef.get(); + + print( + 'Connected to directly configured database and read ' + '${counterSnapshot.value}', + ); + } catch (err) { + print(err); + } + + _counterSubscription = _counterRef.onValue.listen( + (DatabaseEvent event) { + setState(() { + _error = null; + _counter = (event.snapshot.value ?? 0) as int; + }); + }, + onError: (Object o) { + final error = o as FirebaseException; + setState(() { + _error = error; + }); + }, + ); + + final messagesQuery = _messagesRef.limitToLast(10); + + _messagesSubscription = messagesQuery.onChildAdded.listen( + (DatabaseEvent event) { + print('Child added: ${event.snapshot.value}'); + }, + onError: (Object o) { + final error = o as FirebaseException; + print('Error: ${error.code} ${error.message}'); + }, + ); } @override @@ -107,33 +148,47 @@ class _MyHomePageState extends State { } Future _incrementAsTransaction() async { - // Increment counter in transaction. - final TransactionResult transactionResult = - await _counterRef.runTransaction((MutableData mutableData) { - mutableData.value = (mutableData.value ?? 0) + 1; - return mutableData; - }); - - if (transactionResult.committed) { - await _messagesRef.push().set({ - _kTestKey: '$_kTestValue ${transactionResult.dataSnapshot?.value}' + try { + final transactionResult = await _counterRef.runTransaction((mutableData) { + return Transaction.success((mutableData as int? ?? 0) + 1); }); - } else { - print('Transaction not committed.'); - if (transactionResult.error != null) { - print(transactionResult.error!.message); + + if (transactionResult.committed) { + final newMessageRef = _messagesRef.push(); + await newMessageRef.set({ + _kTestKey: '$_kTestValue ${transactionResult.snapshot.value}' + }); } + } on FirebaseException catch (e) { + print(e.message); } } + Future _deleteMessage(DataSnapshot snapshot) async { + final messageRef = _messagesRef.child(snapshot.key!); + await messageRef.remove(); + } + + void _setAnchorToBottom(bool? value) { + if (value == null) { + return; + } + + setState(() { + _anchorToBottom = value; + }); + } + @override Widget build(BuildContext context) { + if (!initialized) return Container(); + return Scaffold( appBar: AppBar( title: const Text('Flutter Database Example'), ), body: Column( - children: [ + children: [ Flexible( child: Center( child: _error == null @@ -147,19 +202,12 @@ class _MyHomePageState extends State { ), ), ElevatedButton( - onPressed: () async { - await _incrementAsTransaction(); - }, - child: const Text('Increment as transaction')), + onPressed: _incrementAsTransaction, + child: const Text('Increment as transaction'), + ), ListTile( leading: Checkbox( - onChanged: (bool? value) { - if (value != null) { - setState(() { - _anchorToBottom = value; - }); - } - }, + onChanged: _setAnchorToBottom, value: _anchorToBottom, ), title: const Text('Anchor to bottom'), @@ -169,19 +217,15 @@ class _MyHomePageState extends State { key: ValueKey(_anchorToBottom), query: _messagesRef, reverse: _anchorToBottom, - itemBuilder: (BuildContext context, DataSnapshot snapshot, - Animation animation, int index) { + itemBuilder: (context, snapshot, animation, index) { return SizeTransition( sizeFactor: animation, child: ListTile( trailing: IconButton( - onPressed: () => - _messagesRef.child(snapshot.key!).remove(), + onPressed: () => _deleteMessage(snapshot), icon: const Icon(Icons.delete), ), - title: Text( - '$index: ${snapshot.value.toString()}', - ), + title: Text('$index: ${snapshot.value.toString()}'), ), ); }, diff --git a/packages/firebase_database/firebase_database/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_database/firebase_database/example/macos/Runner.xcodeproj/project.pbxproj index a15dde26cd42..d0be8b0077a8 100644 --- a/packages/firebase_database/firebase_database/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_database/firebase_database/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,11 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B550B1FE23F53792007DADD5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B550B1FD23F53792007DADD5 /* GoogleService-Info.plist */; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1A3C0FEB219DE3059F49DD9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EDA759B4F18DE63DF0790FB /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -51,8 +47,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -72,7 +66,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; @@ -82,7 +75,6 @@ B51643519054BD100F273F38 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; B550B1FD23F53792007DADD5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; D71BFD332DFDD49FEC2851C7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,8 +82,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, E1A3C0FEB219DE3059F49DD9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -157,8 +147,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -284,7 +272,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -311,10 +299,10 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist b/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist index 3e13a9908257..79520ce6a75f 100644 --- a/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist +++ b/packages/firebase_database/firebase_database/example/macos/Runner/GoogleService-Info.plist @@ -2,39 +2,37 @@ - AD_UNIT_ID_FOR_BANNER_TEST - ca-app-pub-3940256099942544/2934735716 - AD_UNIT_ID_FOR_INTERSTITIAL_TEST - ca-app-pub-3940256099942544/4411468910 CLIENT_ID - 159623150305-1l2uq0jjcvricku3ah3fuav0v0nvrakp.apps.googleusercontent.com + 448618578101-muccra9c59ddgpooibm65023uqidlsk2.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.159623150305-1l2uq0jjcvricku3ah3fuav0v0nvrakp + com.googleusercontent.apps.448618578101-muccra9c59ddgpooibm65023uqidlsk2 + ANDROID_CLIENT_ID + 448618578101-26jgjs0rtl4ts2i667vjb28kldvs2kp6.apps.googleusercontent.com API_KEY - AIzaSyDyzecVw1zXTpBKwfFHxpl7QyYBhimNhUk + AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0 GCM_SENDER_ID - 159623150305 + 448618578101 PLIST_VERSION 1 BUNDLE_ID io.flutter.plugins.firebaseDatabaseExample PROJECT_ID - flutter-firebase-plugins + react-native-firebase-testing STORAGE_BUCKET - flutter-firebase-plugins.appspot.com + react-native-firebase-testing.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED IS_APPINVITE_ENABLED - + IS_GCM_ENABLED IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:159623150305:ios:a837cdfe238b8a54 + 1:448618578101:ios:bd1531505d78ada5ac3efc DATABASE_URL - https://flutter-firebase-plugins.firebaseio.com + https://react-native-firebase-testing.firebaseio.com \ No newline at end of file diff --git a/packages/firebase_database/firebase_database/example/macos/Runner/Info.plist b/packages/firebase_database/firebase_database/example/macos/Runner/Info.plist index 4789daa6a443..fe18eee6d47d 100644 --- a/packages/firebase_database/firebase_database/example/macos/Runner/Info.plist +++ b/packages/firebase_database/firebase_database/example/macos/Runner/Info.plist @@ -28,5 +28,10 @@ MainMenu NSPrincipalClass NSApplication + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/packages/firebase_database/firebase_database/example/pubspec.yaml b/packages/firebase_database/firebase_database/example/pubspec.yaml index 44840b8b3f1f..f5cab14473cb 100755 --- a/packages/firebase_database/firebase_database/example/pubspec.yaml +++ b/packages/firebase_database/firebase_database/example/pubspec.yaml @@ -10,8 +10,6 @@ dependencies: path: ../../../firebase_core/firebase_core firebase_database: path: ../ - firebase_database_platform_interface: - path: ../../firebase_database_platform_interface flutter: sdk: flutter @@ -24,13 +22,11 @@ dependency_overrides: path: ../../../firebase_core/firebase_core_web firebase_database: path: ../ - firebase_database_platform_interface: - path: ../../firebase_database_platform_interface firebase_database_web: path: ../../firebase_database_web dev_dependencies: - drive: 1.0.0-1.0.nullsafety.0 + drive: 1.0.0-1.0.nullsafety.1 flutter_driver: sdk: flutter test: any diff --git a/packages/firebase_database/firebase_database/example/test_driver/data_snapshot_e2e.dart b/packages/firebase_database/firebase_database/example/test_driver/data_snapshot_e2e.dart new file mode 100644 index 000000000000..ccaa2e48e007 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/test_driver/data_snapshot_e2e.dart @@ -0,0 +1,152 @@ +import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void runDataSnapshotTests() { + group('DataSnapshot', () { + late DatabaseReference ref; + + setUp(() async { + ref = FirebaseDatabase.instance.ref('tests'); + + // Wipe the database before each test + await ref.remove(); + }); + + test('it returns the correct key', () async { + final s = await ref.get(); + expect(s.key, 'tests'); + }); + + group('value', () { + test('it returns a string value', () async { + await ref.set('foo'); + final s = await ref.get(); + expect(s.value, 'foo'); + }); + + test('it returns a number value', () async { + await ref.set(123); + final s = await ref.get(); + expect(s.value, 123); + }); + + test('it returns a bool value', () async { + await ref.set(false); + final s = await ref.get(); + expect(s.value, false); + }); + + test('it returns a null value', () async { + await ref.set(null); + final s = await ref.get(); + expect(s.value, isNull); + }); + + test('it returns a List value', () async { + final data = [ + 'a', + 2, + true, + ['foo'], + { + 0: 'hello', + 1: 'foo', + } + ]; + await ref.set(data); + final s = await ref.get(); + expect( + s.value, + equals([ + 'a', + 2, + true, + ['foo'], + ['hello', 'foo'] + ]), + ); + }); + + test('it returns a Map value', () async { + final data = {'foo': 'bar'}; + await ref.set(data); + final s = await ref.get(); + expect(s.value, equals(data)); + }); + + test('non-string Map keys are converted to strings', () async { + final data = {1: 'foo', 2: 'bar', 'foo': 'bar'}; + await ref.set(data); + final s = await ref.get(); + expect(s.value, equals({'1': 'foo', '2': 'bar', 'foo': 'bar'})); + }); + }); + + test('setWithPriority returns the correct priority', () async { + await ref.setWithPriority('foo', 1); + final s = await ref.get(); + expect(s.priority, 1); + }); + + test('setPriority returns the correct priority', () async { + await ref.set('foo'); + await ref.setPriority(2); + final s = await ref.get(); + expect(s.priority, 2); + }); + + test('exists returns true', () async { + await ref.set('foo'); + final s = await ref.get(); + expect(s.exists, isTrue); + }); + + test('exists returns false', () async { + final s = await ref.get(); + expect(s.exists, isFalse); + }); + + test('hasChild returns false', () async { + final s = await ref.get(); + expect(s.hasChild('bar'), isFalse); + }); + + test('hasChild returns true', () async { + await ref.set({ + 'foo': {'bar': 'baz'} + }); + final s = await ref.get(); + expect(s.hasChild('bar'), isFalse); + }); + + test('child returns the correct snapshot for lists', () async { + await ref.set([0, 1]); + final s = await ref.get(); + expect(s.child('1'), isA()); + expect(s.child('1').value, 1); + }); + + test('child returns the correct snapshot', () async { + await ref.set({ + 'foo': {'bar': 'baz'} + }); + final s = await ref.get(); + expect(s.child('foo/bar'), isA()); + expect(s.child('foo/bar').value, 'baz'); + }); + + test('children returns the children in order', () async { + await ref.set({ + 'a': 3, + 'b': 2, + 'c': 1, + }); + final s = await ref.orderByValue().get(); + + List children = s.children.toList(); + expect(children[0].value, 1); + expect(children[1].value, 2); + expect(children[2].value, 3); + }); + }); +} diff --git a/packages/firebase_database/firebase_database/example/test_driver/database_e2e.dart b/packages/firebase_database/firebase_database/example/test_driver/database_e2e.dart new file mode 100644 index 000000000000..18fb34074d53 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/test_driver/database_e2e.dart @@ -0,0 +1,75 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'firebase_database_e2e.dart'; + +void runDatabaseTests() { + group('FirebaseDatabase.ref()', () { + setUp(() async { + await database.ref('tests/flutterfire').set(0); + }); + + test('returns a correct reference', () async { + final ref = database.ref('tests/flutterfire'); + expect(ref.key, 'flutterfire'); + expect(ref.parent, isNotNull); + expect(ref.parent!.key, 'tests'); + expect(ref.parent!.parent, isNotNull); + expect(ref.parent!.parent?.key, isNull); + + final snapshot = await ref.get(); + expect(snapshot.key, 'flutterfire'); + expect(snapshot.value, 0); + }); + + test( + 'returns a reference to the root of the database if no path specified', + () async { + final rootRef = database.ref(); + expect(rootRef.key, isNull); + expect(rootRef.parent, isNull); + + final childRef = rootRef.child('tests/flutterfire'); + final snapshot = await childRef.get(); + expect(snapshot.key, 'flutterfire'); + expect(snapshot.value, 0); + }, + ); + }); + + group('FirebaseDatabase.refFromURL()', () { + test('correctly returns a ref for database root', () async { + final ref = database + .refFromURL('https://react-native-firebase-testing.firebaseio.com'); + expect(ref.key, isNull); + + final refWithTrailingSlash = database + .refFromURL('https://react-native-firebase-testing.firebaseio.com/'); + expect(refWithTrailingSlash.key, isNull); + }); + + test('correctly returns a ref for any database path', () async { + final ref = database.refFromURL( + 'https://react-native-firebase-testing.firebaseio.com/foo', + ); + expect(ref.key, 'foo'); + + final refWithNestedPath = database.refFromURL( + 'https://react-native-firebase-testing.firebaseio.com/foo/bar', + ); + expect(refWithNestedPath.parent?.key, 'foo'); + expect(refWithNestedPath.key, 'bar'); + }); + + test('throws [ArgumentError] if not a valid https:// url', () async { + expect(() => database.refFromURL('foo'), throwsArgumentError); + }); + + test('throws [ArgumentError] if database url does not match instance url', + () async { + expect( + () => database.refFromURL('https://some-other-database.firebaseio.com'), + throwsArgumentError, + ); + }); + }); +} diff --git a/packages/firebase_database/firebase_database/example/test_driver/database_reference_e2e.dart b/packages/firebase_database/firebase_database/example/test_driver/database_reference_e2e.dart new file mode 100644 index 000000000000..9b763ae985bb --- /dev/null +++ b/packages/firebase_database/firebase_database/example/test_driver/database_reference_e2e.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'firebase_database_e2e.dart'; + +void runDatabaseReferenceTests() { + group('DatabaseReference', () { + late DatabaseReference ref; + + setUp(() async { + ref = database.ref('tests'); + + await ref.remove(); + }); + + group('set()', () { + test('sets value', () async { + final v = Random.secure().nextInt(1024); + await ref.set(v); + final actual = await ref.get(); + expect(actual.value, v); + }); + + test( + 'throws "permission-denied" on a ref with no read permission', + () async { + await expectLater( + database.ref('denied_read').get(), + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'permission-denied', + ) + .having( + (error) => error.message, + 'message', + predicate( + (String message) => + message.contains("doesn't have permission"), + ), + ), + ), + ); + }, + skip: true, // TODO Fails on CI even though works locally + ); + + test('removes a value if set to null', () async { + final v = Random.secure().nextInt(1024); + await ref.set(v); + final before = await ref.get(); + expect(before.value, v); + + await ref.set(null); + final after = await ref.get(); + expect(after.value, isNull); + expect(after.exists, isFalse); + }); + }); + + group('setPriority()', () { + test('sets a priority', () async { + await ref.set('foo'); + await ref.setPriority(2); + final snapshot = await ref.get(); + expect(snapshot.priority, 2); + }); + }); + + group('setWithPriority()', () { + test('sets a non-null value with a non-null priority', () async { + await Future.wait([ + ref.child('first').setWithPriority(1, 10), + ref.child('second').setWithPriority(2, 1), + ref.child('third').setWithPriority(3, 5), + ]); + + final snapshot = await ref.orderByPriority().get(); + final keys = snapshot.children.map((child) => child.key).toList(); + expect(keys, ['second', 'third', 'first']); + }); + }); + + group('update()', () { + test('updates value at given location', () async { + await ref.set({'foo': 'bar'}); + final newValue = Random.secure().nextInt(255) + 1; + await ref.update({'bar': newValue}); + final actual = await ref.get(); + + expect(actual.value, { + 'foo': 'bar', + 'bar': newValue, + }); + }); + }); + + group('runTransaction()', () { + setUp(() async { + await ref.set(0); + }); + + test('aborts a transaction', () async { + await ref.set(5); + final snapshot = await ref.get(); + expect(snapshot.value, 5); + + final result = await ref.runTransaction((value) { + final nextValue = (value as int? ?? 0) + 1; + if (nextValue > 5) { + return Transaction.abort(); + } + return Transaction.success(nextValue); + }); + + expect(result.committed, false); + expect(result.snapshot.value, 5); + }); + + test('executes transaction', () async { + final snapshot = await ref.get(); + final value = (snapshot.value ?? 0) as int; + final result = await ref.runTransaction((value) { + return Transaction.success((value as int? ?? 0) + 1); + }); + + expect(result.committed, true); + expect((result.snapshot.value ?? 0) as int > value, true); + expect(result.snapshot.key, ref.key); + }); + + test('get primitive list values', () async { + List data = ['first', 'second']; + final FirebaseDatabase database = FirebaseDatabase.instance; + final DatabaseReference ref = database.ref('tests/list-values'); + + await ref.set({'list': data}); + + final transactionResult = await ref.runTransaction((mutableData) { + return Transaction.success(mutableData); + }); + + var value = transactionResult.snapshot.value as dynamic; + expect(value, isNotNull); + expect(value['list'], data); + }); + }); + }); +} diff --git a/packages/firebase_database/firebase_database/example/test_driver/firebase_database_configuration_e2e.dart b/packages/firebase_database/firebase_database/example/test_driver/firebase_database_configuration_e2e.dart new file mode 100644 index 000000000000..a805b5acfe4e --- /dev/null +++ b/packages/firebase_database/firebase_database/example/test_driver/firebase_database_configuration_e2e.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'firebase_database_e2e.dart'; + +const MAX_CACHE_SIZE = 100 * 1024 * 1024; +const MIN_CACHE_SIZE = 1042 * 1024; + +void runConfigurationTests() { + group('FirebaseDatabase configuration', () { + test( + 'setPersistenceCacheSizeBytes Integer', + () { + database.setPersistenceCacheSizeBytes(MIN_CACHE_SIZE); + }, + // Skipped because it is not supported on web + skip: kIsWeb, + ); + + test( + 'setPersistenceCacheSizeBytes Long', + () { + database.setPersistenceCacheSizeBytes(MAX_CACHE_SIZE); + }, + // Skipped because it is not supported on web + skip: kIsWeb, + ); + + test('setLoggingEnabled to true', () { + database.setLoggingEnabled(true); + }); + + test('setLoggingEnabled to false', () { + database.setLoggingEnabled(false); + }); + }); +} diff --git a/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e.dart b/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e.dart index 5232f8254db9..6273a31303bb 100644 --- a/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e.dart +++ b/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e.dart @@ -1,120 +1,51 @@ -// ignore_for_file: require_trailing_commas -import 'dart:io'; - import 'package:drive/drive.dart' as drive; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'data_snapshot_e2e.dart'; +import 'database_e2e.dart'; +import 'database_reference_e2e.dart'; +import 'firebase_database_configuration_e2e.dart'; import 'query_e2e.dart'; -final List> testDocuments = [ - {'ref': 'one', 'value': 23}, - {'ref': 'two', 'value': 56}, - {'ref': 'three', 'value': 9}, - {'ref': 'four', 'value': 40} -]; - -Future setTestData() { - final FirebaseDatabase database = FirebaseDatabase.instance; - const String orderTestPath = 'ordered/'; - return Future.wait(testDocuments.map((map) { - String child = map['ref']! as String; - return database.reference().child('$orderTestPath/$child').set(map); - })); -} - -void testsMain() { - group('FirebaseDatabase', () { - // initialize the firebase - setUp(() async { - await Firebase.initializeApp(); - }); - - // set up dummy data - setUpAll(() async { - await setTestData(); - }); - - test('setPersistenceCacheSizeBytes Integer', () async { - final FirebaseDatabase database = FirebaseDatabase.instance; - - await database.setPersistenceCacheSizeBytes(2147483647); - // Skipped because it is not supported on web - }, skip: kIsWeb); - - test('setPersistenceCacheSizeBytes Long', () async { - final FirebaseDatabase database = FirebaseDatabase.instance; - await database.setPersistenceCacheSizeBytes(2147483648); - // Skipped because it is not supported on web - }, skip: kIsWeb); - - test('setLoggingEnabled to true', () async { - final FirebaseDatabase database = FirebaseDatabase.instance; - await database.setLoggingEnabled(true); - // Skipped because it needs to be initialized first on android. - }, skip: !kIsWeb && Platform.isAndroid); - - test('setLoggingEnabled to false', () async { - final FirebaseDatabase database = FirebaseDatabase.instance; - await database.setLoggingEnabled(false); - // Skipped because it needs to be initialized first on android. - }, skip: !kIsWeb && Platform.isAndroid); - - group('runTransaction', () { - test('update and check values', () async { - final FirebaseDatabase database = FirebaseDatabase.instance; - final DatabaseReference ref = database.reference().child('flutterfire'); +late FirebaseDatabase database; - await ref.set(0); +// The port we've set the Firebase Database emulator to run on via the +// `firebase.json` configuration file. +const emulatorPort = 9000; - final DataSnapshot snapshot = await ref.once(); - final int value = snapshot.value ?? 0; - final TransactionResult transactionResult = - await ref.runTransaction((MutableData mutableData) { - mutableData.value = (mutableData.value ?? 0) + 1; - return mutableData; - }); +// Android device emulators consider localhost of the host machine as 10.0.2.2 +// so let's use that if running on Android. +final emulatorHost = + (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) + ? '10.0.2.2' + : 'localhost'; - expect(transactionResult.committed, true); - expect(transactionResult.dataSnapshot!.value > value, true); - }); - - test('get primitive list values', () async { - List data = ['first', 'second']; - final FirebaseDatabase database = FirebaseDatabase.instance; - final DatabaseReference ref = database.reference().child('list-values'); - - await ref.set({'list': data}); - - final transactionResult = await ref.runTransaction((mutableData) { - return mutableData; - }); - - expect(transactionResult.dataSnapshot!.value['list'], data); - }); - }); - - test('DataSnapshot supports null childKeys for maps', () async { - // Regression test for https://github.com/FirebaseExtended/flutterfire/issues/6002 - - final ref = FirebaseDatabase.instance.reference().child('flutterfire'); - - final transactionResult = await ref.runTransaction((mutableData) { - mutableData.value = {'v': 'vala'}; - return mutableData; - }); - - expect(transactionResult.committed, true); - expect( - transactionResult.dataSnapshot!.value, - {'v': 'vala'}, - ); - }); - - runQueryTests(); +void testsMain() { + setUpAll(() async { + await Firebase.initializeApp( + options: const FirebaseOptions( + apiKey: 'AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0', + appId: '1:448618578101:ios:2bc5c1fe2ec336f8ac3efc', + messagingSenderId: '448618578101', + projectId: 'react-native-firebase-testing', + databaseURL: 'https://react-native-firebase-testing.firebaseio.com', + storageBucket: 'react-native-firebase-testing.appspot.com', + ), + ); + database = FirebaseDatabase.instance; + database.useDatabaseEmulator(emulatorHost, emulatorPort); }); + + runConfigurationTests(); + runDatabaseTests(); + runDatabaseReferenceTests(); + runQueryTests(); + runDataSnapshotTests(); + // TODO(ehesp): Fix broken tests + // runOnDisconnectTests(); } void main() => drive.main(testsMain); diff --git a/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e_test.dart b/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e_test.dart index b1f6a8414461..9940272d4af6 100644 --- a/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e_test.dart +++ b/packages/firebase_database/firebase_database/example/test_driver/firebase_database_e2e_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: require_trailing_commas -// @dart = 2.9 import 'package:drive/drive_driver.dart' as drive; void main() => drive.main(); diff --git a/packages/firebase_database/firebase_database/example/test_driver/on_disconnect_e2e_test.dart b/packages/firebase_database/firebase_database/example/test_driver/on_disconnect_e2e_test.dart new file mode 100644 index 000000000000..d66d68df86b8 --- /dev/null +++ b/packages/firebase_database/firebase_database/example/test_driver/on_disconnect_e2e_test.dart @@ -0,0 +1,79 @@ +// Copyright 2021 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 'package:firebase_database/firebase_database.dart'; +import 'package:test/test.dart'; + +void runOnDisconnectTests() { + group('OnDisconnect', () { + late FirebaseDatabase database; + late DatabaseReference ref; + + setUp(() async { + database = FirebaseDatabase.instance; + ref = database.ref('tests'); + + // Wipe the database before each test + await ref.remove(); + }); + + Future toggleState() async { + await database.goOffline(); + await database.goOnline(); + } + + tearDown(() async { + await FirebaseDatabase.instance.goOnline(); + }); + + test('sets a value on disconnect', () async { + await ref.onDisconnect().set('foo'); + await toggleState(); + var snapshot = await ref.get(); + expect(snapshot.value, 'foo'); + }); + + test('sets a value with priority on disconnect', () async { + await ref.onDisconnect().setWithPriority('foo', 3); + await toggleState(); + var snapshot = await ref.get(); + expect(snapshot.value, 'foo'); + expect(snapshot.priority, 3); + }); + + test('removes a node on disconnect', () async { + await ref.set('foo'); + await ref.onDisconnect().remove(); + await toggleState(); + var snapshot = await ref.get(); + expect(snapshot.exists, isFalse); + }); + + test('updates a node on disconnect', () async { + await ref.set({'foo': 'bar'}); + await ref.onDisconnect().update({'bar': 'baz'}); + await toggleState(); + var snapshot = await ref.get(); + expect( + snapshot.value, + equals({ + 'foo': 'bar', + 'bar': 'baz', + }), + ); + }); + + test('cancels disconnect operations', () async { + await ref.set('foo'); + await ref.onDisconnect().remove(); + await ref.onDisconnect().cancel(); + await toggleState(); + var snapshot = await ref.get(); + expect( + snapshot.value, + 'foo', + ); + }); + }); +} diff --git a/packages/firebase_database/firebase_database/example/test_driver/query_e2e.dart b/packages/firebase_database/firebase_database/example/test_driver/query_e2e.dart index 4e7643d2a911..c11b5871b8e5 100644 --- a/packages/firebase_database/firebase_database/example/test_driver/query_e2e.dart +++ b/packages/firebase_database/firebase_database/example/test_driver/query_e2e.dart @@ -1,153 +1,473 @@ -// ignore_for_file: require_trailing_commas - import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'firebase_database_e2e.dart'; - void runQueryTests() { - group('$Query', () { - late FirebaseDatabase database; - - setUpAll(() async { - database = FirebaseDatabase.instance; - await setTestData(); - }); - - test('once', () async { - final dataSnapshot = - await database.reference().child('ordered/one').once(); - expect(dataSnapshot, isNot(null)); - expect(dataSnapshot.key, 'one'); - expect(dataSnapshot.value['ref'], 'one'); - expect(dataSnapshot.value['value'], 23); - }); - - test('get', () async { - final dataSnapshot = - await database.reference().child('ordered/two').get(); - expect(dataSnapshot, isNot(null)); - expect(dataSnapshot.key, 'two'); - expect(dataSnapshot.value['ref'], 'two'); - expect(dataSnapshot.value['value'], 56); - }); - - test('DataSnapshot.exists is false for no data', () async { - final databaseRef = - database.reference().child('a-non-existing-reference'); - final dataSnapshot = await databaseRef.get(); - expect(dataSnapshot.exists, false); - }); - - test('DataSnapshot.exists is true for existing data', () async { - final databaseRef = database.reference().child('ordered/one'); - final dataSnapshot = await databaseRef.get(); - expect(dataSnapshot.exists, true); - }); - - test('correct order returned from query', () async { - final c = Completer>>(); - final items = >[]; - - // ignore: unawaited_futures - database - .reference() - .child('ordered') - .orderByChild('value') - .onChildAdded - .forEach((element) { - items.add(element.snapshot.value.cast()); - if (items.length == testDocuments.length) c.complete(items); - }); - - final snapshots = await c.future; - - final documents = snapshots.map((v) => v['value'] as int).toList(); - final ordered = testDocuments.map((doc) => doc['value']).toList()..sort(); - - expect(documents[0], ordered[0]); - expect(documents[1], ordered[1]); - expect(documents[2], ordered[2]); - expect(documents[3], ordered[3]); - }); - - test('limitToFirst', () async { - final snapshot = - await database.reference().child('ordered').limitToFirst(2).once(); - Map data = snapshot.value; - expect(data.length, 2); - - final snapshot1 = await database - .reference() - .child('ordered') - .limitToFirst(testDocuments.length + 2) - .once(); - Map data1 = snapshot1.value; - expect(data1.length, testDocuments.length); - }); - - test('limitToLast', () async { - final snapshot = - await database.reference().child('ordered').limitToLast(3).once(); - Map data = snapshot.value; - expect(data.length, 3); - - final snapshot1 = await database - .reference() - .child('ordered') - .limitToLast(testDocuments.length + 2) - .once(); - Map data1 = snapshot1.value; - expect(data1.length, testDocuments.length); - }); - - test('startAt & endAt', () async { - // query to get the data that has key starts with t only - final snapshot = await database - .reference() - .child('ordered') - .orderByKey() - .startAt('t') - .endAt('t\uf8ff') - .once(); - Map data = snapshot.value; - bool eachKeyStartsWithF = true; - data.forEach((key, value) { - if (!key.toString().startsWith('t')) { - eachKeyStartsWithF = false; - } - }); - expect(eachKeyStartsWithF, true); - // as there are two snaps that starts with t (two, three) - expect(data.length, 2); - - final snapshot1 = await database - .reference() - .child('ordered') - .orderByKey() - .startAt('t') - .endAt('three') - .once(); - Map data1 = snapshot1.value; - // as the endAt is equal to 'three' and this will skip the data with key 'two'. - expect(data1.length, 1); - }); - - test('equalTo', () async { - final snapshot = await database - .reference() - .child('ordered') - .orderByKey() - .equalTo('one') - .once(); - - Map data = snapshot.value; - - expect(data.containsKey('one'), true); - expect(data['one']['ref'], 'one'); - expect(data['one']['value'], 23); + group('Query', () { + late DatabaseReference ref; + + setUp(() async { + ref = FirebaseDatabase.instance.ref('tests'); + + // Wipe the database before each test + await ref.remove(); + }); + + group('startAt', () { + test('returns null when no order modifier is applied', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + }); + + final snapshot = await ref.startAt(2).get(); + expect(snapshot.value, isNull); + }); + + test('starts at the correct value', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }); + + final snapshot = await ref.orderByValue().startAt(2).get(); + + final expected = ['b', 'c', 'd']; + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('startAfter', () { + test('returns null when no order modifier is applied', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + }); + + final snapshot = await ref.startAfter(2).get(); + expect(snapshot.value, isNull); + }); + + test('starts after the correct value', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }); + + // TODO(ehesp): Using `get` returns the wrong results. Have flagged with SDK team. + final e = await ref.orderByValue().startAfter(2).once(); + + final expected = ['c', 'd']; + + expect(e.snapshot.children.length, expected.length); + e.snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('endAt', () { + test('returns all values when no order modifier is applied', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + }); + + final expected = ['a', 'b', 'c']; + + final snapshot = await ref.endAt(2).get(); + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + + test('ends at the correct value', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }); + + final snapshot = await ref.orderByValue().endAt(2).get(); + + final expected = ['a', 'b']; + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('endBefore', () { + test('returns all values when no order modifier is applied', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + }); + + final expected = ['a', 'b', 'c']; + + final snapshot = await ref.endBefore(2).get(); + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + + test('ends before the correct value', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }); + + final snapshot = await ref.orderByValue().endBefore(2).get(); + + final expected = ['a']; + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('equalTo', () { + test('returns null when no order modifier is applied', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + }); + + final snapshot = await ref.equalTo(2).get(); + expect(snapshot.value, isNull); + }); + + test('returns the correct value', () async { + await ref.set({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + 'e': 2, + }); + + final snapshot = await ref.orderByValue().equalTo(2).get(); + + final expected = ['b', 'e']; + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('limitToFirst', () { + test('returns a limited array', () async { + await ref.set({ + 0: 'foo', + 1: 'bar', + 2: 'baz', + }); + + final snapshot = await ref.limitToFirst(2).get(); + + final expected = ['foo', 'bar']; + expect(snapshot.value, equals(expected)); + }); + + test('returns a limited object', () async { + await ref.set({ + 'a': 'foo', + 'b': 'bar', + 'c': 'baz', + }); + + final snapshot = await ref.limitToFirst(2).get(); + + final expected = { + 'a': 'foo', + 'b': 'bar', + }; + + expect(snapshot.value, equals(expected)); + }); + + test('returns null when no limit is possible', () async { + await ref.set('foo'); + + final snapshot = await ref.limitToFirst(2).get(); + + expect(snapshot.value, isNull); + }); + }); + + group('limitToLast', () { + test('returns a limited array', () async { + await ref.set({ + 0: 'foo', + 1: 'bar', + 2: 'baz', + }); + + final snapshot = await ref.limitToLast(2).get(); + + final expected = [null, 'bar', 'baz']; + expect(snapshot.value, equals(expected)); + }); + + test('returns a limited object', () async { + await ref.set({ + 'a': 'foo', + 'b': 'bar', + 'c': 'baz', + }); + + final snapshot = await ref.limitToLast(2).get(); + + final expected = { + 'b': 'bar', + 'c': 'baz', + }; + + expect(snapshot.value, equals(expected)); + }); + + test('returns null when no limit is possible', () async { + await ref.set('foo'); + + final snapshot = await ref.limitToLast(2).get(); + + expect(snapshot.value, isNull); + }); + }); + + group('orderByChild', () { + test('orders by a child value', () async { + await ref.set({ + 'a': { + 'string': 'foo', + 'number': 10, + }, + 'b': { + 'string': 'bar', + 'number': 5, + }, + 'c': { + 'string': 'baz', + 'number': 8, + }, + }); + + final snapshot = await ref.orderByChild('number').get(); + + final expected = ['b', 'c', 'a']; + expect(snapshot.children.length, equals(expected.length)); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('orderByKey', () { + test('orders by a key', () async { + await ref.set({ + 'b': { + 'string': 'bar', + 'number': 5, + }, + 'a': { + 'string': 'foo', + 'number': 10, + }, + 'c': { + 'string': 'baz', + 'number': 8, + }, + }); + + final snapshot = await ref.orderByKey().get(); + + final expected = ['a', 'b', 'c']; + + expect(snapshot.children.length, expected.length); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('orderByPriority', () { + test('orders by priority', () async { + await ref.set({ + 'a': { + 'string': 'foo', + 'number': 10, + }, + 'b': { + 'string': 'bar', + 'number': 5, + }, + 'c': { + 'string': 'baz', + 'number': 8, + }, + }); + + await Future.wait([ + ref.child('a').setPriority(2), + ref.child('b').setPriority(3), + ref.child('c').setPriority(1), + ]); + + final snapshot = await ref.orderByPriority().get(); + + final expected = ['c', 'a', 'b']; + expect(snapshot.children.length, equals(expected.length)); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('orderByValue', () { + test('orders by a value', () async { + await ref.set({ + 'a': 2, + 'b': 3, + 'c': 1, + }); + + await Future.wait([ + ref.child('a').setPriority(2), + ref.child('b').setPriority(3), + ref.child('c').setPriority(1), + ]); + + final snapshot = await ref.orderByValue().get(); + + final expected = ['c', 'a', 'b']; + expect(snapshot.children.length, equals(expected.length)); + snapshot.children.toList().forEachIndexed((i, childSnapshot) { + expect(childSnapshot.key, expected[i]); + }); + }); + }); + + group('onChildAdded', () { + test('emits an event when a child is added', () async { + expect( + ref.onChildAdded, + emitsInOrder([ + isA() + .having((s) => s.snapshot.value, 'value', 'foo') + .having((e) => e.type, 'type', DatabaseEventType.childAdded), + isA() + .having((s) => s.snapshot.value, 'value', 'bar') + .having((e) => e.type, 'type', DatabaseEventType.childAdded), + ]), + ); + + await ref.child('foo').set('foo'); + await ref.child('bar').set('bar'); + }); + }); + + group('onChildRemoved', () { + test('emits an event when a child is removed', () async { + await ref.child('foo').set('foo'); + await ref.child('bar').set('bar'); + + expect( + ref.onChildRemoved, + emitsInOrder([ + isA() + .having((s) => s.snapshot.value, 'value', 'bar') + .having((e) => e.type, 'type', DatabaseEventType.childRemoved), + ]), + ); + // Give time for listen to be registered on native. + // TODO is there a better way to do this? + await Future.delayed(const Duration(seconds: 1)); + await ref.child('bar').remove(); + }); + }); + + group('onChildChanged', () { + test('emits an event when a child is changed', () async { + await ref.child('foo').set('foo'); + await ref.child('bar').set('bar'); + + expect( + ref.onChildChanged, + emitsInOrder([ + isA() + .having((s) => s.snapshot.key, 'key', 'bar') + .having((s) => s.snapshot.value, 'value', 'baz') + .having((e) => e.type, 'type', DatabaseEventType.childChanged), + isA() + .having((s) => s.snapshot.key, 'key', 'foo') + .having((s) => s.snapshot.value, 'value', 'bar') + .having((e) => e.type, 'type', DatabaseEventType.childChanged), + ]), + ); + // Give time for listen to be registered on native. + // TODO is there a better way to do this? + await Future.delayed(const Duration(seconds: 1)); + await ref.child('bar').set('baz'); + await ref.child('foo').set('bar'); + }); + }); + + group('onChildMoved', () { + test('emits an event when a child is moved', () async { + await ref.set({ + 'alex': {'nuggets': 60}, + 'rob': {'nuggets': 56}, + 'vassili': {'nuggets': 55.5}, + 'tony': {'nuggets': 52}, + 'greg': {'nuggets': 52}, + }); + + expect( + ref.orderByChild('nuggets').onChildMoved, + emitsInOrder([ + isA().having((s) => s.snapshot.value, 'value', { + 'nuggets': 57 + }).having((e) => e.type, 'type', DatabaseEventType.childMoved), + isA().having((s) => s.snapshot.value, 'value', { + 'nuggets': 61 + }).having((e) => e.type, 'type', DatabaseEventType.childMoved), + ]), + ); + // Give time for listen to be registered on native. + // TODO is there a better way to do this? + await Future.delayed(const Duration(seconds: 1)); + await ref.child('greg/nuggets').set(57); + await ref.child('rob/nuggets').set(61); + }); }); }); } diff --git a/packages/firebase_database/firebase_database/example/test_driver/utils/extensions.dart b/packages/firebase_database/firebase_database/example/test_driver/utils/extensions.dart new file mode 100644 index 000000000000..853cab4fbdad --- /dev/null +++ b/packages/firebase_database/firebase_database/example/test_driver/utils/extensions.dart @@ -0,0 +1,13 @@ +import 'package:firebase_database/firebase_database.dart' show DataSnapshot; + +extension KeysGetter on DataSnapshot { + List get keys { + final keys = []; + + children.forEach((snapshot) { + keys.add(snapshot.key!); + }); + + return keys; + } +} diff --git a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.h b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.h new file mode 100644 index 000000000000..0bb555293d7b --- /dev/null +++ b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.h @@ -0,0 +1,22 @@ +// Copyright 2021 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 +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTFirebaseDatabaseObserveStreamHandler : NSObject +- (instancetype)initWithFIRDatabaseQuery:(FIRDatabaseQuery *)databaseQuery + andOnDisposeBlock:(void (^)(void))disposeBlock; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.m b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.m new file mode 100644 index 000000000000..9a1e1fe1ab37 --- /dev/null +++ b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.m @@ -0,0 +1,74 @@ +// Copyright 2021 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 +#import + +#import "FLTFirebaseDatabaseObserveStreamHandler.h" +#import "FLTFirebaseDatabaseUtils.h" + +@interface FLTFirebaseDatabaseObserveStreamHandler () +@property(readwrite) FIRDatabaseHandle databaseHandle; +@property(readonly) FIRDatabaseQuery *databaseQuery; +@property(readwrite) void (^disposeBlock)(void); +@end + +@implementation FLTFirebaseDatabaseObserveStreamHandler + +- (instancetype)initWithFIRDatabaseQuery:(FIRDatabaseQuery *)databaseQuery + andOnDisposeBlock:(void (^)(void))disposeBlock { + self = [super init]; + if (self) { + _databaseQuery = databaseQuery; + _disposeBlock = disposeBlock; + } + return self; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + NSString *eventTypeString = arguments[@"eventType"]; + id observeBlock = ^(FIRDataSnapshot *snapshot, NSString *previousChildKey) { + NSMutableDictionary *eventDictionary = [@{ + @"eventType" : eventTypeString, + } mutableCopy]; + [eventDictionary addEntriesFromDictionary:[FLTFirebaseDatabaseUtils + dictionaryFromSnapshot:snapshot + withPreviousChildKey:previousChildKey]]; + dispatch_async(dispatch_get_main_queue(), ^{ + events(eventDictionary); + }); + }; + + id cancelBlock = ^(NSError *error) { + NSArray *codeAndMessage = [FLTFirebaseDatabaseUtils codeAndMessageFromNSError:error]; + NSString *code = codeAndMessage[0]; + NSString *message = codeAndMessage[1]; + NSDictionary *details = @{ + @"code" : code, + @"message" : message, + }; + dispatch_async(dispatch_get_main_queue(), ^{ + events([FLTFirebasePlugin createFlutterErrorFromCode:code + message:message + optionalDetails:details + andOptionalNSError:error]); + }); + }; + + _databaseHandle = [_databaseQuery + observeEventType:[FLTFirebaseDatabaseUtils eventTypeFromString:eventTypeString] + andPreviousSiblingKeyWithBlock:observeBlock + withCancelBlock:cancelBlock]; + + return nil; +} + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + _disposeBlock(); + [_databaseQuery removeObserverWithHandle:_databaseHandle]; + return nil; +} + +@end diff --git a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.h b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.h index 27a5fe2306c4..e01abbdd6433 100644 --- a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.h +++ b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.h @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2021 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 @@ -8,10 +8,8 @@ #import #endif -@interface FLTFirebaseDatabasePlugin : NSObject - -+ (NSMutableArray *)getSnapshotChildKeys:(FIRDataSnapshot *)dataSnapshot; - -@property(nonatomic) NSMutableDictionary *updatedSnapshots; +#import +#import +@interface FLTFirebaseDatabasePlugin : FLTFirebasePlugin @end diff --git a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.m b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.m index 1e661a626b20..e7c1ea5e1a2f 100644 --- a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.m +++ b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabasePlugin.m @@ -1,369 +1,388 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2021 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 "FLTFirebaseDatabasePlugin.h" +#import -#import -#import +#import "FLTFirebaseDatabaseObserveStreamHandler.h" +#import "FLTFirebaseDatabasePlugin.h" +#import "FLTFirebaseDatabaseUtils.h" -static FlutterError *getFlutterError(NSError *error) { - if (error == nil) return nil; +NSString *const kFLTFirebaseDatabaseChannelName = @"plugins.flutter.io/firebase_database"; - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code] - message:error.domain - details:error.localizedDescription]; +@implementation FLTFirebaseDatabasePlugin { + // Used by FlutterStreamHandlers. + NSObject *_binaryMessenger; + NSMutableDictionary *_listenerCounts; + NSMutableDictionary *_streamHandlers; + // Used by transactions. + FlutterMethodChannel *_channel; } -static NSDictionary *getDictionaryFromError(NSError *error) { - if (!error) { - return nil; - } - return @{ - @"code" : @(error.code), - @"message" : error.domain ?: [NSNull null], - @"details" : error.localizedDescription ?: [NSNull null], - }; -} - -@interface FLTFirebaseDatabasePlugin () - -@end - -FIRDatabaseReference *getReference(FIRDatabase *database, NSDictionary *arguments) { - NSString *path = arguments[@"path"]; - FIRDatabaseReference *ref = database.reference; - if ([path length] > 0) ref = [ref child:path]; - return ref; -} +#pragma mark - FlutterPlugin -FIRDatabaseQuery *getDatabaseQuery(FIRDatabase *database, NSDictionary *arguments) { - FIRDatabaseQuery *query = getReference(database, arguments); - NSDictionary *parameters = arguments[@"parameters"]; - NSString *orderBy = parameters[@"orderBy"]; - if ([orderBy isEqualToString:@"child"]) { - query = [query queryOrderedByChild:parameters[@"orderByChildKey"]]; - } else if ([orderBy isEqualToString:@"key"]) { - query = [query queryOrderedByKey]; - } else if ([orderBy isEqualToString:@"value"]) { - query = [query queryOrderedByValue]; - } else if ([orderBy isEqualToString:@"priority"]) { - query = [query queryOrderedByPriority]; - } - id startAt = parameters[@"startAt"]; - if (startAt) { - id startAtKey = parameters[@"startAtKey"]; - if (startAtKey) { - query = [query queryStartingAtValue:startAt childKey:startAtKey]; - } else { - query = [query queryStartingAtValue:startAt]; - } - } - id endAt = parameters[@"endAt"]; - if (endAt) { - id endAtKey = parameters[@"endAtKey"]; - if (endAtKey) { - query = [query queryEndingAtValue:endAt childKey:endAtKey]; - } else { - query = [query queryEndingAtValue:endAt]; - } - } - id equalTo = parameters[@"equalTo"]; - if (equalTo) { - id equalToKey = parameters[@"equalToKey"]; - if (equalToKey) { - query = [query queryEqualToValue:equalTo childKey:equalToKey]; - } else { - query = [query queryEqualToValue:equalTo]; - } - } - NSNumber *limitToFirst = parameters[@"limitToFirst"]; - if (limitToFirst) { - query = [query queryLimitedToFirst:limitToFirst.intValue]; - } - NSNumber *limitToLast = parameters[@"limitToLast"]; - if (limitToLast) { - query = [query queryLimitedToLast:limitToLast.intValue]; - } - return query; -} - -FIRDataEventType parseEventType(NSString *eventTypeString) { - if ([@"EventType.childAdded" isEqual:eventTypeString]) { - return FIRDataEventTypeChildAdded; - } else if ([@"EventType.childRemoved" isEqual:eventTypeString]) { - return FIRDataEventTypeChildRemoved; - } else if ([@"EventType.childChanged" isEqual:eventTypeString]) { - return FIRDataEventTypeChildChanged; - } else if ([@"EventType.childMoved" isEqual:eventTypeString]) { - return FIRDataEventTypeChildMoved; - } else if ([@"EventType.value" isEqual:eventTypeString]) { - return FIRDataEventTypeValue; - } - assert(false); - return 0; -} - -id roundDoubles(id value) { - // Workaround for https://github.com/firebase/firebase-ios-sdk/issues/91 - // The Firebase iOS SDK sometimes returns doubles when ints were stored. - // We detect doubles that can be converted to ints without loss of precision - // and convert them. - if ([value isKindOfClass:[NSNumber class]]) { - CFNumberType type = CFNumberGetType((CFNumberRef)value); - if (type == kCFNumberDoubleType || type == kCFNumberFloatType) { - if ((double)(long long)[value doubleValue] == [value doubleValue]) { - return [NSNumber numberWithLongLong:(long long)[value doubleValue]]; - } - } - } else if ([value isKindOfClass:[NSArray class]]) { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:[value count]]; - [value enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - [result addObject:roundDoubles(obj)]; - }]; - return result; - } else if ([value isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[value count]]; - [value enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - result[key] = roundDoubles(obj); - }]; - return result; +- (instancetype)init:(NSObject *)messenger + andChannel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _channel = channel; + _binaryMessenger = messenger; + _listenerCounts = [NSMutableDictionary dictionary]; + _streamHandlers = [NSMutableDictionary dictionary]; } - return value; + return self; } -// TODO(Salakar): Should also implement io.flutter.plugins.firebase.core.FlutterFirebasePlugin when -// reworked. -@interface FLTFirebaseDatabasePlugin () -@property(nonatomic, retain) FlutterMethodChannel *channel; -@end - -@implementation FLTFirebaseDatabasePlugin - + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_database" + [FlutterMethodChannel methodChannelWithName:kFLTFirebaseDatabaseChannelName binaryMessenger:[registrar messenger]]; - FLTFirebaseDatabasePlugin *instance = [[FLTFirebaseDatabasePlugin alloc] init]; - instance.channel = channel; + FLTFirebaseDatabasePlugin *instance = + [[FLTFirebaseDatabasePlugin alloc] init:[registrar messenger] andChannel:channel]; [registrar addMethodCallDelegate:instance channel:channel]; + [[FLTFirebasePluginRegistry sharedInstance] registerFirebasePlugin:instance]; - SEL sel = NSSelectorFromString(@"registerLibrary:withVersion:"); - if ([FIRApp respondsToSelector:sel]) { - [FIRApp performSelector:sel withObject:LIBRARY_NAME withObject:LIBRARY_VERSION]; - } +#if TARGET_OS_OSX + // Publish does not exist on MacOS version of FlutterPluginRegistrar. +#else + [registrar publish:instance]; +#endif } -+ (NSMutableArray *)getSnapshotChildKeys:(FIRDataSnapshot *)dataSnapshot { - NSMutableArray *childKeys = [NSMutableArray array]; - if (dataSnapshot.childrenCount > 0) { - NSEnumerator *children = [dataSnapshot children]; - FIRDataSnapshot *child; - child = [children nextObject]; - while (child) { - [childKeys addObject:child.key]; - child = [children nextObject]; - } +- (void)cleanupWithCompletion:(void (^)(void))completion { + for (NSString *handlerId in self->_streamHandlers) { + NSObject *handler = self->_streamHandlers[handlerId]; + [handler onCancelWithArguments:nil]; + } + [self->_streamHandlers removeAllObjects]; + if (completion != nil) { + completion(); } - return childKeys; } -- (instancetype)init { - self = [super init]; - if (self) { - self.updatedSnapshots = [NSMutableDictionary new]; - } - return self; +- (void)detachFromEngineForRegistrar:(NSObject *)registrar { + [self cleanupWithCompletion:nil]; } -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - FIRDatabase *database; - NSString *appName = call.arguments[@"app"]; - NSString *databaseURL = call.arguments[@"databaseURL"]; - // TODO(Salakar): `appName` Should never be null after upcoming re-work in Dart. - if (![appName isEqual:[NSNull null]] && ![databaseURL isEqual:[NSNull null]]) { - database = [FIRDatabase databaseForApp:[FLTFirebasePlugin firebaseAppNamed:appName] - URL:databaseURL]; - } else if (![appName isEqual:[NSNull null]]) { - database = [FIRDatabase databaseForApp:[FLTFirebasePlugin firebaseAppNamed:appName]]; - } else if (![databaseURL isEqual:[NSNull null]]) { - database = [FIRDatabase databaseWithURL:databaseURL]; - } else { - database = [FIRDatabase database]; - } - void (^defaultCompletionBlock)(NSError *, FIRDatabaseReference *) = - ^(NSError *error, FIRDatabaseReference *ref) { - result(getFlutterError(error)); +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutterResult { + FLTFirebaseMethodCallErrorBlock errorBlock = + ^(NSString *_Nullable code, NSString *_Nullable message, NSDictionary *_Nullable details, + NSError *_Nullable error) { + if (code == nil) { + NSArray *codeAndErrorMessage = [FLTFirebaseDatabaseUtils codeAndMessageFromNSError:error]; + code = codeAndErrorMessage[0]; + message = codeAndErrorMessage[1]; + details = @{ + @"code" : code, + @"message" : message, + }; + } + if ([@"unknown" isEqualToString:code]) { + NSLog(@"FLTFirebaseDatabase: An error occurred while calling method %@", call.method); + } + flutterResult([FLTFirebasePlugin createFlutterErrorFromCode:code + message:message + optionalDetails:details + andOptionalNSError:error]); }; + + FLTFirebaseMethodCallResult *methodCallResult = + [FLTFirebaseMethodCallResult createWithSuccess:flutterResult andErrorBlock:errorBlock]; + if ([@"FirebaseDatabase#goOnline" isEqualToString:call.method]) { - [database goOnline]; - result(nil); + [self databaseGoOnline:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"FirebaseDatabase#goOffline" isEqualToString:call.method]) { - [database goOffline]; - result(nil); + [self databaseGoOffline:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"FirebaseDatabase#purgeOutstandingWrites" isEqualToString:call.method]) { - [database purgeOutstandingWrites]; - result(nil); - } else if ([@"FirebaseDatabase#setPersistenceEnabled" isEqualToString:call.method]) { - NSNumber *value = call.arguments[@"enabled"]; - @try { - database.persistenceEnabled = value.boolValue; - result([NSNumber numberWithBool:YES]); - } @catch (NSException *exception) { - if ([@"FIRDatabaseAlreadyInUse" isEqualToString:exception.name]) { - // Database is already in use, e.g. after hot reload/restart. - result([NSNumber numberWithBool:NO]); - } else { - @throw; - } - } - } else if ([@"FirebaseDatabase#setPersistenceCacheSizeBytes" isEqualToString:call.method]) { - NSNumber *value = call.arguments[@"cacheSize"]; - @try { - database.persistenceCacheSizeBytes = value.unsignedIntegerValue; - result([NSNumber numberWithBool:YES]); - } @catch (NSException *exception) { - if ([@"FIRDatabaseAlreadyInUse" isEqualToString:exception.name]) { - // Database is already in use, e.g. after hot reload/restart. - result([NSNumber numberWithBool:NO]); - } else { - @throw; - } - } - } else if ([@"FirebaseDatabase#setLoggingEnabled" isEqualToString:call.method]) { - BOOL enabled = call.arguments[@"enabled"]; - [FIRDatabase setLoggingEnabled:enabled]; - result(nil); + [self databasePurgeOutstandingWrites:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"DatabaseReference#set" isEqualToString:call.method]) { - [getReference(database, call.arguments) setValue:call.arguments[@"value"] - andPriority:call.arguments[@"priority"] - withCompletionBlock:defaultCompletionBlock]; + [self databaseSet:call.arguments withMethodCallResult:methodCallResult]; + } else if ([@"DatabaseReference#setWithPriority" isEqualToString:call.method]) { + [self databaseSetWithPriority:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"DatabaseReference#update" isEqualToString:call.method]) { - [getReference(database, call.arguments) updateChildValues:call.arguments[@"value"] - withCompletionBlock:defaultCompletionBlock]; + [self databaseUpdate:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"DatabaseReference#setPriority" isEqualToString:call.method]) { - [getReference(database, call.arguments) setPriority:call.arguments[@"priority"] - withCompletionBlock:defaultCompletionBlock]; + [self databaseSetPriority:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"DatabaseReference#runTransaction" isEqualToString:call.method]) { - [getReference(database, call.arguments) - runTransactionBlock:^FIRTransactionResult *_Nonnull(FIRMutableData *_Nonnull currentData) { - // Create semaphore to allow native side to wait while snapshot - // updates occur on the Dart side. - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - - NSObject *snapshot = - @{@"key" : currentData.key ?: [NSNull null], @"value" : currentData.value}; - - __block bool shouldAbort = false; - - [self.channel invokeMethod:@"DoTransaction" - arguments:@{ - @"transactionKey" : call.arguments[@"transactionKey"], - @"snapshot" : snapshot - } - result:^(id _Nullable result) { - if ([result isKindOfClass:[FlutterError class]]) { - FlutterError *flutterError = ((FlutterError *)result); - NSLog(@"Error code: %@", flutterError.code); - NSLog(@"Error message: %@", flutterError.message); - NSLog(@"Error details: %@", flutterError.details); - shouldAbort = true; - } else if ([result isEqual:FlutterMethodNotImplemented]) { - NSLog(@"DoTransaction not implemented on the Dart side."); - shouldAbort = true; - } else { - [self.updatedSnapshots - setObject:result - forKey:call.arguments[@"transactionKey"]]; - } - dispatch_semaphore_signal(semaphore); - }]; - - // Wait while Dart side updates the snapshot. Incoming transactionTimeout is in - // milliseconds so converting to nanoseconds for use with dispatch_semaphore_wait. - long result = dispatch_semaphore_wait( - semaphore, - dispatch_time(DISPATCH_TIME_NOW, - [call.arguments[@"transactionTimeout"] integerValue] * 1000000)); - - if (result == 0 && !shouldAbort) { - // Set FIRMutableData value to value returned from the Dart side. - currentData.value = - [self.updatedSnapshots objectForKey:call.arguments[@"transactionKey"]][@"value"]; - } else { - if (result != 0) { - NSLog(@"Transaction at %@ timed out.", [getReference(database, call.arguments) URL]); - } - return [FIRTransactionResult abort]; - } - - return [FIRTransactionResult successWithValue:currentData]; - } - andCompletionBlock:^(NSError *_Nullable error, BOOL committed, - FIRDataSnapshot *_Nullable snapshot) { - // Invoke transaction completion on the Dart side. - result(@{ - @"transactionKey" : call.arguments[@"transactionKey"], - @"error" : getDictionaryFromError(error) ?: [NSNull null], - @"committed" : [NSNumber numberWithBool:committed], - @"snapshot" : @{@"key" : snapshot.key ?: [NSNull null], @"value" : snapshot.value} - }); - }]; + [self databaseRunTransaction:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"OnDisconnect#set" isEqualToString:call.method]) { - [getReference(database, call.arguments) onDisconnectSetValue:call.arguments[@"value"] - andPriority:call.arguments[@"priority"] - withCompletionBlock:defaultCompletionBlock]; + [self onDisconnectSet:call.arguments withMethodCallResult:methodCallResult]; + } else if ([@"OnDisconnect#setWithPriority" isEqualToString:call.method]) { + [self onDisconnectSetWithPriority:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"OnDisconnect#update" isEqualToString:call.method]) { - [getReference(database, call.arguments) onDisconnectUpdateChildValues:call.arguments[@"value"] - withCompletionBlock:defaultCompletionBlock]; + [self onDisconnectUpdate:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"OnDisconnect#cancel" isEqualToString:call.method]) { - [getReference(database, call.arguments) - cancelDisconnectOperationsWithCompletionBlock:defaultCompletionBlock]; + [self onDisconnectCancel:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"Query#get" isEqualToString:call.method]) { - [getReference(database, call.arguments) getDataWithCompletionBlock:^( - NSError *error, FIRDataSnapshot *snapshot) { - result(@{ - @"error" : getDictionaryFromError(error) ?: [NSNull null], - @"snapshot" : - @{@"key" : snapshot.key ?: [NSNull null], @"value" : snapshot.value ?: [NSNull null]} - }); - }]; - } else if ([@"Query#observe" isEqualToString:call.method]) { - FIRDataEventType eventType = parseEventType(call.arguments[@"eventType"]); - __block FIRDatabaseHandle handle = [getDatabaseQuery(database, call.arguments) - observeEventType:eventType - andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *previousSiblingKey) { - [self.channel - invokeMethod:@"Event" - arguments:@{ - @"handle" : [NSNumber numberWithUnsignedInteger:handle], - @"snapshot" : @{ - @"key" : snapshot.key ?: [NSNull null], - @"value" : roundDoubles(snapshot.value) ?: [NSNull null], - }, - @"previousSiblingKey" : previousSiblingKey ?: [NSNull null], - @"childKeys" : [FLTFirebaseDatabasePlugin getSnapshotChildKeys:snapshot] - }]; - } - withCancelBlock:^(NSError *error) { - [self.channel invokeMethod:@"Error" - arguments:@{ - @"handle" : [NSNumber numberWithUnsignedInteger:handle], - @"error" : getDictionaryFromError(error), - }]; - }]; - result([NSNumber numberWithUnsignedInteger:handle]); - } else if ([@"Query#removeObserver" isEqualToString:call.method]) { - FIRDatabaseHandle handle = [call.arguments[@"handle"] unsignedIntegerValue]; - [getDatabaseQuery(database, call.arguments) removeObserverWithHandle:handle]; - result(nil); + [self queryGet:call.arguments withMethodCallResult:methodCallResult]; } else if ([@"Query#keepSynced" isEqualToString:call.method]) { - NSNumber *value = call.arguments[@"value"]; - [getDatabaseQuery(database, call.arguments) keepSynced:value.boolValue]; - result(nil); + [self queryKeepSynced:call.arguments withMethodCallResult:methodCallResult]; + } else if ([@"Query#observe" isEqualToString:call.method]) { + [self queryObserve:call.arguments withMethodCallResult:methodCallResult]; } else { - result(FlutterMethodNotImplemented); + methodCallResult.success(FlutterMethodNotImplemented); + } +} + +#pragma mark - FLTFirebasePlugin + +- (void)didReinitializeFirebaseCore:(void (^)(void))completion { + [self cleanupWithCompletion:completion]; +} + +- (NSDictionary *_Nonnull)pluginConstantsForFIRApp:(FIRApp *)firebase_app { + return @{}; +} + +- (NSString *_Nonnull)firebaseLibraryName { + return LIBRARY_NAME; +} + +- (NSString *_Nonnull)firebaseLibraryVersion { + return LIBRARY_VERSION; +} + +- (NSString *_Nonnull)flutterChannelName { + return kFLTFirebaseDatabaseChannelName; +} + +#pragma mark - Database API + +- (void)databaseGoOnline:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabase *database = [FLTFirebaseDatabaseUtils databaseFromArguments:arguments]; + [database goOnline]; + result.success(nil); +} + +- (void)databaseGoOffline:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabase *database = [FLTFirebaseDatabaseUtils databaseFromArguments:arguments]; + [database goOffline]; + result.success(nil); +} + +- (void)databasePurgeOutstandingWrites:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabase *database = [FLTFirebaseDatabaseUtils databaseFromArguments:arguments]; + [database purgeOutstandingWrites]; + result.success(nil); +} + +- (void)databaseSet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference setValue:arguments[@"value"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)databaseSetWithPriority:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference setValue:arguments[@"value"] + andPriority:arguments[@"priority"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)databaseUpdate:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference updateChildValues:arguments[@"value"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)databaseSetPriority:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference setPriority:arguments[@"priority"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)databaseRunTransaction:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + int transactionKey = [arguments[@"transactionKey"] intValue]; + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + + __weak FLTFirebaseDatabasePlugin *weakSelf = self; + [reference + runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + __strong FLTFirebaseDatabasePlugin *strongSelf = weakSelf; + // Create semaphore to allow native side to wait while updates occur on the Dart side. + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + // Whether the transaction was aborted in Dart by the user or by a Dart exception + // occurring. + __block bool aborted = false; + // Whether an exception occurred in users Dart transaction handler. + __block bool exception = false; + + id methodCallResultHandler = ^(id _Nullable result) { + aborted = [result[@"aborted"] boolValue]; + exception = [result[@"exception"] boolValue]; + currentData.value = result[@"value"]; + dispatch_semaphore_signal(semaphore); + }; + + [strongSelf->_channel invokeMethod:@"FirebaseDatabase#callTransactionHandler" + arguments:@{ + @"transactionKey" : @(transactionKey), + @"snapshot" : @{ + @"key" : currentData.key ?: [NSNull null], + @"value" : currentData.value ?: [NSNull null], + } + } + result:methodCallResultHandler]; + // Wait while Dart side updates the value. + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + + if (aborted || exception) { + return [FIRTransactionResult abort]; + } + return [FIRTransactionResult successWithValue:currentData]; + } + andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(@{ + @"committed" : @(committed), + @"snapshot" : [FLTFirebaseDatabaseUtils dictionaryFromSnapshot:snapshot], + }); + } + } + withLocalEvents:[arguments[@"transactionApplyLocally"] boolValue]]; +} + +- (void)onDisconnectSet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference onDisconnectSetValue:arguments[@"value"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)onDisconnectSetWithPriority:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference onDisconnectSetValue:arguments[@"value"] + andPriority:arguments[@"priority"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)onDisconnectUpdate:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference onDisconnectUpdateChildValues:arguments[@"value"] + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)onDisconnectCancel:(id)arguments + withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseReference *reference = + [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + [reference + cancelDisconnectOperationsWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + result.success(nil); + } + }]; +} + +- (void)queryGet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseQuery *query = [FLTFirebaseDatabaseUtils databaseQueryFromArguments:arguments]; + [query getDataWithCompletionBlock:^(NSError *error, FIRDataSnapshot *snapshot) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else + result.success(@{ + @"snapshot" : [FLTFirebaseDatabaseUtils dictionaryFromSnapshot:snapshot], + }); + }]; +} + +- (void)queryKeepSynced:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseQuery *query = [FLTFirebaseDatabaseUtils databaseQueryFromArguments:arguments]; + [query keepSynced:[arguments[@"value"] boolValue]]; + result.success(nil); +} + +- (void)queryObserve:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDatabaseQuery *databaseQuery = [FLTFirebaseDatabaseUtils databaseQueryFromArguments:arguments]; + NSString *eventChannelNamePrefix = arguments[@"eventChannelNamePrefix"]; + int newListenersCount; + @synchronized(_listenerCounts) { + NSNumber *currentListenersCount = _listenerCounts[eventChannelNamePrefix]; + newListenersCount = currentListenersCount == nil ? 1 : [currentListenersCount intValue] + 1; + _listenerCounts[eventChannelNamePrefix] = @(newListenersCount); } + NSString *eventChannelName = + [NSString stringWithFormat:@"%@#%d", eventChannelNamePrefix, newListenersCount]; + + FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:eventChannelName + binaryMessenger:_binaryMessenger]; + __weak FLTFirebaseDatabasePlugin *weakSelf = self; + FLTFirebaseDatabaseObserveStreamHandler *streamHandler = + [[FLTFirebaseDatabaseObserveStreamHandler alloc] + initWithFIRDatabaseQuery:databaseQuery + andOnDisposeBlock:^() { + __strong FLTFirebaseDatabasePlugin *strongSelf = weakSelf; + [eventChannel setStreamHandler:nil]; + @synchronized(strongSelf->_listenerCounts) { + NSNumber *currentListenersCount = + strongSelf->_listenerCounts[eventChannelNamePrefix]; + strongSelf->_listenerCounts[eventChannelNamePrefix] = + @(currentListenersCount == nil ? 0 : [currentListenersCount intValue] - 1); + } + }]; + [eventChannel setStreamHandler:streamHandler]; + _streamHandlers[eventChannelName] = streamHandler; + result.success(eventChannelName); } @end diff --git a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseUtils.h b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseUtils.h new file mode 100644 index 000000000000..aa2400e9ad85 --- /dev/null +++ b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseUtils.h @@ -0,0 +1,19 @@ +// Copyright 2021 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 +#import + +@interface FLTFirebaseDatabaseUtils : NSObject + ++ (dispatch_queue_t)dispatchQueue; ++ (FIRDatabase *)databaseFromArguments:(id)arguments; ++ (FIRDatabaseReference *)databaseReferenceFromArguments:(id)arguments; ++ (FIRDatabaseQuery *)databaseQueryFromArguments:(id)arguments; ++ (NSDictionary *)dictionaryFromSnapshot:(FIRDataSnapshot *)snapshot + withPreviousChildKey:(NSString *)previousChildName; ++ (NSDictionary *)dictionaryFromSnapshot:(FIRDataSnapshot *)snapshot; ++ (NSArray *)codeAndMessageFromNSError:(NSError *)error; ++ (FIRDataEventType)eventTypeFromString:(NSString *)eventTypeString; + +@end diff --git a/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseUtils.m b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseUtils.m new file mode 100644 index 000000000000..db931bdc5bb0 --- /dev/null +++ b/packages/firebase_database/firebase_database/ios/Classes/FLTFirebaseDatabaseUtils.m @@ -0,0 +1,263 @@ +// Copyright 2021 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 "FLTFirebaseDatabaseUtils.h" +#import + +@implementation FLTFirebaseDatabaseUtils +static __strong NSMutableDictionary *cachedDatabaseInstances = nil; + ++ (dispatch_queue_t)dispatchQueue { + static dispatch_once_t once; + __strong static dispatch_queue_t sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = + dispatch_queue_create("io.flutter.plugins.firebase.database", DISPATCH_QUEUE_SERIAL); + }); + return sharedInstance; +} + ++ (FIRDatabase *)databaseFromArguments:(id)arguments { + NSString *appName = arguments[@"appName"] == nil ? @"[DEFAULT]" : arguments[@"appName"]; + NSString *databaseURL = arguments[@"databaseURL"] == nil ? @"" : arguments[@"databaseURL"]; + NSString *instanceKey = [appName stringByAppendingString:databaseURL]; + if (cachedDatabaseInstances == nil) { + cachedDatabaseInstances = [[NSMutableDictionary alloc] init]; + } + FIRDatabase *cachedInstance = cachedDatabaseInstances[instanceKey]; + if (cachedInstance != nil) { + return cachedInstance; + } + + FIRApp *app = [FLTFirebasePlugin firebaseAppNamed:appName]; + FIRDatabase *database; + + if (databaseURL.length == 0) { + database = [FIRDatabase databaseForApp:app]; + } else { + database = [FIRDatabase databaseForApp:app URL:databaseURL]; + } + + // [database setCallbackQueue:[self dispatchQueue]]; + NSNumber *persistenceEnabled = arguments[@"persistenceEnabled"]; + if (persistenceEnabled != nil) { + database.persistenceEnabled = [persistenceEnabled boolValue]; + } + + NSNumber *cacheSizeBytes = arguments[@"cacheSizeBytes"]; + if (cacheSizeBytes != nil) { + database.persistenceCacheSizeBytes = [cacheSizeBytes unsignedIntegerValue]; + } + + NSNumber *loggingEnabled = arguments[@"loggingEnabled"]; + if (loggingEnabled != nil) { + [FIRDatabase setLoggingEnabled:[loggingEnabled boolValue]]; + } + + NSString *emulatorHost = arguments[@"emulatorHost"]; + NSNumber *emulatorPort = arguments[@"emulatorPort"]; + if (emulatorHost != nil && emulatorPort != nil) { + [database useEmulatorWithHost:emulatorHost port:[emulatorPort integerValue]]; + } + + cachedDatabaseInstances[instanceKey] = database; + return database; +} + ++ (FIRDatabaseReference *)databaseReferenceFromArguments:(id)arguments { + FIRDatabase *database = [FLTFirebaseDatabaseUtils databaseFromArguments:arguments]; + return [database referenceWithPath:arguments[@"path"]]; +} + ++ (FIRDatabaseQuery *)databaseQuery:(FIRDatabaseQuery *)query applyLimitModifier:(id)modifier { + NSString *name = modifier[@"name"]; + NSNumber *limit = modifier[@"limit"]; + if ([name isEqualToString:@"limitToFirst"]) { + return [query queryLimitedToFirst:limit.unsignedIntValue]; + } + if ([name isEqualToString:@"limitToLast"]) { + return [query queryLimitedToLast:limit.unsignedIntValue]; + } + return query; +} + ++ (FIRDatabaseQuery *)databaseQuery:(FIRDatabaseQuery *)query applyOrderModifier:(id)modifier { + NSString *name = [modifier valueForKey:@"name"]; + if ([name isEqualToString:@"orderByKey"]) { + return [query queryOrderedByKey]; + } + if ([name isEqualToString:@"orderByValue"]) { + return [query queryOrderedByValue]; + } + if ([name isEqualToString:@"orderByPriority"]) { + return [query queryOrderedByPriority]; + } + if ([name isEqualToString:@"orderByChild"]) { + NSString *path = [modifier valueForKey:@"path"]; + return [query queryOrderedByChild:path]; + } + return query; +} + ++ (FIRDatabaseQuery *)databaseQuery:(FIRDatabaseQuery *)query applyCursorModifier:(id)modifier { + NSString *name = [modifier valueForKey:@"name"]; + NSString *key = [modifier valueForKey:@"key"]; + id value = [modifier valueForKey:@"value"]; + if ([name isEqualToString:@"startAt"]) { + if (key != nil) { + return [query queryStartingAtValue:value childKey:key]; + } else { + return [query queryStartingAtValue:value]; + } + } + if ([name isEqualToString:@"startAfter"]) { + if (key != nil) { + return [query queryStartingAfterValue:value childKey:key]; + } else { + return [query queryStartingAfterValue:value]; + } + } + if ([name isEqualToString:@"endAt"]) { + if (key != nil) { + return [query queryEndingAtValue:value childKey:key]; + } else { + return [query queryEndingAtValue:value]; + } + } + if ([name isEqualToString:@"endBefore"]) { + if (key != nil) { + return [query queryEndingBeforeValue:value childKey:key]; + } else { + return [query queryEndingBeforeValue:value]; + } + } + return query; +} + ++ (FIRDatabaseQuery *)databaseQueryFromArguments:(id)arguments { + FIRDatabaseQuery *query = [FLTFirebaseDatabaseUtils databaseReferenceFromArguments:arguments]; + NSArray *modifiers = arguments[@"modifiers"]; + for (NSDictionary *modifier in modifiers) { + NSString *type = [modifier valueForKey:@"type"]; + if ([type isEqualToString:@"limit"]) { + query = [self databaseQuery:query applyLimitModifier:modifier]; + } else if ([type isEqualToString:@"cursor"]) { + query = [self databaseQuery:query applyCursorModifier:modifier]; + } else if ([type isEqualToString:@"orderBy"]) { + query = [self databaseQuery:query applyOrderModifier:modifier]; + } + } + return query; +} + ++ (NSDictionary *)dictionaryFromSnapshot:(FIRDataSnapshot *)snapshot + withPreviousChildKey:(NSString *)previousChildKey { + return @{ + @"snapshot" : [self dictionaryFromSnapshot:snapshot], + @"previousChildKey" : previousChildKey ?: [NSNull null], + }; +} + ++ (NSDictionary *)dictionaryFromSnapshot:(FIRDataSnapshot *)snapshot { + NSMutableArray *childKeys = [NSMutableArray array]; + if (snapshot.childrenCount > 0) { + NSEnumerator *children = [snapshot children]; + FIRDataSnapshot *child; + child = [children nextObject]; + while (child) { + [childKeys addObject:child.key]; + child = [children nextObject]; + } + } + + return @{ + @"key" : snapshot.key ?: [NSNull null], + @"value" : snapshot.value ?: [NSNull null], + @"priority" : snapshot.priority ?: [NSNull null], + @"childKeys" : childKeys, + }; +} + ++ (NSArray *)codeAndMessageFromNSError:(NSError *)error { + NSString *code = @"unknown"; + + if (error == nil) { + return @[ code, @"An unknown error has occurred." ]; + } + + NSString *message; + + switch (error.code) { + case 1: + code = @"permission-denied"; + message = @"Client doesn't have permission to access the desired data."; + break; + case 2: + code = @"unavailable"; + message = @"The service is unavailable."; + break; + case 3: + code = @"write-cancelled"; + message = @"The write was cancelled by the user."; + break; + case -1: + code = @"data-stale"; + message = @"The transaction needs to be run again with current data."; + break; + case -2: + code = @"failure"; + message = @"The server indicated that this operation failed."; + break; + case -4: + code = @"disconnected"; + message = @"The operation had to be aborted due to a network disconnect."; + break; + case -6: + code = @"expired-token"; + message = @"The supplied auth token has expired."; + break; + case -7: + code = @"invalid-token"; + message = @"The supplied auth token was invalid."; + break; + case -8: + code = @"max-retries"; + message = @"The transaction had too many retries."; + break; + case -9: + code = @"overridden-by-set"; + message = @"The transaction was overridden by a subsequent set"; + break; + case -11: + code = @"user-code-exception"; + message = @"User code called from the Firebase Database runloop threw an exception."; + break; + case -24: + code = @"network-error"; + message = @"The operation could not be performed due to a network error."; + break; + default: + code = @"unknown"; + message = [error localizedDescription]; + } + + return @[ code, message ]; +} + ++ (FIRDataEventType)eventTypeFromString:(NSString *)eventTypeString { + if ([eventTypeString isEqualToString:@"value"]) { + return FIRDataEventTypeValue; + } else if ([eventTypeString isEqualToString:@"childAdded"]) { + return FIRDataEventTypeChildAdded; + } else if ([eventTypeString isEqualToString:@"childChanged"]) { + return FIRDataEventTypeChildChanged; + } else if ([eventTypeString isEqualToString:@"childRemoved"]) { + return FIRDataEventTypeChildRemoved; + } else if ([eventTypeString isEqualToString:@"childMoved"]) { + return FIRDataEventTypeChildMoved; + } + return FIRDataEventTypeValue; +} + +@end diff --git a/packages/firebase_database/firebase_database/lib/firebase_database.dart b/packages/firebase_database/firebase_database/lib/firebase_database.dart index a16a349fa86d..92b2d6bdc8f9 100755 --- a/packages/firebase_database/firebase_database/lib/firebase_database.dart +++ b/packages/firebase_database/firebase_database/lib/firebase_database.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -8,15 +7,23 @@ library firebase_database; import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' + show FirebasePluginPlatform; import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; export 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart' - show ServerValue, MutableData, TransactionHandler, EventType; + show ServerValue, TransactionHandler, DatabaseEventType, Transaction; + +part 'src/data_snapshot.dart'; + +part 'src/database_event.dart'; part 'src/database_reference.dart'; -part 'src/event.dart'; + part 'src/firebase_database.dart'; + part 'src/on_disconnect.dart'; + part 'src/query.dart'; + +part 'src/transaction_result.dart'; diff --git a/packages/firebase_database/firebase_database/lib/src/data_snapshot.dart b/packages/firebase_database/firebase_database/lib/src/data_snapshot.dart new file mode 100644 index 000000000000..e9b7ecf2f5db --- /dev/null +++ b/packages/firebase_database/firebase_database/lib/src/data_snapshot.dart @@ -0,0 +1,42 @@ +part of firebase_database; + +/// A DataSnapshot contains data from a Firebase Database location. +/// Any time you read Firebase data, you receive the data as a DataSnapshot. +class DataSnapshot { + final DataSnapshotPlatform _delegate; + + DataSnapshot._(this._delegate) { + DataSnapshotPlatform.verifyExtends(_delegate); + } + + /// The key of the location that generated this DataSnapshot or null if at + /// database root. + String? get key => _delegate.key; + + /// The Reference for the location that generated this DataSnapshot. + DatabaseReference get ref => DatabaseReference._(_delegate.ref); + + /// Returns the contents of this data snapshot as native types. + Object? get value => _delegate.value; + + /// Gets the priority value of the data in this [DataSnapshot] or null if no + /// priority set. + Object? get priority => _delegate.priority; + + /// Ascertains whether the value exists at the Firebase Database location. + bool get exists => _delegate.exists; + + /// Returns true if the specified child path has (non-null) data. + bool hasChild(String path) => _delegate.hasChild(path); + + /// Gets another [DataSnapshot] for the location at the specified relative path. + /// The relative path can either be a simple child name (for example, "ada") + /// or a deeper, slash-separated path (for example, "ada/name/first"). + /// If the child location has no data, an empty DataSnapshot (that is, a + /// DataSnapshot whose [value] is null) is returned. + DataSnapshot child(String path) => DataSnapshot._(_delegate.child(path)); + + /// An iterator for snapshots of the child nodes in this snapshot. + Iterable get children => + _delegate.children.map((e) => DataSnapshot._(e)); +} diff --git a/packages/firebase_database/firebase_database/lib/src/database_event.dart b/packages/firebase_database/firebase_database/lib/src/database_event.dart new file mode 100644 index 000000000000..ea1bf2ef5000 --- /dev/null +++ b/packages/firebase_database/firebase_database/lib/src/database_event.dart @@ -0,0 +1,25 @@ +// 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. + +part of firebase_database; + +/// `DatabaseEvent` encapsulates a DataSnapshot and possibly also the key of its +/// previous sibling, which can be used to order the snapshots. +class DatabaseEvent { + final DatabaseEventPlatform _delegate; + + DatabaseEvent._(this._delegate) { + DatabaseEventPlatform.verifyExtends(_delegate); + } + + /// The type of event. + DatabaseEventType get type => _delegate.type; + + /// The [DataSnapshot] for this event. + DataSnapshot get snapshot => DataSnapshot._(_delegate.snapshot); + + /// A string containing the key of the previous sibling child by sort order, + /// or null if it is the first child. + String? get previousChildKey => _delegate.previousChildKey; +} diff --git a/packages/firebase_database/firebase_database/lib/src/database_reference.dart b/packages/firebase_database/firebase_database/lib/src/database_reference.dart index 0cf3a459ff56..02ba220f5e33 100644 --- a/packages/firebase_database/firebase_database/lib/src/database_reference.dart +++ b/packages/firebase_database/firebase_database/lib/src/database_reference.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -9,41 +8,43 @@ part of firebase_database; /// Database and can be used for reading or writing data to that location. /// /// This class is the starting point for all Firebase Database operations. -/// After you’ve obtained your first DatabaseReference via -/// `FirebaseDatabase.reference()`, you can use it to read data +/// After you’ve obtained your first `DatabaseReference` via +/// `FirebaseDatabase.instance.ref()`, you can use it to read data /// (ie. `onChildAdded`), write data (ie. `setValue`), and to create new /// `DatabaseReference`s (ie. `child`). class DatabaseReference extends Query { - DatabaseReferencePlatform _databaseReferencePlatform; + DatabaseReferencePlatform _delegate; - DatabaseReference._(this._databaseReferencePlatform) - : super._(_databaseReferencePlatform); + DatabaseReference._(this._delegate) : super._(_delegate); /// Gets a DatabaseReference for the location at the specified relative /// path. The relative path can either be a simple child key (e.g. ‘fred’) or /// a deeper slash-separated path (e.g. ‘fred/name/first’). DatabaseReference child(String path) { - return DatabaseReference._(_databaseReferencePlatform.child(path)); + return DatabaseReference._(_delegate.child(path)); } /// Gets a DatabaseReference for the parent location. If this instance /// refers to the root of your Firebase Database, it has no parent, and /// therefore parent() will return null. - DatabaseReference? parent() { - if (_databaseReferencePlatform.pathComponents.isEmpty) { + DatabaseReference? get parent { + final _platformParent = _delegate.parent; + + if (_platformParent == null) { return null; } - return DatabaseReference._(_databaseReferencePlatform.parent()!); + + return DatabaseReference._(_platformParent); } - /// Gets a FIRDatabaseReference for the root location. - DatabaseReference root() { - return DatabaseReference._(_databaseReferencePlatform.root()); + /// Gets a [DatabaseReference] for the root location. + DatabaseReference get root { + return DatabaseReference._(_delegate.root()); } /// Gets the last token in a Firebase Database location (e.g. ‘fred’ in /// https://SampleChat.firebaseIO-demo.com/users/fred) - String get key => _databaseReferencePlatform.pathComponents.last; + String? get key => _delegate.key; /// Generates a new child location using a unique key and returns a /// DatabaseReference to it. This is useful when the children of a Firebase @@ -53,10 +54,26 @@ class DatabaseReference extends Query { /// client-generated timestamp so that the resulting list will be /// chronologically-sorted. DatabaseReference push() { - return DatabaseReference._(_databaseReferencePlatform.push()); + return DatabaseReference._(_delegate.push()); + } + + /// Write a `value` to the location. + /// + /// This will overwrite any data at this location and all child locations. + /// + /// Data types that are allowed are String, boolean, int, double, Map, List. + /// + /// The effect of the write will be visible immediately and the corresponding + /// events will be triggered. Synchronization of the data to the Firebase + /// Database servers will also be started. + /// + /// Passing null for the new value means all data at this location or any + /// child location will be deleted. + Future set(Object? value) { + return _delegate.set(value); } - /// Write `value` to the location with the specified `priority` if applicable. + /// Write a `value` to the location with the specified `priority` if applicable. /// /// This will overwrite any data at this location and all child locations. /// @@ -68,13 +85,39 @@ class DatabaseReference extends Query { /// /// Passing null for the new value means all data at this location or any /// child location will be deleted. - Future set(dynamic value, {dynamic priority}) { - return _databaseReferencePlatform.set(value, priority: priority); + /// Note: [priority] can be a [String], [double] or [null] value. + Future setWithPriority(Object? value, Object? priority) { + assert(priority == null || priority is String || priority is num); + return _delegate.setWithPriority(value, priority); } - /// Update the node with the `value` - Future update(Map value) { - return _databaseReferencePlatform.update(value); + /// Writes multiple values to the Database at once. + /// + /// The values argument contains multiple property-value pairs that will be + /// written to the Database together. Each child property can either be a + /// simple property (for example, "name") or a relative path (for example, + /// "name/first") from the current location to the data to update. + /// + /// As opposed to the [set] method, [update] can be use to selectively update + /// only the referenced properties at the current location + /// (instead of replacing all the child properties at the current location). + /// + /// The effect of the write will be visible immediately, and the corresponding + /// events ('value', 'child_added', etc.) will be triggered. Synchronization + /// of the data to the Firebase servers will also be started, and the + /// returned [Future] will resolve when complete. + /// + /// A single [update] will generate a single "value" event at the location + /// where the [update] was performed, regardless of how many children were modified. + /// + /// Note that modifying data with [update] will cancel any pending transactions + /// at that location, so extreme care should be taken if mixing [update] and + /// [runTransaction] to modify the same data. + /// + /// Passing null to a [Map] value in [update] will remove the remove the value + /// at the specified location. + Future update(Map value) { + return _delegate.update(value); } /// Sets a priority for the data at this Firebase Database location. @@ -101,8 +144,11 @@ class DatabaseReference extends Query { /// Note that priorities are parsed and ordered as IEEE 754 double-precision /// floating-point numbers. Keys are always stored as strings and are treated /// as numbers only when they can be parsed as a 32-bit integer. - Future setPriority(dynamic priority) async { - return _databaseReferencePlatform.setPriority(priority); + /// + /// Note: [priority] can be a [String], [double] or [null] value. + Future setPriority(Object? priority) async { + assert(priority == null || priority is String || priority is num); + return _delegate.setPriority(priority); } /// Remove the data at this Firebase Database location. Any data at child @@ -119,31 +165,18 @@ class DatabaseReference extends Query { /// this Firebase Database location. Future runTransaction( TransactionHandler transactionHandler, { - Duration timeout = const Duration(seconds: 5), + bool applyLocally = true, }) async { - TransactionResultPlatform transactionResult = - await _databaseReferencePlatform.runTransaction( - transactionHandler, - timeout: timeout, - ); return TransactionResult._( - transactionResult.error == null - ? null - : DatabaseError._(transactionResult.error!), - transactionResult.committed, - DataSnapshot._(transactionResult.dataSnapshot), + await _delegate.runTransaction( + transactionHandler, + applyLocally: applyLocally, + ), ); } + /// Returns an [OnDisconnect] instance. OnDisconnect onDisconnect() { - return OnDisconnect._(_databaseReferencePlatform.onDisconnect()); + return OnDisconnect._(_delegate.onDisconnect()); } } - -class TransactionResult { - const TransactionResult._(this.error, this.committed, this.dataSnapshot); - - final DatabaseError? error; - final bool committed; - final DataSnapshot? dataSnapshot; -} diff --git a/packages/firebase_database/firebase_database/lib/src/event.dart b/packages/firebase_database/firebase_database/lib/src/event.dart deleted file mode 100644 index c199eee5080c..000000000000 --- a/packages/firebase_database/firebase_database/lib/src/event.dart +++ /dev/null @@ -1,57 +0,0 @@ -// ignore_for_file: require_trailing_commas -// 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. - -part of firebase_database; - -/// `Event` encapsulates a DataSnapshot and possibly also the key of its -/// previous sibling, which can be used to order the snapshots. -class Event { - final EventPlatform _delegate; - - Event._(this._delegate); - - DataSnapshot get snapshot => DataSnapshot._(_delegate.snapshot); - - String? get previousSiblingKey => _delegate.previousSiblingKey; -} - -/// A DataSnapshot contains data from a Firebase Database location. -/// Any time you read Firebase data, you receive the data as a DataSnapshot. -class DataSnapshot { - final DataSnapshotPlatform? _delegate; - - DataSnapshot._(this._delegate); - - /// The key of the location that generated this DataSnapshot. - String? get key => _delegate?.key; - - /// Returns the contents of this data snapshot as native types. - dynamic get value => _delegate?.value; - - /// Ascertains whether the value exists at the Firebase Database location. - bool get exists => _delegate?.exists ?? false; -} - -/// A DatabaseError contains code, message and details of a Firebase Database -/// Error that results from a transaction operation at a Firebase Database -/// location. -class DatabaseError { - DatabaseErrorPlatform _delegate; - - DatabaseError._(this._delegate); - - /// One of the defined status codes, depending on the error. - int get code => _delegate.code; - - /// A human-readable description of the error. - String get message => _delegate.message; - - /// Human-readable details on the error and additional information. - String get details => _delegate.details; - - @override - // ignore: no_runtimetype_tostring - String toString() => '$runtimeType($code, $message, $details)'; -} diff --git a/packages/firebase_database/firebase_database/lib/src/firebase_database.dart b/packages/firebase_database/firebase_database/lib/src/firebase_database.dart index 11ca5907401a..a914fc632f52 100644 --- a/packages/firebase_database/firebase_database/lib/src/firebase_database.dart +++ b/packages/firebase_database/firebase_database/lib/src/firebase_database.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -6,35 +5,110 @@ part of firebase_database; /// The entry point for accessing a Firebase Database. You can get an instance -/// by calling `FirebaseDatabase.instance`. To access a location in the database -/// and read or write data, use `reference()`. -class FirebaseDatabase { +/// by calling `FirebaseDatabase.instance` or `FirebaseDatabase.instanceFor()`. +class FirebaseDatabase extends FirebasePluginPlatform { + /// Returns an instance of [FirebaseDatabase] with the given [FirebaseApp] and/or + /// [databaseURL). + @Deprecated( + 'Accessing FirebaseDatabase via the constructor is now deprecated. Use `.instanceFor` instead.', + ) + factory FirebaseDatabase({FirebaseApp? app, String? databaseURL}) { + return FirebaseDatabase.instanceFor( + app: app ?? Firebase.app(), + databaseURL: databaseURL, + ); + } + + FirebaseDatabase._({required this.app, this.databaseURL}) + : super(app.name, 'plugins.flutter.io/firebase_database'); + + /// The [FirebaseApp] for this current [FirebaseDatabase] instance. + FirebaseApp app; + + /// A custom Database URL for this instance. + String? databaseURL; + + static final Map _cachedInstances = {}; + + /// Returns an instance using the default [FirebaseApp]. + static FirebaseDatabase get instance { + return FirebaseDatabase.instanceFor( + app: Firebase.app(), + ); + } + + /// Returns an instance using a specified [FirebaseApp]. + static FirebaseDatabase instanceFor({ + required FirebaseApp app, + String? databaseURL, + }) { + if (_cachedInstances.containsKey(app.name)) { + return _cachedInstances[app.name]!; + } + + FirebaseDatabase newInstance = + FirebaseDatabase._(app: app, databaseURL: databaseURL); + _cachedInstances[app.name] = newInstance; + + return newInstance; + } + /// Gets an instance of [FirebaseDatabase]. /// /// If [app] is specified, its options should include a [databaseURL]. - DatabasePlatform? _delegatePackingProperty; DatabasePlatform get _delegate { - _delegatePackingProperty ??= DatabasePlatform.instance; - return _delegatePackingProperty!; + return _delegatePackingProperty ??= + DatabasePlatform.instanceFor(app: app, databaseURL: databaseURL); } - FirebaseDatabase({FirebaseApp? app, String? databaseURL}) - : _delegatePackingProperty = app != null || databaseURL != null - ? DatabasePlatform.instanceFor(app: app, databaseURL: databaseURL) - : DatabasePlatform.instance; + /// Changes this instance to point to a FirebaseDatabase emulator running locally. + /// + /// Set the [host] of the local emulator, such as "localhost" + /// Set the [port] of the local emulator, such as "9000" (default is 9000) + /// + /// Note: Must be called immediately, prior to accessing FirebaseFirestore methods. + /// Do not use with production credentials as emulator traffic is not encrypted. + void useDatabaseEmulator(String host, int port) { + _delegate.useDatabaseEmulator(host, port); + } + + /// Returns a [DatabaseReference] accessing the root of the database. + @Deprecated('Deprecated in favor of calling `ref()`.') + DatabaseReference reference() => ref(); - static FirebaseDatabase _instance = FirebaseDatabase(); + /// Returns a [DatabaseReference] representing the location in the Database + /// corresponding to the provided path. + /// If no path is provided, the Reference will point to the root of the Database. + DatabaseReference ref([String? path]) { + return DatabaseReference._(_delegate.ref(path)); + } - /// Gets the instance of FirebaseDatabase for the default Firebase app. - static FirebaseDatabase get instance => _instance; + /// Returns a [DatabaseReference] representing the location in the Database + /// corresponding to the provided Firebase URL. + DatabaseReference refFromURL(String url) { + if (!url.startsWith('https://')) { + throw ArgumentError.value(url, 'must be a valid URL', 'url'); + } - @visibleForTesting - static MethodChannel get channel => MethodChannelDatabase.channel; + Uri uri = Uri.parse(url); + String? currentDatabaseUrl = databaseURL ?? app.options.databaseURL; + if (currentDatabaseUrl != null) { + if (uri.origin != currentDatabaseUrl) { + throw ArgumentError.value( + url, + 'must equal the current FirebaseDatabase instance databaseURL', + 'url', + ); + } + } - /// Gets a DatabaseReference for the root of your Firebase Database. - DatabaseReference reference() => DatabaseReference._(_delegate.reference()); + if (uri.pathSegments.isNotEmpty) { + return DatabaseReference._(_delegate.ref(uri.path)); + } + return DatabaseReference._(_delegate.ref()); + } /// Attempts to sets the database persistence to [enabled]. /// @@ -54,7 +128,7 @@ class FirebaseDatabase { /// to `true`, the data will be persisted to on-device (disk) storage and will /// thus be available again when the app is restarted (even when there is no /// network connectivity at that time). - Future setPersistenceEnabled(bool enabled) async { + void setPersistenceEnabled(bool enabled) { return _delegate.setPersistenceEnabled(enabled); } @@ -62,7 +136,7 @@ class FirebaseDatabase { /// /// By default the Firebase Database client will use up to 10MB of disk space /// to cache data. If the cache grows beyond this size, the client will start - /// removing data that hasn’t been recently used. If you find that your + /// removing data that hasn't been recently used. If you find that your /// application caches too little or too much data, call this method to change /// the cache size. /// @@ -75,14 +149,14 @@ class FirebaseDatabase { /// Note that the specified cache size is only an approximation and the size /// on disk may temporarily exceed it at times. Cache sizes smaller than 1 MB /// or greater than 100 MB are not supported. - Future setPersistenceCacheSizeBytes(int cacheSize) async { + void setPersistenceCacheSizeBytes(int cacheSize) { return _delegate.setPersistenceCacheSizeBytes(cacheSize); } /// Enables verbose diagnostic logging for debugging your application. /// This must be called before any other usage of FirebaseDatabase instance. /// By default, diagnostic logging is disabled. - Future setLoggingEnabled(bool enabled) { + void setLoggingEnabled(bool enabled) { return _delegate.setLoggingEnabled(enabled); } diff --git a/packages/firebase_database/firebase_database/lib/src/on_disconnect.dart b/packages/firebase_database/firebase_database/lib/src/on_disconnect.dart index 14b6a4bc2f65..0f5ac85279bf 100644 --- a/packages/firebase_database/firebase_database/lib/src/on_disconnect.dart +++ b/packages/firebase_database/firebase_database/lib/src/on_disconnect.dart @@ -1,29 +1,60 @@ -// ignore_for_file: require_trailing_commas // 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. part of firebase_database; +/// The onDisconnect class allows you to write or clear data when your client +/// disconnects from the Database server. These updates occur whether your +/// client disconnects cleanly or not, so you can rely on them to clean up data +/// even if a connection is dropped or a client crashes. +/// +/// The onDisconnect class is most commonly used to manage presence in +/// applications where it is useful to detect how many clients are connected +/// and when other clients disconnect. +/// +/// To avoid problems when a connection is dropped before the requests can be +/// transferred to the Database server, these functions should be called before +/// writing any data. +/// +/// Note that onDisconnect operations are only triggered once. If you want an +/// operation to occur each time a disconnect occurs, you'll need to +/// re-establish the onDisconnect operations each time you reconnect. class OnDisconnect { - OnDisconnectPlatform _onDisconnectPlatform; + OnDisconnectPlatform _delegate; - OnDisconnect._(this._onDisconnectPlatform) - : path = _onDisconnectPlatform.reference.path; + OnDisconnect._(this._delegate) { + OnDisconnectPlatform.verifyExtends(_delegate); + } - final String path; + /// Ensures the data at this location is set to the specified value when the + /// client is disconnected (due to closing the browser, navigating to a new + /// page, or network issues). + Future set(Object? value) { + return _delegate.set(value); + } - Future set(dynamic value, {dynamic priority}) { - return _onDisconnectPlatform.set(value, priority: priority); + /// Ensures the data at this location is set with a priority to the specified + /// value when the client is disconnected (due to closing the browser, + /// navigating to a new page, or network issues). + Future setWithPriority(Object? value, Object? priority) { + return _delegate.setWithPriority(value, priority); } + /// Ensures the data at this location is deleted when the client is + /// disconnected (due to closing the browser, navigating to a new page, + /// or network issues). Future remove() => set(null); + /// Cancels all previously queued onDisconnect() set or update events for + /// this location and all children. Future cancel() { - return _onDisconnectPlatform.cancel(); + return _delegate.cancel(); } - Future update(Map value) { - return _onDisconnectPlatform.update(value); + /// Writes multiple values at this location when the client is disconnected + /// (due to closing the browser, navigating to a new page, or network issues). + Future update(Map value) { + return _delegate.update(value); } } diff --git a/packages/firebase_database/firebase_database/lib/src/query.dart b/packages/firebase_database/firebase_database/lib/src/query.dart index df3faf23189c..c79427e08dc5 100644 --- a/packages/firebase_database/firebase_database/lib/src/query.dart +++ b/packages/firebase_database/firebase_database/lib/src/query.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -7,91 +6,168 @@ part of firebase_database; /// Represents a query over the data at a particular location. class Query { - Query._(this._queryPlatform); + Query._(this._queryDelegate, [QueryModifiers? modifiers]) + : _modifiers = modifiers ?? QueryModifiers([]); - final QueryPlatform _queryPlatform; + final QueryPlatform _queryDelegate; - /// Slash-delimited path representing the database location of this query. - String get path => _queryPlatform.path; + final QueryModifiers _modifiers; - Map buildArguments() { - return _queryPlatform.buildArguments(); - } - - /// Listens for a single value event and then stops listening. - Future once() async => - DataSnapshot._(await _queryPlatform.once()); + /// Obtains a [DatabaseReference] corresponding to this query's location. + DatabaseReference get ref => DatabaseReference._(_queryDelegate.ref); /// Gets the most up-to-date result for this query. Future get() async { - return DataSnapshot._(await _queryPlatform.get()); + return DataSnapshot._(await _queryDelegate.get(_modifiers)); + } + + /// Listens for exactly one event of the specified event type, and then stops listening. + /// Defaults to [DatabaseEventType.value] if no [eventType] provided. + Future once([ + DatabaseEventType eventType = DatabaseEventType.value, + ]) async { + switch (eventType) { + case DatabaseEventType.childAdded: + return DatabaseEvent._( + await _queryDelegate.onChildAdded(_modifiers).first, + ); + case DatabaseEventType.childRemoved: + return DatabaseEvent._( + await _queryDelegate.onChildRemoved(_modifiers).first, + ); + case DatabaseEventType.childChanged: + return DatabaseEvent._( + await _queryDelegate.onChildChanged(_modifiers).first, + ); + case DatabaseEventType.childMoved: + return DatabaseEvent._( + await _queryDelegate.onChildMoved(_modifiers).first, + ); + case DatabaseEventType.value: + return DatabaseEvent._(await _queryDelegate.onValue(_modifiers).first); + } } /// Fires when children are added. - Stream get onChildAdded => _queryPlatform.onChildAdded - .handleError((error) => DatabaseError._(error)) - .map((item) => Event._(item)); + Stream get onChildAdded => _queryDelegate + .onChildAdded(_modifiers) + .map((item) => DatabaseEvent._(item)); /// Fires when children are removed. `previousChildKey` is null. - Stream get onChildRemoved => _queryPlatform.onChildRemoved - .handleError((error) => DatabaseError._(error)) - .map((item) => Event._(item)); + Stream get onChildRemoved => _queryDelegate + .onChildRemoved(_modifiers) + .map((item) => DatabaseEvent._(item)); /// Fires when children are changed. - Stream get onChildChanged => _queryPlatform.onChildChanged - .handleError((error) => DatabaseError._(error)) - .map((item) => Event._(item)); + Stream get onChildChanged => _queryDelegate + .onChildChanged(_modifiers) + .map((item) => DatabaseEvent._(item)); /// Fires when children are moved. - Stream get onChildMoved => _queryPlatform.onChildMoved - .handleError((error) => DatabaseError._(error)) - .map((item) => Event._(item)); + Stream get onChildMoved => _queryDelegate + .onChildMoved(_modifiers) + .map((item) => DatabaseEvent._(item)); /// Fires when the data at this location is updated. `previousChildKey` is null. - Stream get onValue => _queryPlatform.onValue - .handleError((error) => DatabaseError._(error)) - .map((item) => Event._(item)); + Stream get onValue => + _queryDelegate.onValue(_modifiers).map((item) => DatabaseEvent._(item)); /// Create a query constrained to only return child nodes with a value greater /// than or equal to the given value, using the given orderBy directive or /// priority as default, and optionally only child nodes with a key greater /// than or equal to the given key. - Query startAt(dynamic value, {String? key}) { - return Query._(_queryPlatform.startAt(value, key: key)); + Query startAt(Object? value, {String? key}) { + return Query._( + _queryDelegate, + _modifiers.start(StartCursorModifier.startAt(value, key)), + ); + } + + /// Creates a [Query] with the specified starting point (exclusive). + /// Using [startAt], [startAfter], [endBefore], [endAt] and [equalTo] + /// allows you to choose arbitrary starting and ending points for your + /// queries. + /// + /// The starting point is exclusive. + /// + /// If only a value is provided, children with a value greater than + /// the specified value will be included in the query. + /// If a key is specified, then children must have a value greater than + /// or equal to the specified value and a a key name greater than + /// the specified key. + Query startAfter(Object? value, {String? key}) { + return Query._( + _queryDelegate, + _modifiers.start(StartCursorModifier.startAfter(value, key)), + ); } /// Create a query constrained to only return child nodes with a value less /// than or equal to the given value, using the given orderBy directive or /// priority as default, and optionally only child nodes with a key less /// than or equal to the given key. - Query endAt(dynamic value, {String? key}) { - return Query._(_queryPlatform.endAt(value, key: key)); + Query endAt(Object? value, {String? key}) { + return Query._( + _queryDelegate, + _modifiers.end(EndCursorModifier.endAt(value, key)), + ); + } + + /// Creates a [Query] with the specified ending point (exclusive) + /// The ending point is exclusive. If only a value is provided, + /// children with a value less than the specified value will be included in + /// the query. If a key is specified, then children must have a value lesss + /// than or equal to the specified value and a a key name less than the + /// specified key. + Query endBefore(Object? value, {String? key}) { + return Query._( + _queryDelegate, + _modifiers.end(EndCursorModifier.endBefore(value, key)), + ); } /// Create a query constrained to only return child nodes with the given /// `value` (and `key`, if provided). /// /// If a key is provided, there is at most one such child as names are unique. - Query equalTo(dynamic value, {String? key}) { - return Query._(_queryPlatform.equalTo(value, key: key)); + Query equalTo(Object? value, {String? key}) { + return Query._( + _queryDelegate, + _modifiers + .start(StartCursorModifier.startAt(value, key)) + .end(EndCursorModifier.endAt(value, key)), + ); } /// Create a query with limit and anchor it to the start of the window. Query limitToFirst(int limit) { - return Query._(_queryPlatform.limitToFirst(limit)); + return Query._( + _queryDelegate, + _modifiers.limit(LimitModifier.limitToFirst(limit)), + ); } /// Create a query with limit and anchor it to the end of the window. Query limitToLast(int limit) { - return Query._(_queryPlatform.limitToLast(limit)); + return Query._( + _queryDelegate, + _modifiers.limit(LimitModifier.limitToLast(limit)), + ); } - /// Generate a view of the data sorted by values of a particular child key. + /// Generate a view of the data sorted by values of a particular child path. /// /// Intended to be used in combination with [startAt], [endAt], or /// [equalTo]. - Query orderByChild(String key) { - return Query._(_queryPlatform.orderByChild(key)); + Query orderByChild(String path) { + assert( + path.isNotEmpty, + 'The key cannot be empty. Use `orderByValue` instead', + ); + return Query._( + _queryDelegate, + _modifiers.order(OrderModifier.orderByChild(path)), + ); } /// Generate a view of the data sorted by key. @@ -99,7 +175,12 @@ class Query { /// Intended to be used in combination with [startAt], [endAt], or /// [equalTo]. Query orderByKey() { - return Query._(_queryPlatform.orderByKey()); + return Query._( + _queryDelegate, + _modifiers.order( + OrderModifier.orderByKey(), + ), + ); } /// Generate a view of the data sorted by value. @@ -107,7 +188,12 @@ class Query { /// Intended to be used in combination with [startAt], [endAt], or /// [equalTo]. Query orderByValue() { - return Query._(_queryPlatform.orderByValue()); + return Query._( + _queryDelegate, + _modifiers.order( + OrderModifier.orderByValue(), + ), + ); } /// Generate a view of the data sorted by priority. @@ -115,18 +201,19 @@ class Query { /// Intended to be used in combination with [startAt], [endAt], or /// [equalTo]. Query orderByPriority() { - return Query._(_queryPlatform.orderByPriority()); + return Query._( + _queryDelegate, + _modifiers.order( + OrderModifier.orderByPriority(), + ), + ); } - /// Obtains a DatabaseReference corresponding to this query's location. - DatabaseReference reference() => - DatabaseReference._(_queryPlatform.reference()); - /// By calling keepSynced(true) on a location, the data for that location will /// automatically be downloaded and kept in sync, even when no listeners are /// attached for that location. Additionally, while a location is kept synced, /// it will not be evicted from the persistent disk cache. Future keepSynced(bool value) { - return _queryPlatform.keepSynced(value); + return _queryDelegate.keepSynced(_modifiers, value); } } diff --git a/packages/firebase_database/firebase_database/lib/src/transaction_result.dart b/packages/firebase_database/firebase_database/lib/src/transaction_result.dart new file mode 100644 index 000000000000..c4bf2f98bf71 --- /dev/null +++ b/packages/firebase_database/firebase_database/lib/src/transaction_result.dart @@ -0,0 +1,24 @@ +// Copyright 2021 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. + +part of firebase_database; + +/// Instances of this class represent the outcome of a transaction. +class TransactionResult { + TransactionResultPlatform _delegate; + + TransactionResult._(this._delegate) { + TransactionResultPlatform.verifyExtends(_delegate); + } + + /// The [committed] status associated to this transaction result. + bool get committed { + return _delegate.committed; + } + + /// The [DataSnapshot] associated to this transaction result. + DataSnapshot get snapshot { + return DataSnapshot._(_delegate.snapshot); + } +} diff --git a/packages/firebase_database/firebase_database/lib/ui/analysis_options.yaml b/packages/firebase_database/firebase_database/lib/ui/analysis_options.yaml new file mode 100644 index 000000000000..eeb87d974d54 --- /dev/null +++ b/packages/firebase_database/firebase_database/lib/ui/analysis_options.yaml @@ -0,0 +1,8 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# in the LICENSE file. + +include: ../../../../../analysis_options.yaml +linter: + rules: + public_member_api_docs: false diff --git a/packages/firebase_database/firebase_database/lib/ui/firebase_animated_list.dart b/packages/firebase_database/firebase_database/lib/ui/firebase_animated_list.dart index b73ebaa0e855..9aedcae454be 100755 --- a/packages/firebase_database/firebase_database/lib/ui/firebase_animated_list.dart +++ b/packages/firebase_database/firebase_database/lib/ui/firebase_animated_list.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -204,7 +203,10 @@ class FirebaseAnimatedListState extends State { } Widget _buildItem( - BuildContext context, int index, Animation animation) { + BuildContext context, + int index, + Animation animation, + ) { return widget.itemBuilder(context, _model[index], animation, index); } diff --git a/packages/firebase_database/firebase_database/lib/ui/firebase_list.dart b/packages/firebase_database/firebase_database/lib/ui/firebase_list.dart index 849974de000e..2d91c69c70fd 100644 --- a/packages/firebase_database/firebase_database/lib/ui/firebase_list.dart +++ b/packages/firebase_database/firebase_database/lib/ui/firebase_list.dart @@ -1,25 +1,28 @@ -// ignore_for_file: require_trailing_commas // 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:collection'; -import '../firebase_database.dart' - show DatabaseError, DataSnapshot, Event, Query; +import 'package:firebase_core/firebase_core.dart'; + +import '../firebase_database.dart' show DataSnapshot, DatabaseEvent, Query; import 'utils/stream_subscriber_mixin.dart'; typedef ChildCallback = void Function(int index, DataSnapshot snapshot); typedef ChildMovedCallback = void Function( - int fromIndex, int toIndex, DataSnapshot snapshot); + int fromIndex, + int toIndex, + DataSnapshot snapshot, +); typedef ValueCallback = void Function(DataSnapshot snapshot); -typedef ErrorCallback = void Function(DatabaseError error); +typedef ErrorCallback = void Function(FirebaseException error); /// Sorts the results of `query` on the client side using `DataSnapshot.key`. class FirebaseList extends ListBase with // ignore: prefer_mixin - StreamSubscriberMixin { + StreamSubscriberMixin { FirebaseList({ required this.query, this.onChildAdded, @@ -103,46 +106,45 @@ class FirebaseList extends ListBase throw FallThroughError(); } - void _onChildAdded(Event event) { + void _onChildAdded(DatabaseEvent event) { int index = 0; - if (event.previousSiblingKey != null) { - index = _indexForKey(event.previousSiblingKey!) + 1; + if (event.previousChildKey != null) { + index = _indexForKey(event.previousChildKey!) + 1; } _snapshots.insert(index, event.snapshot); onChildAdded!(index, event.snapshot); } - void _onChildRemoved(Event event) { + void _onChildRemoved(DatabaseEvent event) { final index = _indexForKey(event.snapshot.key!); _snapshots.removeAt(index); onChildRemoved!(index, event.snapshot); } - void _onChildChanged(Event event) { + void _onChildChanged(DatabaseEvent event) { final index = _indexForKey(event.snapshot.key!); _snapshots[index] = event.snapshot; onChildChanged!(index, event.snapshot); } - void _onChildMoved(Event event) { + void _onChildMoved(DatabaseEvent event) { final fromIndex = _indexForKey(event.snapshot.key!); _snapshots.removeAt(fromIndex); int toIndex = 0; - if (event.previousSiblingKey != null) { - final prevIndex = _indexForKey(event.previousSiblingKey!); + if (event.previousChildKey != null) { + final prevIndex = _indexForKey(event.previousChildKey!); toIndex = prevIndex + 1; } _snapshots.insert(toIndex, event.snapshot); onChildMoved!(fromIndex, toIndex, event.snapshot); } - void _onValue(Event event) { + void _onValue(DatabaseEvent event) { onValue!(event.snapshot); } void _onError(Object o) { - final DatabaseError error = o as DatabaseError; - onError?.call(error); + onError?.call(o as FirebaseException); } } diff --git a/packages/firebase_database/firebase_database/lib/ui/firebase_sorted_list.dart b/packages/firebase_database/firebase_database/lib/ui/firebase_sorted_list.dart index 40a69d247c84..4ce0e21e7f73 100644 --- a/packages/firebase_database/firebase_database/lib/ui/firebase_sorted_list.dart +++ b/packages/firebase_database/firebase_database/lib/ui/firebase_sorted_list.dart @@ -1,26 +1,23 @@ -// ignore_for_file: require_trailing_commas // 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:collection'; -import '../firebase_database.dart' - show DatabaseError, DataSnapshot, Event, Query; +import 'package:firebase_core/firebase_core.dart'; + +import '../firebase_database.dart' show DataSnapshot, DatabaseEvent, Query; import 'firebase_list.dart' show ChildCallback, ErrorCallback, ValueCallback; import 'utils/stream_subscriber_mixin.dart'; /// Sorts the results of `query` on the client side using to the `comparator`. -/// -// TODO(lesnitsky) We don't support children moving around. -// TODO(lesnitsky) Right now this naively sorting the list after an insert. // We can be smarter about how we handle insertion and keep the list always // sorted. See example here: // https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FUISortedArray.m class FirebaseSortedList extends ListBase with // ignore: prefer_mixin - StreamSubscriberMixin { + StreamSubscriberMixin { FirebaseSortedList({ required this.query, required this.comparator, @@ -91,13 +88,13 @@ class FirebaseSortedList extends ListBase // Do not call super.clear(), it will set the length, it's unsupported. } - void _onChildAdded(Event event) { + void _onChildAdded(DatabaseEvent event) { _snapshots.add(event.snapshot); _snapshots.sort(comparator); onChildAdded!(_snapshots.indexOf(event.snapshot), event.snapshot); } - void _onChildRemoved(Event event) { + void _onChildRemoved(DatabaseEvent event) { final DataSnapshot snapshot = _snapshots.firstWhere((DataSnapshot snapshot) { return snapshot.key == event.snapshot.key; @@ -107,7 +104,7 @@ class FirebaseSortedList extends ListBase onChildRemoved!(index, snapshot); } - void _onChildChanged(Event event) { + void _onChildChanged(DatabaseEvent event) { final DataSnapshot snapshot = _snapshots.firstWhere((DataSnapshot snapshot) { return snapshot.key == event.snapshot.key; @@ -117,12 +114,11 @@ class FirebaseSortedList extends ListBase onChildChanged!(index, event.snapshot); } - void _onValue(Event event) { + void _onValue(DatabaseEvent event) { onValue!(event.snapshot); } void _onError(Object o) { - final DatabaseError error = o as DatabaseError; - onError?.call(error); + onError?.call(o as FirebaseException); } } diff --git a/packages/firebase_database/firebase_database/lib/ui/utils/stream_subscriber_mixin.dart b/packages/firebase_database/firebase_database/lib/ui/utils/stream_subscriber_mixin.dart index 61896478a216..dfe0993f734f 100644 --- a/packages/firebase_database/firebase_database/lib/ui/utils/stream_subscriber_mixin.dart +++ b/packages/firebase_database/firebase_database/lib/ui/utils/stream_subscriber_mixin.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // 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. @@ -24,7 +23,6 @@ abstract class StreamSubscriberMixin { /// Cancels all streams that were previously added with listen(). void cancelSubscriptions() { for (final subscription in _subscriptions) { - // TODO(rrousselGit) await `cancel` subscription.cancel(); } } diff --git a/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseObserveStreamHandler.h b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseObserveStreamHandler.h new file mode 120000 index 000000000000..2a79f7999aca --- /dev/null +++ b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseObserveStreamHandler.h @@ -0,0 +1 @@ +../../ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.h \ No newline at end of file diff --git a/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseObserveStreamHandler.m b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseObserveStreamHandler.m new file mode 120000 index 000000000000..d948596301e5 --- /dev/null +++ b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseObserveStreamHandler.m @@ -0,0 +1 @@ +../../ios/Classes/FLTFirebaseDatabaseObserveStreamHandler.m \ No newline at end of file diff --git a/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseUtils.h b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseUtils.h new file mode 120000 index 000000000000..939d52eccfa3 --- /dev/null +++ b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseUtils.h @@ -0,0 +1 @@ +../../ios/Classes/FLTFirebaseDatabaseUtils.h \ No newline at end of file diff --git a/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseUtils.m b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseUtils.m new file mode 120000 index 000000000000..a7125aa60d08 --- /dev/null +++ b/packages/firebase_database/firebase_database/macos/Classes/FLTFirebaseDatabaseUtils.m @@ -0,0 +1 @@ +../../ios/Classes/FLTFirebaseDatabaseUtils.m \ No newline at end of file diff --git a/packages/firebase_database/firebase_database/pubspec.yaml b/packages/firebase_database/firebase_database/pubspec.yaml index 8a38f2a56c2b..45faed2660c4 100755 --- a/packages/firebase_database/firebase_database/pubspec.yaml +++ b/packages/firebase_database/firebase_database/pubspec.yaml @@ -11,13 +11,13 @@ environment: dependencies: firebase_core: ^1.10.0 + firebase_core_platform_interface: ^4.2.0 firebase_database_platform_interface: ^0.1.0+4 firebase_database_web: ^0.1.2+1 flutter: sdk: flutter dev_dependencies: - firebase_core_platform_interface: ^4.2.0 flutter_test: sdk: flutter mockito: ^5.0.2 diff --git a/packages/firebase_database/firebase_database/test/firebase_list_test.dart b/packages/firebase_database/firebase_database/test/firebase_list_test.dart index 5660f0668035..b5206825c4ff 100644 --- a/packages/firebase_database/firebase_database/test/firebase_list_test.dart +++ b/packages/firebase_database/firebase_database/test/firebase_list_test.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // Copyright 2018 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. @@ -9,28 +8,28 @@ import 'package:firebase_database/firebase_database.dart'; import 'package:firebase_database/ui/firebase_list.dart'; import 'package:firebase_database/ui/firebase_sorted_list.dart'; import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FirebaseList', () { - late StreamController onChildAddedStreamController; - late StreamController onChildRemovedStreamController; - late StreamController onChildChangedStreamController; - late StreamController onChildMovedStreamController; - late StreamController onValue; + late StreamController onChildAddedStreamController; + late StreamController onChildRemovedStreamController; + late StreamController onChildChangedStreamController; + late StreamController onChildMovedStreamController; + late StreamController onValue; late MockQuery query; late FirebaseList list; late Completer callbackCompleter; setUp(() { - onChildAddedStreamController = StreamController(); - onChildRemovedStreamController = StreamController(); - onChildChangedStreamController = StreamController(); - onChildMovedStreamController = StreamController(); - onValue = StreamController(); + onChildAddedStreamController = StreamController(); + onChildRemovedStreamController = StreamController(); + onChildChangedStreamController = StreamController(); + onChildMovedStreamController = StreamController(); + onValue = StreamController(); query = MockQuery( onChildAddedStreamController.stream, onChildRemovedStreamController.stream, @@ -71,22 +70,22 @@ void main() { return result; } - Future processChildAddedEvent(Event event) { + Future processChildAddedEvent(DatabaseEvent event) { onChildAddedStreamController.add(event); return resetCompleterOnCallback(); } - Future processChildRemovedEvent(Event event) { + Future processChildRemovedEvent(DatabaseEvent event) { onChildRemovedStreamController.add(event); return resetCompleterOnCallback(); } - Future processChildChangedEvent(Event event) { + Future processChildChangedEvent(DatabaseEvent event) { onChildChangedStreamController.add(event); return resetCompleterOnCallback(); } - Future processChildMovedEvent(Event event) { + Future processChildMovedEvent(DatabaseEvent event) { onChildMovedStreamController.add(event); return resetCompleterOnCallback(); } @@ -94,7 +93,13 @@ void main() { test('can add to empty list', () async { final DataSnapshot snapshot = MockDataSnapshot('key10', 10); expect( - await processChildAddedEvent(MockEvent(null, snapshot)), + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot, + ), + ), ListChange.at(0, snapshot), ); expect(list, [snapshot]); @@ -103,9 +108,21 @@ void main() { test('can add before first element', () async { final DataSnapshot snapshot1 = MockDataSnapshot('key10', 10); final DataSnapshot snapshot2 = MockDataSnapshot('key20', 20); - await processChildAddedEvent(MockEvent(null, snapshot2)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot2, + ), + ); expect( - await processChildAddedEvent(MockEvent(null, snapshot1)), + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot1, + ), + ), ListChange.at(0, snapshot1), ); expect(list, [snapshot1, snapshot2]); @@ -114,9 +131,21 @@ void main() { test('can add after last element', () async { final DataSnapshot snapshot1 = MockDataSnapshot('key10', 10); final DataSnapshot snapshot2 = MockDataSnapshot('key20', 20); - await processChildAddedEvent(MockEvent(null, snapshot1)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot1, + ), + ); expect( - await processChildAddedEvent(MockEvent('key10', snapshot2)), + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + 'key10', + snapshot2, + ), + ), ListChange.at(1, snapshot2), ); expect(list, [snapshot1, snapshot2]); @@ -124,9 +153,21 @@ void main() { test('can remove from singleton list', () async { final DataSnapshot snapshot = MockDataSnapshot('key10', 10); - await processChildAddedEvent(MockEvent(null, snapshot)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot, + ), + ); expect( - await processChildRemovedEvent(MockEvent(null, snapshot)), + await processChildRemovedEvent( + MockEvent( + DatabaseEventType.childRemoved, + null, + snapshot, + ), + ), ListChange.at(0, snapshot), ); expect(list, isEmpty); @@ -135,10 +176,28 @@ void main() { test('can remove former of two elements', () async { final DataSnapshot snapshot1 = MockDataSnapshot('key10', 10); final DataSnapshot snapshot2 = MockDataSnapshot('key20', 20); - await processChildAddedEvent(MockEvent(null, snapshot2)); - await processChildAddedEvent(MockEvent(null, snapshot1)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot2, + ), + ); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot1, + ), + ); expect( - await processChildRemovedEvent(MockEvent(null, snapshot1)), + await processChildRemovedEvent( + MockEvent( + DatabaseEventType.childRemoved, + null, + snapshot1, + ), + ), ListChange.at(0, snapshot1), ); expect(list, [snapshot2]); @@ -147,10 +206,28 @@ void main() { test('can remove latter of two elements', () async { final DataSnapshot snapshot1 = MockDataSnapshot('key10', 10); final DataSnapshot snapshot2 = MockDataSnapshot('key20', 20); - await processChildAddedEvent(MockEvent(null, snapshot2)); - await processChildAddedEvent(MockEvent(null, snapshot1)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot2, + ), + ); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot1, + ), + ); expect( - await processChildRemovedEvent(MockEvent('key10', snapshot2)), + await processChildRemovedEvent( + MockEvent( + DatabaseEventType.childRemoved, + 'key10', + snapshot2, + ), + ), ListChange.at(1, snapshot2), ); expect(list, [snapshot1]); @@ -161,11 +238,31 @@ void main() { final DataSnapshot snapshot2a = MockDataSnapshot('key20', 20); final DataSnapshot snapshot2b = MockDataSnapshot('key20', 25); final DataSnapshot snapshot3 = MockDataSnapshot('key30', 30); - await processChildAddedEvent(MockEvent(null, snapshot3)); - await processChildAddedEvent(MockEvent(null, snapshot2a)); - await processChildAddedEvent(MockEvent(null, snapshot1)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot3, + ), + ); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot2a, + ), + ); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot1, + ), + ); expect( - await processChildChangedEvent(MockEvent('key10', snapshot2b)), + await processChildChangedEvent( + MockEvent(DatabaseEventType.childChanged, 'key10', snapshot2b), + ), ListChange.at(1, snapshot2b), ); expect(list, [snapshot1, snapshot2b, snapshot3]); @@ -174,11 +271,35 @@ void main() { final DataSnapshot snapshot1 = MockDataSnapshot('key10', 10); final DataSnapshot snapshot2 = MockDataSnapshot('key20', 20); final DataSnapshot snapshot3 = MockDataSnapshot('key30', 30); - await processChildAddedEvent(MockEvent(null, snapshot3)); - await processChildAddedEvent(MockEvent(null, snapshot2)); - await processChildAddedEvent(MockEvent(null, snapshot1)); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot3, + ), + ); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot2, + ), + ); + await processChildAddedEvent( + MockEvent( + DatabaseEventType.childAdded, + null, + snapshot1, + ), + ); expect( - await processChildMovedEvent(MockEvent('key30', snapshot1)), + await processChildMovedEvent( + MockEvent( + DatabaseEventType.childMoved, + 'key30', + snapshot1, + ), + ), ListChange.move(0, 2, snapshot1), ); expect(list, [snapshot2, snapshot3, snapshot1]); @@ -186,11 +307,11 @@ void main() { }); test('FirebaseList listeners are optional', () { - final onChildAddedStreamController = StreamController(); - final onChildRemovedStreamController = StreamController(); - final onChildChangedStreamController = StreamController(); - final onChildMovedStreamController = StreamController(); - final onValue = StreamController(); + final onChildAddedStreamController = StreamController(); + final onChildRemovedStreamController = StreamController(); + final onChildChangedStreamController = StreamController(); + final onChildMovedStreamController = StreamController(); + final onValue = StreamController(); addTearDown(() { onChildChangedStreamController.close(); onChildRemovedStreamController.close(); @@ -217,11 +338,11 @@ void main() { }); test('FirebaseSortedList listeners are optional', () { - final onChildAddedStreamController = StreamController(); - final onChildRemovedStreamController = StreamController(); - final onChildChangedStreamController = StreamController(); - final onChildMovedStreamController = StreamController(); - final onValue = StreamController(); + final onChildAddedStreamController = StreamController(); + final onChildRemovedStreamController = StreamController(); + final onChildChangedStreamController = StreamController(); + final onChildMovedStreamController = StreamController(); + final onValue = StreamController(); addTearDown(() { onChildChangedStreamController.close(); onChildRemovedStreamController.close(); @@ -258,19 +379,19 @@ class MockQuery extends Mock implements Query { ); @override - final Stream onChildAdded; + final Stream onChildAdded; @override - final Stream onChildRemoved; + final Stream onChildRemoved; @override - final Stream onChildChanged; + final Stream onChildChanged; @override - final Stream onChildMoved; + final Stream onChildMoved; @override - final Stream onValue; + final Stream onValue; } class ListChange { @@ -304,30 +425,33 @@ class ListChange { int get hashCode => index; } -class MockEvent implements Event { - MockEvent(this.previousSiblingKey, this.snapshot); +class MockEvent implements DatabaseEvent { + MockEvent(this.type, this.previousChildKey, this.snapshot); @override - final String? previousSiblingKey; + final DatabaseEventType type; + + @override + final String? previousChildKey; @override final DataSnapshot snapshot; @override // ignore: no_runtimetype_tostring - String toString() => '$runtimeType[$previousSiblingKey, $snapshot]'; + String toString() => '$runtimeType[$previousChildKey, $snapshot]'; @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object o) { return o is MockEvent && - previousSiblingKey == o.previousSiblingKey && + previousChildKey == o.previousChildKey && snapshot == o.snapshot; } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => previousSiblingKey.hashCode; + int get hashCode => previousChildKey.hashCode; } class MockDataSnapshot implements DataSnapshot { @@ -355,4 +479,23 @@ class MockDataSnapshot implements DataSnapshot { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => key.hashCode; + + @override + bool hasChild(String path) { + throw UnimplementedError(); + } + + @override + DataSnapshot child(String path) { + throw UnimplementedError(); + } + + @override + Object? get priority => throw UnimplementedError(); + + @override + DatabaseReference get ref => throw UnimplementedError(); + + @override + Iterable get children => throw UnimplementedError(); } diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/firebase_database_platform_interface.dart b/packages/firebase_database/firebase_database_platform_interface/lib/firebase_database_platform_interface.dart index 5814d3959d0f..511b9f7caa06 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/firebase_database_platform_interface.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/firebase_database_platform_interface.dart @@ -1,157 +1,16 @@ -// ignore_for_file: require_trailing_commas // 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. library firebase_database_platform_interface; -import 'dart:async'; -import 'dart:math'; - -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -part 'src/method_channel/method_channel_database.dart'; -part 'src/method_channel/method_channel_database_reference.dart'; -part 'src/method_channel/method_channel_on_disconnect.dart'; -part 'src/method_channel/method_channel_query.dart'; -part 'src/method_channel/utils/push_id_generator.dart'; -part 'src/platform_interface/database_reference.dart'; -part 'src/platform_interface/event.dart'; -part 'src/platform_interface/on_disconnect.dart'; -part 'src/platform_interface/query.dart'; - -/// Defines an interface to work with [FirebaseDatabase] on web and mobile -abstract class DatabasePlatform extends PlatformInterface { - /// The [FirebaseApp] instance to which this [FirebaseDatabase] belongs. - /// - /// If null, the default [FirebaseApp] is used. - final FirebaseApp? app; - - /// Gets an instance of [FirebaseDatabase]. - /// - /// If [app] is specified, its options should include a [databaseURL]. - - DatabasePlatform({this.app, this.databaseURL}) : super(token: _token); - - static final Object _token = Object(); - - /// Create an instance using [app] using the existing implementation - factory DatabasePlatform.instanceFor( - {FirebaseApp? app, String? databaseURL}) { - return DatabasePlatform.instance.withApp(app, databaseURL); - } - - /// The current default [DatabasePlatform] instance. - /// - /// It will always default to [MethodChannelDatabase] - /// if no web implementation was provided. - static DatabasePlatform _instance = MethodChannelDatabase(); - - static DatabasePlatform get instance { - return _instance; - } - - static set instance(DatabasePlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Create a new [DatabasePlatform] with a [FirebaseApp] instance - DatabasePlatform withApp(FirebaseApp? app, String? databaseURL) { - throw UnimplementedError('withApp() not implemented'); - } - - /// - String? appName() { - throw UnimplementedError('appName() not implemented'); - } - - /// The URL to which this [FirebaseDatabase] belongs - /// - /// If null, the URL of the specified [FirebaseApp] is used - final String? databaseURL; - - /// Gets a DatabaseReference for the root of your Firebase Database. - DatabaseReferencePlatform reference() { - throw UnimplementedError('reference() not implemented'); - } - - /// Attempts to sets the database persistence to [enabled]. - /// - /// This property must be set before calling methods on database references - /// and only needs to be called once per application. The returned [Future] - /// will complete with `true` if the operation was successful or `false` if - /// the persistence could not be set (because database references have - /// already been created). - /// - /// The Firebase Database client will cache synchronized data and keep track - /// of all writes you’ve initiated while your application is running. It - /// seamlessly handles intermittent network connections and re-sends write - /// operations when the network connection is restored. - /// - /// However by default your write operations and cached data are only stored - /// in-memory and will be lost when your app restarts. By setting [enabled] - /// to `true`, the data will be persisted to on-device (disk) storage and will - /// thus be available again when the app is restarted (even when there is no - /// network connectivity at that time). - Future setPersistenceEnabled(bool enabled) async { - throw UnimplementedError('setPersistenceEnabled() not implemented'); - } - - /// Attempts to set the size of the persistence cache. - /// - /// By default the Firebase Database client will use up to 10MB of disk space - /// to cache data. If the cache grows beyond this size, the client will start - /// removing data that hasn’t been recently used. If you find that your - /// application caches too little or too much data, call this method to change - /// the cache size. - /// - /// This property must be set before calling methods on database references - /// and only needs to be called once per application. The returned [Future] - /// will complete with `true` if the operation was successful or `false` if - /// the value could not be set (because database references have already been - /// created). - /// - /// Note that the specified cache size is only an approximation and the size - /// on disk may temporarily exceed it at times. Cache sizes smaller than 1 MB - /// or greater than 100 MB are not supported. - Future setPersistenceCacheSizeBytes(int cacheSize) async { - throw UnimplementedError('setPersistenceCacheSizeBytes() not implemented'); - } - - /// Enables verbose diagnostic logging for debugging your application. - /// This must be called before any other usage of FirebaseDatabase instance. - /// By default, diagnostic logging is disabled. - Future setLoggingEnabled(bool enabled) { - throw UnimplementedError('setLoggingEnabled() not implemented'); - } - - /// Resumes our connection to the Firebase Database backend after a previous - /// [goOffline] call. - Future goOnline() { - throw UnimplementedError('goOnline() not implemented'); - } - - /// Shuts down our connection to the Firebase Database backend until - /// [goOnline] is called. - Future goOffline() { - throw UnimplementedError('goOffline() not implemented'); - } - - /// The Firebase Database client automatically queues writes and sends them to - /// the server at the earliest opportunity, depending on network connectivity. - /// In some cases (e.g. offline usage) there may be a large number of writes - /// waiting to be sent. Calling this method will purge all outstanding writes - /// so they are abandoned. - /// - /// All writes will be purged, including transactions and onDisconnect writes. - /// The writes will be rolled back locally, perhaps triggering events for - /// affected event listeners, and the client will not (re-)send them to the - /// Firebase Database backend. - Future purgeOutstandingWrites() { - throw UnimplementedError('purgeOutstandingWrites() not implemented'); - } -} +export 'src/transaction.dart'; +export 'src/platform_interface/platform_interface_data_snapshot.dart'; +export 'src/platform_interface/platform_interface_database.dart'; +export 'src/platform_interface/platform_interface_database_event.dart'; +export 'src/platform_interface/platform_interface_database_reference.dart'; +export 'src/platform_interface/platform_interface_on_disconnect.dart'; +export 'src/platform_interface/platform_interface_query.dart'; +export 'src/platform_interface/platform_interface_transaction_result.dart'; +export 'src/server_value.dart'; +export 'src/query_modifiers.dart'; diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_data_snapshot.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_data_snapshot.dart new file mode 100644 index 000000000000..e89f6b48a1e0 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_data_snapshot.dart @@ -0,0 +1,111 @@ +// Copyright 2021 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; + +/// Represents a query over the data at a particular location. +class MethodChannelDataSnapshot extends DataSnapshotPlatform { + MethodChannelDataSnapshot( + this._ref, + this._data, + ) : super(_ref, _data); + + DatabaseReferencePlatform _ref; + + final Map _data; + + @override + DataSnapshotPlatform child(String childPath) { + Object? childValue = value; + final chunks = childPath.split('/').toList(); + + while (childValue != null && chunks.isNotEmpty) { + final c = chunks.removeAt(0); + if (childValue is List) { + final index = int.tryParse(c); + if (index == null || index < 0 || index > childValue.length - 1) { + return MethodChannelDataSnapshot( + _ref.child(childPath), + { + 'key': _ref.child(childPath).key, + 'value': null, + 'priority': null, + 'childKeys': [], + }, + ); + } + childValue = childValue[index]; + continue; + } + + if (childValue is Map) { + childValue = childValue[c]; + } + } + + if (childValue == null) { + return MethodChannelDataSnapshot( + _ref.child(childPath), + { + 'key': _ref.child(childPath).key, + 'value': null, + 'priority': null, + 'childKeys': [], + }, + ); + } + + return MethodChannelDataSnapshot( + _ref.child(childPath), + { + 'key': _ref.child(childPath).key, + 'value': childValue, + 'priority': null, + 'childKeys': _childKeysFromValue(childValue), + }, + ); + } + + @override + Iterable get children { + List _childKeys = List.from(_data['childKeys']); + + return Iterable.generate(_childKeys.length, + (int index) { + String childKey = _childKeys[index]; + + dynamic childValue; + if (value != null) { + if (value is Map) { + childValue = (value! as Map)[childKey]; + } else if (value is List) { + childValue = (value! as List)[int.parse(childKey)]; + } + } + + return MethodChannelDataSnapshot( + _ref.child(childKey), + { + 'key': childKey, + 'value': childValue, + 'priority': null, + 'childKeys': _childKeysFromValue(childValue), + }, + ); + }); + } +} + +List _childKeysFromValue(Object? value) { + List childChildKeys = []; + if (value is Map) { + childChildKeys = List.from(value.keys.toList()); + } else if (value is List) { + childChildKeys = List.generate( + value.length, + (int index) => '${index - 1}', + ); + } + return childChildKeys; +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database.dart index 09d38e04af90..74162775bc3a 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database.dart @@ -1,37 +1,63 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:firebase_database_platform_interface/src/method_channel/utils/utils.dart'; +import 'package:flutter/services.dart'; + +import 'method_channel_database_reference.dart'; +import 'utils/exception.dart'; + +class MethodChannelArguments { + MethodChannelArguments(this.app); + + FirebaseApp app; +} /// The entry point for accessing a FirebaseDatabase. /// /// You can get an instance by calling [FirebaseDatabase.instance]. class MethodChannelDatabase extends DatabasePlatform { - /// Gets an instance of [FirebaseDatabase]. - /// - /// If [app] is specified, its options should include a [databaseURL]. MethodChannelDatabase({FirebaseApp? app, String? databaseURL}) : super(app: app, databaseURL: databaseURL) { if (_initialized) return; + channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { - case 'Event': - EventPlatform event = EventPlatform(call.arguments); - _observers[call.arguments['handle']]?.add(event); - return null; - case 'Error': - final DatabaseErrorPlatform error = - DatabaseErrorPlatform(call.arguments['error']); - _observers[call.arguments['handle']]?.addError(error); - return null; - case 'DoTransaction': - final MutableData mutableData = - MutableData.private(call.arguments['snapshot']); - final MutableData updated = - _transactions[call.arguments['transactionKey']]!(mutableData); - return {'value': updated.value}; + case 'FirebaseDatabase#callTransactionHandler': + Object? value; + bool aborted = false; + bool exception = false; + final key = call.arguments['transactionKey']; + + try { + final handler = transactions[key]; + if (handler == null) { + // This shouldn't happen but on the off chance that it does, e.g. + // as a side effect of Hot Reloading/Restarting, then we should + // just abort the transaction. + aborted = true; + } else { + Transaction transaction = + handler(call.arguments['snapshot']['value']); + aborted = transaction.aborted; + value = transaction.value; + } + } catch (e) { + exception = true; + // We store thrown errors so we can rethrow when the runTransaction + // Future completes from native code - to avoid serializing the error + // and sending it to native only to have to send it back again. + transactionErrors[key] = e; + } + + return { + if (value != null) 'value': transformValue(value), + 'aborted': aborted, + 'exception': exception, + }; default: throw MissingPluginException( '${call.method} method not implemented on the Dart side.', @@ -41,137 +67,98 @@ class MethodChannelDatabase extends DatabasePlatform { _initialized = true; } - @override - DatabasePlatform withApp(FirebaseApp? app, String? databaseURL) => - MethodChannelDatabase( - app: app, - databaseURL: databaseURL, - ); + static final transactions = {}; + static final transactionErrors = {}; - @override - String? appName() => app?.name; + static bool _initialized = false; - static final Map> _observers = - >{}; + bool? _persistenceEnabled; + int? _cacheSizeBytes; + bool? _loggingEnabled; + String? _emulatorHost; + int? _emulatorPort; - static final Map _transactions = - {}; + @override + Map getChannelArguments([Map? other]) { + return { + 'appName': app!.name, + if (databaseURL != null) 'databaseURL': databaseURL, + if (_persistenceEnabled != null) + 'persistenceEnabled': _persistenceEnabled, + if (_cacheSizeBytes != null) 'cacheSizeBytes': _cacheSizeBytes, + if (_loggingEnabled != null) 'loggingEnabled': _loggingEnabled, + if (_emulatorHost != null) 'emulatorHost': _emulatorHost, + if (_emulatorPort != null) 'emulatorPort': _emulatorPort, + }..addAll(other ?? {}); + } - static bool _initialized = false; + /// Gets a [DatabasePlatform] with specific arguments such as a different + /// [FirebaseApp]. + @override + DatabasePlatform delegateFor({ + required FirebaseApp app, + String? databaseURL, + }) { + return MethodChannelDatabase(app: app, databaseURL: databaseURL); + } /// The [MethodChannel] used to communicate with the native plugin static const MethodChannel channel = MethodChannel('plugins.flutter.io/firebase_database'); - /// Gets a DatabaseReference for the root of your Firebase Database. @override - DatabaseReferencePlatform reference() { + void useDatabaseEmulator(String host, int port) { + _emulatorHost = host; + _emulatorPort = port; + } + + @override + DatabaseReferencePlatform ref([String? path]) { return MethodChannelDatabaseReference( database: this, - pathComponents: [], + pathComponents: path?.split('/').toList() ?? const [], ); } - /// Attempts to sets the database persistence to [enabled]. - /// - /// This property must be set before calling methods on database references - /// and only needs to be called once per application. The returned [Future] - /// will complete with `true` if the operation was successful or `false` if - /// the persistence could not be set (because database references have - /// already been created). - /// - /// The Firebase Database client will cache synchronized data and keep track - /// of all writes you’ve initiated while your application is running. It - /// seamlessly handles intermittent network connections and re-sends write - /// operations when the network connection is restored. - /// - /// However by default your write operations and cached data are only stored - /// in-memory and will be lost when your app restarts. By setting [enabled] - /// to `true`, the data will be persisted to on-device (disk) storage and will - /// thus be available again when the app is restarted (even when there is no - /// network connectivity at that time). @override - Future setPersistenceEnabled(bool enabled) async { - final bool? result = await channel.invokeMethod( - 'FirebaseDatabase#setPersistenceEnabled', - { - 'app': app?.name, - 'databaseURL': databaseURL, - 'enabled': enabled, - }, - ); - return result!; + void setPersistenceEnabled(bool enabled) { + _persistenceEnabled = enabled; } - /// Attempts to set the size of the persistence cache. - /// - /// By default the Firebase Database client will use up to 10MB of disk space - /// to cache data. If the cache grows beyond this size, the client will start - /// removing data that hasn’t been recently used. If you find that your - /// application caches too little or too much data, call this method to change - /// the cache size. - /// - /// This property must be set before calling methods on database references - /// and only needs to be called once per application. The returned [Future] - /// will complete with `true` if the operation was successful or `false` if - /// the value could not be set (because database references have already been - /// created). - /// - /// Note that the specified cache size is only an approximation and the size - /// on disk may temporarily exceed it at times. Cache sizes smaller than 1 MB - /// or greater than 100 MB are not supported. @override - Future setPersistenceCacheSizeBytes(int cacheSize) async { - final bool? result = await channel.invokeMethod( - 'FirebaseDatabase#setPersistenceCacheSizeBytes', - { - 'app': app?.name, - 'databaseURL': databaseURL, - 'cacheSize': cacheSize, - }, - ); - return result!; + void setPersistenceCacheSizeBytes(int cacheSize) { + _cacheSizeBytes = cacheSize; } - /// Enables verbose diagnostic logging for debugging your application. - /// This must be called before any other usage of FirebaseDatabase instance. - /// By default, diagnostic logging is disabled. @override - Future setLoggingEnabled(bool enabled) { - return channel.invokeMethod( - 'FirebaseDatabase#setLoggingEnabled', - { - 'app': app?.name, - 'databaseURL': databaseURL, - 'enabled': enabled - }, - ); + void setLoggingEnabled(bool enabled) { + _loggingEnabled = enabled; } - /// Resumes our connection to the Firebase Database backend after a previous - /// [goOffline] call. @override Future goOnline() { - return channel.invokeMethod( - 'FirebaseDatabase#goOnline', - { - 'app': app?.name, - 'databaseURL': databaseURL, - }, - ); + try { + return channel.invokeMethod( + 'FirebaseDatabase#goOnline', + getChannelArguments(), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } /// Shuts down our connection to the Firebase Database backend until /// [goOnline] is called. @override Future goOffline() { - return channel.invokeMethod( - 'FirebaseDatabase#goOffline', - { - 'app': app?.name, - 'databaseURL': databaseURL, - }, - ); + try { + return channel.invokeMethod( + 'FirebaseDatabase#goOffline', + getChannelArguments(), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } /// The Firebase Database client automatically queues writes and sends them to @@ -186,12 +173,13 @@ class MethodChannelDatabase extends DatabasePlatform { /// Firebase Database backend. @override Future purgeOutstandingWrites() { - return channel.invokeMethod( - 'FirebaseDatabase#purgeOutstandingWrites', - { - 'app': app?.name, - 'databaseURL': databaseURL, - }, - ); + try { + return channel.invokeMethod( + 'FirebaseDatabase#purgeOutstandingWrites', + getChannelArguments(), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } } diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_event.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_event.dart new file mode 100644 index 000000000000..fd701f5d2d5a --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_event.dart @@ -0,0 +1,20 @@ +// Copyright 2021 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; + +import 'method_channel_data_snapshot.dart'; + +class MethodChannelDatabaseEvent extends DatabaseEventPlatform { + MethodChannelDatabaseEvent(this._ref, this._data) : super(_data); + + DatabaseReferencePlatform _ref; + + Map _data; + + @override + DataSnapshotPlatform get snapshot { + return MethodChannelDataSnapshot(_ref, Map.from(_data['snapshot'])); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_reference.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_reference.dart index 037ed3548f6b..03768a7c5d6d 100644 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_reference.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_database_reference.dart @@ -1,16 +1,23 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:firebase_database_platform_interface/src/method_channel/utils/utils.dart'; + +import 'method_channel_database.dart'; +import 'method_channel_on_disconnect.dart'; +import 'method_channel_query.dart'; +import 'method_channel_transaction_result.dart'; +import 'utils/exception.dart'; +import 'utils/push_id_generator.dart'; /// DatabaseReference represents a particular location in your Firebase /// Database and can be used for reading or writing data to that location. /// /// This class is the starting point for all Firebase Database operations. /// After you’ve obtained your first DatabaseReference via -/// `FirebaseDatabase.reference()`, you can use it to read data +/// `FirebaseDatabase.ref()`, you can use it to read data /// (ie. `onChildAdded`), write data (ie. `setValue`), and to create new /// `DatabaseReference`s (ie. `child`). class MethodChannelDatabaseReference extends MethodChannelQuery @@ -22,35 +29,29 @@ class MethodChannelDatabaseReference extends MethodChannelQuery }) : super( database: database, pathComponents: pathComponents, - parameters: {}, ); - /// Gets a DatabaseReference for the location at the specified relative - /// path. The relative path can either be a simple child key (e.g. ‘fred’) or - /// a deeper slash-separated path (e.g. ‘fred/name/first’). @override DatabaseReferencePlatform child(String path) { return MethodChannelDatabaseReference( - database: database, - pathComponents: List.from(pathComponents) - ..addAll(path.split('/'))); + database: database, + pathComponents: List.from(pathComponents) + ..addAll(path.split('/')), + ); } - /// Gets a DatabaseReference for the parent location. If this instance - /// refers to the root of your Firebase Database, it has no parent, and - /// therefore parent() will return null. @override - DatabaseReferencePlatform? parent() { + DatabaseReferencePlatform? get parent { if (pathComponents.isEmpty) { return null; } + return MethodChannelDatabaseReference( database: database, pathComponents: (List.from(pathComponents))..removeLast(), ); } - /// Gets a FIRDatabaseReference for the root location. @override DatabaseReferencePlatform root() { return MethodChannelDatabaseReference( @@ -59,18 +60,9 @@ class MethodChannelDatabaseReference extends MethodChannelQuery ); } - /// Gets the last token in a Firebase Database location (e.g. ‘fred’ in - /// https://SampleChat.firebaseIO-demo.com/users/fred) @override - String get key => pathComponents.last; - - /// Generates a new child location using a unique key and returns a - /// DatabaseReference to it. This is useful when the children of a Firebase - /// Database location represent a list of items. - /// - /// The unique key generated by childByAutoId: is prefixed with a - /// client-generated timestamp so that the resulting list will be - /// chronologically-sorted. + String? get key => pathComponents.isEmpty ? null : pathComponents.last; + @override DatabaseReferencePlatform push() { return MethodChannelDatabaseReference( @@ -80,148 +72,119 @@ class MethodChannelDatabaseReference extends MethodChannelQuery ); } - /// Write `value` to the location with the specified `priority` if applicable. - /// - /// This will overwrite any data at this location and all child locations. - /// - /// Data types that are allowed are String, boolean, int, double, Map, List. - /// - /// The effect of the write will be visible immediately and the corresponding - /// events will be triggered. Synchronization of the data to the Firebase - /// Database servers will also be started. - /// - /// Passing null for the new value means all data at this location or any - /// child location will be deleted. @override - Future set(dynamic value, {dynamic priority}) { - return MethodChannelDatabase.channel.invokeMethod( - 'DatabaseReference#set', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'value': value, - 'priority': priority, - }, - ); + Future set(Object? value) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'DatabaseReference#set', + database.getChannelArguments({ + 'path': path, + if (value != null) 'value': transformValue(value), + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } - /// Update the node with the `value` @override - Future update(Map value) { - return MethodChannelDatabase.channel.invokeMethod( - 'DatabaseReference#update', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'value': value, - }, - ); + Future setWithPriority(Object? value, Object? priority) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'DatabaseReference#setWithPriority', + database.getChannelArguments({ + 'path': path, + if (value != null) 'value': transformValue(value), + if (priority != null) 'priority': priority, + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } - /// Sets a priority for the data at this Firebase Database location. - /// - /// Priorities can be used to provide a custom ordering for the children at a - /// location (if no priorities are specified, the children are ordered by - /// key). - /// - /// You cannot set a priority on an empty location. For this reason - /// set() should be used when setting initial data with a specific priority - /// and setPriority() should be used when updating the priority of existing - /// data. - /// - /// Children are sorted based on this priority using the following rules: - /// - /// Children with no priority come first. Children with a number as their - /// priority come next. They are sorted numerically by priority (small to - /// large). Children with a string as their priority come last. They are - /// sorted lexicographically by priority. Whenever two children have the same - /// priority (including no priority), they are sorted by key. Numeric keys - /// come first (sorted numerically), followed by the remaining keys (sorted - /// lexicographically). - /// - /// Note that priorities are parsed and ordered as IEEE 754 double-precision - /// floating-point numbers. Keys are always stored as strings and are treated - /// as numbers only when they can be parsed as a 32-bit integer. @override - Future setPriority(dynamic priority) async { - return MethodChannelDatabase.channel.invokeMethod( - 'DatabaseReference#setPriority', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'priority': priority, - }, - ); + Future update(Map value) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'DatabaseReference#update', + database.getChannelArguments({ + 'path': path, + 'value': transformValue(value), + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } + } + + @override + Future setPriority(Object? priority) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'DatabaseReference#setPriority', + database.getChannelArguments({ + 'path': path, + if (priority != null) 'priority': priority, + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } - /// Remove the data at this Firebase Database location. Any data at child - /// locations will also be deleted. - /// - /// The effect of the delete will be visible immediately and the corresponding - /// events will be triggered. Synchronization of the delete to the Firebase - /// Database servers will also be started. - /// - /// remove() is equivalent to calling set(null) @override Future remove() => set(null); - /// Performs an optimistic-concurrency transactional update to the data at - /// this Firebase Database location. @override Future runTransaction( TransactionHandler transactionHandler, { - Duration timeout = const Duration(seconds: 5), + bool applyLocally = true, }) async { - assert( - timeout.inMilliseconds > 0, - 'Transaction timeout must be more than 0 milliseconds.', - ); - - final completer = Completer(); - - final int transactionKey = MethodChannelDatabase._transactions.isEmpty - ? 0 - : MethodChannelDatabase._transactions.keys.last + 1; - - MethodChannelDatabase._transactions[transactionKey] = transactionHandler; - - TransactionResultPlatform toTransactionResult(Map map) { - final DatabaseErrorPlatform? databaseError = - map['error'] != null ? DatabaseErrorPlatform(map['error']) : null; - final bool committed = map['committed']; - final DataSnapshotPlatform? dataSnapshot = map['snapshot'] != null - ? DataSnapshotPlatform.fromJson(map['snapshot'], null) - : null; - - MethodChannelDatabase._transactions.remove(transactionKey); - - return TransactionResultPlatform(databaseError, committed, dataSnapshot); + const channel = MethodChannelDatabase.channel; + final handlers = MethodChannelDatabase.transactions; + final handlerErrors = MethodChannelDatabase.transactionErrors; + final key = handlers.isEmpty ? 0 : handlers.keys.last + 1; + + // Store the handler to be called at a later time by native method channels. + MethodChannelDatabase.transactions[key] = transactionHandler; + + try { + final result = await channel.invokeMethod( + 'DatabaseReference#runTransaction', + database.getChannelArguments({ + 'path': path, + 'transactionApplyLocally': applyLocally, + 'transactionKey': key, + }), + ); + + // We store Dart only errors that occur inside users handlers - to avoid + // serializing the error and sending it to native only to have to send it + // back again. If we stored one, throw it now. + final possibleError = handlerErrors[key]; + if (possibleError != null) { + throw possibleError; + } + + return MethodChannelTransactionResult( + result!['committed'] as bool, + this, + Map.from(result!['snapshot']), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } finally { + handlers.remove(key); + handlerErrors.remove(key); } - - await MethodChannelDatabase.channel.invokeMethod( - 'DatabaseReference#runTransaction', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'transactionKey': transactionKey, - 'transactionTimeout': timeout.inMilliseconds - }, - ).then((dynamic response) { - completer.complete(toTransactionResult(response)); - }); - - return completer.future; } @override OnDisconnectPlatform onDisconnect() { return MethodChannelOnDisconnect( database: database, - reference: this, + ref: this, ); } } diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_on_disconnect.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_on_disconnect.dart index 70e2f3ff4ee5..e784b5f0a8c4 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_on_disconnect.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_on_disconnect.dart @@ -1,60 +1,87 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:firebase_database_platform_interface/src/method_channel/utils/utils.dart'; + +import 'method_channel_database.dart'; +import 'utils/exception.dart'; /// Represents a query over the data at a particular location. class MethodChannelOnDisconnect extends OnDisconnectPlatform { /// Create a [MethodChannelQuery] from [DatabaseReferencePlatform] - MethodChannelOnDisconnect( - {required DatabasePlatform database, - required DatabaseReferencePlatform reference}) - : path = reference.path, - super(database: database, reference: reference); + MethodChannelOnDisconnect({ + required DatabasePlatform database, + required DatabaseReferencePlatform ref, + }) : super(database: database, ref: ref); - final String path; + @override + Future set(Object? value) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'OnDisconnect#set', + database.getChannelArguments({ + 'path': ref.path, + if (value != null) 'value': transformValue(value), + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } + } @override - Future set(dynamic value, {dynamic priority}) { - return MethodChannelDatabase.channel.invokeMethod( - 'OnDisconnect#set', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'value': value, - 'priority': priority - }, - ); + Future setWithPriority(Object? value, Object? priority) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'OnDisconnect#setWithPriority', + database.getChannelArguments( + { + 'path': ref.path, + if (value != null) 'value': transformValue(value), + if (priority != null) 'priority': priority + }, + ), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } @override Future remove() => set(null); @override - Future cancel() { - return MethodChannelDatabase.channel.invokeMethod( - 'OnDisconnect#cancel', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path - }, - ); + Future cancel() async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'OnDisconnect#cancel', + database.getChannelArguments({ + 'appName': database.app!.name, + 'databaseURL': database.databaseURL, + 'path': ref.path + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } @override - Future update(Map value) { - return MethodChannelDatabase.channel.invokeMethod( - 'OnDisconnect#update', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'value': value - }, - ); + Future update(Map value) async { + try { + await MethodChannelDatabase.channel.invokeMethod( + 'OnDisconnect#update', + database.getChannelArguments({ + 'appName': database.app!.name, + 'databaseURL': database.databaseURL, + 'path': ref.path, + 'value': transformValue(value), + }), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } } diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_query.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_query.dart index 90910953d98f..9478381f8a2c 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_query.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_query.dart @@ -1,229 +1,115 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'method_channel_data_snapshot.dart'; +import 'method_channel_database.dart'; +import 'method_channel_database_event.dart'; +import 'method_channel_database_reference.dart'; +import 'utils/exception.dart'; /// Represents a query over the data at a particular location. class MethodChannelQuery extends QueryPlatform { /// Create a [MethodChannelQuery] from [pathComponents] MethodChannelQuery({ required DatabasePlatform database, - required List pathComponents, - Map parameters = const {}, - }) : super( - database: database, - parameters: parameters, - pathComponents: pathComponents, - ); + required this.pathComponents, + }) : super(database: database); - @override - Stream observe(EventType eventType) { - late 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. - late StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( - onListen: () { - _handle = MethodChannelDatabase.channel.invokeMethod( - 'Query#observe', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'parameters': parameters, - 'eventType': eventType.toString(), - }, - ).then((value) { - MethodChannelDatabase._observers[value!] = controller; - return value; - }); - }, - onCancel: () { - _handle.then((int handle) async { - await MethodChannelDatabase.channel.invokeMethod( - 'Query#removeObserver', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'parameters': parameters, - 'handle': handle, - }, - ); - MethodChannelDatabase._observers.remove(handle); - }); - }, - ); - return controller.stream; - } + static Map> observers = {}; - /// Slash-delimited path representing the database location of this query. - @override - String get path => pathComponents.join('/'); + final List pathComponents; - /// Gets the most up-to-date result for this query. @override - Future get() async { - final result = - await MethodChannelDatabase.channel.invokeMethod>( - 'Query#get', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - }, - ); - if (result!.containsKey('error') && result['error'] != null) { - final errorMap = result['error']; - throw FirebaseException( - plugin: 'firebase_database', - code: 'get-failed', - message: errorMap['details'], - ); - } else { - return DataSnapshotPlatform.fromJson(result['snapshot'], null); - } + String get path { + return pathComponents.join('/'); } - /// Create a query constrained to only return child nodes with a value greater - /// than or equal to the given value, using the given orderBy directive or - /// priority as default, and optionally only child nodes with a key greater - /// than or equal to the given key. - @override - QueryPlatform startAt(dynamic value, {String? key}) { - assert(!this.parameters.containsKey('startAt')); - assert(value is String || - value is bool || - value is double || - value is int || - value == null); - final Map parameters = {'startAt': value}; - if (key != null) parameters['startAtKey'] = key; - return _copyWithParameters(parameters); - } - - /// Create a query constrained to only return child nodes with a value less - /// than or equal to the given value, using the given orderBy directive or - /// priority as default, and optionally only child nodes with a key less - /// than or equal to the given key. - @override - QueryPlatform endAt(dynamic value, {String? key}) { - assert(!this.parameters.containsKey('endAt')); - assert(value is String || - value is bool || - value is double || - value is int || - value == null); - final Map parameters = {'endAt': value}; - if (key != null) parameters['endAtKey'] = key; - return _copyWithParameters(parameters); - } - - /// Create a query constrained to only return child nodes with the given - /// `value` (and `key`, if provided). - /// - /// If a key is provided, there is at most one such child as names are unique. - @override - QueryPlatform equalTo(dynamic value, {String? key}) { - assert(!this.parameters.containsKey('equalTo')); - assert(value is String || - value is bool || - value is double || - value is int || - value == null); - final Map parameters = {'equalTo': value}; - if (key != null) parameters['equalToKey'] = key; - return _copyWithParameters(parameters); - } - - /// Create a query with limit and anchor it to the start of the window. - @override - QueryPlatform limitToFirst(int limit) { - assert(!parameters.containsKey('limitToFirst')); - return _copyWithParameters({'limitToFirst': limit}); - } - - /// Create a query with limit and anchor it to the end of the window. - @override - QueryPlatform limitToLast(int limit) { - assert(!parameters.containsKey('limitToLast')); - return _copyWithParameters({'limitToLast': limit}); - } + MethodChannel get channel => MethodChannelDatabase.channel; - /// Generate a view of the data sorted by values of a particular child key. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. @override - QueryPlatform orderByChild(String key) { - assert(!parameters.containsKey('orderBy')); - return _copyWithParameters( - {'orderBy': 'child', 'orderByChildKey': key}, + Stream observe( + QueryModifiers modifiers, + DatabaseEventType eventType, + ) async* { + const channel = MethodChannelDatabase.channel; + List> modifierList = modifiers.toList(); + // Create a unique event channel naming prefix using path, app name, + // databaseUrl, event type and ordered modifier list + String eventChannelNamePrefix = + '$path-${database.app!.name}-${database.databaseURL}-$eventType-$modifierList'; + + // Create the EventChannel on native. + final channelName = await channel.invokeMethod( + 'Query#observe', + database.getChannelArguments({ + 'path': path, + 'modifiers': modifierList, + 'eventChannelNamePrefix': eventChannelNamePrefix, + }), ); - } - - /// Generate a view of the data sorted by key. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. - @override - QueryPlatform orderByKey() { - assert(!parameters.containsKey('orderBy')); - return _copyWithParameters({'orderBy': 'key'}); - } - /// Generate a view of the data sorted by value. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. - @override - QueryPlatform orderByValue() { - assert(!parameters.containsKey('orderBy')); - return _copyWithParameters({'orderBy': 'value'}); + yield* EventChannel(channelName!) + .receiveBroadcastStream({ + 'eventType': eventTypeToString(eventType), + }) + .map( + (event) => + MethodChannelDatabaseEvent(ref, Map.from(event)), + ) + .handleError( + (e, s) => throw convertPlatformException(e, s), + test: (err) => err is PlatformException, + ); } - /// Generate a view of the data sorted by priority. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. + /// Gets the most up-to-date result for this query. @override - QueryPlatform orderByPriority() { - assert(!parameters.containsKey('orderBy')); - return _copyWithParameters({'orderBy': 'priority'}); + Future get(QueryModifiers modifiers) async { + try { + final result = await channel.invokeMapMethod( + 'Query#get', + database.getChannelArguments({ + 'path': path, + 'modifiers': modifiers.toList(), + }), + ); + return MethodChannelDataSnapshot( + ref, + Map.from(result!['snapshot']), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } /// Obtains a DatabaseReference corresponding to this query's location. @override - DatabaseReferencePlatform reference() => MethodChannelDatabaseReference( - database: database, pathComponents: pathComponents); + DatabaseReferencePlatform get ref { + return MethodChannelDatabaseReference( + database: database, + pathComponents: pathComponents, + ); + } /// By calling keepSynced(true) on a location, the data for that location will /// automatically be downloaded and kept in sync, even when no listeners are /// attached for that location. Additionally, while a location is kept synced, /// it will not be evicted from the persistent disk cache. @override - Future keepSynced(bool value) { - return MethodChannelDatabase.channel.invokeMethod( - 'Query#keepSynced', - { - 'app': database.app?.name, - 'databaseURL': database.databaseURL, - 'path': path, - 'parameters': parameters, - 'value': value - }, - ); - } - - MethodChannelQuery _copyWithParameters(Map parameters) { - return MethodChannelQuery( - database: database, - pathComponents: pathComponents, - parameters: Map.unmodifiable( - Map.from(this.parameters)..addAll(parameters), - ), - ); + Future keepSynced(QueryModifiers modifiers, bool value) async { + try { + await channel.invokeMethod( + 'Query#keepSynced', + database.getChannelArguments( + {'path': path, 'modifiers': modifiers.toList(), 'value': value}, + ), + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } } } diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_transaction_result.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_transaction_result.dart new file mode 100644 index 000000000000..0abbd19fce02 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/method_channel_transaction_result.dart @@ -0,0 +1,21 @@ +// Copyright 2021 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; + +import 'method_channel_data_snapshot.dart'; + +class MethodChannelTransactionResult extends TransactionResultPlatform { + MethodChannelTransactionResult(bool committed, this._ref, this._snapshot) + : super(committed); + + DatabaseReferencePlatform _ref; + + Map _snapshot; + + @override + DataSnapshotPlatform get snapshot { + return MethodChannelDataSnapshot(_ref, _snapshot); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/exception.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/exception.dart new file mode 100644 index 000000000000..dc7fbeab7bbb --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/exception.dart @@ -0,0 +1,46 @@ +// Copyright 2021, 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. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; + +/// Catches a [PlatformException] and returns an [Exception]. +/// +/// If the [Exception] is a [PlatformException], a [FirebaseException] is returned. +Exception convertPlatformException(Object exception, [StackTrace? stackTrace]) { + if (exception is! Exception || exception is! PlatformException) { + throw exception; + } + + return platformExceptionToFirebaseException(exception, stackTrace); +} + +/// Converts a [PlatformException] into a [FirebaseException]. +/// +/// A [PlatformException] can only be converted to a [FirebaseException] if the +/// `details` of the exception exist. Firebase returns specific codes and messages +/// which can be converted into user friendly exceptions. +FirebaseException platformExceptionToFirebaseException( + PlatformException platformException, [ + StackTrace? stackTrace, +]) { + Map? details = platformException.details != null + ? Map.from(platformException.details) + : null; + + String code = 'unknown'; + String message = platformException.message ?? ''; + + if (details != null) { + code = details['code'] ?? code; + message = details['message'] ?? message; + } + + return FirebaseException( + plugin: 'firebase_database', + code: code, + message: message, + stackTrace: stackTrace, + ); +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/push_id_generator.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/push_id_generator.dart index 23d9497001ef..c1378823b6e6 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/push_id_generator.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/push_id_generator.dart @@ -1,11 +1,9 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'dart:math'; -// ignore: avoid_classes_with_only_static_members /// Utility class for generating Firebase child node keys. /// /// Since the Flutter plugin API is asynchronous, there's no way for us diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/utils.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/utils.dart new file mode 100644 index 000000000000..bf7d519f77ce --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/method_channel/utils/utils.dart @@ -0,0 +1,23 @@ +// Copyright 2021 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. + +Map mapKeysToString(Map value) { + Map newMap = {}; + value.forEach((key, value) { + newMap[key.toString()] = transformValue(value); + }); + return newMap; +} + +Object? transformValue(Object? value) { + if (value is Map) { + return mapKeysToString(value); + } + + if (value is List) { + return value.map(transformValue).toList(); + } + + return value; +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/event.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/event.dart deleted file mode 100755 index 54df7e35a620..000000000000 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/event.dart +++ /dev/null @@ -1,122 +0,0 @@ -// ignore_for_file: require_trailing_commas -// 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. - -part of firebase_database_platform_interface; - -/// Enum to define various types of database events -enum EventType { - /// Event for [onChildAdded] listener - childAdded, - - /// Event for [onChildRemoved] listener - childRemoved, - - /// Event for [onChildChanged] listener - childChanged, - - /// Event for [onChildMoved] listener - childMoved, - - /// Event for [onValue] listener - value, -} - -/// `Event` encapsulates a DataSnapshot and possibly also the key of its -/// previous sibling, which can be used to order the snapshots. -class EventPlatform { - EventPlatform(Map _data) - : previousSiblingKey = _data['previousSiblingKey'] as String?, - snapshot = DataSnapshotPlatform.fromJson( - _data['snapshot']! as Map, - _data['childKeys'] as List?); - - /// create [EventPlatform] from [DataSnapshotPlatform] - EventPlatform.fromDataSnapshotPlatform( - this.snapshot, - this.previousSiblingKey, - ); - - final DataSnapshotPlatform snapshot; - - final String? previousSiblingKey; -} - -/// A DataSnapshot contains data from a Firebase Database location. -/// Any time you read Firebase data, you receive the data as a DataSnapshot. -class DataSnapshotPlatform { - DataSnapshotPlatform(this.key, this.value) : exists = value != null; - - factory DataSnapshotPlatform.fromJson( - Map _data, - List? childKeys, - ) { - Object? dataValue = _data['value']; - Object? value; - - if (dataValue is Map && childKeys != null) { - value = {for (final key in childKeys) key: dataValue[key]}; - } else if (dataValue is List && childKeys != null) { - value = - childKeys.map((key) => dataValue[int.parse(key! as String)]).toList(); - } else { - value = dataValue; - } - return DataSnapshotPlatform(_data['key'] as String?, value); - } - - /// The key of the location that generated this DataSnapshot. - final String? key; - - /// Returns the contents of this data snapshot as native types. - final dynamic value; - - /// Ascertains whether the value exists at the Firebase Database location. - final bool exists; -} - -/// A dataSnapshot class which can be mutated. Specially used with transactions. -class MutableData { - @visibleForTesting - MutableData.private(this._data); - - /// generate [MutableData] from key and value - MutableData(String key, dynamic value) - : _data = { - 'key': key, - 'value': value, - }; - - final Map _data; - - /// The key of the location that generated this MutableData. - String get key => _data['key']; - - /// Returns the mutable contents of this MutableData as native types. - dynamic get value => _data['value']; - - set value(dynamic newValue) => _data['value'] = newValue; -} - -/// A DatabaseError contains code, message and details of a Firebase Database -/// Error that results from a transaction operation at a Firebase Database -/// location. -class DatabaseErrorPlatform { - DatabaseErrorPlatform(this._data); - - Map _data; - - /// One of the defined status codes, depending on the error. - int get code => _data['code']; - - /// A human-readable description of the error. - String get message => _data['message']; - - /// Human-readable details on the error and additional information. - String get details => _data['details']; - - @override - // ignore: no_runtimetype_tostring - String toString() => '$runtimeType($code, $message, $details)'; -} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_data_snapshot.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_data_snapshot.dart new file mode 100644 index 000000000000..62427ed84ef9 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_data_snapshot.dart @@ -0,0 +1,57 @@ +// Copyright 2021 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +abstract class DataSnapshotPlatform extends PlatformInterface { + DataSnapshotPlatform(this.ref, this._data) : super(token: _token); + + static final Object _token = Object(); + + final Map _data; + + /// Throws an [AssertionError] if [instance] does not extend + /// [DocumentSnapshotPlatform]. + /// + /// This is used by the app-facing [DocumentSnapshot] to ensure that + /// the object in which it's going to delegate calls has been + /// constructed properly. + static void verifyExtends(DataSnapshotPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + } + + /// The Reference for the location that generated this DataSnapshot. + final DatabaseReferencePlatform ref; + + /// The key of the location that generated this DataSnapshot. + String? get key { + return _data['key']; + } + + bool get exists { + return _data['value'] != null; + } + + Object? get value { + return _data['value']; + } + + Object? get priority { + return _data['priority']; + } + + /// Returns true if the specified child path has (non-null) data. + bool hasChild(String path) { + return child(path).exists; + } + + DataSnapshotPlatform child(String childPath) { + throw UnimplementedError('child has not been implemented'); + } + + Iterable get children { + throw UnimplementedError('get children has not been implemented'); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database.dart new file mode 100644 index 000000000000..2ebdf129fcd6 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database.dart @@ -0,0 +1,167 @@ +// Copyright 2021 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 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:meta/meta.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../method_channel/method_channel_database.dart'; + +/// Defines an interface to work with [FirebaseDatabase] on web and mobile +abstract class DatabasePlatform extends PlatformInterface { + /// The [FirebaseApp] instance to which this [FirebaseDatabase] belongs. + /// + /// If null, the default [FirebaseApp] is used. + final FirebaseApp? app; + + /// Gets an instance of [FirebaseDatabase]. + /// + /// If [app] is specified, its options should include a [databaseURL]. + + DatabasePlatform({this.app, this.databaseURL}) : super(token: _token); + + static final Object _token = Object(); + + /// Create an instance using [app] using the existing implementation + factory DatabasePlatform.instanceFor({ + required FirebaseApp app, + String? databaseURL, + }) { + return DatabasePlatform.instance + .delegateFor(app: app, databaseURL: databaseURL); + } + + /// The current default [DatabasePlatform] instance. + /// + /// It will always default to [MethodChannelDatabase] + /// if no web implementation was provided. + static DatabasePlatform? _instance; + + /// The current default [DatabasePlatform] instance. + /// + /// It will always default to [MethodChannelDatabase] + /// if no other implementation was provided. + static DatabasePlatform get instance { + return _instance ??= MethodChannelDatabase(); + } + + static set instance(DatabasePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Enables delegates to create new instances of themselves if a none default + /// [FirebaseApp] instance is required by the user. + @protected + DatabasePlatform delegateFor({ + required FirebaseApp app, + String? databaseURL, + }) { + throw UnimplementedError('delegateFor() is not implemented'); + } + + /// The URL to which this [FirebaseDatabase] belongs + /// + /// If null, the URL of the specified [FirebaseApp] is used + final String? databaseURL; + + /// Returns any arguments to be provided to a [MethodChannel]. + Map getChannelArguments([Map? other]) { + throw UnimplementedError('getChannelArguments() is not implemented'); + } + + /// Changes this instance to point to a FirebaseDatabase emulator running locally. + /// + /// Set the [host] of the local emulator, such as "localhost" + /// Set the [port] of the local emulator, such as "9000" (default is 9000) + /// + /// Note: Must be called immediately, prior to accessing FirebaseFirestore methods. + /// Do not use with production credentials as emulator traffic is not encrypted. + void useDatabaseEmulator(String host, int port) { + throw UnimplementedError('useDatabaseEmulator() not implemented'); + } + + /// Gets a DatabaseReference for the root of your Firebase Database. + DatabaseReferencePlatform ref([String? path]) { + throw UnimplementedError('ref() not implemented'); + } + + /// Attempts to sets the database persistence to [enabled]. + /// + /// This property must be set before calling methods on database references + /// and only needs to be called once per application. The returned [Future] + /// will complete with `true` if the operation was successful or `false` if + /// the persistence could not be set (because database references have + /// already been created). + /// + /// The Firebase Database client will cache synchronized data and keep track + /// of all writes you’ve initiated while your application is running. It + /// seamlessly handles intermittent network connections and re-sends write + /// operations when the network connection is restored. + /// + /// However by default your write operations and cached data are only stored + /// in-memory and will be lost when your app restarts. By setting [enabled] + /// to `true`, the data will be persisted to on-device (disk) storage and will + /// thus be available again when the app is restarted (even when there is no + /// network connectivity at that time). + void setPersistenceEnabled(bool enabled) { + throw UnimplementedError('setPersistenceEnabled() not implemented'); + } + + /// Attempts to set the size of the persistence cache. + /// + /// By default the Firebase Database client will use up to 10MB of disk space + /// to cache data. If the cache grows beyond this size, the client will start + /// removing data that hasn’t been recently used. If you find that your + /// application caches too little or too much data, call this method to change + /// the cache size. + /// + /// This property must be set before calling methods on database references + /// and only needs to be called once per application. The returned [Future] + /// will complete with `true` if the operation was successful or `false` if + /// the value could not be set (because database references have already been + /// the value could not be set (because database references have already been + /// created). + /// + /// Note that the specified cache size is only an approximation and the size + /// on disk may temporarily exceed it at times. Cache sizes smaller than 1 MB + /// or greater than 100 MB are not supported. + void setPersistenceCacheSizeBytes(int cacheSize) { + throw UnimplementedError('setPersistenceCacheSizeBytes() not implemented'); + } + + /// Enables verbose diagnostic logging for debugging your application. + /// This must be called before any other usage of FirebaseDatabase instance. + /// By default, diagnostic logging is disabled. + void setLoggingEnabled(bool enabled) { + throw UnimplementedError('setLoggingEnabled() not implemented'); + } + + /// Resumes our connection to the Firebase Database backend after a previous + /// [goOffline] call. + Future goOnline() { + throw UnimplementedError('goOnline() not implemented'); + } + + /// Shuts down our connection to the Firebase Database backend until + /// [goOnline] is called. + Future goOffline() { + throw UnimplementedError('goOffline() not implemented'); + } + + /// The Firebase Database client automatically queues writes and sends them to + /// the server at the earliest opportunity, depending on network connectivity. + /// In some cases (e.g. offline usage) there may be a large number of writes + /// waiting to be sent. Calling this method will purge all outstanding writes + /// so they are abandoned. + /// + /// All writes will be purged, including transactions and onDisconnect writes. + /// The writes will be rolled back locally, perhaps triggering events for + /// affected event listeners, and the client will not (re-)send them to the + /// Firebase Database backend. + Future purgeOutstandingWrites() { + throw UnimplementedError('purgeOutstandingWrites() not implemented'); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database_event.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database_event.dart new file mode 100755 index 000000000000..1ee4ee9aa4db --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database_event.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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Enum to define various types of database events +enum DatabaseEventType { + /// Event for [onChildAdded] listener + childAdded, + + /// Event for [onChildRemoved] listener + childRemoved, + + /// Event for [onChildChanged] listener + childChanged, + + /// Event for [onChildMoved] listener + childMoved, + + /// Event for [onValue] listener + value, +} + +const _eventTypeFromStringMap = { + 'childAdded': DatabaseEventType.childAdded, + 'childRemoved': DatabaseEventType.childRemoved, + 'childChanged': DatabaseEventType.childChanged, + 'childMoved': DatabaseEventType.childMoved, + 'value': DatabaseEventType.value, +}; + +const _eventTypeToStringMap = { + DatabaseEventType.childAdded: 'childAdded', + DatabaseEventType.childRemoved: 'childRemoved', + DatabaseEventType.childChanged: 'childChanged', + DatabaseEventType.childMoved: 'childMoved', + DatabaseEventType.value: 'value', +}; + +DatabaseEventType eventTypeFromString(String value) { + if (!_eventTypeFromStringMap.containsKey(value)) { + throw Exception('Unknown event type: $value'); + } + return _eventTypeFromStringMap[value]!; +} + +String eventTypeToString(DatabaseEventType value) { + if (!_eventTypeToStringMap.containsKey(value)) { + throw Exception('Unknown event type: $value'); + } + return _eventTypeToStringMap[value]!; +} + +/// `Event` encapsulates a DataSnapshot and possibly also the key of its +/// previous sibling, which can be used to order the snapshots. +abstract class DatabaseEventPlatform extends PlatformInterface { + DatabaseEventPlatform(this._data) : super(token: _token); + + static final Object _token = Object(); + + Map _data; + + /// Throws an [AssertionError] if [instance] does not extend + /// [DatabaseEventPlatform]. + static void verifyExtends(DatabaseEventPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + } + + DataSnapshotPlatform get snapshot { + throw UnimplementedError('get snapshot is not implemented'); + } + + String? get previousChildKey { + return _data['previousChildKey']; + } + + DatabaseEventType get type { + return eventTypeFromString(_data['eventType']); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/database_reference.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database_reference.dart similarity index 72% rename from packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/database_reference.dart rename to packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database_reference.dart index cb68806079ff..94711b48f7cb 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/database_reference.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_database_reference.dart @@ -1,24 +1,23 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; /// DatabaseReference represents a particular location in your Firebase /// Database and can be used for reading or writing data to that location. /// /// This class is the starting point for all Firebase Database operations. /// After you’ve obtained your first DatabaseReference via -/// `FirebaseDatabase.reference()`, you can use it to read data +/// `FirebaseDatabase.ref()`, you can use it to read data /// (ie. `onChildAdded`), write data (ie. `setValue`), and to create new /// `DatabaseReference`s (ie. `child`). /// Note: [QueryPlatform] extends PlatformInterface already. abstract class DatabaseReferencePlatform extends QueryPlatform { /// Create a [DatabaseReferencePlatform] using [pathComponents] DatabaseReferencePlatform._( - DatabasePlatform database, List pathComponents) - : super(database: database, pathComponents: pathComponents); + DatabasePlatform database, + ) : super(database: database); /// Gets a DatabaseReference for the location at the specified relative /// path. The relative path can either be a simple child key (e.g. ‘fred’) or @@ -30,7 +29,7 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { /// Gets a DatabaseReference for the parent location. If this instance /// refers to the root of your Firebase Database, it has no parent, and /// therefore parent() will return null. - DatabaseReferencePlatform? parent() { + DatabaseReferencePlatform? get parent { throw UnimplementedError('parent() not implemented'); } @@ -41,7 +40,7 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { /// Gets the last token in a Firebase Database location (e.g. ‘fred’ in /// https://SampleChat.firebaseIO-demo.com/users/fred) - String get key => pathComponents.last; + String? get key => throw UnimplementedError('key() is not implemented'); /// Generates a new child location using a unique key and returns a /// DatabaseReference to it. This is useful when the children of a Firebase @@ -54,7 +53,7 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { throw UnimplementedError('push() not implemented'); } - /// Write `value` to the location with the specified `priority` if applicable. + /// Write `value` to the location. /// /// This will overwrite any data at this location and all child locations. /// @@ -66,12 +65,28 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { /// /// Passing null for the new value means all data at this location or any /// child location will be deleted. - Future set(dynamic value, {dynamic priority}) { + Future set(Object? value) { + throw UnimplementedError('set() not implemented'); + } + + /// Write a `value` to the location with the specified `priority` if applicable. + /// + /// This will overwrite any data at this location and all child locations. + /// + /// Data types that are allowed are String, boolean, int, double, Map, List. + /// + /// The effect of the write will be visible immediately and the corresponding + /// events will be triggered. Synchronization of the data to the Firebase + /// Database servers will also be started. + /// + /// Passing null for the new value means all data at this location or any + /// child location will be deleted. + Future setWithPriority(Object? value, Object? priority) { throw UnimplementedError('set() not implemented'); } /// Update the node with the `value` - Future update(Map value) { + Future update(Map value) { throw UnimplementedError('update() not implemented'); } @@ -99,7 +114,7 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { /// Note that priorities are parsed and ordered as IEEE 754 double-precision /// floating-point numbers. Keys are always stored as strings and are treated /// as numbers only when they can be parsed as a 32-bit integer. - Future setPriority(dynamic priority) async { + Future setPriority(Object? priority) async { throw UnimplementedError('setPriority() not implemented'); } @@ -111,13 +126,14 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { /// Database servers will also be started. /// /// remove() is equivalent to calling set(null) - Future remove() => set(null); + Future remove() => throw UnimplementedError('remove() not implemented'); /// Performs an optimistic-concurrency transactional update to the data at /// this Firebase Database location. Future runTransaction( - TransactionHandler transactionHandler, - {Duration timeout = const Duration(seconds: 5)}) async { + TransactionHandler transactionHandler, { + bool applyLocally = true, + }) async { throw UnimplementedError('runTransaction() not implemented'); } @@ -126,39 +142,3 @@ abstract class DatabaseReferencePlatform extends QueryPlatform { throw UnimplementedError('onDisconnect() not implemented'); } } - -class ServerValue { - static const Map timestamp = { - '.sv': 'timestamp' - }; - - /// Returns a placeholder value that can be used to atomically increment the - /// current database value by the provided delta. - static Map increment(int delta) { - return { - '.sv': {'increment': delta} - }; - } -} - -/// Interface for [TransactionHandler] -typedef TransactionHandler = MutableData Function(MutableData mutableData); - -/// Interface for [TransactionResultPlatform] -class TransactionResultPlatform extends PlatformInterface { - /// Constructor for [TransactionResultPlatform] - TransactionResultPlatform(this.error, this.committed, this.dataSnapshot) - : super(token: _token); - - static final Object _token = Object(); - - /// [DatabaseErrorPlatform] associated to this transaction result - final DatabaseErrorPlatform? error; - - /// [committed] status associated to this transaction result - final bool committed; - - /// [DataSnapshotPlatform] status associated to this transaction result - - final DataSnapshotPlatform? dataSnapshot; -} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/on_disconnect.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_on_disconnect.dart similarity index 63% rename from packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/on_disconnect.dart rename to packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_on_disconnect.dart index 99e09c569745..aa6ea548889c 100755 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/on_disconnect.dart +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_on_disconnect.dart @@ -1,30 +1,43 @@ -// ignore_for_file: require_trailing_commas // 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. -part of firebase_database_platform_interface; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The [onDisconnect] class allows you to write or clear data when your client disconnects from the Database server. /// These updates occur whether your client disconnects cleanly or not, so you can rely on them to clean up data even if a connection is dropped or a client crashes. abstract class OnDisconnectPlatform extends PlatformInterface { /// Create a [OnDisconnectPlatform] instance - OnDisconnectPlatform({required this.database, required this.reference}) + OnDisconnectPlatform({required this.database, required this.ref}) : super(token: _token); + /// Throws an [AssertionError] if [instance] does not extend + /// [OnDisconnectPlatform]. + static void verifyExtends(OnDisconnectPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + } + static final Object _token = Object(); /// The Database instance associated with this [OnDisconnectPlatform] class final DatabasePlatform database; /// The DatabaseReference instance associated with this [OnDisconnectPlatform] class - final DatabaseReferencePlatform reference; + final DatabaseReferencePlatform ref; /// Ensures the data at this location is set to the specified value when the client is disconnected - Future set(dynamic value, {dynamic priority}) { + Future set(Object? value) { throw UnimplementedError('set() not implemented'); } + /// Ensures the data at this location is set with a priority to the specified + /// value when the client is disconnected (due to closing the browser, + /// navigating to a new page, or network issues). + Future setWithPriority(Object? value, Object? priority) { + throw UnimplementedError('setWithPriority() not implemented'); + } + /// Ensures the data at this location is deleted when the client is disconnected Future remove() => set(null); @@ -34,7 +47,7 @@ abstract class OnDisconnectPlatform extends PlatformInterface { } /// Writes multiple values at this location when the client is disconnected - Future update(Map value) { + Future update(Map value) { throw UnimplementedError('update() not implemented'); } } diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_query.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_query.dart new file mode 100755 index 000000000000..2c3204d2b446 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_query.dart @@ -0,0 +1,70 @@ +// 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Represents a query over the data at a particular location. +abstract class QueryPlatform extends PlatformInterface { + /// The Database instance associated with this query + final DatabasePlatform database; + + static final Object _token = Object(); + + /// Create a [QueryPlatform] instance + QueryPlatform({ + required this.database, + }) : super(token: _token); + + /// Returns the path to this reference. + String get path { + throw UnimplementedError('get path not implemented'); + } + + /// Obtains a DatabaseReference corresponding to this query's location. + DatabaseReferencePlatform get ref { + throw UnimplementedError('get ref() not implemented'); + } + + /// Assigns the proper event type to a stream for [DatabaseEventPlatform] + Stream observe( + QueryModifiers modifiers, + DatabaseEventType eventType, + ) { + throw UnimplementedError('observe() not implemented'); + } + + /// Gets the most up-to-date result for this query. + Future get(QueryModifiers modifiers) { + throw UnimplementedError('get() not implemented'); + } + + /// Fires when children are added. + Stream onChildAdded(QueryModifiers modifiers) => + observe(modifiers, DatabaseEventType.childAdded); + + /// Fires when children are removed. `previousChildKey` is null. + Stream onChildRemoved(QueryModifiers modifiers) => + observe(modifiers, DatabaseEventType.childRemoved); + + /// Fires when children are changed. + Stream onChildChanged(QueryModifiers modifiers) => + observe(modifiers, DatabaseEventType.childChanged); + + /// Fires when children are moved. + Stream onChildMoved(QueryModifiers modifiers) => + observe(modifiers, DatabaseEventType.childMoved); + + /// Fires when the data at this location is updated. `previousChildKey` is null. + Stream onValue(QueryModifiers modifiers) => + observe(modifiers, DatabaseEventType.value); + + /// By calling keepSynced(true) on a location, the data for that location will + /// automatically be downloaded and kept in sync, even when no listeners are + /// attached for that location. Additionally, while a location is kept synced, + /// it will not be evicted from the persistent disk cache. + Future keepSynced(QueryModifiers modifiers, bool value) { + throw UnimplementedError('keepSynced() not implemented'); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_transaction_result.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_transaction_result.dart new file mode 100644 index 000000000000..1a88b814ffd1 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/platform_interface_transaction_result.dart @@ -0,0 +1,33 @@ +// Copyright 2021 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Interface for [TransactionHandler] +typedef TransactionHandler = Transaction Function(Object? value); + +/// Interface for [TransactionResultPlatform] +class TransactionResultPlatform extends PlatformInterface { + /// Constructor for [TransactionResultPlatform] + TransactionResultPlatform( + this.committed, + ) : super(token: _token); + + /// Throws an [AssertionError] if [instance] does not extend + /// [TransactionResultPlatform]. + static void verifyExtends(TransactionResultPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + } + + static final Object _token = Object(); + + /// The [committed] status associated to this transaction result. + final bool committed; + + /// The [DataSnapshotPlatform] associated to this transaction result. + DataSnapshotPlatform get snapshot { + throw UnimplementedError('get snapshot is not implemented'); + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/query.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/query.dart deleted file mode 100755 index 5458bab5796c..000000000000 --- a/packages/firebase_database/firebase_database_platform_interface/lib/src/platform_interface/query.dart +++ /dev/null @@ -1,144 +0,0 @@ -// ignore_for_file: require_trailing_commas -// 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. - -part of firebase_database_platform_interface; - -/// Represents a query over the data at a particular location. -abstract class QueryPlatform extends PlatformInterface { - /// The Database instance associated with this query - final DatabasePlatform database; - - /// The pathComponents associated with this query - final List pathComponents; - - /// The parameters associated with this query - final Map parameters; - - static final Object _token = Object(); - - /// Create a [QueryPlatform] instance - QueryPlatform({ - required this.database, - required this.pathComponents, - this.parameters = const {}, - }) : super(token: _token); - - /// Slash-delimited path representing the database location of this query. - String get path => throw UnimplementedError('path not implemented'); - - /// Assigns the proper event type to a stream for [EventPlatform] - Stream observe(EventType eventType) { - throw UnimplementedError('observe() not implemented'); - } - - Map buildArguments() { - return { - ...parameters, - 'path': path, - }; - } - - /// Listens for a single value event and then stops listening. - Future once() async => (await onValue.first).snapshot; - - /// Gets the most up-to-date result for this query. - Future get() { - throw UnimplementedError('get() not implemented'); - } - - /// Fires when children are added. - Stream get onChildAdded => observe(EventType.childAdded); - - /// Fires when children are removed. `previousChildKey` is null. - Stream get onChildRemoved => observe(EventType.childRemoved); - - /// Fires when children are changed. - Stream get onChildChanged => observe(EventType.childChanged); - - /// Fires when children are moved. - Stream get onChildMoved => observe(EventType.childMoved); - - /// Fires when the data at this location is updated. `previousChildKey` is null. - Stream get onValue => observe(EventType.value); - - /// Create a query constrained to only return child nodes with a value greater - /// than or equal to the given value, using the given orderBy directive or - /// priority as default, and optionally only child nodes with a key greater - /// than or equal to the given key. - QueryPlatform startAt(dynamic value, {String? key}) { - throw UnimplementedError('startAt() not implemented'); - } - - /// Create a query constrained to only return child nodes with a value less - /// than or equal to the given value, using the given orderBy directive or - /// priority as default, and optionally only child nodes with a key less - /// than or equal to the given key. - QueryPlatform endAt(dynamic value, {String? key}) { - throw UnimplementedError('endAt() not implemented'); - } - - /// Create a query constrained to only return child nodes with the given - /// `value` (and `key`, if provided). - /// - /// If a key is provided, there is at most one such child as names are unique. - QueryPlatform equalTo(dynamic value, {String? key}) { - throw UnimplementedError('equalTo() not implemented'); - } - - /// Create a query with limit and anchor it to the start of the window. - QueryPlatform limitToFirst(int limit) { - throw UnimplementedError('limitToFirst() not implemented'); - } - - /// Create a query with limit and anchor it to the end of the window. - QueryPlatform limitToLast(int limit) { - throw UnimplementedError('limitToLast() not implemented'); - } - - /// Generate a view of the data sorted by values of a particular child key. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. - QueryPlatform orderByChild(String key) { - throw UnimplementedError('orderByChild() not implemented'); - } - - /// Generate a view of the data sorted by key. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. - QueryPlatform orderByKey() { - throw UnimplementedError('orderByKey() not implemented'); - } - - /// Generate a view of the data sorted by value. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. - QueryPlatform orderByValue() { - throw UnimplementedError('orderByValue() not implemented'); - } - - /// Generate a view of the data sorted by priority. - /// - /// Intended to be used in combination with [startAt], [endAt], or - /// [equalTo]. - QueryPlatform orderByPriority() { - throw UnimplementedError('orderByPriority() not implemented'); - } - - /// By calling keepSynced(true) on a location, the data for that location will - /// automatically be downloaded and kept in sync, even when no listeners are - /// attached for that location. Additionally, while a location is kept synced, - /// it will not be evicted from the persistent disk cache. - Future keepSynced(bool value) { - throw UnimplementedError('keepSynced() not implemented'); - } - - /// Obtains a DatabaseReference corresponding to this query's location. - DatabaseReferencePlatform reference() { - throw UnimplementedError('reference() not implemented'); - } -} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/query_modifiers.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/query_modifiers.dart new file mode 100644 index 000000000000..bdf525fb246c --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/query_modifiers.dart @@ -0,0 +1,246 @@ +// Copyright 2021 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. + +/// Represents the available modifiers of a Query instance. +class QueryModifiers { + /// Constructs a new [QueryModifiers] instance with a given modifier list. + QueryModifiers(this._modifiers); + + final List _modifiers; + + LimitModifier? _limit; + OrderModifier? _order; + StartCursorModifier? _start; + EndCursorModifier? _end; + + /// Transforms the instance into an ordered serializable list. + List> toList() { + return _modifiers.map((m) => m.toMap()).toList(growable: false); + } + + /// Returns the current ordered modifiers list. + Iterable toIterable() { + return _modifiers; + } + + /// Creates a start cursor modifier. + QueryModifiers start(StartCursorModifier modifier) { + assert( + _start == null, + 'A starting point was already set (by another call to `startAt`, `startAfter`, or `equalTo`)', + ); + _assertCursorValue(modifier.value); + _start = modifier; + return _add(modifier); + } + + /// Creates an end cursor modifier. + QueryModifiers end(EndCursorModifier modifier) { + assert( + _end == null, + 'A ending point was already set (by another call to `endAt`, `endBefore` or `equalTo`)', + ); + _assertCursorValue(modifier.value); + _end = modifier; + return _add(modifier); + } + + /// Creates an limitTo modifier. + QueryModifiers limit(LimitModifier modifier) { + assert( + _limit == null, + 'A limit was already set (by another call to `limitToFirst` or `limitToLast`)', + ); + assert(modifier.value >= 0); + _limit = modifier; + return _add(modifier); + } + + /// Creates an orderBy modifier. + QueryModifiers order(OrderModifier modifier) { + assert( + _order == null, + 'An order has already been set, you cannot combine multiple order by calls', + ); + _order = modifier; + return _add(modifier); + } + + /// Adds a modifier, validates and returns a new [QueryModifiers] instance. + QueryModifiers _add(QueryModifier modifier) { + _modifiers.add(modifier); + _validate(); + return QueryModifiers(_modifiers); + } + + /// Validates the current modifiers. + void _validate() { + if (_order?.name == 'orderByKey') { + if (_start != null) { + assert( + _start!.key == null, + 'When ordering by key, you may only pass a value argument with no key to `startAt`, `endAt`, or `equalTo`', + ); + assert( + _start!.value is String, + 'When ordering by key, you may only pass a value argument as a String to `startAt`, `endAt`, or `equalTo`', + ); + } + + if (_end != null) { + assert( + _end!.key == null, + 'When ordering by key, you may only pass a value argument with no key to `startAt`, `endAt`, or `equalTo`', + ); + assert( + _end!.value is String, + 'When ordering by key, you may only pass a value argument as a String to `startAt`, `endAt`, or `equalTo`', + ); + } + } + + if (_order?.name == 'orderByPriority') { + if (_start != null) { + _assertPriorityValue(_start!.value); + } + if (_end != null) { + _assertPriorityValue(_end!.value); + } + } + } + + /// Asserts a query modifier value is a valid type. + void _assertCursorValue(Object? value) { + assert( + value is String || value is bool || value is num || value == null, + 'value must be a String, Boolean, Number or null.', + ); + } + + /// Asserts a given value is a valid priority. + void _assertPriorityValue(Object? value) { + assert( + value == null || value is String || value is num, + 'When ordering by priority, the first value of an order must be a valid priority value (null, String or Number)', + ); + } +} + +/// A single interface for all modifiers to implement. +abstract class QueryModifier { + /// Constructs a new [QueryModifier] instance. + QueryModifier(this.name); + + /// The modifier name, e.g. startAt, endBefore, limitToLast etc. + final String name; + + /// Converts the modifier into a serializable map. + Map toMap(); +} + +/// A modifier representing a limit query. +class LimitModifier implements QueryModifier { + LimitModifier._(this.name, this.value); + + /// Creates a new `limitToFirst` modifier with a limit. + LimitModifier.limitToFirst(int limit) : this._('limitToFirst', limit); + + /// Creates a new `limitToLast` modifier with a limit. + LimitModifier.limitToLast(int limit) : this._('limitToLast', limit); + + /// The limit value applied to the query. + final int value; + + @override + final String name; + + @override + Map toMap() { + return {'type': 'limit', 'name': name, 'limit': value}; + } +} + +/// A modifier representing a start cursor query. +class StartCursorModifier extends _CursorModifier { + StartCursorModifier._(String name, Object? value, String? key) + : super(name, value, key); + + /// Creates a new `startAt` modifier with an optional key. + StartCursorModifier.startAt(Object? value, String? key) + : this._('startAt', value, key); + + /// Creates a new `startAfter` modifier with an optional key. + StartCursorModifier.startAfter(Object? value, String? key) + : this._('startAfter', value, key); +} + +/// A modifier representing a end cursor query. +class EndCursorModifier extends _CursorModifier { + EndCursorModifier._(String name, Object? value, String? key) + : super(name, value, key); + + /// Creates a new `endAt` modifier with an optional key. + EndCursorModifier.endAt(Object? value, String? key) + : this._('endAt', value, key); + + /// Creates a new `endBefore` modifier with an optional key. + EndCursorModifier.endBefore(Object? value, String? key) + : this._('endBefore', value, key); +} + +/// Underlying cursor query modifier for start and end points. +class _CursorModifier implements QueryModifier { + _CursorModifier(this.name, this.value, this.key); + + @override + final String name; + + /// The value to identify what value the cursor should target. + final Object? value; + + /// An optional key for the cursor query. + final String? key; + + @override + Map toMap() { + return { + 'type': 'cursor', + 'name': name, + if (value != null) 'value': value, + if (key != null) 'key': key, + }; + } +} + +/// A modifier representing an order modifier. +class OrderModifier implements QueryModifier { + OrderModifier._(this.name, this.path); + + /// Creates a new `orderByChild` modifier with path. + OrderModifier.orderByChild(String path) : this._('orderByChild', path); + + /// Creates a new `orderByKey` modifier. + OrderModifier.orderByKey() : this._('orderByKey', null); + + /// Creates a new `orderByValue` modifier. + OrderModifier.orderByValue() : this._('orderByValue', null); + + /// Creates a new `orderByPriority` modifier. + OrderModifier.orderByPriority() : this._('orderByPriority', null); + + @override + final String name; + + /// A path value when ordering by a child path. + final String? path; + + @override + Map toMap() { + return { + 'type': 'orderBy', + 'name': name, + if (path != null) 'path': path, + }; + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/server_value.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/server_value.dart new file mode 100644 index 000000000000..a6c699f1bc5e --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/server_value.dart @@ -0,0 +1,17 @@ +// Copyright 2021 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 ServerValue { + static const Map timestamp = { + '.sv': 'timestamp' + }; + + /// Returns a placeholder value that can be used to atomically increment the + /// current database value by the provided delta. + static Map increment(int delta) { + return { + '.sv': {'increment': delta} + }; + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/lib/src/transaction.dart b/packages/firebase_database/firebase_database_platform_interface/lib/src/transaction.dart new file mode 100644 index 000000000000..53c50de54c8c --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/lib/src/transaction.dart @@ -0,0 +1,21 @@ +// Copyright 2021 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. + +/// The pending result of a [TransactionHandler]. +class Transaction { + Transaction._(this.aborted, this.value); + + /// The transaction was successful and should update the reference to the new + /// [value] provided. + Transaction.success(Object? value) : this._(false, value); + + /// The transaction should be aborted. + Transaction.abort() : this._(true, null); + + /// Whether the transaction was aborted. + final bool aborted; + + /// The new value that will be set if the transaction was not [aborted]. + final Object? value; +} diff --git a/packages/firebase_database/firebase_database_platform_interface/test/database_reference_test.dart b/packages/firebase_database/firebase_database_platform_interface/test/database_reference_test.dart new file mode 100644 index 000000000000..ac0075d06f35 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/test/database_reference_test.dart @@ -0,0 +1,36 @@ +import 'package:firebase_database_platform_interface/src/method_channel/method_channel_database.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MethodChannelDatabase database; + + setUp(() { + database = MethodChannelDatabase(); + }); + + group('DatabaseReference.key', () { + test('null for root', () { + final ref = database.ref(); + expect(ref.key, null); + }); + + test('last component of the path for non-root locations', () { + final ref = database.ref('path/to/value'); + expect(ref.key, 'value'); + }); + }); + + group('DatabaseReference.parent', () { + test('null for root', () { + final ref = database.ref(); + expect(ref.parent, null); + }); + + test('correct ref for nodes with parents', () { + final ref = database.ref('path/to/value'); + expect(ref.parent!.key, 'to'); + }); + }); +} diff --git a/packages/firebase_database/firebase_database_platform_interface/test/firebase_database_test.dart b/packages/firebase_database/firebase_database_platform_interface/test/firebase_database_test.dart old mode 100755 new mode 100644 index 50e40832da1b..5b97d0613af5 --- a/packages/firebase_database/firebase_database_platform_interface/test/firebase_database_test.dart +++ b/packages/firebase_database/firebase_database_platform_interface/test/firebase_database_test.dart @@ -1,616 +1,9 @@ -// ignore_for_file: require_trailing_commas -// Copyright 2017 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:firebase_core/firebase_core.dart'; -import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'test_common.dart'; +import './method_channel_test.dart' as method_channel; +import './database_reference_test.dart' as database_reference; +import './query_modifiers_test.dart' as query_modifiers; void main() { - initializeMethodChannel(); - late FirebaseApp app; - - setUpAll(() async { - app = await Firebase.initializeApp( - name: 'testApp', - options: const FirebaseOptions( - appId: '1:1234567890:ios:42424242424242', - apiKey: '123', - projectId: '123', - messagingSenderId: '1234567890', - ), - ); - }); - - group('$MethodChannelDatabase', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/firebase_database', - ); - int mockHandleId = 0; - final List log = []; - - const String databaseURL = 'https://fake-database-url2.firebaseio.com'; - late MethodChannelDatabase database; - - setUp(() async { - database = MethodChannelDatabase(app: app, databaseURL: databaseURL); - - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'Query#observe': - return mockHandleId++; - case 'FirebaseDatabase#setPersistenceEnabled': - return true; - case 'FirebaseDatabase#setPersistenceCacheSizeBytes': - return true; - case 'DatabaseReference#runTransaction': - late Map updatedValue; - Future simulateEvent( - int transactionKey, final MutableData mutableData) async { - await ServicesBinding.instance!.defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - channel.codec.encodeMethodCall( - MethodCall( - 'DoTransaction', - { - 'transactionKey': transactionKey, - 'snapshot': { - 'key': mutableData.key, - 'value': mutableData.value, - }, - }, - ), - ), - (data) { - updatedValue = channel.codec - .decodeEnvelope(data!)['value'] - .cast(); - }, - ); - } - - await simulateEvent( - 0, - MutableData.private({ - 'key': 'fakeKey', - 'value': {'fakeKey': 'fakeValue'}, - })); - - return { - 'error': null, - 'committed': true, - 'snapshot': { - 'key': 'fakeKey', - 'value': updatedValue - }, - 'childKeys': ['fakeKey'] - }; - default: - return null; - } - }); - log.clear(); - }); - - test('setPersistenceEnabled', () async { - expect(await database.setPersistenceEnabled(false), true); - expect(await database.setPersistenceEnabled(true), true); - expect( - log, - [ - isMethodCall( - 'FirebaseDatabase#setPersistenceEnabled', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'enabled': false, - }, - ), - isMethodCall( - 'FirebaseDatabase#setPersistenceEnabled', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'enabled': true, - }, - ), - ], - ); - }); - - test('setPersistentCacheSizeBytes', () async { - expect(await database.setPersistenceCacheSizeBytes(42), true); - expect( - log, - [ - isMethodCall( - 'FirebaseDatabase#setPersistenceCacheSizeBytes', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'cacheSize': 42, - }, - ), - ], - ); - }); - - test('goOnline', () async { - await database.goOnline(); - expect( - log, - [ - isMethodCall( - 'FirebaseDatabase#goOnline', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - }, - ), - ], - ); - }); - - test('goOffline', () async { - await database.goOffline(); - expect( - log, - [ - isMethodCall( - 'FirebaseDatabase#goOffline', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - }, - ), - ], - ); - }); - - test('purgeOutstandingWrites', () async { - await database.purgeOutstandingWrites(); - expect( - log, - [ - isMethodCall( - 'FirebaseDatabase#purgeOutstandingWrites', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - }, - ), - ], - ); - }); - - group('$MethodChannelDatabaseReference', () { - test('set', () async { - final dynamic value = {'hello': 'world'}; - final dynamic serverValue = { - 'qux': ServerValue.increment(8) - }; - const int priority = 42; - await database.reference().child('foo').set(value); - await database.reference().child('bar').set(value, priority: priority); - await database.reference().child('baz').set(serverValue); - expect( - log, - [ - isMethodCall( - 'DatabaseReference#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'value': value, - 'priority': null, - }, - ), - isMethodCall( - 'DatabaseReference#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'bar', - 'value': value, - 'priority': priority, - }, - ), - isMethodCall( - 'DatabaseReference#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'baz', - 'value': { - 'qux': { - '.sv': {'increment': 8} - } - }, - 'priority': null, - }, - ), - ], - ); - }); - test('update', () async { - final dynamic value = {'hello': 'world'}; - await database.reference().child('foo').update(value); - expect( - log, - [ - isMethodCall( - 'DatabaseReference#update', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'value': value, - }, - ), - ], - ); - }); - - test('setPriority', () async { - const int priority = 42; - await database.reference().child('foo').setPriority(priority); - expect( - log, - [ - isMethodCall( - 'DatabaseReference#setPriority', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'priority': priority, - }, - ), - ], - ); - }); - - test('runTransaction', () async { - final TransactionResultPlatform transactionResult = await database - .reference() - .child('foo') - .runTransaction((MutableData? mutableData) { - mutableData!.value['fakeKey'] = - 'updated ${mutableData.value['fakeKey']}'; - return mutableData; - }); - expect( - log, - [ - isMethodCall( - 'DatabaseReference#runTransaction', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'transactionKey': 0, - 'transactionTimeout': 5000, - }, - ), - ], - ); - expect(transactionResult.committed, equals(true)); - expect( - transactionResult.dataSnapshot!.value, - equals({'fakeKey': 'updated fakeValue'}), - ); - }); - }); - - group('$MethodChannelOnDisconnect', () { - test('set', () async { - final dynamic value = {'hello': 'world'}; - const int priority = 42; - final DatabaseReferencePlatform ref = database.reference(); - await ref.child('foo').onDisconnect().set(value); - await ref.child('bar').onDisconnect().set(value, priority: priority); - await ref.child('psi').onDisconnect().set(value, priority: 'priority'); - await ref.child('por').onDisconnect().set(value, priority: value); - expect( - log, - [ - isMethodCall( - 'OnDisconnect#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'value': value, - 'priority': null, - }, - ), - isMethodCall( - 'OnDisconnect#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'bar', - 'value': value, - 'priority': priority, - }, - ), - isMethodCall( - 'OnDisconnect#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'psi', - 'value': value, - 'priority': 'priority', - }, - ), - isMethodCall( - 'OnDisconnect#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'por', - 'value': value, - 'priority': value, - }, - ), - ], - ); - }); - test('update', () async { - final dynamic value = {'hello': 'world'}; - await database.reference().child('foo').onDisconnect().update(value); - expect( - log, - [ - isMethodCall( - 'OnDisconnect#update', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'value': value, - }, - ), - ], - ); - }); - test('cancel', () async { - await database.reference().child('foo').onDisconnect().cancel(); - expect( - log, - [ - isMethodCall( - 'OnDisconnect#cancel', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - }, - ), - ], - ); - }); - test('remove', () async { - await database.reference().child('foo').onDisconnect().remove(); - expect( - log, - [ - isMethodCall( - 'OnDisconnect#set', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': 'foo', - 'value': null, - 'priority': null, - }, - ), - ], - ); - }); - }); - - group('$MethodChannelQuery', () { - test('keepSynced, simple query', () async { - const String path = 'foo'; - final QueryPlatform query = database.reference().child(path); - await query.keepSynced(true); - expect( - log, - [ - isMethodCall( - 'Query#keepSynced', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': path, - 'parameters': {}, - 'value': true, - }, - ), - ], - ); - }); - test('keepSynced, complex query', () async { - const int startAt = 42; - const String path = 'foo'; - const String childKey = 'bar'; - const bool endAt = true; - const String endAtKey = 'baz'; - final QueryPlatform query = database - .reference() - .child(path) - .orderByChild(childKey) - .startAt(startAt) - .endAt(endAt, key: endAtKey); - await query.keepSynced(false); - final Map expectedParameters = { - 'orderBy': 'child', - 'orderByChildKey': childKey, - 'startAt': startAt, - 'endAt': endAt, - 'endAtKey': endAtKey, - }; - expect( - log, - [ - isMethodCall( - 'Query#keepSynced', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': path, - 'parameters': expectedParameters, - 'value': false - }, - ), - ], - ); - }); - test('observing error events', () async { - mockHandleId = 99; - const int errorCode = 12; - const String errorDetails = 'Some details'; - final QueryPlatform query = database.reference().child('some path'); - Future simulateError(String errorMessage) async { - await ServicesBinding.instance!.defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - channel.codec.encodeMethodCall( - MethodCall('Error', { - 'handle': 99, - 'error': { - 'code': errorCode, - 'message': errorMessage, - 'details': errorDetails, - }, - }), - ), - (_) {}); - } - - final AsyncQueue errors = - AsyncQueue(); - - // Subscribe and allow subscription to complete. - final StreamSubscription subscription = - query.onValue.listen((_) {}, onError: errors.add); - await Future.delayed(Duration.zero); - - await simulateError('Bad foo'); - await simulateError('Bad bar'); - final DatabaseErrorPlatform error1 = await errors.remove(); - final DatabaseErrorPlatform error2 = await errors.remove(); - await subscription.cancel(); - expect(error1.toString(), - 'DatabaseErrorPlatform(12, Bad foo, Some details)'); - expect(error1.code, errorCode); - expect(error1.message, 'Bad foo'); - expect(error1.details, errorDetails); - expect(error2.code, errorCode); - expect(error2.message, 'Bad bar'); - expect(error2.details, errorDetails); - }); - - test('observing value events', () async { - mockHandleId = 87; - const String path = 'foo'; - final QueryPlatform query = database.reference().child(path); - Future simulateEvent(String value) async { - await ServicesBinding.instance!.defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - channel.codec.encodeMethodCall( - MethodCall('Event', { - 'handle': 87, - 'snapshot': { - 'key': path, - 'value': value, - }, - }), - ), - (_) {}); - } - - final AsyncQueue events = AsyncQueue(); - - // Subscribe and allow subscription to complete. - final StreamSubscription subscription = - query.onValue.listen(events.add); - await Future.delayed(Duration.zero); - - await simulateEvent('1'); - await simulateEvent('2'); - final EventPlatform event1 = await events.remove(); - final EventPlatform event2 = await events.remove(); - expect(event1.snapshot.key, path); - expect(event1.snapshot.value, '1'); - expect(event2.snapshot.key, path); - expect(event2.snapshot.value, '2'); - - // Cancel subscription and allow cancellation to complete. - await subscription.cancel(); - await Future.delayed(Duration.zero); - - expect( - log, - [ - isMethodCall( - 'Query#observe', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': path, - 'parameters': {}, - 'eventType': 'EventType.value', - }, - ), - isMethodCall( - 'Query#removeObserver', - arguments: { - 'app': app.name, - 'databaseURL': databaseURL, - 'path': path, - 'parameters': {}, - 'handle': 87, - }, - ), - ], - ); - }); - }); - }); -} - -/// Queue whose remove operation is asynchronous, awaiting a corresponding add. -class AsyncQueue { - Map> _completers = >{}; - int _nextToRemove = 0; - int _nextToAdd = 0; - - void add(T element) { - _completer(_nextToAdd++).complete(element); - } - - Future remove() { - return _completer(_nextToRemove++).future; - } - - Completer _completer(int index) { - if (_completers.containsKey(index)) { - return _completers.remove(index)!; - } else { - return _completers[index] = Completer(); - } - } + method_channel.main(); + database_reference.main(); + query_modifiers.main(); } diff --git a/packages/firebase_database/firebase_database_platform_interface/test/method_channel_test.dart b/packages/firebase_database/firebase_database_platform_interface/test/method_channel_test.dart new file mode 100755 index 000000000000..9ec54e8e8df5 --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/test/method_channel_test.dart @@ -0,0 +1,582 @@ +// Copyright 2017 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:firebase_core/firebase_core.dart'; +import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:firebase_database_platform_interface/src/method_channel/method_channel_database.dart'; +import 'package:firebase_database_platform_interface/src/method_channel/method_channel_database_reference.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_common.dart'; + +void main() { + initializeMethodChannel(); + late FirebaseApp app; + late BinaryMessenger messenger; + + setUpAll(() async { + app = await Firebase.initializeApp( + name: 'testApp', + options: const FirebaseOptions( + appId: '1:1234567890:ios:42424242424242', + apiKey: '123', + projectId: '123', + messagingSenderId: '1234567890', + ), + ); + + messenger = ServicesBinding.instance!.defaultBinaryMessenger; + }); + + group('MethodChannelDatabase', () { + const channel = MethodChannel('plugins.flutter.io/firebase_database'); + const eventChannel = MethodChannel('mock/path'); + + final List log = []; + + const String databaseURL = 'https://fake-database-url2.firebaseio.com'; + late MethodChannelDatabase database; + + setUp(() async { + database = MethodChannelDatabase(app: app, databaseURL: databaseURL); + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + switch (methodCall.method) { + case 'Query#observe': + return 'mock/path'; + case 'DatabaseReference#runTransaction': + late Map updatedValue; + + Future simulateTransaction( + int transactionKey, + String key, + dynamic data, + ) async { + await messenger.handlePlatformMessage( + channel.name, + channel.codec.encodeMethodCall( + MethodCall( + 'FirebaseDatabase#callTransactionHandler', + { + 'transactionKey': transactionKey, + 'snapshot': { + 'key': key, + 'value': data, + }, + }, + ), + ), + (data) { + final decoded = channel.codec.decodeEnvelope(data!); + updatedValue = + Map.from(decoded.cast()['value']); + }, + ); + } + + await simulateTransaction(0, 'fakeKey', {'fakeKey': 'fakeValue'}); + + return { + 'error': null, + 'committed': true, + 'snapshot': { + 'key': 'fakeKey', + 'value': updatedValue + }, + 'childKeys': ['fakeKey'] + }; + default: + return null; + } + }); + + log.clear(); + }); + + test('setting database instance options', () async { + database.setLoggingEnabled(true); + database.setPersistenceCacheSizeBytes(10000); + database.setPersistenceEnabled(true); + database.useDatabaseEmulator('localhost', 1234); + // Options are only sent on subsequent calls to method channel. + await database.goOnline(); + expect( + log, + [ + isMethodCall( + 'FirebaseDatabase#goOnline', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'persistenceEnabled': true, + 'cacheSizeBytes': 10000, + 'loggingEnabled': true, + 'emulatorHost': 'localhost', + 'emulatorPort': 1234 + }, + ), + ], + ); + }); + + test('goOnline', () async { + await database.goOnline(); + expect( + log, + [ + isMethodCall( + 'FirebaseDatabase#goOnline', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + }, + ), + ], + ); + }); + + test('goOffline', () async { + await database.goOffline(); + expect( + log, + [ + isMethodCall( + 'FirebaseDatabase#goOffline', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + }, + ), + ], + ); + }); + + test('purgeOutstandingWrites', () async { + await database.purgeOutstandingWrites(); + expect( + log, + [ + isMethodCall( + 'FirebaseDatabase#purgeOutstandingWrites', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + }, + ), + ], + ); + }); + + group('$MethodChannelDatabaseReference', () { + test('set & setWithPriority', () async { + final dynamic value = {'hello': 'world'}; + final dynamic serverValue = { + 'qux': ServerValue.increment(8) + }; + const int priority = 42; + await database.ref('foo').set(value); + await database.ref('bar').setWithPriority(value, priority); + await database.ref('bar').setWithPriority(value, null); + await database.ref('baz').set(serverValue); + expect( + log, + [ + isMethodCall( + 'DatabaseReference#set', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + 'value': value, + }, + ), + isMethodCall( + 'DatabaseReference#setWithPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'bar', + 'value': value, + 'priority': priority, + }, + ), + isMethodCall( + 'DatabaseReference#setWithPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'bar', + 'value': value, + }, + ), + isMethodCall( + 'DatabaseReference#set', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'baz', + 'value': { + 'qux': { + '.sv': {'increment': 8} + } + }, + }, + ), + ], + ); + }); + test('update', () async { + final dynamic value = {'hello': 'world'}; + await database.ref('foo').update(value); + expect( + log, + [ + isMethodCall( + 'DatabaseReference#update', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + 'value': value, + }, + ), + ], + ); + }); + + test('setPriority', () async { + const int priority = 42; + await database.ref('foo').setPriority(priority); + expect( + log, + [ + isMethodCall( + 'DatabaseReference#setPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + 'priority': priority, + }, + ), + ], + ); + }); + + test('runTransaction', () async { + final ref = database.ref('foo'); + + final result = await ref.runTransaction((value) { + return Transaction.success({ + ...value! as Map, + 'fakeKey': 'updated ${(value as Map)['fakeKey']}', + }); + }); + + expect( + log, + [ + isMethodCall( + 'DatabaseReference#runTransaction', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + 'transactionApplyLocally': true, + 'transactionKey': 0, + }, + ), + ], + ); + + expect(result.committed, equals(true)); + + expect( + result.snapshot.value, + equals({'fakeKey': 'updated fakeValue'}), + ); + }); + }); + + group('MethodChannelOnDisconnect', () { + test('set', () async { + final dynamic value = {'hello': 'world'}; + const int priority = 42; + final DatabaseReferencePlatform ref = database.ref(); + await ref.child('foo').onDisconnect().set(value); + await ref.child('bar').onDisconnect().setWithPriority(value, priority); + await ref + .child('psi') + .onDisconnect() + .setWithPriority(value, 'priority'); + await ref.child('por').onDisconnect().setWithPriority(value, value); + await ref.child('por').onDisconnect().setWithPriority(value, null); + expect( + log, + [ + isMethodCall( + 'OnDisconnect#set', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + 'value': value, + }, + ), + isMethodCall( + 'OnDisconnect#setWithPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'bar', + 'value': value, + 'priority': priority, + }, + ), + isMethodCall( + 'OnDisconnect#setWithPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'psi', + 'value': value, + 'priority': 'priority', + }, + ), + isMethodCall( + 'OnDisconnect#setWithPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'por', + 'value': value, + 'priority': value, + }, + ), + isMethodCall( + 'OnDisconnect#setWithPriority', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'por', + 'value': value, + }, + ), + ], + ); + }); + test('update', () async { + final dynamic value = {'hello': 'world'}; + await database.ref('foo').onDisconnect().update(value); + expect( + log, + [ + isMethodCall( + 'OnDisconnect#update', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + 'value': value, + }, + ), + ], + ); + }); + test('cancel', () async { + await database.ref('foo').onDisconnect().cancel(); + expect( + log, + [ + isMethodCall( + 'OnDisconnect#cancel', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + }, + ), + ], + ); + }); + test('remove', () async { + await database.ref('foo').onDisconnect().remove(); + expect( + log, + [ + isMethodCall( + // Internally calls set(null). + 'OnDisconnect#set', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': 'foo', + }, + ), + ], + ); + }); + }); + + group('MethodChannelQuery', () { + test('keepSynced, simple query', () async { + const String path = 'foo'; + final QueryPlatform query = database.ref(path); + await query.keepSynced(QueryModifiers([]), true); + expect( + log, + [ + isMethodCall( + 'Query#keepSynced', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': path, + 'modifiers': [], + 'value': true, + }, + ), + ], + ); + }); + test('observing error events', () async { + const String errorCode = 'some-error'; + + final QueryPlatform query = database.ref('some/path'); + + Future simulateError(String errorMessage) async { + await eventChannel.binaryMessenger.handlePlatformMessage( + eventChannel.name, + eventChannel.codec.encodeErrorEnvelope( + code: errorCode, + message: errorMessage, + details: { + 'code': errorCode, + 'message': errorMessage, + }, + ), + (_) {}, + ); + } + + final errors = AsyncQueue(); + + final subscription = query + .onValue(QueryModifiers([])) + .listen((_) {}, onError: errors.add); + await Future.delayed(Duration.zero); + + await simulateError('Bad foo'); + await simulateError('Bad bar'); + + final FirebaseException error1 = await errors.remove(); + final FirebaseException error2 = await errors.remove(); + + await subscription.cancel(); + + expect( + error1.toString(), + startsWith('[firebase_database/some-error] Bad foo'), + ); + + expect(error1.code, errorCode); + expect(error1.message, 'Bad foo'); + + expect(error2.code, errorCode); + expect(error2.message, 'Bad bar'); + }); + + test('observing value events', () async { + const String path = 'foo'; + final QueryPlatform query = database.ref(path); + + Future simulateEvent(Map event) async { + await eventChannel.binaryMessenger.handlePlatformMessage( + eventChannel.name, + eventChannel.codec.encodeSuccessEnvelope(event), + (_) {}, + ); + } + + Map createValueEvent(dynamic value) { + return { + 'eventType': 'value', + 'snapshot': { + 'value': value, + 'key': path.split('/').last, + }, + }; + } + + final AsyncQueue events = + AsyncQueue(); + + // Subscribe and allow subscription to complete. + final subscription = + query.onValue(QueryModifiers([])).listen(events.add); + await Future.delayed(Duration.zero); + + await simulateEvent(createValueEvent(1)); + await simulateEvent(createValueEvent(2)); + + final DatabaseEventPlatform event1 = await events.remove(); + final DatabaseEventPlatform event2 = await events.remove(); + + expect(event1.snapshot.key, path); + expect(event1.snapshot.value, 1); + expect(event2.snapshot.key, path); + expect(event2.snapshot.value, 2); + + // Cancel subscription and allow cancellation to complete. + await subscription.cancel(); + await Future.delayed(Duration.zero); + + expect( + log, + [ + isMethodCall( + 'Query#observe', + arguments: { + 'appName': app.name, + 'databaseURL': databaseURL, + 'path': path, + 'modifiers': [], + 'eventChannelNamePrefix': + 'foo-testApp-https://fake-database-url2.firebaseio.com-DatabaseEventType.value-[]', + }, + ) + ], + ); + }); + }); + }); +} + +/// Queue whose remove operation is asynchronous, awaiting a corresponding add. +class AsyncQueue { + Map> _completers = >{}; + int _nextToRemove = 0; + int _nextToAdd = 0; + + void add(T element) { + _completer(_nextToAdd++).complete(element); + } + + Future remove() { + return _completer(_nextToRemove++).future; + } + + Completer _completer(int index) { + if (_completers.containsKey(index)) { + return _completers.remove(index)!; + } else { + return _completers[index] = Completer(); + } + } +} diff --git a/packages/firebase_database/firebase_database_platform_interface/test/query_modifiers_test.dart b/packages/firebase_database/firebase_database_platform_interface/test/query_modifiers_test.dart new file mode 100644 index 000000000000..e9c4865eca9c --- /dev/null +++ b/packages/firebase_database/firebase_database_platform_interface/test/query_modifiers_test.dart @@ -0,0 +1,213 @@ +// Copyright 2021 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 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('QueryModifiers', () { + test('toList() returns an list', () { + final instance = QueryModifiers([]); + expect(instance.toList(), isA>>()); + }); + + test('toIterable() returns an iterable', () { + final instance = QueryModifiers([]); + expect(instance.toIterable(), isA>()); + }); + + group('start()', () { + test('fails assertion if a starting point is already set', () { + final instance = QueryModifiers([]); + instance.start(StartCursorModifier.startAt('foo', 'bar')); + + expect( + () => instance.start(StartCursorModifier.startAfter('foo', 'bar')), + throwsAssertionError, + ); + }); + + test('fails assertion if value is not valid', () { + final instance = QueryModifiers([]); + + expect( + () => instance.start(StartCursorModifier.startAfter({}, 'bar')), + throwsAssertionError, + ); + }); + + test('it adds to the modifier list', () { + final instance = QueryModifiers([]); + expect(instance.toList().length, 0); + + instance.start(StartCursorModifier.startAfter('foo', 'bar')); + + expect( + instance.toList(), + equals([ + { + 'type': 'cursor', + 'name': 'startAfter', + 'value': 'foo', + 'key': 'bar' + } + ]), + ); + }); + }); + + group('end()', () { + test('fails assertion if a ending point is already set', () { + final instance = QueryModifiers([]); + instance.end(EndCursorModifier.endAt('foo', 'bar')); + + expect( + () => instance.end(EndCursorModifier.endBefore('foo', 'bar')), + throwsAssertionError, + ); + }); + + test('fails assertion if value is not valid', () { + final instance = QueryModifiers([]); + + expect( + () => instance.end(EndCursorModifier.endBefore([], 'bar')), + throwsAssertionError, + ); + }); + + test('it adds to the modifier list', () { + final instance = QueryModifiers([]); + expect(instance.toList().length, 0); + + instance.end(EndCursorModifier.endAt('foo', 'bar')); + + expect( + instance.toList(), + equals([ + {'type': 'cursor', 'name': 'endAt', 'value': 'foo', 'key': 'bar'} + ]), + ); + }); + }); + + group('limit()', () { + test('fails assertion if a limit is already set', () { + final instance = QueryModifiers([]); + instance.limit(LimitModifier.limitToFirst(10)); + + expect( + () => instance.limit(LimitModifier.limitToLast(10)), + throwsAssertionError, + ); + }); + + test('fails assertion if value is not valid', () { + final instance = QueryModifiers([]); + + expect( + () => instance.limit(LimitModifier.limitToLast(-2)), + throwsAssertionError, + ); + }); + + test('it adds to the modifier list', () { + final instance = QueryModifiers([]); + expect(instance.toList().length, 0); + + instance.limit(LimitModifier.limitToLast(10)); + + expect( + instance.toList(), + equals([ + {'type': 'limit', 'name': 'limitToLast', 'limit': 10} + ]), + ); + }); + }); + + group('order()', () { + test('fails assertion if a order is already set', () { + final instance = QueryModifiers([]); + instance.order(OrderModifier.orderByKey()); + + expect( + () => instance.order(OrderModifier.orderByPriority()), + throwsAssertionError, + ); + }); + + test('it adds to the modifier list', () { + final instance = QueryModifiers([]); + expect(instance.toList().length, 0); + + instance.order(OrderModifier.orderByPriority()); + + expect( + instance.toList(), + equals([ + { + 'type': 'orderBy', + 'name': 'orderByPriority', + } + ]), + ); + }); + }); + + group('validation', () { + test( + 'it fails assertion when ordering by key, but the key provided to a cursor modifier is also set', + () { + final instance = QueryModifiers([]); + + instance.start(StartCursorModifier.startAt('foo', 'bar')); + + expect( + () => instance.order(OrderModifier.orderByKey()), + throwsAssertionError, + ); + }); + + test( + 'it fails assertion when ordering by key, but the value provided to a cursor modifier is not a string', + () { + final instance = QueryModifiers([]); + + instance.start(StartCursorModifier.startAt(123, null)); + + expect( + () => instance.order(OrderModifier.orderByKey()), + throwsAssertionError, + ); + }); + + test( + 'it fails assertion when ordering by priority, but start cursor value is not a valid priority value', + () { + final instance = QueryModifiers([]); + + instance.start(StartCursorModifier.startAfter(true, null)); + + expect( + () => instance.order(OrderModifier.orderByPriority()), + throwsAssertionError, + ); + }); + + test( + 'it fails assertion when ordering by priority, but end cursor value is not a valid priority value', + () { + final instance = QueryModifiers([]); + + instance.end(EndCursorModifier.endBefore(true, null)); + + expect( + () => instance.order(OrderModifier.orderByPriority()), + throwsAssertionError, + ); + }); + }); + }); +} diff --git a/packages/firebase_database/firebase_database_platform_interface/test/test_common.dart b/packages/firebase_database/firebase_database_platform_interface/test/test_common.dart index 0db9fc267ad1..b4cb1b28d03b 100644 --- a/packages/firebase_database/firebase_database_platform_interface/test/test_common.dart +++ b/packages/firebase_database/firebase_database_platform_interface/test/test_common.dart @@ -1,4 +1,3 @@ -// ignore_for_file: require_trailing_commas // Copyright 2020, 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. diff --git a/packages/firebase_database/firebase_database_web/lib/firebase_database_web.dart b/packages/firebase_database/firebase_database_web/lib/firebase_database_web.dart index f4a5932c926f..47d03c372d8b 100755 --- a/packages/firebase_database/firebase_database_web/lib/firebase_database_web.dart +++ b/packages/firebase_database/firebase_database_web/lib/firebase_database_web.dart @@ -5,6 +5,7 @@ library firebase_database_web; import 'dart:async'; +import 'dart:js_util' as util; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_web/firebase_core_web.dart'; @@ -13,12 +14,20 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'src/interop/database.dart' as database_interop; +part './src/data_snapshot_web.dart'; + +part './src/database_event_web.dart'; + part './src/database_reference_web.dart'; part './src/ondisconnect_web.dart'; part './src/query_web.dart'; +part './src/transaction_result_web.dart'; + +part './src/utils/exception.dart'; + part './src/utils/snapshot_utils.dart'; /// Web implementation for [DatabasePlatform] @@ -48,15 +57,14 @@ class FirebaseDatabaseWeb extends DatabasePlatform { : super(app: app, databaseURL: databaseURL); @override - DatabasePlatform withApp(FirebaseApp? app, String? databaseURL) => - FirebaseDatabaseWeb(app: app, databaseURL: databaseURL); - - @override - String? appName() => app?.name; + DatabasePlatform delegateFor( + {required FirebaseApp app, String? databaseURL}) { + return FirebaseDatabaseWeb(app: app, databaseURL: databaseURL); + } @override - DatabaseReferencePlatform reference() { - return DatabaseReferenceWeb(_delegate, this, []); + DatabaseReferencePlatform ref([String? path]) { + return DatabaseReferenceWeb(this, _delegate.ref(path)); } /// This is not supported on web. However, @@ -69,33 +77,46 @@ class FirebaseDatabaseWeb extends DatabasePlatform { /// On the web, real-time database offline mode work in Tunnel mode not with airplane mode. /// check the https://stackoverflow.com/a/32530269/3452078 @override - Future setPersistenceEnabled(bool enabled) async { + void setPersistenceEnabled(bool enabled) { throw UnsupportedError("setPersistenceEnabled() is not supported for web"); } @override - Future setPersistenceCacheSizeBytes(int cacheSize) async { + void setPersistenceCacheSizeBytes(int cacheSize) { throw UnsupportedError( "setPersistenceCacheSizeBytes() is not supported for web"); } @override - Future setLoggingEnabled(bool enabled) async { + void setLoggingEnabled(bool enabled) { database_interop.enableLogging(enabled); } @override Future goOnline() async { - _delegate.goOnline(); + try { + _delegate.goOnline(); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override Future goOffline() async { - _delegate.goOffline(); + try { + _delegate.goOffline(); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override Future purgeOutstandingWrites() async { throw UnsupportedError("purgeOutstandingWrites() is not supported for web"); } + + @override + void useDatabaseEmulator(String host, int port) { + _delegate.useDatabaseEmulator(host, port); + } } diff --git a/packages/firebase_database/firebase_database_web/lib/src/data_snapshot_web.dart b/packages/firebase_database/firebase_database_web/lib/src/data_snapshot_web.dart new file mode 100644 index 000000000000..74e29257fae8 --- /dev/null +++ b/packages/firebase_database/firebase_database_web/lib/src/data_snapshot_web.dart @@ -0,0 +1,34 @@ +part of firebase_database_web; + +/// Web implementation for firebase [DataSnapshotPlatform] +class DataSnapshotWeb extends DataSnapshotPlatform { + final database_interop.DataSnapshot _delegate; + + DataSnapshotWeb(DatabaseReferencePlatform ref, this._delegate) + : super(ref, { + 'key': _delegate.key, + 'value': _delegate.val(), + 'priority': _delegate.getPriority(), + }); + + @override + DataSnapshotPlatform child(String childPath) { + return DataSnapshotWeb(ref, _delegate.child(childPath)); + } + + @override + Iterable get children { + List snapshots = []; + + // This creates an in-order array + _delegate.forEach((snapshot) { + snapshots.add(snapshot); + }); + + return Iterable.generate(snapshots.length, + (int index) { + database_interop.DataSnapshot snapshot = snapshots[index]; + return DataSnapshotWeb(ref.child(snapshot.key), snapshot); + }); + } +} diff --git a/packages/firebase_database/firebase_database_web/lib/src/database_event_web.dart b/packages/firebase_database/firebase_database_web/lib/src/database_event_web.dart new file mode 100644 index 000000000000..d7f04fe044fc --- /dev/null +++ b/packages/firebase_database/firebase_database_web/lib/src/database_event_web.dart @@ -0,0 +1,26 @@ +// Copyright 2021 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. + +part of firebase_database_web; + +/// Web implementation for firebase [DataSnapshotPlatform] +class DatabaseEventWeb extends DatabaseEventPlatform { + DatabaseEventWeb( + this._ref, + DatabaseEventType eventType, + this._event, + ) : super({ + 'previousChildKey': _event.prevChildKey, + 'eventType': eventTypeToString(eventType), + }); + + final DatabaseReferencePlatform _ref; + + final database_interop.QueryEvent _event; + + @override + DataSnapshotPlatform get snapshot { + return webSnapshotToPlatformSnapshot(_ref, _event.snapshot); + } +} diff --git a/packages/firebase_database/firebase_database_web/lib/src/database_reference_web.dart b/packages/firebase_database/firebase_database_web/lib/src/database_reference_web.dart index 02815db45305..c1b32a9c99f9 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/database_reference_web.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/database_reference_web.dart @@ -10,63 +10,75 @@ class DatabaseReferenceWeb extends QueryWeb /// Builds an instance of [DatabaseReferenceWeb] delegating to a package:firebase [DatabaseReferencePlatform] /// to delegate queries to underlying firebase web plugin DatabaseReferenceWeb( - database_interop.Database firebaseDatabase, - DatabasePlatform databasePlatform, - List pathComponents, - ) : super( - firebaseDatabase, - databasePlatform, - pathComponents, - pathComponents.isEmpty - ? firebaseDatabase.ref("/") - : firebaseDatabase.ref(pathComponents.join("/")), - ); + DatabasePlatform _database, + this._delegate, + ) : super(_database, _delegate); + + final database_interop.DatabaseReference _delegate; @override DatabaseReferencePlatform child(String path) { - return DatabaseReferenceWeb(_firebaseDatabase, database, - List.from(pathComponents)..addAll(path.split("/"))); + return DatabaseReferenceWeb(_database, _delegate.child(path)); } @override - DatabaseReferencePlatform? parent() { - if (pathComponents.isEmpty) return null; - return DatabaseReferenceWeb(_firebaseDatabase, database, - List.from(pathComponents)..removeLast()); + DatabaseReferencePlatform? get parent { + database_interop.DatabaseReference? parent = _delegate.parent; + + if (parent == null) { + return null; + } + + return DatabaseReferenceWeb(_database, parent); } @override DatabaseReferencePlatform root() { - return DatabaseReferenceWeb(_firebaseDatabase, database, []); + return DatabaseReferenceWeb(_database, _delegate.root); } @override - String get key => pathComponents.last; + String? get key => _delegate.key; @override DatabaseReferencePlatform push() { - final String key = PushIdGenerator.generatePushChildName(); - final List childPath = List.from(pathComponents)..add(key); - return DatabaseReferenceWeb(_firebaseDatabase, database, childPath); + return DatabaseReferenceWeb(_database, _delegate.push()); + } + + @override + Future set(Object? value) async { + try { + await _delegate.set(value); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override - Future set(value, {priority}) { - if (priority == null) { - return _firebaseQuery.ref.set(value); - } else { - return _firebaseQuery.ref.setWithPriority(value, priority); + Future setWithPriority(Object? value, Object? priority) async { + try { + await _delegate.setWithPriority(value, priority); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); } } @override - Future update(Map value) { - return _firebaseQuery.ref.update(value); + Future update(Map value) async { + try { + await _delegate.update(value); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override - Future setPriority(priority) { - return _firebaseQuery.ref.setPriority(priority); + Future setPriority(priority) async { + try { + await _delegate.setPriority(priority); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override @@ -74,34 +86,21 @@ class DatabaseReferenceWeb extends QueryWeb return set(null); } - /// on the web, [timeout] parameter is ignored. - /// transaction((_) => null) doesn't work when compiled to JS - /// probably because of https://github.com/dart-lang/sdk/issues/24088 @override Future runTransaction( - transactionHandler, { - Duration timeout = const Duration(seconds: 5), + TransactionHandler transactionHandler, { + bool applyLocally = true, }) async { try { - final ref = _firebaseQuery.ref; - final transaction = await ref.transaction(transactionHandler); - - return TransactionResultPlatform( - null, - transaction.committed, - fromWebSnapshotToPlatformSnapShot(transaction.snapshot), - ); - } on DatabaseErrorPlatform catch (e) { - return TransactionResultPlatform( - e, - false, - null, - ); + return TransactionResultWeb._( + this, await _delegate.transaction(transactionHandler, applyLocally)); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); } } @override OnDisconnectPlatform onDisconnect() { - return OnDisconnectWeb._(_firebaseQuery.ref.onDisconnect(), database, this); + return OnDisconnectWeb._(_delegate.onDisconnect(), database, this); } } diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/app.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/app.dart index e561b7f123d7..6cde4ed8ecf9 100644 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/app.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/app.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references, require_trailing_commas +// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references // 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. diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/app_interop.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/app_interop.dart index 66a46a2ddc3e..4014964df5f1 100644 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/app_interop.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/app_interop.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references, require_trailing_commas +// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references // 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. diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/database.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/database.dart index cbb99f862206..13fcc42464cf 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/database.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/database.dart @@ -10,6 +10,8 @@ import 'dart:async'; import 'package:firebase_core_web/firebase_core_web_interop.dart' as core_interop; import 'package:firebase_database_platform_interface/firebase_database_platform_interface.dart'; +import 'package:firebase_database_web/firebase_database_web.dart' + show convertFirebaseDatabaseException; import 'package:flutter/widgets.dart'; import 'package:js/js.dart'; import 'package:js/js_util.dart'; @@ -70,9 +72,12 @@ class Database /// state with the server state. void goOnline() => jsObject.goOnline(); + void useDatabaseEmulator(String host, int port) => + jsObject.useEmulator(host, port); + /// Returns a [DatabaseReference] to the root or provided [path]. - DatabaseReference ref([String? path]) => - DatabaseReference.getInstance(jsObject.ref(path)); + DatabaseReference ref([String? path = '/']) => + DatabaseReference.getInstance(jsObject.ref(path ?? '/')); /// Returns a [DatabaseReference] from provided [url]. /// Url must be in the same domain as the current database. @@ -90,11 +95,14 @@ class DatabaseReference /// The last part of the current path. /// It is `null` in case of root DatabaseReference. - String get key => jsObject.key; + String? get key => jsObject.key; /// The parent location of a DatabaseReference. - DatabaseReference get parent => - DatabaseReference.getInstance(jsObject.parent); + DatabaseReference? get parent { + final jsParent = jsObject.parent; + if (jsParent == null) return null; + return DatabaseReference.getInstance(jsParent); + } /// The root location of a DatabaseReference. DatabaseReference get root => DatabaseReference.getInstance(jsObject.root); @@ -179,23 +187,26 @@ class DatabaseReference /// /// Set [applyLocally] to `false` to not see intermediate states. Future transaction( - TransactionHandler transactionUpdate, [ - bool applyLocally = true, - ]) async { + TransactionHandler transactionUpdate, bool applyLocally) async { final c = Completer(); final transactionUpdateWrap = allowInterop((update) { - final dartUpdate = MutableData('key', dartify(update)); - final result = jsify(transactionUpdate(dartUpdate).value); - return result; + final dartUpdate = dartify(update); + final transaction = transactionUpdate(dartUpdate); + if (transaction.aborted) { + return undefined; + } + return jsify(transaction.value); }); - final onCompleteWrap = allowInterop((error, commited, snapshot) { + final onCompleteWrap = allowInterop((error, committed, snapshot) { if (error != null) { - c.completeError(DatabaseErrorPlatform(dartify(error))); + final dartified = dartify(error); + + c.completeError(convertFirebaseDatabaseException(dartified)); } else { c.complete(Transaction( - committed: commited, + committed: committed, snapshot: DataSnapshot._fromJsObject(snapshot), )); } @@ -223,7 +234,7 @@ class DatabaseReference /// /// Database database = firebase.database(); /// database.ref('messages').onValue.listen((QueryEvent e) { -/// DataSnapshot datasnapshot = e.snapshot; +/// DataSnapshot dataSnapshot = e.snapshot; /// //... /// }); class QueryEvent { @@ -307,6 +318,16 @@ class Query ? jsObject.endAt(jsify(value)) : jsObject.endAt(jsify(value), key)); + /// Creates a [Query] with the specified ending point (exclusive) + /// The ending point is exclusive. If only a value is provided, + /// children with a value less than the specified value will be included in + /// the query. If a key is specified, then children must have a value lesss + /// than or equal to the specified value and a a key name less than the + /// specified key. + Query endBefore(value, [String? key]) => Query.fromJsObject(key == null + ? jsObject.endBefore(jsify(value)) + : jsObject.endBefore(jsify(value), key)); + /// Returns a Query which includes children which match the specified [value]. /// /// The [value] must be a [num], [String], [bool], or `null`, or the error @@ -346,10 +367,14 @@ class Query streamController.add(QueryEvent(DataSnapshot.getInstance(data), string)); }); + final cancelCallbackWrap = allowInterop((Object error) { + final dartified = dartify(error); + streamController.addError(convertFirebaseDatabaseException(dartified)); + streamController.close(); + }); + void startListen() { - // TODO(lesnitsky) – should probably implement cancel callback - // See https://firebase.google.com/docs/reference/js/firebase.database.Query#on - jsObject.on(eventType, callbackWrap); + jsObject.on(eventType, callbackWrap, cancelCallbackWrap); } void stopListen() { @@ -402,6 +427,10 @@ class Query : jsObject.startAt(jsify(value), key), ); + Query startAfter(value, [String? key]) => Query.fromJsObject(key == null + ? jsObject.startAfter(jsify(value)) + : jsObject.startAfter(jsify(value), key)); + /// Returns a String representation of Query object. @override String toString() => jsObject.toString(); diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/database_interop.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/database_interop.dart index 4d69f9fc4c4c..54f1b8c7b5a7 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/database_interop.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/database_interop.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references, require_trailing_commas +// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references // 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. @@ -13,9 +13,11 @@ import 'package:firebase_core_web/firebase_core_web_interop.dart' import 'package:firebase_database_web/src/interop/app_interop.dart'; import 'package:js/js.dart'; +part 'data_snapshot_interop.dart'; + part 'query_interop.dart'; + part 'reference_interop.dart'; -part 'data_snapshot_interop.dart'; external void enableLogging([logger, bool persistent]); @@ -40,6 +42,8 @@ abstract class DatabaseJsImpl { external void goOnline(); + external void useEmulator(String host, int port); + external ReferenceJsImpl ref([String? path]); external ReferenceJsImpl refFromURL(String url); diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/firebase_interop.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/firebase_interop.dart index ea321cf2a50e..6b006e504ee1 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/firebase_interop.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/firebase_interop.dart @@ -1,10 +1,8 @@ -// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references, require_trailing_commas +// ignore_for_file: public_member_api_docs, avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references // 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. -// ignore_for_file: public_member_api_docs - @JS('firebase') library firebase.firebase_interop; diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/query_interop.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/query_interop.dart index b97ae0e70335..9233b50b87c7 100644 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/query_interop.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/query_interop.dart @@ -8,8 +8,14 @@ abstract class QueryJsImpl { external PromiseJsImpl get(); + external QueryJsImpl startAt(value, [String key]); + + external QueryJsImpl startAfter(value, [String key]); + external QueryJsImpl endAt(value, [String key]); + external QueryJsImpl endBefore(value, [String key]); + external QueryJsImpl equalTo(value, [String key]); external bool isEqual(QueryJsImpl other); @@ -47,8 +53,6 @@ abstract class QueryJsImpl { external QueryJsImpl orderByValue(); - external QueryJsImpl startAt(value, [String key]); - external Object toJSON(); @override diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/reference_interop.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/reference_interop.dart index 27a3556c3663..3e48297472ed 100644 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/reference_interop.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/reference_interop.dart @@ -7,18 +7,12 @@ abstract class TransactionResultJsImpl { @JS('Reference') abstract class ReferenceJsImpl extends QueryJsImpl { - external String get key; + external String? get key; - external set key(String s); - - external ReferenceJsImpl get parent; - - external set parent(ReferenceJsImpl r); + external ReferenceJsImpl? get parent; external ReferenceJsImpl get root; - external set root(ReferenceJsImpl r); - external ReferenceJsImpl child(String path); external OnDisconnectJsImpl onDisconnect(); diff --git a/packages/firebase_database/firebase_database_web/lib/src/interop/utils/utils.dart b/packages/firebase_database/firebase_database_web/lib/src/interop/utils/utils.dart index 4a5ec4e0a2f9..6279a3afeddf 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/interop/utils/utils.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/interop/utils/utils.dart @@ -1,9 +1,10 @@ -// ignore_for_file: avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references, require_trailing_commas +// ignore_for_file: public_member_api_docs, avoid_unused_constructor_parameters, non_constant_identifier_names, comment_references // 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. -// ignore_for_file: public_member_api_docs +@JS('firebase.app') +library firebase_interop.core.app; import 'dart:async'; @@ -110,3 +111,6 @@ bool _isBasicType(Object? value) { /// Resolves error. void Function(Object) resolveError(Completer c) => allowInterop(c.completeError); + +@JS('undefined') +external Object undefined; diff --git a/packages/firebase_database/firebase_database_web/lib/src/ondisconnect_web.dart b/packages/firebase_database/firebase_database_web/lib/src/ondisconnect_web.dart index 0178d96a1a06..d8dd508ecbcb 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/ondisconnect_web.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/ondisconnect_web.dart @@ -6,32 +6,56 @@ part of firebase_database_web; /// Web implementation for firebase [OnDisconnectPlatform] class OnDisconnectWeb extends OnDisconnectPlatform { - final database_interop.OnDisconnect _onDisconnect; + final database_interop.OnDisconnect _delegate; OnDisconnectWeb._( - this._onDisconnect, + this._delegate, DatabasePlatform database, - DatabaseReferencePlatform reference, - ) : super(database: database, reference: reference); + DatabaseReferencePlatform ref, + ) : super(database: database, ref: ref); @override - Future set(value, {priority}) { - if (priority != null) return _onDisconnect.set(value); - return _onDisconnect.setWithPriority(value, priority); + Future set(Object? value) async { + try { + await _delegate.set(value); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override - Future remove() { - return _onDisconnect.remove(); + Future setWithPriority(Object? value, Object? priority) async { + try { + await _delegate.setWithPriority(value, priority); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override - Future cancel() { - return _onDisconnect.cancel(); + Future remove() async { + try { + await _delegate.remove(); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override - Future update(Map value) { - return _onDisconnect.update(value); + Future cancel() async { + try { + await _delegate.cancel(); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } + } + + @override + Future update(Map value) async { + try { + await _delegate.update(value); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } } diff --git a/packages/firebase_database/firebase_database_web/lib/src/query_web.dart b/packages/firebase_database/firebase_database_web/lib/src/query_web.dart index 40a48f4d7c7b..e64f9a3a086d 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/query_web.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/query_web.dart @@ -6,116 +6,130 @@ part of firebase_database_web; /// An implementation of [QueryPlatform] which proxies calls to js objects class QueryWeb extends QueryPlatform { - final database_interop.Database _firebaseDatabase; - final database_interop.Query _firebaseQuery; + final DatabasePlatform _database; + final database_interop.Query _queryDelegate; QueryWeb( - this._firebaseDatabase, - DatabasePlatform databasePlatform, - List pathComponents, - this._firebaseQuery, - ) : super(database: databasePlatform, pathComponents: pathComponents); - - @override - DatabaseReferencePlatform reference() => - DatabaseReferenceWeb(_firebaseDatabase, database, pathComponents); - - @override - String get path => pathComponents.join('/'); - - @override - Future get() async { - final snapshot = await _firebaseQuery.get(); - return fromWebSnapshotToPlatformSnapShot(snapshot); - } - - @override - Future once() async { - return fromWebSnapshotToPlatformSnapShot( - (await _firebaseQuery.once("value")).snapshot, - ); + this._database, + this._queryDelegate, + ) : super(database: _database); + + database_interop.Query _getQueryDelegateInstance(QueryModifiers modifiers) { + database_interop.Query instance = _queryDelegate; + + modifiers.toIterable().forEach((modifier) { + if (modifier is LimitModifier) { + if (modifier.name == 'limitToFirst') { + instance = instance.limitToFirst(modifier.value); + } + if (modifier.name == 'limitToLast') { + instance = instance.limitToLast(modifier.value); + } + } + + if (modifier is StartCursorModifier) { + if (modifier.name == 'startAt') { + instance = instance.startAt(modifier.value, modifier.key); + } + if (modifier.name == 'startAfter') { + instance = instance.startAfter(modifier.value, modifier.key); + } + } + + if (modifier is EndCursorModifier) { + if (modifier.name == 'endAt') { + instance = instance.endAt(modifier.value, modifier.key); + } + if (modifier.name == 'endBefore') { + instance = instance.endBefore(modifier.value, modifier.key); + } + } + + if (modifier is OrderModifier) { + if (modifier.name == 'orderByChild') { + instance = instance.orderByChild(modifier.path!); + } + if (modifier.name == 'orderByKey') { + instance = instance.orderByKey(); + } + if (modifier.name == 'orderByValue') { + instance = instance.orderByValue(); + } + if (modifier.name == 'orderByPriority') { + instance = instance.orderByPriority(); + } + } + }); + + return instance; } @override - QueryPlatform startAt(dynamic value, {String? key}) { - return _withQuery(_firebaseQuery.startAt(value, key)); - } + DatabaseReferencePlatform get ref => + DatabaseReferenceWeb(_database, _queryDelegate.ref); @override - QueryPlatform endAt(value, {String? key}) { - return _withQuery(_firebaseQuery.endAt(value, key)); - } - - @override - QueryPlatform equalTo(value, {String? key}) { - return _withQuery(_firebaseQuery.equalTo(value, key)); - } - - @override - QueryPlatform limitToFirst(int limit) { - return _withQuery(_firebaseQuery.limitToFirst(limit)); - } - - @override - QueryPlatform limitToLast(int limit) { - return _withQuery(_firebaseQuery.limitToLast(limit)); - } - - @override - QueryPlatform orderByChild(String key) { - return _withQuery(_firebaseQuery.orderByChild(key)); - } - - @override - QueryPlatform orderByKey() { - return _withQuery(_firebaseQuery.orderByKey()); - } - - @override - QueryPlatform orderByPriority() { - return _withQuery(_firebaseQuery.orderByPriority()); - } - - @override - QueryPlatform orderByValue() { - return _withQuery(_firebaseQuery.orderByValue()); + Future get(QueryModifiers modifiers) async { + try { + final result = await _getQueryDelegateInstance(modifiers).get(); + return webSnapshotToPlatformSnapshot(ref, result); + } catch (e, s) { + throw convertFirebaseDatabaseException(e, s); + } } @override - Future keepSynced(bool value) async { + Future keepSynced(QueryModifiers modifiers, bool value) async { throw UnsupportedError('keepSynced() is not supported on web'); } @override - Stream observe(EventType eventType) { + Stream observe( + QueryModifiers modifiers, DatabaseEventType eventType) { + database_interop.Query instance = _getQueryDelegateInstance(modifiers); + switch (eventType) { - case EventType.childAdded: - return _webStreamToPlatformStream(_firebaseQuery.onChildAdded); - case EventType.childChanged: - return _webStreamToPlatformStream(_firebaseQuery.onChildChanged); - case EventType.childMoved: - return _webStreamToPlatformStream(_firebaseQuery.onChildMoved); - case EventType.childRemoved: - return _webStreamToPlatformStream(_firebaseQuery.onChildRemoved); - case EventType.value: - return _webStreamToPlatformStream(_firebaseQuery.onValue); + case DatabaseEventType.childAdded: + return _webStreamToPlatformStream( + eventType, + instance.onChildAdded, + ); + case DatabaseEventType.childChanged: + return _webStreamToPlatformStream( + eventType, + instance.onChildChanged, + ); + case DatabaseEventType.childMoved: + return _webStreamToPlatformStream( + eventType, + instance.onChildMoved, + ); + case DatabaseEventType.childRemoved: + return _webStreamToPlatformStream( + eventType, + instance.onChildRemoved, + ); + case DatabaseEventType.value: + return _webStreamToPlatformStream(eventType, instance.onValue); default: throw Exception("Invalid event type: $eventType"); } } - Stream _webStreamToPlatformStream( - Stream stream) { - return stream.map((database_interop.QueryEvent event) => - fromWebEventToPlatformEvent(event)); - } - - QueryPlatform _withQuery(newQuery) { - return QueryWeb( - _firebaseDatabase, - database, - pathComponents, - newQuery, - ); + Stream _webStreamToPlatformStream( + DatabaseEventType eventType, + Stream stream, + ) { + return stream + .map( + (database_interop.QueryEvent event) => webEventToPlatformEvent( + ref, + eventType, + event, + ), + ) + .handleError((e, s) { + throw convertFirebaseDatabaseException(e, s); + }); } } diff --git a/packages/firebase_database/firebase_database_web/lib/src/transaction_result_web.dart b/packages/firebase_database/firebase_database_web/lib/src/transaction_result_web.dart new file mode 100644 index 000000000000..da96880e193a --- /dev/null +++ b/packages/firebase_database/firebase_database_web/lib/src/transaction_result_web.dart @@ -0,0 +1,19 @@ +// Copyright 2021 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. + +part of firebase_database_web; + +class TransactionResultWeb extends TransactionResultPlatform { + TransactionResultWeb._(this._ref, this._delegate) + : super(_delegate.committed); + + final database_interop.Transaction _delegate; + + final DatabaseReferencePlatform _ref; + + @override + DataSnapshotPlatform get snapshot { + return webSnapshotToPlatformSnapshot(_ref, _delegate.snapshot); + } +} diff --git a/packages/firebase_database/firebase_database_web/lib/src/utils/exception.dart b/packages/firebase_database/firebase_database_web/lib/src/utils/exception.dart new file mode 100644 index 000000000000..3e0cc0599995 --- /dev/null +++ b/packages/firebase_database/firebase_database_web/lib/src/utils/exception.dart @@ -0,0 +1,40 @@ +// Copyright 2021 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. + +part of firebase_database_web; + +FirebaseException convertFirebaseDatabaseException(Object exception, + [StackTrace? stackTrace]) { + String code = 'unknown'; + String message = util.getProperty(exception, 'message'); + + // FirebaseWeb SDK for Database has no error codes, so we manually map known + // messages to known error codes for cross platform consistency. + if (message.toLowerCase().contains('index not defined')) { + code = 'index-not-defined'; + } else if (message.toLowerCase().contains('permission denied')) { + code = 'permission-denied'; + } else if (message + .toLowerCase() + .contains('transaction needs to be run again with current data')) { + code = 'data-stale'; + } else if (message + .toLowerCase() + .contains('transaction had too many retries')) { + code = 'max-retries'; + } else if (message.toLowerCase().contains('service is unavailable')) { + code = 'unavailable'; + } else if (message.toLowerCase().contains('network error')) { + code = 'network-error'; + } else if (message.toLowerCase().contains('write was canceled')) { + code = 'write-cancelled'; + } + + return FirebaseException( + plugin: 'firebase_database', + code: code, + message: message, + stackTrace: stackTrace, + ); +} diff --git a/packages/firebase_database/firebase_database_web/lib/src/utils/snapshot_utils.dart b/packages/firebase_database/firebase_database_web/lib/src/utils/snapshot_utils.dart index c9900aae6519..6d660594175a 100755 --- a/packages/firebase_database/firebase_database_web/lib/src/utils/snapshot_utils.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/utils/snapshot_utils.dart @@ -5,13 +5,18 @@ part of firebase_database_web; /// Builds [EventPlatform] instance form web event instance -EventPlatform fromWebEventToPlatformEvent(database_interop.QueryEvent event) { - return EventPlatform.fromDataSnapshotPlatform( - fromWebSnapshotToPlatformSnapShot(event.snapshot), event.prevChildKey); +DatabaseEventPlatform webEventToPlatformEvent( + DatabaseReferencePlatform ref, + DatabaseEventType eventType, + database_interop.QueryEvent event, +) { + return DatabaseEventWeb(ref, eventType, event); } /// Builds [DataSnapshotPlatform] instance form web snapshot instance -DataSnapshotPlatform fromWebSnapshotToPlatformSnapShot( - database_interop.DataSnapshot snapshot) { - return DataSnapshotPlatform(snapshot.key, snapshot.val()); +DataSnapshotPlatform webSnapshotToPlatformSnapshot( + DatabaseReferencePlatform ref, + database_interop.DataSnapshot snapshot, +) { + return DataSnapshotWeb(ref, snapshot); }