diff --git a/.github/workflows/binary-combine-android.yml b/.github/workflows/binary-combine-android.yml index d41d28c78..6187c54f9 100644 --- a/.github/workflows/binary-combine-android.yml +++ b/.github/workflows/binary-combine-android.yml @@ -9,11 +9,6 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-latest steps: - - name: Fetch x86 build - uses: actions/download-artifact@v4 - with: - name: librealm-android-x86 - path: packages/realm_dart/binary/android - name: Fetch x86_64 build uses: actions/download-artifact@v4 with: @@ -41,7 +36,6 @@ jobs: uses: geekyeggo/delete-artifact@v4 with: name: | - librealm-android-x86 librealm-android-x86_64 librealm-android-armeabi-v7a librealm-android-arm64-v8a diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index b013d965e..67352f8ef 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -53,7 +53,7 @@ jobs: - name: Downgrade XCode for MacOS if: matrix.build == 'macos' - run: sudo xcode-select -s /Applications/Xcode_14.0.1.app + run: sudo xcodes select 14.3.1 - name: Build if: steps.check-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f16a9122..f0b577f51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,12 @@ name: Realm Dart CI on: + workflow_dispatch: push: branches: - main pull_request: + env: REALM_CI: true @@ -43,13 +45,13 @@ jobs: with: runner: ubuntu-20.04 binary: android - build: '["android-x86", "android-x86_64", "android-armeabi-v7a", "android-arm64-v8a"]' + build: '["android-x86_64", "android-armeabi-v7a", "android-arm64-v8a"]' build-ios: name: Build IOS uses: ./.github/workflows/build-native.yml with: - runner: macos-latest + runner: macos-12 binary: ios build: '["ios-device", "ios-simulator", "ios-catalyst"]' @@ -229,7 +231,7 @@ jobs: secrets: inherit with: os: macos - runner: macos-13 # workaround to: https://github.com/flutter/flutter/issues/118469 latest is still macos-12 ¯\_(ツ)_/¯ + runner: macos-latest differentiator: fm${{ github.run_id }}${{ github.run_attempt }} cleanup-cluster-flutter-macos: @@ -279,7 +281,7 @@ jobs: differentiator: fi${{ github.run_id }}${{ github.run_attempt }} flutter-tests-ios: - runs-on: macos-latest + runs-on: macos-12 name: Flutter Tests iOS timeout-minutes: 45 needs: @@ -388,10 +390,10 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Gradle cache - uses: gradle/gradle-build-action@v3 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - - name: Set up Java + - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'temurin' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 3f3c037ad..06a0edf42 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -37,7 +37,7 @@ jobs: run: dart pub global activate melos - name: Update pubspec.yaml files - run: REALM_VERSION=${{ inputs.version}} melos run update:version:realm + run: REALM_VERSION=${{ steps.update-changelog.outputs.new-version }} melos run update:version:realm - name: Update realm.podspec uses: jacobtomlinson/gha-find-replace@b76729678e8d52dadb12e0e16454a93e301a919d #! 2.0.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index 2411f1d50..483d755b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,7 @@ "geospatial", "HRESULT", "keepalive", + "keypaths", "loggable", "maccatalyst", "mugaritz", diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ad745e8..9dba2700b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,139 @@ ## vNext (TBD) +### Enhancements +* The download progress estimate reported by `Session.getProgressStream` will now return meaningful estimated values, while previously it always returned 1. (Issue [#1564](https://github.com/realm/realm-dart/issues/1564)) + +### Fixed +* None + +### Compatibility +* Realm Studio: 15.0.0 or later. + +### Internal +* Using Core x.y.z. + +## 2.3.0 (2024-05-23) + +### Enhancements +* Added support for creating and storing a RealmObject using the `Realm.dynamic` API: `realm.dynamic.create("Person", primaryKey: 123)`. (PR [#1669](https://github.com/realm/realm-dart/pull/1669)) +* Added support for setting properties on a RealmObject using the dynamic API: `obj.dynamic.set("name", "Peter")`. (PR [#1669](https://github.com/realm/realm-dart/pull/1669)) +* Listening for `.changes` on a dynamic object (obtained via the `realm.dynamic` API) no longer throws. (Issue [#1668](https://github.com/realm/realm-dart/issues/1668)) +* Nested collections have full support for automatic client reset. (Core 14.7.0) + +### Fixed +* Private fields did not work with default values. (Issue [#1663](https://github.com/realm/realm-dart/issues/1663)) +* Invoke scheduler callback on Zone.current. (Issue [#1676](https://github.com/realm/realm-dart/issues/1676)) + +* Having links in a nested collections would leave the file inconsistent if the top object is removed. (Core 14.7.0) + +* Accessing App.currentUser from within a notification produced by App.switchUser() (which includes notifications for a newly logged in user) would deadlock. (Core 14.7.0) + +* Inserting the same typed link to the same key in a dictionary more than once would incorrectly create multiple backlinks to the object. This did not appear to cause any crashes later, but would have affecting explicit backlink count queries (eg: `...@links.@count`) and possibly notifications. (Core 14.7.0) + + +### Compatibility +* Realm Studio: 15.0.0 or later. + +### Internal +* Using Core 14.7.0. + +## 2.2.1 (2024-05-02) + +### Fixed +* `realm_privacy` bundle mistakenly included an exe-file preventing app store submissions. (Issue [#1656](https://github.com/realm/realm-dart/issues/1656)) + +### Compatibility +* Realm Studio: 15.0.0 or later. + +### Internal +* Using Core 14.6.2. +* Drop build of `x86` android slice. (Issue [#1670](https://github.com/realm/realm-dart/issues/1670)) + +## 2.2.0 (2024-05-01) + +### Enhancements +* Allow configuration of generator per model class. Currently support specifying the constructor style to use. + ```dart + const config = GeneratorConfig(ctorStyle: CtorStyle.allNamed); + const realmModel = RealmModel.using(baseType: ObjectType.realmObject, generatorConfig: config); + + @realmModel + class _Person { + late String name; + int age = 42; + } + ``` + will generate a constructor like: + ```dart + Person({ + required String name, + int age = 42, + }) { ... } + ``` + (Issue [#292](https://github.com/realm/realm-dart/issues/292)) +* Add privacy manifest to apple binaries. (Issue [#1551](https://github.com/realm/realm-dart/issues/1551)) + +### Fixed +* Avoid: Attempt to execute code removed by Dart AOT compiler (TFA). (Issue [#1647](https://github.com/realm/realm-dart/issues/1647)) +* Fixed nullability annotations for the experimental API `App.baseUrl` and `App.updateBaseUrl`. The former is guaranteed not to be `null`, while the latter will now accept a `null` argument, in which case the base url will be restored to its default value. (Issue [#1523](https://github.com/realm/realm-dart/issues/1523)) +* `App.users` included logged out users only if they were logged out while the App instance existed. It now always includes all logged out users. (Core 14.6.0) +* Fixed several issues around encrypted file portability (copying a "bundled" encrypted Realm from one device to another): (Core 14.6.0) + * Fixed `Assertion failed: new_size % (1ULL << m_page_shift) == 0` when opening an encrypted Realm less than 64Mb that was generated on a platform with a different page size than the current platform. + * Fixed a `DecryptionFailed` exception thrown when opening a small (<4k of data) Realm generated on a device with a page size of 4k if it was bundled and opened on a device with a larger page size. + * Fixed an issue during a subsequent open of an encrypted Realm for some rare allocation patterns when the top ref was within ~50 bytes of the end of a page. This could manifest as a DecryptionFailed exception or as an assertion: `encrypted_file_mapping.hpp:183: Assertion failed: local_ndx < m_page_state.size()`. +* Schema initialization could hit an assertion failure if the sync client applied a downloaded changeset while the Realm file was in the process of being opened. (Core 14.6.0) +* Improve performance of "chained OR equality" queries for UUID/ObjectId types and RQL parsed "IN" queries on string/int/uuid/objectid types. (Core 14.6.0) +* Fixed a bug when running a IN query (or a query of the pattern `x == 1 OR x == 2 OR x == 3`) when evaluating on a string property with an empty string in the search condition. Matches with an empty string would have been evaluated as if searching for a null string instead. (Core 14.6.2) + +### Compatibility +* Realm Studio: 15.0.0 or later. + +### Internal +* Using Core 14.6.2. +* Flutter: ^3.19.0 +* Dart: ^3.3.0 + + +## 2.1.0 (2024-04-17) + ### Enhancements * Improve file compaction performance on platforms with page sizes greater than 4k (for example arm64 Apple platforms) for files less than 256 pages in size (Core 14.4.0). +* Added support for specifying key paths when listening to notifications on an object with the `RealmObject.changesFor([List? keyPaths])` method. The key paths indicates which changes in properties should raise a notification. + ```dart + @RealmModel() + class _Person { + late String name; + late int age; + late List<_Person> friends; + } + + // .... + + // Only changes to person.age and person.friends will raise a notification + person.changesFor(["age", "friends"]).listen( .... ) + ``` * Add better hint to error message, if opening native library fails. (Issue [#1595](https://github.com/realm/realm-dart/issues/1595)) * Added support for specifying schema version on `Configuration.flexibleSync`. This allows you to take advantage of an upcoming server-side feature that will allow schema migrations for synchronized Realms. (Issue [#1599](https://github.com/realm/realm-dart/issues/1599)) * The default base url in `AppConfiguration` has been updated to point to `services.cloud.mongodb.com`. See https://www.mongodb.com/docs/atlas/app-services/domain-migration/ for more information. (Issue [#1549](https://github.com/realm/realm-dart/issues/1549)) -* The download progress estimate reported by `Session.getProgressStream` will now return meaningful estimated values, while previously it always returned 1. (Issue [#1564](https://github.com/realm/realm-dart/issues/1564)) +* Don't ignore private fields on realm models. (Issue [#1367](https://github.com/realm/realm-dart/issues/1367)) +* Improve performance of `RealmValue.operator==` when containing binary data. (PR [#1628](https://github.com/realm/realm-dart/pull/1628)) ### Fixed * Using valid const, but non-literal expressions, such as negation of numbers, as an initializer would fail. (Issue [#1606](https://github.com/realm/realm-dart/issues/1606)) -* Backlinks mistakenly included in EJson serialization. ([Issue #1616](https://github.com/realm/realm-dart/issues/1616)) +* Backlinks mistakenly included in EJson serialization. (Issue [#1616](https://github.com/realm/realm-dart/issues/1616)) * Fix an assertion failure "m_lock_info && m_lock_info->m_file.get_path() == m_filename" that appears to be related to opening a Realm while the file is in the process of being closed on another thread. (Core 14.5.0) * Fixed diverging history due to a bug in the replication code when setting default null values (embedded objects included). (Core 14.5.0) * Null pointer exception may be triggered when logging out and async commits callbacks not executed. (Core 14.5.0) +* Comparing RealmValue containing a collection to itself would return false. Semantics changed to ensure reference equality always imply equality. (Issue [[#1632](https://github.com/realm/realm-dart/issues/1632)]) +* Clearing a nested collection could end with a crash. (Core 14.5.1) +* Removing nested collections in RealmValue for synced realms throws. (Core 14.5.1) +* Fixed crash when integrating removal of already removed dictionary key. (Core 14.5.2) ### Compatibility * Realm Studio: 15.0.0 or later. ### Internal -* Using Core 14.5.0. +* Using Core 14.5.2. ## 2.0.0 (2024-03-20) @@ -69,9 +184,9 @@ ``` * Removed `SchemaObject.properties` - instead, `SchemaObject` is now an iterable collection of `Property`. (Issue [#1449](https://github.com/realm/realm-dart/issues/1449)) * `SyncProgress.transferredBytes` and `SyncProgress.transferableBytes` have been consolidated into `SyncProgress.progressEstimate`. The values reported previously were incorrect and did not accurately represent bytes either. The new field better conveys the uncertainty around the progress being reported. With this release, we're reporting accurate estimates for upload progress, but estimating downloads is still unreliable. A future server and SDK release will add better estimations for download progress. (Issue [#1562](https://github.com/realm/realm-dart/issues/1562)) -* `Realm.logger` is no longer settable, and no longer implements `Logger` from package `logging`. In particular you can no longer call `Realm.logger.level =`. Instead you should call `Realm.logger.setLogLevel(RealmLogLevel level, {RealmLogCategory? category})` that takes an optional category. If no category is exlicitly given, then `RealmLogCategory.realm` is assumed. +* `Realm.logger` is no longer settable, and no longer implements `Logger` from package `logging`. In particular you can no longer call `Realm.logger.level =`. Instead you should call `Realm.logger.setLogLevel(RealmLogLevel level, {RealmLogCategory? category})` that takes an optional category. If no category is explicitly given, then `RealmLogCategory.realm` is assumed. - Also, note that setting a level is no longer local to the current isolate, but shared accross all isolates. At the core level there is just one process wide logger. + Also, note that setting a level is no longer local to the current isolate, but shared across all isolates. At the core level there is just one process wide logger. Categories form a hierarchy and setting the log level of a parent category will override the level of its children. The hierarchy is exposed in a type safe manner with: ```dart @@ -813,7 +928,7 @@ class _Address { * Queries on results didn't filter the existing results. ([#908](https://github.com/realm/realm-dart/issues/908)). Example ```dart - expect(realm.query('FALSEPREDICATE').query('TRUEPREDICATE'), isEmpty); //<-- Fails if a Persion object exists + expect(realm.query('FALSEPREDICATE').query('TRUEPREDICATE'), isEmpty); //<-- Fails if a Person object exists ``` * Fixed copying of native structs for session errors and http requests. ([#924](https://github.com/realm/realm-dart/pull/924)) * Fixed a crash when closing the SyncSession on App instance teardown. ([#5752](https://github.com/realm/realm-core/issues/5752)) @@ -976,7 +1091,7 @@ class _Address { ```dart final subscription = realm.all().changes.listen((changes) { - changes.inserted // indexes of inserted ojbects + changes.inserted // indexes of inserted objects changes.modified // indexes of modified objects changes.deleted // indexes of deleted objects changes.newModified // indexes of modified objects after deletions and insertions are accounted for. @@ -1116,7 +1231,7 @@ Notes: This release is a prerelease version. All API's might change without warn Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. ### Enhancements -* Completеly rewritten from the ground up with sound null safety and using Dart FFI +* Completely rewritten from the ground up with sound null safety and using Dart FFI ### Fixed * Realm close stops internal scheduler. @@ -1132,7 +1247,7 @@ Notes: This release is a prerelease version. All API's might change without warn Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. ### Enhancements -* Completеly rewritten from the ground up with sound null safety and using Dart FFI +* Completely rewritten from the ground up with sound null safety and using Dart FFI ### Compatibility * Dart ^2.15 on Windows, MacOS and Linux diff --git a/README.md b/README.md index c13aa18f2..22650094e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This repository holds the source code for the Realm SDK for Flutter™ and Dart * **Simple:** Realm’s object-oriented data model is simple to learn, doesn’t need an ORM, and the [API](https://pub.dev/documentation/realm/latest/) lets you write less code to get apps up & running in minutes. * **Modern:** Realm supports latest Dart and Flutter versions and is built with sound null-safety. * **Fast:** Realm is faster than even raw SQLite on common operations while maintaining an extremely rich feature set. -* **[MongoDB Atlas Device Sync](https://www.mongodb.com/atlas/app-services/device-sync)**: Makes it simple to keep data in sync across users, devices, and your backend in real-time. Get started for free with [a template application](https://github.com/mongodb/template-app-dart-flutter-todo) and [create the cloud backend](https://mongodb.com/realm/register?utm_medium=github_atlas_CTA&utm_source=realm_dart_github). +* **[MongoDB Atlas Device Sync](https://www.mongodb.com/docs/atlas/app-services/sync/)**: Makes it simple to keep data in sync across users, devices, and your backend in real-time. Get started for free with [a template application](https://github.com/mongodb/template-app-dart-flutter-todo) and [create the cloud backend](https://mongodb.com/realm/register?utm_medium=github_atlas_CTA&utm_source=realm_dart_github). ## Getting Started @@ -90,7 +90,7 @@ For API documentation go to Use [realm](https://pub.dev/packages/realm) package for Flutter and [realm_dart](https://pub.dev/packages/realm_dart) package for Dart applications. -For complete documentation of the SDKs, go to the [Realm SDK documentation](https://docs.mongodb.com/realm/sdk/flutter/). +For complete documentation of the SDKs, go to the [Realm SDK documentation](https://www.mongodb.com/docs/atlas/device-sdks/sdk/flutter/). If you are using the Realm SDK for the first time, refer to the [Quick Start documentation](https://www.mongodb.com/docs/realm/sdk/flutter/quick-start/). @@ -352,7 +352,7 @@ This section is about how to use the Realm with [Device Sync](https://www.mongod 1. Add a sync subscription and write data. Only data matching the query in the subscription will be synced to the server and only data matching the subscription will be downloaded to the local device realm file. - + ``` dart realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.add(realm.query(r'status == $0 AND progressMinutes == $1', ["completed", 100])); diff --git a/analysis_options.yaml b/analysis_options.yaml index dc3d7cd3d..16bcf0407 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -19,7 +19,6 @@ linter: rules: avoid_relative_lib_imports: false package_api_docs: true - dangling_library_doc_comments: false # For more information about the core and recommended set of lints, see # https://dart.dev/go/core-lints diff --git a/melos.yaml b/melos.yaml index 87c874661..7cc7c4cba 100644 --- a/melos.yaml +++ b/melos.yaml @@ -14,8 +14,8 @@ packages: command: bootstrap: environment: - sdk: ^3.0.0 - flutter: ^3.10.0 + sdk: ^3.3.0 + flutter: ^3.19.0 dev_dependencies: lints: ^3.0.0 diff --git a/packages/ejson/pubspec.yaml b/packages/ejson/pubspec.yaml index 6c2dae36d..02b1a68be 100644 --- a/packages/ejson/pubspec.yaml +++ b/packages/ejson/pubspec.yaml @@ -16,7 +16,7 @@ version: 0.3.0 repository: https://github.com/realm/realm-dart/ejson/packages/ejson environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: collection: ^1.17.0 diff --git a/packages/ejson_analyzer/pubspec.yaml b/packages/ejson_analyzer/pubspec.yaml index 0f1b0040a..f9367a39d 100644 --- a/packages/ejson_analyzer/pubspec.yaml +++ b/packages/ejson_analyzer/pubspec.yaml @@ -1,11 +1,15 @@ name: ejson_analyzer -description: A starting point for Dart libraries or applications. +description: >- + Common analysis code for EJSON packages. + + BSON is a binary format used to store JSON-like documents efficiently. + EJSON extends JSON defining how all BSON types should be represented in JSON. version: 0.3.0 repository: https://github.com/realm/realm-dart/ejson/packages/ejson_analyzer environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: analyzer: ^6.0.0 diff --git a/packages/ejson_annotation/pubspec.yaml b/packages/ejson_annotation/pubspec.yaml index a7a56db62..0edefcaf9 100644 --- a/packages/ejson_annotation/pubspec.yaml +++ b/packages/ejson_annotation/pubspec.yaml @@ -9,7 +9,7 @@ version: 0.3.0 repository: https://github.com/realm/realm-dart/ejson/packages/ejson_annotation environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dev_dependencies: lints: ^3.0.0 diff --git a/packages/ejson_generator/pubspec.yaml b/packages/ejson_generator/pubspec.yaml index 545536bcc..2e102840f 100644 --- a/packages/ejson_generator/pubspec.yaml +++ b/packages/ejson_generator/pubspec.yaml @@ -16,7 +16,7 @@ version: 0.3.0 repository: https://github.com/realm/realm-dart/ejson/packages/ejson_generator environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: analyzer: ^6.0.0 diff --git a/packages/ejson_lint/example/pubspec.yaml b/packages/ejson_lint/example/pubspec.yaml index 20e90f955..5f6c9e8e5 100644 --- a/packages/ejson_lint/example/pubspec.yaml +++ b/packages/ejson_lint/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 publish_to: none environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: ejson_annotation: ^0.3.0 diff --git a/packages/ejson_lint/pubspec.yaml b/packages/ejson_lint/pubspec.yaml index 3b6f3d4c1..f8a0b99f5 100644 --- a/packages/ejson_lint/pubspec.yaml +++ b/packages/ejson_lint/pubspec.yaml @@ -1,11 +1,15 @@ name: ejson_lint -description: A starting point for Dart libraries or applications. +description: >- + Lints for EJSON. + + BSON is a binary format used to store JSON-like documents efficiently. + EJSON extends JSON defining how all BSON types should be represented in JSON. version: 0.3.0 repository: https://github.com/realm/realm-dart/ejson/packages/ejson_lint environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: analyzer: ^6.0.0 diff --git a/packages/realm/bin/realm.dart b/packages/realm/bin/realm.dart index 80569db28..a8e88e123 100644 --- a/packages/realm/bin/realm.dart +++ b/packages/realm/bin/realm.dart @@ -1,6 +1,6 @@ // Copyright 2021 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 - import 'package:realm_dart/src/cli/main.dart' as x; + void main(List arguments) => x.main(arguments); diff --git a/packages/realm/example/lib/main.realm.dart b/packages/realm/example/lib/main.realm.dart index c4891d8de..e43d88cff 100644 --- a/packages/realm/example/lib/main.realm.dart +++ b/packages/realm/example/lib/main.realm.dart @@ -54,6 +54,10 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Car freeze() => RealmObjectBase.freezeObject(this); @@ -133,6 +137,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm/example/pubspec.yaml b/packages/realm/example/pubspec.yaml index cf00bb98e..507f4ac87 100644 --- a/packages/realm/example/pubspec.yaml +++ b/packages/realm/example/pubspec.yaml @@ -5,13 +5,13 @@ version: 2.0.0-alpha.2 publish_to: "none" environment: - sdk: ^3.0.0 - flutter: ^3.10.0 + sdk: ^3.3.0 + flutter: ^3.19.0 dependencies: flutter: sdk: flutter - realm: ^2.0.0 + realm: ^2.3.0 characters: ^1.1.0 dev_dependencies: diff --git a/packages/realm/ios/Resources/PrivacyInfo.xcprivacy b/packages/realm/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..2e4e91574 --- /dev/null +++ b/packages/realm/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,31 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPITypeReasons + + C617.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + + + NSPrivacyAccessedAPITypeReasons + + E174.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + + + NSPrivacyTracking + + + diff --git a/packages/realm/ios/realm.podspec b/packages/realm/ios/realm.podspec index 4fe59ef17..ea3217075 100644 --- a/packages/realm/ios/realm.podspec +++ b/packages/realm/ios/realm.podspec @@ -19,7 +19,7 @@ puts "bundleId is #{bundleId}" Pod::Spec.new do |s| s.name = 'realm' - s.version = '2.0.0' + s.version = '2.3.0' s.summary = 'The official Realm SDK for Flutter' s.description = <<-DESC Realm is a mobile database - an alternative to SQLite and key-value stores. @@ -33,23 +33,11 @@ Pod::Spec.new do |s| s.vendored_frameworks = 'realm_dart.xcframework' s.dependency 'Flutter' s.platform = :ios, '8.0' - s.compiler_flags = "-DBUNDLE_ID='\"#{bundleId}\"'" + s.compiler_flags = "-DBUNDLE_ID='\"#{bundleId}\"'" s.library = 'c++', 'z', 'compression' s.swift_version = '5.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', - 'CURRENT_PROJECT_VERSION' => s.version, - 'VERSIONING_SYSTEM' => 'apple-generic', - 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17', - 'CLANG_CXX_LIBRARY' => 'libc++', - # Flutter.framework does not contain a i386 slice. - # Only x86_64 simulators are supported. Using EXCLUDED_ARCHS to exclude i386 arch. - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - 'HEADER_SEARCH_PATHS' => [ - '"$(PODS_TARGET_SRCROOT)/Classes"', - ], - 'FRAMEWORK_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/**"' - } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } #Use --debug to debug the install command on both prepare_command and script_phase below s.prepare_command = "source \"#{project_dir}/Flutter/flutter_export_environment.sh\" && cd \"$FLUTTER_APPLICATION_PATH\" && \"$FLUTTER_ROOT/bin/dart\" run realm install --target-os-type ios" s.script_phases = [ @@ -63,4 +51,5 @@ Pod::Spec.new do |s| :execution_position => :before_compile } ] + s.resource_bundles = { 'realm_privacy' => [ 'Resources/PrivacyInfo.xcprivacy' ] } end \ No newline at end of file diff --git a/packages/realm/macos/Resources/PrivacyInfo.xcprivacy b/packages/realm/macos/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..2e4e91574 --- /dev/null +++ b/packages/realm/macos/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,31 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPITypeReasons + + C617.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + + + NSPrivacyAccessedAPITypeReasons + + E174.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + + + NSPrivacyTracking + + + diff --git a/packages/realm/macos/realm.podspec b/packages/realm/macos/realm.podspec index 60b74b0c0..53b968505 100644 --- a/packages/realm/macos/realm.podspec +++ b/packages/realm/macos/realm.podspec @@ -36,7 +36,7 @@ puts "bundleId is #{bundleId}" Pod::Spec.new do |s| s.name = 'realm' - s.version = '2.0.0' + s.version = '2.3.0' s.summary = 'The official Realm SDK for Flutter' s.description = <<-DESC Realm is a mobile database - an alternative to SQLite and key-value stores. @@ -45,12 +45,12 @@ Pod::Spec.new do |s| s.license = { :file => '../LICENSE' } s.author = { 'Realm' => 'help@realm.io' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' s.platform = :osx, '10.11' - s.compiler_flags = "-DBUNDLE_ID='\"#{bundleId}\"'" - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.compiler_flags = "-DBUNDLE_ID='\"#{bundleId}\"'" + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' s.vendored_libraries = "#{realmLibName}" s.prepare_command = "touch #{realmPackageDir}/librealm_dart.dylib" #librealm_dart.dylib is needed before the build is started @@ -65,4 +65,5 @@ Pod::Spec.new do |s| :execution_position => :before_compile } ] + s.resource_bundles = { 'realm_privacy' => [ 'Resources/PrivacyInfo.xcprivacy' ] } end diff --git a/packages/realm/pubspec.yaml b/packages/realm/pubspec.yaml index 185a9e2a1..ca476a6e2 100644 --- a/packages/realm/pubspec.yaml +++ b/packages/realm/pubspec.yaml @@ -1,19 +1,19 @@ name: realm description: The official Realm SDK for Flutter. Realm is a mobile database - an alternative to SQLite and key-value stores. -version: 2.0.0 +version: 2.3.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart issue_tracker: https://github.com/realm/realm-dart/issues environment: - sdk: ^3.0.0 - flutter: ^3.10.0 + sdk: ^3.3.0 + flutter: ^3.19.0 dependencies: flutter: sdk: flutter - realm_dart: ^2.0.0 + realm_dart: ^2.3.0 flutter: plugin: diff --git a/packages/realm/tests/pubspec.yaml b/packages/realm/tests/pubspec.yaml index 1d981c27e..fdbe289c0 100644 --- a/packages/realm/tests/pubspec.yaml +++ b/packages/realm/tests/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: "none" version: 2.0.0-alpha.2 environment: - sdk: ^3.0.0 - flutter: ^3.10.0 + sdk: ^3.3.0 + flutter: ^3.19.0 dependencies: flutter: diff --git a/packages/realm_common/lib/src/realm_common_base.dart b/packages/realm_common/lib/src/realm_common_base.dart index caea8a336..742085801 100644 --- a/packages/realm_common/lib/src/realm_common_base.dart +++ b/packages/realm_common/lib/src/realm_common_base.dart @@ -20,10 +20,9 @@ enum ObjectType { /// to query or modify it. asymmetricObject('AsymmetricObject', 2); - const ObjectType([this._className = 'Unknown', this._flags = -1]); + const ObjectType(this._className, this._flags); final String _className; - final int _flags; } @@ -33,15 +32,49 @@ extension ObjectTypeInternal on ObjectType { String get className => _className; } +/// An enum controlling the constructor type generated for a [RealmModel]. +enum CtorStyle { + /// Generate a constructor with only optional parameters named. + /// All required parameters will be positional. + /// This is the default, unless overridden in the build config. + onlyOptionalNamed, + + /// Generate a constructor with all parameters named. + allNamed, +} + +/// Class used to define the desired constructor behavior for a [RealmModel]. +/// +/// {@category Annotations} +class GeneratorConfig { + /// The style to use for the generated constructor + final CtorStyle ctorStyle; + + const GeneratorConfig({this.ctorStyle = CtorStyle.onlyOptionalNamed}); +} + /// Annotation class used to define `Realm` data model classes and their properties /// /// {@category Annotations} class RealmModel { - /// The base type of the object - final ObjectType type; - - /// Creates a new instance of [RealmModel] specifying the desired base type. - const RealmModel([this.type = ObjectType.realmObject]); + /// The base type of the generated object class + final ObjectType baseType; + + /// The generator configuration to use for this model + final GeneratorConfig generatorConfig; + + // NOTE: To avoid a breaking change, we keep this old constructor and add a new one + /// Creates a new instance of [RealmModel] optionally specifying the [baseType]. + const RealmModel([ + ObjectType baseType = ObjectType.realmObject, + ]) : this.using(baseType: baseType); + + /// Creates a new instance of [RealmModel] optionally specifying the [baseType] + /// and [generatorConfig]. + const RealmModel.using({ + this.baseType = ObjectType.realmObject, + this.generatorConfig = const GeneratorConfig(), + }); } /// MapTo annotation for class level and class member level. @@ -97,11 +130,3 @@ class Backlink { final Symbol fieldName; const Backlink(this.fieldName); } - -/// @nodoc -class Tuple { - T1 item1; - T2 item2; - - Tuple(this.item1, this.item2); -} diff --git a/packages/realm_common/lib/src/realm_types.dart b/packages/realm_common/lib/src/realm_types.dart index dcd9233e1..af031ae7e 100644 --- a/packages/realm_common/lib/src/realm_types.dart +++ b/packages/realm_common/lib/src/realm_types.dart @@ -6,7 +6,6 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:objectid/objectid.dart'; import 'package:sane_uuid/uuid.dart'; -import 'package:collection/collection.dart'; Type _typeOf() => T; @@ -86,18 +85,12 @@ enum RealmCollectionType { _3, // ignore: unused_field, constant_identifier_names map; - String get plural { - switch (this) { - case RealmCollectionType.list: - return "lists"; - case RealmCollectionType.set: - return "sets"; - case RealmCollectionType.map: - return "maps"; - default: - return "none"; - } - } + String get plural => switch (this) { + RealmCollectionType.list => 'lists', + RealmCollectionType.set => 'sets', + RealmCollectionType.map => 'maps', + _ => 'none', + }; } /// A base class of all Realm errors. @@ -187,7 +180,7 @@ enum RealmValueType { bool get isCollection => this == RealmValueType.list || this == RealmValueType.map; } -/// A type that can represent any valid realm data type, except collections and embedded objects. +/// A type that can represent any valid realm data type, except embedded objects. /// /// You can use [RealmValue] to declare fields on realm models, in which case it must be non-nullable, /// but it can wrap a null-value. List of [RealmValue] (`List`) are also legal. @@ -278,21 +271,15 @@ class RealmValue { } @override - operator ==(Object? other) { - // We always return false when comparing two RealmValue collections. - if (type.isCollection) { - return false; - } - + operator ==(Object other) { + if (identical(this, other)) return true; + final v = value; if (other is RealmValue) { - if (value is Uint8List && other.value is Uint8List) { - return ListEquality().equals(value as Uint8List, other.value as Uint8List); - } - - return type == other.type && value == other.value; + final ov = other.value; + if (v is Uint8List && ov is Uint8List) return memEquals(v, ov); // special case binary data + return type == other.type && v == ov; } - - return value == other; + return v == other; // asymmetric comparison for convenience } @override @@ -302,6 +289,28 @@ class RealmValue { String toString() => 'RealmValue($value)'; } +/// Compares two [Uint8List]s by comparing 8 bytes at a time. +bool memEquals(Uint8List x, Uint8List y) { + if (identical(x, y)) return true; + if (x.lengthInBytes != y.lengthInBytes) return false; + + var words = x.lengthInBytes ~/ 8; // number of full words + var xW = x.buffer.asUint64List(0, words); + var yW = y.buffer.asUint64List(0, words); + + // compare words + for (var i = 0; i < xW.length; i += 1) { + if (xW[i] != yW[i]) return false; + } + + // compare remaining bytes + for (var i = words * 8; i < x.lengthInBytes; i += 1) { + if (x[i] != y[i]) return false; + } + + return true; // no diff, they are equal +} + /// A base type for the supported geospatial shapes. sealed class GeoShape {} diff --git a/packages/realm_common/pubspec.yaml b/packages/realm_common/pubspec.yaml index 0299a6c6c..b9d7cfcca 100644 --- a/packages/realm_common/pubspec.yaml +++ b/packages/realm_common/pubspec.yaml @@ -3,14 +3,14 @@ description: >- Hosts the common code shared between realm, realm_dart and realm_generator packages. This package is part of the official Realm Flutter and Realm Dart SDKs. -version: 2.0.0 +version: 2.3.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart issue_tracker: https://github.com/realm/realm-dart/issues environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: collection: ^1.18.0 diff --git a/packages/realm_dart/CMakePresets.json b/packages/realm_dart/CMakePresets.json index a3ff6e63b..b83696281 100644 --- a/packages/realm_dart/CMakePresets.json +++ b/packages/realm_dart/CMakePresets.json @@ -87,14 +87,6 @@ "ANDROID_NDK_HOME": "$env{ANDROID_NDK}" } }, - { - "name": "android-x86", - "displayName": "Android x86", - "inherits": "android", - "cacheVariables": { - "CMAKE_ANDROID_ARCH_ABI": "x86" - } - }, { "name": "android-x86_64", "displayName": "Android x86_64", @@ -138,12 +130,6 @@ "displayName": "x64", "configuration": "Debug" }, - { - "name": "android-x86", - "configurePreset": "android-x86", - "displayName": "x86", - "configuration": "Debug" - }, { "name": "android-x86_64", "configurePreset": "android-x86_64", diff --git a/packages/realm_dart/bin/realm_dart.dart b/packages/realm_dart/bin/realm_dart.dart index 80569db28..a8e88e123 100644 --- a/packages/realm_dart/bin/realm_dart.dart +++ b/packages/realm_dart/bin/realm_dart.dart @@ -1,6 +1,6 @@ // Copyright 2021 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 - import 'package:realm_dart/src/cli/main.dart' as x; + void main(List arguments) => x.main(arguments); diff --git a/packages/realm_dart/dart_test.yaml b/packages/realm_dart/dart_test.yaml new file mode 100644 index 000000000..8e7275b44 --- /dev/null +++ b/packages/realm_dart/dart_test.yaml @@ -0,0 +1,2 @@ +tags: + baas: { timeout: 2x } \ No newline at end of file diff --git a/packages/realm_dart/dev/lib/src/build.dart b/packages/realm_dart/dev/lib/src/build.dart index 99a4b293a..e6659975a 100644 --- a/packages/realm_dart/dev/lib/src/build.dart +++ b/packages/realm_dart/dev/lib/src/build.dart @@ -68,7 +68,6 @@ enum OS { enum Target { androidArm(Architecture.arm, OS.android), androidArm64(Architecture.arm64, OS.android), - androidIA32(Architecture.ia32, OS.android), androidX64(Architecture.x64, OS.android), // only for emulator // androidRiscv64, // not supported by realm currently // fuchsiaArm64, // -"- etc. diff --git a/packages/realm_dart/example/bin/myapp.realm.dart b/packages/realm_dart/example/bin/myapp.realm.dart index 273a97422..1a13aa667 100644 --- a/packages/realm_dart/example/bin/myapp.realm.dart +++ b/packages/realm_dart/example/bin/myapp.realm.dart @@ -54,6 +54,10 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Car freeze() => RealmObjectBase.freezeObject(this); @@ -133,6 +137,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/example/pubspec.yaml b/packages/realm_dart/example/pubspec.yaml index bc268f2d7..ccd618765 100644 --- a/packages/realm_dart/example/pubspec.yaml +++ b/packages/realm_dart/example/pubspec.yaml @@ -4,7 +4,7 @@ description: A simple command-line application using Realm Dart SDK. publish_to: none environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: realm_dart: diff --git a/packages/realm_dart/ffigen.yaml b/packages/realm_dart/ffigen.yaml index 31f708fa9..905257949 100644 --- a/packages/realm_dart/ffigen.yaml +++ b/packages/realm_dart/ffigen.yaml @@ -27,6 +27,7 @@ comments: compiler-opts: - '-DRLM_NO_ANON_UNIONS' - '-DFFI_GEN' + - '-DREALM_APP_SERVICES' - '-I src/realm-core/src' - '-I src/dart-dl' sort: true diff --git a/packages/realm_dart/lib/src/app.dart b/packages/realm_dart/lib/src/app.dart index 8872ed119..3e695a645 100644 --- a/packages/realm_dart/lib/src/app.dart +++ b/packages/realm_dart/lib/src/app.dart @@ -7,11 +7,12 @@ import 'dart:io'; import 'dart:isolate'; import 'package:meta/meta.dart'; -import 'package:path/path.dart' as _path; +import 'package:path/path.dart' as path; import '../realm.dart'; import 'credentials.dart'; import 'logging.dart'; +import 'native/app_handle.dart'; import 'native/realm_core.dart'; import 'user.dart'; @@ -125,7 +126,7 @@ class AppConfiguration { this.maxConnectionTimeout = const Duration(minutes: 2), HttpClient? httpClient, }) : baseUrl = baseUrl ?? Uri.parse(realmCore.getDefaultBaseUrl()), - baseFilePath = baseFilePath ?? Directory(_path.dirname(Configuration.defaultRealmPath)), + baseFilePath = baseFilePath ?? Directory(path.dirname(Configuration.defaultRealmPath)), httpClient = httpClient ?? _defaultClient { if (appId == '') { throw RealmException('Supplied appId must be a non-empty value'); @@ -144,7 +145,7 @@ class App implements Finalizable { /// The id of this application. This is the same as the appId in the [AppConfiguration] used to /// create this [App]. - String get id => realmCore.appGetId(this); + String get id => handle.id; /// Create an app with a particular [AppConfiguration]. This constructor should only be used on the main isolate and, /// ideally, only once as soon as the app starts. @@ -163,7 +164,7 @@ class App implements Finalizable { /// on the main isolate. If an App hasn't been already constructed with the same id, will return null. This method is safe to call /// on a background isolate. static App? getById(String id, {Uri? baseUrl}) { - final handle = realmCore.getApp(id, baseUrl?.toString()); + final handle = AppHandle.get(id, baseUrl?.toString()); return handle == null ? null : App._(handle); } @@ -171,18 +172,18 @@ class App implements Finalizable { static AppHandle _createApp(AppConfiguration configuration) { configuration.baseFilePath.createSync(recursive: true); - return realmCore.createApp(configuration); + return AppHandle.from(configuration); } /// Logs in a user with the given credentials. Future logIn(Credentials credentials) async { - var userHandle = await realmCore.logIn(this, credentials); + var userHandle = await handle.logIn(credentials.handle); return UserInternal.create(userHandle, this); } /// Gets the currently logged in [User]. If none exists, `null` is returned. User? get currentUser { - final userHandle = realmCore.getCurrentUser(_handle); + final userHandle = _handle.currentUser; if (userHandle == null) { return null; } @@ -191,22 +192,22 @@ class App implements Finalizable { /// Gets all currently logged in users. Iterable get users { - return realmCore.getUsers(this).map((handle) => UserInternal.create(handle, this)); + return handle.users.map((handle) => UserInternal.create(handle, this)); } /// Removes a [user] and their local data from the device. If the user is logged in, they will be logged out in the process. Future removeUser(User user) async { - return await realmCore.removeUser(this, user); + return await handle.removeUser(user.handle); } /// Deletes a user and all its data from the device as well as the server. Future deleteUser(User user) async { - return await realmCore.deleteUser(this, user); + return await handle.deleteUser(user.handle); } /// Switches the [currentUser] to the one specified in [user]. void switchUser(User user) { - realmCore.switchUser(this, user); + handle.switchUser(user.handle); } /// Provide a hint to this app's sync client to reconnect. @@ -214,22 +215,30 @@ class App implements Finalizable { /// /// The sync client will always attempt to reconnect eventually, this is just a hint. void reconnect() { - realmCore.reconnect(this); + handle.reconnect(); } /// Returns the current value of the base URL used to communicate with the server. + /// + /// If an [updateBaseUrl] operation is currently in progress, this value will not + /// be updated with the new value until that operation has completed. @experimental - Uri? get baseUrl { - return Uri.tryParse(realmCore.getBaseUrl(this) ?? ''); + Uri get baseUrl { + return Uri.parse(handle.baseUrl); } /// Temporarily overrides the [baseUrl] value from [AppConfiguration] with a new [baseUrl] value - /// used for communicating with the server. + /// used for communicating with the server. If set to `null`, the app will revert to the default + /// base url. + /// + /// If this operation fails, the app will continue to use the original base URL. If another [App] + /// operation is started while this function is in progress, that request will use the original + /// base URL location information. /// /// The App will revert to using the value in [AppConfiguration] when it is restarted. @experimental - Future updateBaseUrl(Uri baseUrl) async { - return await realmCore.updateBaseUrl(this, baseUrl); + Future updateBaseUrl(Uri? baseUrl) async { + return await handle.updateBaseUrl(baseUrl); } /// Returns an instance of [EmailPasswordAuthProvider] diff --git a/packages/realm_dart/lib/src/cli/common/archive.dart b/packages/realm_dart/lib/src/cli/common/archive.dart index 23ab1dcbb..eabca8f4a 100644 --- a/packages/realm_dart/lib/src/cli/common/archive.dart +++ b/packages/realm_dart/lib/src/cli/common/archive.dart @@ -1,7 +1,6 @@ // Copyright 2021 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 -/// import 'dart:io'; import 'package:tar/tar.dart'; import 'package:path/path.dart' as path; diff --git a/packages/realm_dart/lib/src/cli/common/utils.dart b/packages/realm_dart/lib/src/cli/common/utils.dart index c7d5c0851..78ac701f9 100644 --- a/packages/realm_dart/lib/src/cli/common/utils.dart +++ b/packages/realm_dart/lib/src/cli/common/utils.dart @@ -20,7 +20,7 @@ extension StringEx on String { bool isRealmCI = Platform.environment['REALM_CI'] != null; -FutureOr safe(FutureOr Function() f, {String message = 'Ignoring error', Function(Object e, StackTrace s)? onError}) async { +FutureOr safe(FutureOr Function() f, {String message = 'Ignoring error', void Function(Object e, StackTrace s)? onError}) async { try { return await f(); } catch (e, s) { diff --git a/packages/realm_dart/lib/src/cli/metrics/metrics_command.dart b/packages/realm_dart/lib/src/cli/metrics/metrics_command.dart index 15106605a..2fb0f1f75 100644 --- a/packages/realm_dart/lib/src/cli/metrics/metrics_command.dart +++ b/packages/realm_dart/lib/src/cli/metrics/metrics_command.dart @@ -18,7 +18,7 @@ import 'options.dart'; import '../common/utils.dart'; // stamped into the library by the build system (see prepare-release.yml) -const realmCoreVersion = '14.3.0'; +const realmCoreVersion = '14.7.0'; class MetricsCommand extends Command { @override diff --git a/packages/realm_dart/lib/src/collections.dart b/packages/realm_dart/lib/src/collections.dart index d41d450a0..71b25dfd0 100644 --- a/packages/realm_dart/lib/src/collections.dart +++ b/packages/realm_dart/lib/src/collections.dart @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import 'dart:ffi'; -import 'native/realm_core.dart'; +import 'native/collection_changes_handle.dart'; /// Contains index information about objects that moved within the same collection. class Move { @@ -47,13 +47,11 @@ class MapChanges { /// Describes the changes in a Realm collection since the last time the notification callback was invoked. class RealmCollectionChanges implements Finalizable { - final RealmCollectionChangesHandle _handle; - CollectionChanges? _values; + final CollectionChangesHandle _handle; + late final CollectionChanges _changes = _handle.changes; RealmCollectionChanges(this._handle); - CollectionChanges get _changes => _values ??= realmCore.getCollectionChanges(_handle); - /// The indexes in the previous version of the collection which have been removed from this one. List get deleted => _changes.deletions; diff --git a/packages/realm_dart/lib/src/configuration.dart b/packages/realm_dart/lib/src/configuration.dart index 45998fcfe..44c11ce92 100644 --- a/packages/realm_dart/lib/src/configuration.dart +++ b/packages/realm_dart/lib/src/configuration.dart @@ -7,12 +7,17 @@ import 'dart:io'; // ignore: no_leading_underscores_for_library_prefixes import 'package:path/path.dart' as _path; + +import 'app.dart'; +import 'init.dart'; import 'logging.dart'; +import 'native/from_native.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; -import 'init.dart'; import 'user.dart'; +const encryptionKeySize = 64; + /// The signature of a callback used to determine if compaction /// should be attempted. /// @@ -225,8 +230,8 @@ abstract class Configuration implements Finalizable { return; } - if (key.length != realmCore.encryptionKeySize) { - throw RealmException("Wrong encryption key size (must be ${realmCore.encryptionKeySize}, but was ${key.length})"); + if (key.length != encryptionKeySize) { + throw RealmException("Wrong encryption key size (must be $encryptionKeySize, but was ${key.length})"); } int notAByteElement = key.firstWhere((e) => e > 255, orElse: () => -1); @@ -368,7 +373,7 @@ class FlexibleSyncConfiguration extends Configuration { }) : super._(); @override - String get _defaultPath => realmCore.getPathForUser(user); + String get _defaultPath => user.handle.path; } extension FlexibleSyncConfigurationInternal on FlexibleSyncConfiguration { @@ -653,7 +658,7 @@ class ClientResetError extends SyncError { throw RealmException("Missing `originalFilePath`"); } - return realmCore.immediatelyRunFileActions(_app!, originalFilePath!); + return _app.handle.resetRealm(originalFilePath!); } } diff --git a/packages/realm_dart/lib/src/credentials.dart b/packages/realm_dart/lib/src/credentials.dart index 9519fee41..14189c169 100644 --- a/packages/realm_dart/lib/src/credentials.dart +++ b/packages/realm_dart/lib/src/credentials.dart @@ -4,12 +4,13 @@ import 'dart:convert'; import 'dart:ffi'; -import 'native/realm_core.dart'; import 'app.dart'; +import 'native/convert.dart'; +import 'native/credentials_handle.dart'; import 'user.dart'; /// An enum containing all authentication providers. These have to be enabled manually for the application before they can be used. -/// [Authentication Providers Docs](https://docs.mongodb.com/realm/authentication/providers/) +/// [Authentication Providers Docs](https://www.mongodb.com/docs/atlas/app-services/authentication/#authentication-providers) /// {@category Application} enum AuthProviderType { /// For authenticating without credentials. @@ -57,44 +58,44 @@ extension AuthProviderTypeInternal on AuthProviderType { /// A class, representing the credentials used for authenticating a [User] /// {@category Application} class Credentials implements Finalizable { - final RealmAppCredentialsHandle _handle; + final CredentialsHandle _handle; /// Returns a [Credentials] object that can be used to authenticate an anonymous user. /// Setting [reuseCredentials] to `false` will create a new anonymous user, upon [App.logIn]. - /// [Anonymous Authentication Docs](https://docs.mongodb.com/realm/authentication/anonymous) - Credentials.anonymous({bool reuseCredentials = true}) : _handle = realmCore.createAppCredentialsAnonymous(reuseCredentials); + /// [Anonymous Authentication Docs](https://www.mongodb.com/docs/atlas/app-services/authentication/anonymous/#anonymous-authentication) + Credentials.anonymous({bool reuseCredentials = true}) : _handle = CredentialsHandle.anonymous(reuseCredentials); /// Returns a [Credentials] object that can be used to authenticate a user with a Google account using an id token. - Credentials.apple(String idToken) : _handle = realmCore.createAppCredentialsApple(idToken); + Credentials.apple(String idToken) : _handle = CredentialsHandle.apple(idToken); /// Returns a [Credentials] object that can be used to authenticate a user with their email and password. /// A user can login with email and password only after they have registered their account and verified their /// email. - /// [Email/Password Authentication Docs](https://docs.mongodb.com/realm/authentication/email-password) - Credentials.emailPassword(String email, String password) : _handle = realmCore.createAppCredentialsEmailPassword(email, password); + /// [Email/Password Authentication Docs](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/#email-password-authentication) + Credentials.emailPassword(String email, String password) : _handle = CredentialsHandle.emailPassword(email, password); /// Returns a [Credentials] object that can be used to authenticate a user with a custom JWT. - /// [Custom-JWT Authentication Docs](https://docs.mongodb.com/realm/authentication/custom-jwt) - Credentials.jwt(String token) : _handle = realmCore.createAppCredentialsJwt(token); + /// [Custom-JWT Authentication Docs](https://www.mongodb.com/docs/atlas/app-services/authentication/custom-jwt/#custom-jwt-authentication) + Credentials.jwt(String token) : _handle = CredentialsHandle.jwt(token); /// Returns a [Credentials] object that can be used to authenticate a user with a Facebook account. - Credentials.facebook(String accessToken) : _handle = realmCore.createAppCredentialsFacebook(accessToken); + Credentials.facebook(String accessToken) : _handle = CredentialsHandle.facebook(accessToken); /// Returns a [Credentials] object that can be used to authenticate a user with a Google account using an authentication code. - Credentials.googleAuthCode(String authCode) : _handle = realmCore.createAppCredentialsGoogleAuthCode(authCode); + Credentials.googleAuthCode(String authCode) : _handle = CredentialsHandle.googleAuthCode(authCode); /// Returns a [Credentials] object that can be used to authenticate a user with a Google account using an id token. - Credentials.googleIdToken(String idToken) : _handle = realmCore.createAppCredentialsGoogleIdToken(idToken); + Credentials.googleIdToken(String idToken) : _handle = CredentialsHandle.googleIdToken(idToken); /// Returns a [Credentials] object that can be used to authenticate a user with a custom Function. /// [Custom Function Authentication Docs](https://www.mongodb.com/docs/atlas/app-services/authentication/custom-function/) - Credentials.function(String payload) : _handle = realmCore.createAppCredentialsFunction(payload); + Credentials.function(String payload) : _handle = CredentialsHandle.function(payload); /// Returns a [Credentials] object that can be used to authenticate a user with an API key. /// To generate an API key, use [ApiKeyClient.create] or the App Services web UI. - Credentials.apiKey(String key) : _handle = realmCore.createAppCredentialsApiKey(key); + Credentials.apiKey(String key) : _handle = CredentialsHandle.apiKey(key); - AuthProviderType get provider => realmCore.userGetCredentialsProviderType(this); + AuthProviderType get provider => handle.providerType; } /// @nodoc @@ -104,7 +105,7 @@ extension CredentialsInternal on Credentials { _handle.keepAlive(); } - RealmAppCredentialsHandle get handle => _handle; + CredentialsHandle get handle => _handle; } /// A class, encapsulating functionality for users, logged in with [Credentials.emailPassword()]. @@ -123,38 +124,38 @@ class EmailPasswordAuthProvider implements Finalizable { /// /// Successful completion indicates that the user has been created on the server and can now be logged in with [Credentials.emailPassword()]. Future registerUser(String email, String password) async { - return realmCore.appEmailPasswordRegisterUser(app, email, password); + return app.handle.registerUser(email, password); } /// Confirms a user with the given token and token id. These are typically included in the registration email. Future confirmUser(String token, String tokenId) { - return realmCore.emailPasswordConfirmUser(app, token, tokenId); + return app.handle.confirmUser(token, tokenId); } /// Resend the confirmation email for a user to the given email. Future resendUserConfirmation(String email) { - return realmCore.emailPasswordResendUserConfirmation(app, email); + return app.handle.resendConfirmation(email); } /// Completes the reset password procedure by providing the desired new [password] using the /// password reset [token] and [tokenId] that were emailed to a user. Future completeResetPassword(String password, String token, String tokenId) { - return realmCore.emailPasswordCompleteResetPassword(app, password, token, tokenId); + return app.handle.completeResetPassword(password, token, tokenId); } /// Sends a password reset email. Future resetPassword(String email) { - return realmCore.emailPasswordResetPassword(app, email); + return app.handle.requestResetPassword(email); } /// Calls the reset password function, configured on the server. Future callResetPasswordFunction(String email, String password, {List? functionArgs}) { - return realmCore.emailPasswordCallResetPasswordFunction(app, email, password, functionArgs != null ? jsonEncode(functionArgs) : null); + return app.handle.callResetPasswordFunction(email, password, functionArgs.convert(jsonEncode)); } /// Retries the custom confirmation function on a user for a given email. Future retryCustomConfirmationFunction(String email) { - return realmCore.emailPasswordRetryCustomConfirmationFunction(app, email); + return app.handle.retryCustomConfirmationFunction(email); } } diff --git a/packages/realm_dart/lib/src/list.dart b/packages/realm_dart/lib/src/list.dart index de5257ae4..be64591cb 100644 --- a/packages/realm_dart/lib/src/list.dart +++ b/packages/realm_dart/lib/src/list.dart @@ -9,7 +9,11 @@ import 'dart:ffi'; import 'package:collection/collection.dart' as collection; import 'collections.dart'; -import 'native/realm_core.dart'; +import 'native/collection_changes_handle.dart'; +import 'native/handle_base.dart'; +import 'native/list_handle.dart'; +import 'native/notification_token_handle.dart'; +import 'native/object_handle.dart'; import 'realm_class.dart'; import 'realm_object.dart'; import 'results.dart'; @@ -31,7 +35,7 @@ abstract class RealmList with RealmEntity implements List, /// Converts this [List] to a [RealmResults]. RealmResults asResults(); - factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); + factory RealmList._(ListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); /// Creates an unmanaged RealmList from [items] factory RealmList(Iterable items) => UnmanagedRealmList(items); @@ -44,7 +48,7 @@ abstract class RealmList with RealmEntity implements List, } class ManagedRealmList with RealmEntity, ListMixin implements RealmList { - final RealmListHandle _handle; + final ListHandle _handle; @override late final RealmObjectMetadata? _metadata; @@ -54,7 +58,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } @override - int get length => realmCore.getListSize(handle); + int get length => handle.size; /// Setting the `length` is a required method on [List], but makes less sense /// for [RealmList]s. You can only decrease the length, increasing it doesn't @@ -99,12 +103,12 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } try { - var value = realmCore.listGetElementAt(this, index); - if (value is RealmObjectHandle) { + var value = handle.elementAt(realm, index); + if (value is ObjectHandle) { late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + (type, targetMetadata) = realm.metadata.getByClassKey(value.classKey); } else { targetMetadata = _metadata!; type = T; @@ -140,19 +144,19 @@ class ManagedRealmList with RealmEntity, ListMixin impleme @override T removeAt(int index) { final result = this[index]; - realmCore.listRemoveElementAt(handle, index); + handle.removeAt(index); return result; } /// Move the element at index [from] to index [to]. void move(int from, int to) { - realmCore.listMoveElement(handle, from, to); + handle.move(from, to); } /// Removes all objects from this list; the length of the list becomes zero. /// The objects are not deleted from the realm, but are no longer referenced from this list. @override - void clear() => realmCore.listClear(handle); + void clear() => handle.clear(); @override int indexOf(covariant T element, [int start = 0]) { @@ -171,12 +175,12 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } if (start < 0) start = 0; - final index = realmCore.listFind(this, element); + final index = handle.indexOf(element); return index < start ? -1 : index; // to align with dart list semantics } @override - bool get isValid => realmCore.listIsValid(this); + bool get isValid => handle.isValid; @override RealmList freeze() { @@ -189,7 +193,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } @override - RealmResults asResults() => RealmResultsInternal.create(realmCore.resultsFromList(this), realm, metadata); + RealmResults asResults() => RealmResultsInternal.create(handle.asResults(), realm, metadata); @override Stream> get changes { @@ -244,7 +248,7 @@ extension RealmListOfObject on RealmList { /// /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { - final handle = realmCore.queryList(asManaged(), query, arguments); + final handle = asManaged().handle.query(query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } } @@ -262,7 +266,7 @@ extension RealmListInternal on RealmList { ManagedRealmList asManaged() => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); - RealmListHandle get handle { + ListHandle get handle { final result = asManaged()._handle; if (result.released) { throw RealmClosedError('Cannot access a list that belongs to a closed Realm'); @@ -277,14 +281,14 @@ extension RealmListInternal on RealmList { return UnmanagedRealmList._(items); } - static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); + static RealmList create(ListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); - static void setValue(RealmListHandle handle, Realm realm, int index, Object? value, {bool update = false, bool insert = false}) { + static void setValue(ListHandle handle, Realm realm, int index, Object? value, {bool update = false, bool insert = false}) { if (index < 0) { throw RealmException("Index can not be negative: $index"); } - final length = realmCore.getListSize(handle); + final length = handle.size; if (index > length) { throw RealmException('Index can not exceed the size of the list: $index, size: $length'); } @@ -295,20 +299,19 @@ extension RealmListInternal on RealmList { throw RealmError("Can't add to list an embedded object that is already managed"); } - final objHandle = - insert || index >= length ? realmCore.listInsertEmbeddedObjectAt(realm, handle, index) : realmCore.listSetEmbeddedObjectAt(realm, handle, index); + final objHandle = insert || index >= length ? handle.insertEmbeddedAt(index) : handle.setEmbeddedAt(index); realm.manageEmbedded(objHandle, value); return; } if (value is RealmValue && value.type.isCollection) { - realmCore.listAddCollectionAt(handle, realm, index, value, insert || index >= length); + handle.addOrUpdateCollectionAt(realm, index, value, insert || index >= length); return; } realm.addUnmanagedRealmObjectFromValue(value, update); - realmCore.listAddElementAt(handle, index, value, insert || index >= length); + handle.addOrUpdateAt(index, value, insert || index >= length); } on Exception catch (e) { throw RealmException("Error setting value at index $index. Error: $e"); } @@ -334,8 +337,8 @@ class ListNotificationsController extends NotificationsContro ListNotificationsController(this.list); @override - RealmNotificationTokenHandle subscribe() { - return realmCore.subscribeListNotifications(list, this); + NotificationTokenHandle subscribe() { + return list.handle.subscribeForNotifications(this); } Stream> createStream() { @@ -345,7 +348,7 @@ class ListNotificationsController extends NotificationsContro @override void onChanges(HandleBase changesHandle) { - if (changesHandle is! RealmCollectionChangesHandle) { + if (changesHandle is! CollectionChangesHandle) { throw RealmError("Invalid changes handle. RealmCollectionChangesHandle expected"); } diff --git a/packages/realm_dart/lib/src/map.dart b/packages/realm_dart/lib/src/map.dart index c7b9f7208..9e02bfb49 100644 --- a/packages/realm_dart/lib/src/map.dart +++ b/packages/realm_dart/lib/src/map.dart @@ -9,7 +9,11 @@ import 'package:collection/collection.dart' as collection; import 'dart:ffi'; import 'collections.dart'; -import 'native/realm_core.dart'; +import 'native/handle_base.dart'; +import 'native/map_changes_handle.dart'; +import 'native/map_handle.dart'; +import 'native/notification_token_handle.dart'; +import 'native/object_handle.dart'; import 'realm_object.dart'; import 'realm_class.dart'; import 'results.dart'; @@ -59,7 +63,7 @@ class UnmanagedRealmMap extends collection.DelegatingMap with RealmEntity, MapMixin implements RealmMap { - final RealmMapHandle _handle; + final MapHandle _handle; late final RealmObjectMetadata? _metadata; @@ -68,7 +72,7 @@ class ManagedRealmMap with RealmEntity, MapMixin i } @override - int get length => realmCore.mapGetSize(handle); + int get length => handle.size; @override T? remove(Object? key) { @@ -77,7 +81,7 @@ class ManagedRealmMap with RealmEntity, MapMixin i } final value = this[key]; - if (realmCore.mapRemoveKey(handle, key)) { + if (handle.remove(key)) { return value; } @@ -91,12 +95,12 @@ class ManagedRealmMap with RealmEntity, MapMixin i } try { - var value = realmCore.mapGetElement(this, key); - if (value is RealmObjectHandle) { + var value = handle.find(realm, key); + if (value is ObjectHandle) { late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + (type, targetMetadata) = realm.metadata.getByClassKey(value.classKey); } else { targetMetadata = _metadata!; type = T; @@ -126,10 +130,10 @@ class ManagedRealmMap with RealmEntity, MapMixin i /// Removes all objects from this map; the length of the map becomes zero. /// The objects are not deleted from the realm, but are no longer referenced from this map. @override - void clear() => realmCore.mapClear(handle); + void clear() => handle.clear(); @override - bool get isValid => realmCore.mapIsValid(this); + bool get isValid => handle.isValid; @override RealmMap freeze() { @@ -151,13 +155,13 @@ class ManagedRealmMap with RealmEntity, MapMixin i } @override - Iterable get keys => RealmResultsInternal.create(realmCore.mapGetKeys(this), realm, null); + Iterable get keys => RealmResultsInternal.create(handle.keys, realm, null); @override - Iterable get values => RealmResultsInternal.create(realmCore.mapGetValues(this), realm, metadata); + Iterable get values => RealmResultsInternal.create(handle.values, realm, metadata); @override - bool containsKey(Object? key) => key is String && realmCore.mapContainsKey(this, key); + bool containsKey(Object? key) => key is String && handle.containsKey(key); @override bool containsValue(Object? value) { @@ -179,7 +183,7 @@ class ManagedRealmMap with RealmEntity, MapMixin i } } - return realmCore.mapContainsValue(this, value); + return handle.containsValue(value); } } @@ -187,13 +191,10 @@ class ManagedRealmMap with RealmEntity, MapMixin i class RealmMapChanges { /// The collection being monitored for changes. final RealmMap map; + final MapChangesHandle handle; + late final MapChanges _changes = handle.changes; - final RealmMapChangesHandle _handle; - MapChanges? _values; - - RealmMapChanges._(this._handle, this.map); - - MapChanges get _changes => _values ??= realmCore.getMapChanges(_handle); + RealmMapChanges._(this.handle, this.map); /// The keys of the map which have been removed. List get deleted => _changes.deletions; @@ -220,7 +221,7 @@ extension RealmMapOfObject on RealmMap { /// /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { - final handle = realmCore.queryMap(asManaged(), query, arguments); + final handle = asManaged().handle.query(query, arguments); return RealmResultsInternal.create(handle, realm, metadata); } } @@ -238,7 +239,7 @@ extension RealmMapInternal on RealmMap { ManagedRealmMap asManaged() => this is ManagedRealmMap ? this as ManagedRealmMap : throw RealmStateError('$this is not managed'); - RealmMapHandle get handle { + MapHandle get handle { final result = asManaged()._handle; if (result.released) { throw RealmClosedError('Cannot access a map that belongs to a closed Realm'); @@ -251,29 +252,28 @@ extension RealmMapInternal on RealmMap { static RealmMap createFromMap(Map map) => UnmanagedRealmMap._(map); - static RealmMap create(RealmMapHandle handle, Realm realm, RealmObjectMetadata? metadata) => - ManagedRealmMap._(handle, realm, metadata); + static RealmMap create(MapHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmMap._(handle, realm, metadata); - static void setValue(RealmMapHandle handle, Realm realm, String key, Object? value, {bool update = false}) { + static void setValue(MapHandle handle, Realm realm, String key, Object? value, {bool update = false}) { try { if (value is EmbeddedObject) { if (value.isManaged) { throw RealmError("Can't add to map an embedded object that is already managed"); } - final objHandle = realmCore.mapInsertEmbeddedObject(realm, handle, key); + final objHandle = handle.insertEmbedded(key); realm.manageEmbedded(objHandle, value); return; } if (value is RealmValue && value.type.isCollection) { - realmCore.mapInsertCollection(handle, realm, key, value); + handle.insertCollection(realm, key, value); return; } realm.addUnmanagedRealmObjectFromValue(value, update); - realmCore.mapInsertValue(handle, key, value); + handle.insert(key, value); } on Exception catch (e) { throw RealmException("Error setting value at key $key. Error: $e"); } @@ -288,8 +288,8 @@ class MapNotificationsController extends NotificationsControl MapNotificationsController(this.map); @override - RealmNotificationTokenHandle subscribe() { - return realmCore.subscribeMapNotifications(map, this); + NotificationTokenHandle subscribe() { + return map.handle.subscribeForNotifications(this); } Stream> createStream() { @@ -299,7 +299,7 @@ class MapNotificationsController extends NotificationsControl @override void onChanges(HandleBase changesHandle) { - if (changesHandle is! RealmMapChangesHandle) { + if (changesHandle is! MapChangesHandle) { throw RealmError("Invalid changes handle. RealmMapChangesHandle expected"); } diff --git a/packages/realm_dart/lib/src/migration.dart b/packages/realm_dart/lib/src/migration.dart index 7c02226a1..86eaa0d50 100644 --- a/packages/realm_dart/lib/src/migration.dart +++ b/packages/realm_dart/lib/src/migration.dart @@ -1,9 +1,9 @@ // Copyright 2022 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 +import 'native/schema_handle.dart'; import 'realm_class.dart'; -import 'native/realm_core.dart'; -import './realm_object.dart'; +import 'realm_object.dart'; /// A [Migration] object is passed to you when you migrate your database from one version /// to another. It contains the properties for the Realm before and after the migration. @@ -33,7 +33,7 @@ class Migration { } final metadata = newRealm.metadata.getByType(T); - final handle = realmCore.findExisting(newRealm, metadata.classKey, oldObject.handle); + final handle = newRealm.handle.findExisting(metadata.classKey, oldObject.handle); if (handle == null) { return null; } @@ -45,7 +45,7 @@ class Migration { /// Renames a property during a migration. void renameProperty(String className, String oldPropertyName, String newPropertyName) { - realmCore.renameProperty(newRealm, className, oldPropertyName, newPropertyName, _schema); + newRealm.handle.renameProperty(className, oldPropertyName, newPropertyName, _schema); } /// Deletes a type during a migration. All the data associated with the type, as well as its schema, @@ -55,7 +55,7 @@ class Migration { /// /// Returns `true` if the table was present in the old Realm and was deleted. Returns `false` if it didn't exist. bool deleteType(String className) { - return realmCore.deleteType(newRealm, className); + return newRealm.handle.deleteType(className); } } diff --git a/packages/realm_dart/lib/src/native/app_handle.dart b/packages/realm_dart/lib/src/native/app_handle.dart new file mode 100644 index 000000000..c766c0453 --- /dev/null +++ b/packages/realm_dart/lib/src/native/app_handle.dart @@ -0,0 +1,427 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import '../init.dart'; +import '../realm_class.dart'; +import '../scheduler.dart'; +import 'convert.dart'; +import 'convert_native.dart'; +import 'credentials_handle.dart'; +import 'error_handling.dart'; +import 'ffi.dart'; +import 'handle_base.dart'; +import 'http_transport_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_core.dart'; +import 'realm_library.dart'; +import 'user_handle.dart'; + +class AppHandle extends HandleBase { + AppHandle(Pointer pointer) : super(pointer, 16); + + static bool _firstTime = true; + factory AppHandle.from(AppConfiguration configuration) { + // to avoid caching apps across hot restarts we clear the cache on the first + // time the ctor is called in the root isolate. + if (_firstTime && _isRootIsolate) { + _firstTime = false; + realmLib.realm_clear_cached_apps(); + } + final httpTransportHandle = HttpTransportHandle.from(configuration.httpClient); + final appConfigHandle = _createAppConfig(configuration, httpTransportHandle); + return AppHandle(realmLib.realm_app_create_cached(appConfigHandle.pointer)); + } + + static AppHandle? get(String id, String? baseUrl) { + return using((arena) { + final outApp = arena>(); + realmLib + .realm_app_get_cached( + id.toCharPtr(arena), + baseUrl == null ? nullptr : baseUrl.toCharPtr(arena), + outApp, + ) + .raiseLastErrorIfFalse(); + return outApp.value.convert(AppHandle.new); + }); + } + + UserHandle? get currentUser { + return realmLib.realm_app_get_current_user(pointer).convert(UserHandle.new); + } + + List get users => using((arena) => _getUsers(arena)); + + List _getUsers(Arena arena, {int expectedSize = 2}) { + final actualCount = arena(); + final usersPtr = arena>(expectedSize); + realmLib.realm_app_get_all_users(pointer, usersPtr, expectedSize, actualCount).raiseLastErrorIfFalse(); + + if (expectedSize < actualCount.value) { + // The supplied array was too small - resize it + arena.free(usersPtr); + return _getUsers(arena, expectedSize: actualCount.value); + } + + final result = []; + for (var i = 0; i < actualCount.value; i++) { + result.add(UserHandle((usersPtr + i).value)); + } + + return result; + } + + Future removeUser(UserHandle user) { + final completer = Completer(); + realmLib + .realm_app_remove_user( + pointer, + user.pointer, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + return completer.future; + } + + void switchUser(UserHandle user) { + using((arena) { + realmLib + .realm_app_switch_user( + pointer, + user.pointer, + ) + .raiseLastErrorIfFalse(); + }); + } + + void reconnect() => realmLib.realm_app_sync_client_reconnect(pointer); + + String get baseUrl { + final customDataPtr = realmLib.realm_app_get_base_url(pointer); + return customDataPtr.cast().toRealmDartString(freeRealmMemory: true)!; + } + + Future updateBaseUrl(Uri? baseUrl) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_update_base_url( + pointer, + baseUrl?.toString().toCharPtr(arena) ?? nullptr, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future refreshCustomData(UserHandle user) { + final completer = Completer(); + realmLib + .realm_app_refresh_custom_data( + pointer, + user.pointer, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + return completer.future; + } + + String get id { + return realmLib.realm_app_get_app_id(pointer).cast().toRealmDartString()!; + } + + Future logIn(CredentialsHandle credentials) { + final completer = Completer(); + realmLib + .realm_app_log_in_with_credentials( + pointer, + credentials.pointer, + realmLib.addresses.realm_dart_user_completion_callback, + createAsyncUserCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + return completer.future; + } + + Future registerUser(String email, String password) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_register_email( + pointer, + email.toCharPtr(arena), + password.toRealmString(arena).ref, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future confirmUser(String token, String tokenId) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_confirm_user( + pointer, + token.toCharPtr(arena), + tokenId.toCharPtr(arena), + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future resendConfirmation(String email) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_resend_confirmation_email( + pointer, + email.toCharPtr(arena), + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future completeResetPassword(String password, String token, String tokenId) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_reset_password( + pointer, + password.toRealmString(arena).ref, + token.toCharPtr(arena), + tokenId.toCharPtr(arena), + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future requestResetPassword(String email) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_send_reset_password_email( + pointer, + email.toCharPtr(arena), + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future callResetPasswordFunction(String email, String password, String? argsAsJSON) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_call_reset_password_function( + pointer, + email.toCharPtr(arena), + password.toRealmString(arena).ref, + argsAsJSON != null ? argsAsJSON.toCharPtr(arena) : nullptr, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future retryCustomConfirmationFunction(String email) { + final completer = Completer(); + using((arena) { + realmLib + .realm_app_email_password_provider_client_retry_custom_confirmation( + pointer, + email.toCharPtr(arena), + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + }); + return completer.future; + } + + Future deleteUser(UserHandle user) { + final completer = Completer(); + realmLib + .realm_app_delete_user( + pointer, + user.pointer, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + return completer.future; + } + + bool resetRealm(String realmPath) { + return using((arena) { + final didRun = arena(); + realmLib + .realm_sync_immediately_run_file_actions( + pointer, + realmPath.toCharPtr(arena), + didRun, + ) + .raiseLastErrorIfFalse(); + return didRun.value; + }); + } + + Future callAppFunction(UserHandle user, String functionName, String? argsAsJSON) { + return using((arena) { + final completer = Completer(); + realmLib + .realm_app_call_function( + pointer, + user.pointer, + functionName.toCharPtr(arena), + argsAsJSON?.toCharPtr(arena) ?? nullptr, + nullptr, + realmLib.addresses.realm_dart_return_string_callback, + createAsyncFunctionCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + return completer.future; + }); + } +} + +Pointer createAsyncFunctionCallbackUserdata(Completer completer) { + final callback = Pointer.fromFunction< + Void Function( + Pointer, + Pointer, + Pointer, + )>(_callAppFunctionCallback); + + final userdata = realmLib.realm_dart_userdata_async_new( + completer, + callback.cast(), + scheduler.handle.pointer, + ); + + return userdata.cast(); +} + +void _callAppFunctionCallback(Pointer userdata, Pointer response, Pointer error) { + final Completer completer = userdata.toObject(); + + if (error != nullptr) { + completer.completeWithAppError(error); + return; + } + + final stringResponse = response.cast().toRealmDartString()!; + completer.complete(stringResponse); +} + +Pointer createAsyncCallbackUserdata(Completer completer) { + final callback = Pointer.fromFunction< + Void Function( + Pointer, + Pointer, + )>(_voidCompletionCallback); + + final userdata = realmLib.realm_dart_userdata_async_new( + completer, + callback.cast(), + scheduler.handle.pointer, + ); + + return userdata.cast(); +} + +void _voidCompletionCallback(Pointer userdata, Pointer error) { + final Completer completer = userdata.toObject(); + + if (error != nullptr) { + completer.completeWithAppError(error); + return; + } + + completer.complete(); +} + +class _AppConfigHandle extends HandleBase { + _AppConfigHandle(Pointer pointer) : super(pointer, 8); +} + +_AppConfigHandle _createAppConfig(AppConfiguration configuration, HttpTransportHandle httpTransport) { + return using((arena) { + final appId = configuration.appId.toCharPtr(arena); + final handle = _AppConfigHandle(realmLib.realm_app_config_new(appId, httpTransport.pointer)); + + realmLib.realm_app_config_set_platform_version(handle.pointer, Platform.operatingSystemVersion.toCharPtr(arena)); + + realmLib.realm_app_config_set_sdk(handle.pointer, 'Dart'.toCharPtr(arena)); + realmLib.realm_app_config_set_sdk_version(handle.pointer, libraryVersion.toCharPtr(arena)); + + final deviceName = realmCore.getDeviceName(); + realmLib.realm_app_config_set_device_name(handle.pointer, deviceName.toCharPtr(arena)); + + final deviceVersion = realmCore.getDeviceVersion(); + realmLib.realm_app_config_set_device_version(handle.pointer, deviceVersion.toCharPtr(arena)); + + realmLib.realm_app_config_set_framework_name(handle.pointer, (isFlutterPlatform ? 'Flutter' : 'Dart VM').toCharPtr(arena)); + realmLib.realm_app_config_set_framework_version(handle.pointer, Platform.version.toCharPtr(arena)); + + realmLib.realm_app_config_set_base_url(handle.pointer, configuration.baseUrl.toString().toCharPtr(arena)); + + realmLib.realm_app_config_set_default_request_timeout(handle.pointer, configuration.defaultRequestTimeout.inMilliseconds); + + realmLib.realm_app_config_set_bundle_id(handle.pointer, realmCore.getBundleId().toCharPtr(arena)); + + realmLib.realm_app_config_set_base_file_path(handle.pointer, configuration.baseFilePath.path.toCharPtr(arena)); + realmLib.realm_app_config_set_metadata_mode(handle.pointer, configuration.metadataPersistenceMode.index); + + if (configuration.metadataEncryptionKey != null && configuration.metadataPersistenceMode == MetadataPersistenceMode.encrypted) { + realmLib.realm_app_config_set_metadata_encryption_key(handle.pointer, configuration.metadataEncryptionKey!.toUint8Ptr(arena)); + } + + return handle; + }); +} + +// TODO: +// We need a pure Dart equivalent of: +// ```dart +// ServiceBinding.rootIsolateToken != null +// ``` +// to get rid of this hack. +final bool _isRootIsolate = Isolate.current.debugName == 'main'; diff --git a/packages/realm_dart/lib/src/native/async_open_task_handle.dart b/packages/realm_dart/lib/src/native/async_open_task_handle.dart new file mode 100644 index 000000000..ddd184276 --- /dev/null +++ b/packages/realm_dart/lib/src/native/async_open_task_handle.dart @@ -0,0 +1,80 @@ +import 'dart:ffi'; + +import 'package:cancellation_token/cancellation_token.dart'; +import 'ffi.dart'; +import 'package:realm_dart/src/native/error_handling.dart'; +import 'package:realm_dart/src/native/realm_bindings.dart'; + +import '../realm_dart.dart'; +import '../scheduler.dart'; +import 'config_handle.dart'; +import 'handle_base.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'session_handle.dart'; + +class AsyncOpenTaskHandle extends HandleBase { + AsyncOpenTaskHandle(Pointer pointer) : super(pointer, 32); + + factory AsyncOpenTaskHandle.from(FlexibleSyncConfiguration config) { + final configHandle = ConfigHandle.from(config); + final asyncOpenTaskPtr = realmLib.realm_open_synchronized(configHandle.pointer).raiseLastErrorIfNull(); + return AsyncOpenTaskHandle(asyncOpenTaskPtr); + } + + Future openAsync(CancellationToken? cancellationToken) { + final completer = CancellableCompleter(cancellationToken); + if (!completer.isCancelled) { + final callback = + Pointer.fromFunction realm, Pointer error)>(_openRealmAsyncCallback); + final userData = realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle.pointer); + realmLib.realm_async_open_task_start( + pointer, + realmLib.addresses.realm_dart_async_open_task_callback, + userData.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ); + } + return completer.future; + } + + void cancel() { + realmLib.realm_async_open_task_cancel(pointer); + } + + AsyncOpenTaskProgressNotificationTokenHandle registerProgressNotifier( + RealmAsyncOpenProgressNotificationsController controller, + ) { + final callback = Pointer.fromFunction(syncProgressCallback); + final userdata = realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle.pointer); + return AsyncOpenTaskProgressNotificationTokenHandle( + realmLib.realm_async_open_task_register_download_progress_notifier( + pointer, + realmLib.addresses.realm_dart_sync_progress_callback, + userdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ), + ); + } +} + +class AsyncOpenTaskProgressNotificationTokenHandle extends HandleBase { + AsyncOpenTaskProgressNotificationTokenHandle(Pointer pointer) : super(pointer, 40); +} + +void _openRealmAsyncCallback(Object userData, Pointer realmSafePtr, Pointer error) { + return using((arena) { + final completer = userData as CancellableCompleter; + if (completer.isCancelled) { + return; + } + if (error != nullptr) { + final err = arena(); + final lastError = realmLib.realm_get_async_error(error, err) ? err.ref.toDart() : null; + completer.completeError(RealmException("Failed to open realm: ${lastError?.message ?? 'Error details missing.'}")); + return; + } + + completer.complete(RealmHandle(realmLib.realm_from_thread_safe_reference(realmSafePtr, scheduler.handle.pointer))); + }); +} diff --git a/packages/realm_dart/lib/src/native/collection_changes_handle.dart b/packages/realm_dart/lib/src/native/collection_changes_handle.dart new file mode 100644 index 000000000..ba5a63e29 --- /dev/null +++ b/packages/realm_dart/lib/src/native/collection_changes_handle.dart @@ -0,0 +1,78 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../collections.dart'; +import 'from_native.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +class CollectionChangesHandle extends HandleBase { + CollectionChangesHandle(Pointer pointer) : super(pointer, 256); + + CollectionChanges get changes { + return using((arena) { + final outNumDeletions = arena(); + final outNumInsertions = arena(); + final outNumModifications = arena(); + final outNumMoves = arena(); + final outCollectionCleared = arena(); + final outCollectionWasDeleted = arena(); + realmLib.realm_collection_changes_get_num_changes( + pointer, + outNumDeletions, + outNumInsertions, + outNumModifications, + outNumMoves, + outCollectionCleared, + outCollectionWasDeleted, + ); + + final deletionsCount = outNumDeletions != nullptr ? outNumDeletions.value : 0; + final insertionCount = outNumInsertions != nullptr ? outNumInsertions.value : 0; + final modificationCount = outNumModifications != nullptr ? outNumModifications.value : 0; + var moveCount = outNumMoves != nullptr ? outNumMoves.value : 0; + + final outDeletionIndexes = arena(deletionsCount); + final outInsertionIndexes = arena(insertionCount); + final outModificationIndexes = arena(modificationCount); + final outModificationIndexesAfter = arena(modificationCount); + final outMoves = arena(moveCount); + + realmLib.realm_collection_changes_get_changes( + pointer, + outDeletionIndexes, + deletionsCount, + outInsertionIndexes, + insertionCount, + outModificationIndexes, + modificationCount, + outModificationIndexesAfter, + modificationCount, + outMoves, + moveCount, + ); + + var elementZero = outMoves; + List moves = List.filled(moveCount, Move(elementZero.ref.from, elementZero.ref.to)); + for (var i = 1; i < moveCount; i++) { + final movePtr = outMoves + i; + moves[i] = Move(movePtr.ref.from, movePtr.ref.to); + } + + return CollectionChanges( + outDeletionIndexes.toIntList(deletionsCount), + outInsertionIndexes.toIntList(insertionCount), + outModificationIndexes.toIntList(modificationCount), + outModificationIndexesAfter.toIntList(modificationCount), + moves, + outCollectionCleared.value, + outCollectionWasDeleted.value, + ); + }); + } +} diff --git a/packages/realm_dart/lib/src/native/collection_handle_base.dart b/packages/realm_dart/lib/src/native/collection_handle_base.dart new file mode 100644 index 000000000..6ac5011c0 --- /dev/null +++ b/packages/realm_dart/lib/src/native/collection_handle_base.dart @@ -0,0 +1,51 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'package:realm_dart/src/native/rooted_handle.dart'; + +import '../realm_class.dart'; +import 'list_handle.dart'; +import 'map_handle.dart'; +import 'realm_bindings.dart'; + +abstract class CollectionHandleBase extends RootedHandleBase { + CollectionHandleBase(super.root, super.pointer, super.size); +} + +void createCollection(Realm realm, RealmValue value, Pointer Function() createList, Pointer Function() createMap) { + CollectionHandleBase? collectionHandle; + try { + switch (value.collectionType) { + case RealmCollectionType.list: + final listHandle = ListHandle(createList(), realm.handle); + collectionHandle = listHandle; + + final list = realm.createList(listHandle, null); + + // Necessary since Core will not clear the collection if the value was already a collection + list.clear(); + + for (final item in value.value as List) { + list.add(item); + } + case RealmCollectionType.map: + final mapHandle = MapHandle(createMap(), realm.handle); + collectionHandle = mapHandle; + + final map = realm.createMap(mapHandle, null); + + // Necessary since Core will not clear the collection if the value was already a collection + map.clear(); + + for (final kvp in (value.value as Map).entries) { + map[kvp.key] = kvp.value; + } + default: + throw RealmStateError('createCollection invoked with type that is not list or map.'); + } + } finally { + collectionHandle?.release(); + } +} diff --git a/packages/realm_dart/lib/src/native/config_handle.dart b/packages/realm_dart/lib/src/native/config_handle.dart new file mode 100644 index 000000000..bb4010a70 --- /dev/null +++ b/packages/realm_dart/lib/src/native/config_handle.dart @@ -0,0 +1,268 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; +import 'dart:ffi'; + +import 'ffi.dart'; +import 'package:realm_dart/src/native/error_handling.dart'; + +import '../configuration.dart'; +import '../migration.dart'; +import '../realm_class.dart'; +import '../scheduler.dart'; +import '../user.dart'; +import 'convert_native.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'schema_handle.dart'; + +class ConfigHandle extends HandleBase { + ConfigHandle(Pointer pointer) : super(pointer, 512); + + factory ConfigHandle.from(Configuration config) { + return using((arena) { + final configHandle = ConfigHandle(realmLib.realm_config_new()); + + if (config.schemaObjects.isNotEmpty) { + final schemaHandle = SchemaHandle.from(config.schemaObjects); + realmLib.realm_config_set_schema(configHandle.pointer, schemaHandle.pointer); + } + + realmLib.realm_config_set_path(configHandle.pointer, config.path.toCharPtr(arena)); + realmLib.realm_config_set_scheduler(configHandle.pointer, scheduler.handle.pointer); + + if (config.fifoFilesFallbackPath != null) { + realmLib.realm_config_set_fifo_path(configHandle.pointer, config.fifoFilesFallbackPath!.toCharPtr(arena)); + } + + // Setting schema version only makes sense for local realms, but core insists it is always set, + // hence we set it to 0 in those cases. + + final schemaVersion = switch (config) { + (LocalConfiguration lc) => lc.schemaVersion, + (FlexibleSyncConfiguration fsc) => fsc.schemaVersion, + _ => 0, + }; + realmLib.realm_config_set_schema_version(configHandle.pointer, schemaVersion); + if (config.maxNumberOfActiveVersions != null) { + realmLib.realm_config_set_max_number_of_active_versions(configHandle.pointer, config.maxNumberOfActiveVersions!); + } + if (config is LocalConfiguration) { + if (config.initialDataCallback != null) { + realmLib.realm_config_set_data_initialization_function( + configHandle.pointer, + Pointer.fromFunction(_initialDataCallback, false), + config.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + ); + } + if (config.isReadOnly) { + realmLib.realm_config_set_schema_mode(configHandle.pointer, realm_schema_mode.RLM_SCHEMA_MODE_IMMUTABLE); + } else if (config.shouldDeleteIfMigrationNeeded) { + realmLib.realm_config_set_schema_mode(configHandle.pointer, realm_schema_mode.RLM_SCHEMA_MODE_SOFT_RESET_FILE); + } + if (config.disableFormatUpgrade) { + realmLib.realm_config_set_disable_format_upgrade(configHandle.pointer, config.disableFormatUpgrade); + } + if (config.shouldCompactCallback != null) { + realmLib.realm_config_set_should_compact_on_launch_function( + configHandle.pointer, + Pointer.fromFunction(_shouldCompactCallback, false), + config.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + ); + } + if (config.migrationCallback != null) { + realmLib.realm_config_set_migration_function( + configHandle.pointer, + Pointer.fromFunction(_migrationCallback, false), + config.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + ); + } + } else if (config is InMemoryConfiguration) { + realmLib.realm_config_set_in_memory(configHandle.pointer, true); + } else if (config is FlexibleSyncConfiguration) { + realmLib.realm_config_set_schema_mode(configHandle.pointer, realm_schema_mode.RLM_SCHEMA_MODE_ADDITIVE_DISCOVERED); + final syncConfigPtr = realmLib.realm_flx_sync_config_new(config.user.handle.pointer).raiseLastErrorIfNull(); + try { + realmLib.realm_sync_config_set_session_stop_policy(syncConfigPtr, config.sessionStopPolicy.index); + realmLib.realm_sync_config_set_resync_mode(syncConfigPtr, config.clientResetHandler.clientResyncMode.index); + final errorHandlerCallback = + Pointer.fromFunction, realm_sync_error_t)>(_syncErrorHandlerCallback); + final errorHandlerUserdata = realmLib.realm_dart_userdata_async_new(config, errorHandlerCallback.cast(), scheduler.handle.pointer); + realmLib.realm_sync_config_set_error_handler(syncConfigPtr, realmLib.addresses.realm_dart_sync_error_handler_callback, errorHandlerUserdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free); + + if (config.clientResetHandler.onBeforeReset != null) { + final syncBeforeResetCallback = Pointer.fromFunction, Pointer)>(_syncBeforeResetCallback); + final beforeResetUserdata = realmLib.realm_dart_userdata_async_new(config, syncBeforeResetCallback.cast(), scheduler.handle.pointer); + + realmLib.realm_sync_config_set_before_client_reset_handler(syncConfigPtr, realmLib.addresses.realm_dart_sync_before_reset_handler_callback, + beforeResetUserdata.cast(), realmLib.addresses.realm_dart_userdata_async_free); + } + + if (config.clientResetHandler.onAfterRecovery != null || config.clientResetHandler.onAfterDiscard != null) { + final syncAfterResetCallback = + Pointer.fromFunction, Pointer, Bool, Pointer)>( + _syncAfterResetCallback); + final afterResetUserdata = realmLib.realm_dart_userdata_async_new(config, syncAfterResetCallback.cast(), scheduler.handle.pointer); + + realmLib.realm_sync_config_set_after_client_reset_handler(syncConfigPtr, realmLib.addresses.realm_dart_sync_after_reset_handler_callback, + afterResetUserdata.cast(), realmLib.addresses.realm_dart_userdata_async_free); + } + + if (config.shouldCompactCallback != null) { + realmLib.realm_config_set_should_compact_on_launch_function( + configHandle.pointer, + Pointer.fromFunction(_shouldCompactCallback, false), + config.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + ); + } + + realmLib.realm_config_set_sync_config(configHandle.pointer, syncConfigPtr); + } finally { + realmLib.realm_release(syncConfigPtr.cast()); + } + } else if (config is DisconnectedSyncConfiguration) { + realmLib.realm_config_set_schema_mode(configHandle.pointer, realm_schema_mode.RLM_SCHEMA_MODE_ADDITIVE_EXPLICIT); + realmLib.realm_config_set_force_sync_history(configHandle.pointer, true); + } + + final key = config.encryptionKey; + if (key != null) { + realmLib.realm_config_set_encryption_key(configHandle.pointer, key.toUint8Ptr(arena), key.length); + } + + // For sync and for dynamic Realms, we need to have a complete view of the schema in Core. + if (config.schemaObjects.isEmpty || config is FlexibleSyncConfiguration) { + realmLib.realm_config_set_schema_subset_mode(configHandle.pointer, realm_schema_subset_mode.RLM_SCHEMA_SUBSET_MODE_COMPLETE); + } + + return configHandle; + }); + } +} + +void _syncAfterResetCallback(Object userdata, Pointer beforeHandle, Pointer afterReference, bool didRecover, + Pointer unlockCallbackFunc) { + _guardSynchronousCallback(() async { + final syncConfig = userdata as FlexibleSyncConfiguration; + final afterResetCallback = didRecover ? syncConfig.clientResetHandler.onAfterRecovery : syncConfig.clientResetHandler.onAfterDiscard; + + if (afterResetCallback == null) { + return; + } + + final beforeRealm = RealmInternal.getUnowned(syncConfig, RealmHandle.unowned(beforeHandle)); + final afterRealm = RealmInternal.getUnowned( + syncConfig, + RealmHandle.unowned(realmLib.realm_from_thread_safe_reference( + afterReference, + scheduler.handle.pointer, + ))); + + try { + await afterResetCallback(beforeRealm, afterRealm); + return; + } finally { + beforeRealm.handle.release(); + afterRealm.handle.release(); + } + }, unlockCallbackFunc); +} + +bool _shouldCompactCallback(Pointer userdata, int totalSize, int usedSize) { + final config = userdata.toObject(); + + if (config is LocalConfiguration) { + return config.shouldCompactCallback!(totalSize, usedSize); + } else if (config is FlexibleSyncConfiguration) { + return config.shouldCompactCallback!(totalSize, usedSize); + } + + return false; +} + +bool _migrationCallback(Pointer userdata, Pointer oldRealmHandle, Pointer newRealmHandle, Pointer schema) { + final oldHandle = RealmHandle.unowned(oldRealmHandle); + final newHandle = RealmHandle.unowned(newRealmHandle); + try { + final LocalConfiguration config = userdata.toObject(); + + final oldSchemaVersion = realmLib.realm_get_schema_version(oldRealmHandle); + final oldConfig = Configuration.local([], path: config.path, isReadOnly: true, schemaVersion: oldSchemaVersion); + final oldRealm = RealmInternal.getUnowned(oldConfig, oldHandle, isInMigration: true); + + final newRealm = RealmInternal.getUnowned(config, newHandle, isInMigration: true); + + final migration = MigrationInternal.create(RealmInternal.getMigrationRealm(oldRealm), newRealm, SchemaHandle.unowned(schema)); + config.migrationCallback!(migration, oldSchemaVersion); + return true; + } catch (ex) { + realmLib.realm_register_user_code_callback_error(ex.toPersistentHandle()); + } finally { + oldHandle.release(); + newHandle.release(); + } + + return false; +} + +void _syncErrorHandlerCallback(Object userdata, Pointer session, realm_sync_error error) { + final syncConfig = userdata as FlexibleSyncConfiguration; + // TODO: Take the app from the session instead of from syncConfig after fixing issue https://github.com/realm/realm-dart/issues/633 + final syncError = SyncErrorInternal.createSyncError(error.toDart(), app: syncConfig.user.app); + + if (syncError is ClientResetError) { + syncConfig.clientResetHandler.onManualReset?.call(syncError); + return; + } + + syncConfig.syncErrorHandler(syncError); +} + +void _syncBeforeResetCallback(Object userdata, Pointer realmPtr, Pointer unlockCallbackFunc) { + _guardSynchronousCallback(() async { + final syncConfig = userdata as FlexibleSyncConfiguration; + var beforeResetCallback = syncConfig.clientResetHandler.onBeforeReset!; + + final realm = RealmInternal.getUnowned(syncConfig, RealmHandle.unowned(realmPtr)); + try { + await beforeResetCallback(realm); + } finally { + realm.handle.release(); + } + }, unlockCallbackFunc); +} + +bool _initialDataCallback(Pointer userdata, Pointer realmPtr) { + final realmHandle = RealmHandle.unowned(realmPtr); + try { + final LocalConfiguration config = userdata.toObject(); + final realm = RealmInternal.getUnowned(config, realmHandle); + config.initialDataCallback!(realm); + return true; + } catch (ex) { + realmLib.realm_register_user_code_callback_error(ex.toPersistentHandle()); + } finally { + realmHandle.release(); + } + + return false; +} + +void _guardSynchronousCallback(FutureOr Function() callback, Pointer unlockCallbackFunc) async { + Pointer userError = nullptr; + try { + await callback(); + } catch (error) { + userError = error.toPersistentHandle(); + } finally { + realmLib.realm_dart_invoke_unlock_callback(userError, unlockCallbackFunc); + } +} diff --git a/packages/realm_dart/lib/src/native/convert.dart b/packages/realm_dart/lib/src/native/convert.dart new file mode 100644 index 000000000..34c258b22 --- /dev/null +++ b/packages/realm_dart/lib/src/native/convert.dart @@ -0,0 +1,24 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'handle_base.dart'; + +extension PointerEx on Pointer { + Pointer? get nullPtrAsNull => this == nullptr ? null : this; + U? convert(U Function(Pointer) convertor) => nullPtrAsNull.convert(convertor); +} + +extension HandleBaseEx on T { + T? get nullPtrAsNull => pointer == nullptr ? null : this; + U? convert(U Function(T) convertor) => nullPtrAsNull.convert(convertor); +} + +extension NullableObjectEx on T? { + U? convert(U Function(T) convertor) { + final self = this; + if (self == null) return null; + return convertor(self); + } +} diff --git a/packages/realm_dart/lib/src/native/convert_native.dart b/packages/realm_dart/lib/src/native/convert_native.dart new file mode 100644 index 000000000..6f7cb2aaf --- /dev/null +++ b/packages/realm_dart/lib/src/native/convert_native.dart @@ -0,0 +1,2 @@ +export 'to_native.dart'; +export 'from_native.dart'; diff --git a/packages/realm_dart/lib/src/native/credentials_handle.dart b/packages/realm_dart/lib/src/native/credentials_handle.dart new file mode 100644 index 000000000..d742d9ac9 --- /dev/null +++ b/packages/realm_dart/lib/src/native/credentials_handle.dart @@ -0,0 +1,82 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../credentials.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; +import 'to_native.dart'; + +class CredentialsHandle extends HandleBase { + CredentialsHandle(Pointer pointer) : super(pointer, 16); + + factory CredentialsHandle.anonymous(bool reuseCredentials) { + return CredentialsHandle(realmLib.realm_app_credentials_new_anonymous(reuseCredentials)); + } + + factory CredentialsHandle.emailPassword(String email, String password) { + return using((arena) { + final emailPtr = email.toCharPtr(arena); + final passwordPtr = password.toRealmString(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_email_password(emailPtr, passwordPtr.ref)); + }); + } + + factory CredentialsHandle.jwt(String token) { + return using((arena) { + final tokenPtr = token.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_jwt(tokenPtr)); + }); + } + + factory CredentialsHandle.apple(String idToken) { + return using((arena) { + final idTokenPtr = idToken.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_apple(idTokenPtr)); + }); + } + + factory CredentialsHandle.facebook(String accessToken) { + return using((arena) { + final accessTokenPtr = accessToken.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_facebook(accessTokenPtr)); + }); + } + + factory CredentialsHandle.googleIdToken(String idToken) { + return using((arena) { + final idTokenPtr = idToken.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_google_id_token(idTokenPtr)); + }); + } + + factory CredentialsHandle.googleAuthCode(String authCode) { + return using((arena) { + final authCodePtr = authCode.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_google_auth_code(authCodePtr)); + }); + } + + factory CredentialsHandle.function(String payload) { + return using((arena) { + final payloadPtr = payload.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_function(payloadPtr)); + }); + } + + factory CredentialsHandle.apiKey(String key) { + return using((arena) { + final keyPtr = key.toCharPtr(arena); + return CredentialsHandle(realmLib.realm_app_credentials_new_api_key(keyPtr)); + }); + } + + AuthProviderType get providerType { + final provider = realmLib.realm_auth_credentials_get_provider(pointer); + return AuthProviderTypeInternal.getByValue(provider); + } +} diff --git a/packages/realm_dart/lib/src/native/decimal128.dart b/packages/realm_dart/lib/src/native/decimal128.dart index 9d0039bbf..ad4df4acd 100644 --- a/packages/realm_dart/lib/src/native/decimal128.dart +++ b/packages/realm_dart/lib/src/native/decimal128.dart @@ -1,7 +1,15 @@ // Copyright 2023 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 -part of 'realm_core.dart'; +import 'dart:convert'; +import 'dart:ffi'; + +import 'ffi.dart'; +import 'package:realm_common/realm_common.dart' as common; + +import 'realm_bindings.dart'; +import 'realm_library.dart'; +import 'to_native.dart'; /// A 128-bit decimal floating point number. class Decimal128 implements Comparable, common.Decimal128 { @@ -15,7 +23,7 @@ class Decimal128 implements Comparable, common.Decimal128 { static final ten = Decimal128.fromInt(10); /// The value NaN. - static final nan = Decimal128._(_realmLib.realm_dart_decimal128_nan()); + static final nan = Decimal128._(realmLib.realm_dart_decimal128_nan()); /// The value +Inf. static final infinity = one / zero; // +Inf @@ -33,7 +41,7 @@ class Decimal128 implements Comparable, common.Decimal128 { static Decimal128? tryParse(String source) { if (!_validInput.hasMatch(source)) return null; return using((arena) { - final result = _realmLib.realm_dart_decimal128_from_string(source.toCharPtr(arena)); + final result = realmLib.realm_dart_decimal128_from_string(source.toCharPtr(arena)); return Decimal128._(result); }); } @@ -45,7 +53,7 @@ class Decimal128 implements Comparable, common.Decimal128 { /// Converts a `int` into a [Decimal128]. factory Decimal128.fromInt(int value) { - return Decimal128._(_realmLib.realm_dart_decimal128_from_int64(value)); + return Decimal128._(realmLib.realm_dart_decimal128_from_int64(value)); } /// Converts a `double` into a [Decimal128]. @@ -54,30 +62,30 @@ class Decimal128 implements Comparable, common.Decimal128 { } /// Returns `true` if `this` is NaN. - bool get isNaN => _realmLib.realm_dart_decimal128_is_nan(_value); + bool get isNaN => realmLib.realm_dart_decimal128_is_nan(_value); /// Adds `this` with `other` and returns a new [Decimal128]. Decimal128 operator +(Decimal128 other) { - return Decimal128._(_realmLib.realm_dart_decimal128_add(_value, other._value)); + return Decimal128._(realmLib.realm_dart_decimal128_add(_value, other._value)); } /// Subtracts `other` from `this` and returns a new [Decimal128]. Decimal128 operator -(Decimal128 other) { - return Decimal128._(_realmLib.realm_dart_decimal128_subtract(_value, other._value)); + return Decimal128._(realmLib.realm_dart_decimal128_subtract(_value, other._value)); } /// Multiplies `this` with `other` and returns a new [Decimal128]. Decimal128 operator *(Decimal128 other) { - return Decimal128._(_realmLib.realm_dart_decimal128_multiply(_value, other._value)); + return Decimal128._(realmLib.realm_dart_decimal128_multiply(_value, other._value)); } /// Divides `this` by `other` and returns a new [Decimal128]. Decimal128 operator /(Decimal128 other) { - return Decimal128._(_realmLib.realm_dart_decimal128_divide(_value, other._value)); + return Decimal128._(realmLib.realm_dart_decimal128_divide(_value, other._value)); } /// Negates `this` and returns a new [Decimal128]. - Decimal128 operator -() => Decimal128._(_realmLib.realm_dart_decimal128_negate(_value)); + Decimal128 operator -() => Decimal128._(realmLib.realm_dart_decimal128_negate(_value)); /// Returns the absolute value of `this`. Decimal128 abs() => this < zero ? -this : this; @@ -89,14 +97,14 @@ class Decimal128 implements Comparable, common.Decimal128 { // WARNING: Don't use identical to ensure nan != nan, // if (identical(this, other)) return true; if (other is Decimal128) { - return _realmLib.realm_dart_decimal128_equal(_value, other._value); + return realmLib.realm_dart_decimal128_equal(_value, other._value); } return false; } /// Returns `true` if `this` is less than `other`. bool operator <(Decimal128 other) { - return _realmLib.realm_dart_decimal128_less_than(_value, other._value); + return realmLib.realm_dart_decimal128_less_than(_value, other._value); } /// Returns `true` if `this` is less than or equal to `other`. @@ -104,27 +112,27 @@ class Decimal128 implements Comparable, common.Decimal128 { /// Returns `true` if `this` is greater than `other`. bool operator >(Decimal128 other) { - return _realmLib.realm_dart_decimal128_greater_than(_value, other._value); + return realmLib.realm_dart_decimal128_greater_than(_value, other._value); } /// Returns `true` if `this` is greater than or equal to `other`. bool operator >=(Decimal128 other) => compareTo(other) >= 0; /// Converts `this` to an `int`. Possibly loosing precision. - int toInt() => _realmLib.realm_dart_decimal128_to_int64(_value); + int toInt() => realmLib.realm_dart_decimal128_to_int64(_value); /// String representation of `this`. @override String toString() { return using((arena) { - final realmString = _realmLib.realm_dart_decimal128_to_string(_value); + final realmString = realmLib.realm_dart_decimal128_to_string(_value); return ascii.decode(realmString.data.cast().asTypedList(realmString.size)); }); } /// Compares `this` to `other`. @override - int compareTo(Decimal128 other) => _realmLib.realm_dart_decimal128_compare_to(_value, other._value); + int compareTo(Decimal128 other) => realmLib.realm_dart_decimal128_compare_to(_value, other._value); } extension Decimal128Internal on Decimal128 { diff --git a/packages/realm_dart/lib/src/native/error_handling.dart b/packages/realm_dart/lib/src/native/error_handling.dart new file mode 100644 index 000000000..68f47fea8 --- /dev/null +++ b/packages/realm_dart/lib/src/native/error_handling.dart @@ -0,0 +1,69 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../realm_object.dart'; +import 'from_native.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +extension PointerEx on Pointer { + Pointer raiseLastErrorIfNull() { + if (this == nullptr) { + _raiseLastError(); + } + return this; + } +} + +extension BoolEx on bool { + void raiseLastErrorIfFalse() { + if (!this) { + _raiseLastError(); + } + } +} + +class LastError { + final int code; + final String? message; + final Object? userError; + + LastError(this.code, [this.message, this.userError]); + + @override + String toString() => "${message ?? 'No message'}. Error code: $code."; +} + +LastError? _getLastError(Allocator allocator) { + final error = allocator(); + final success = realmLib.realm_get_last_error(error); + return success ? error.ref.toDart() : null; +} + +Never _raiseLastError([String? errorMessage]) { + using((arena) { + final lastError = _getLastError(arena); + if (lastError?.userError != null) { + throw UserCallbackException(lastError!.userError!); + } + + final message = '${errorMessage != null ? "$errorMessage. " : ""}${lastError ?? ""}'; + switch (lastError?.code) { + case realm_errno.RLM_ERR_SCHEMA_MISMATCH: + throw MigrationRequiredException(message); + default: + throw RealmException(message); + } + }); +} + +extension RealmErrorEx on realm_error { + LastError toDart() { + final message = this.message.cast().toRealmDartString(); + return LastError(error, message, user_code_error.toUserCodeError()); + } +} diff --git a/packages/realm_dart/lib/src/native/ffi.dart b/packages/realm_dart/lib/src/native/ffi.dart new file mode 100644 index 000000000..7a1ae302c --- /dev/null +++ b/packages/realm_dart/lib/src/native/ffi.dart @@ -0,0 +1,4 @@ +// Import this file instead of package:ffi/ffi.dart +// Hides StringUtf8Pointer.toNativeUtf8 and StringUtf16Pointer since these allows +// silently allocating memory. Use toUtf8Ptr instead +export 'package:ffi/ffi.dart' hide StringUtf8Pointer, StringUtf16Pointer; diff --git a/packages/realm_dart/lib/src/native/from_native.dart b/packages/realm_dart/lib/src/native/from_native.dart new file mode 100644 index 000000000..316331ce2 --- /dev/null +++ b/packages/realm_dart/lib/src/native/from_native.dart @@ -0,0 +1,358 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'ffi.dart'; + +import '../app.dart'; +import '../configuration.dart'; +import '../realm_class.dart'; +import '../user.dart'; +import 'decimal128.dart'; +import 'list_handle.dart'; +import 'map_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +// TODO: Duplicated in to_native.dart +const int _microsecondsPerSecond = 1000 * 1000; +const int _nanosecondsPerMicrosecond = 1000; + +extension RealmValueEx on realm_value_t { + Object? toPrimitiveValue() => toDartValue(realm: null, getList: null, getMap: null); + + Object? toDartValue({required Realm? realm, required Pointer Function()? getList, required Pointer Function()? getMap}) { + switch (type) { + case realm_value_type.RLM_TYPE_NULL: + return null; + case realm_value_type.RLM_TYPE_INT: + return values.integer; + case realm_value_type.RLM_TYPE_BOOL: + return values.boolean; + case realm_value_type.RLM_TYPE_STRING: + return values.string.data.cast().toRealmDartString(length: values.string.size)!; + case realm_value_type.RLM_TYPE_FLOAT: + return values.fnum; + case realm_value_type.RLM_TYPE_DOUBLE: + return values.dnum; + case realm_value_type.RLM_TYPE_LINK: + if (realm == null) { + throw RealmError("A realm instance is required to resolve Backlinks"); + } + final objectKey = values.link.target; + final classKey = values.link.target_table; + if (realm.metadata.getByClassKeyIfExists(classKey) == null) return null; // temporary workaround to avoid crash on assertion + return realm.handle.getObject(classKey, objectKey); + case realm_value_type.RLM_TYPE_BINARY: + return Uint8List.fromList(values.binary.data.asTypedList(values.binary.size)); + case realm_value_type.RLM_TYPE_TIMESTAMP: + final seconds = values.timestamp.seconds; + final nanoseconds = values.timestamp.nanoseconds; + return DateTime.fromMicrosecondsSinceEpoch(seconds * _microsecondsPerSecond + nanoseconds ~/ _nanosecondsPerMicrosecond, isUtc: true); + case realm_value_type.RLM_TYPE_DECIMAL128: + var decimal = values.decimal128; // NOTE: Does not copy the struct! + decimal = realmLib.realm_dart_decimal128_copy(decimal); // This is a workaround to that + return Decimal128Internal.fromNative(decimal); + case realm_value_type.RLM_TYPE_OBJECT_ID: + return ObjectId.fromBytes(values.object_id.bytes.toList(12)); + case realm_value_type.RLM_TYPE_UUID: + final listInt = values.uuid.bytes.toList(16); + return Uuid.fromBytes(Uint8List.fromList(listInt).buffer); + case realm_value_type.RLM_TYPE_LIST: + if (getList == null || realm == null) { + throw RealmException('toDartValue called with a list argument but without a list getter'); + } + + final listHandle = ListHandle(getList(), realm.handle); + return realm.createList(listHandle, null); + case realm_value_type.RLM_TYPE_DICTIONARY: + if (getMap == null || realm == null) { + throw RealmException('toDartValue called with a list argument but without a list getter'); + } + + final mapHandle = MapHandle(getMap(), realm.handle); + return realm.createMap(mapHandle, null); + default: + throw RealmException("realm_value_type $type not supported"); + } + } +} + +extension ArrayUint8Ex on Array { + List toList(int count) { + final result = []; + for (var i = 0; i < count; i++) { + result.add(this[i]); + } + return result; + } +} + +extension PointerSizeEx on Pointer { + List toIntList(int count) { + List result = List.filled(count, value); + for (var i = 1; i < count; i++) { + result[i] = (this + i).value; + } + return result; + } +} + +extension PointerVoidEx on Pointer { + T toObject() { + assert(this != nullptr, "Pointer is null"); + + Object object = realmLib.realm_dart_persistent_handle_to_object(this); + + assert(object is T, "$T expected"); + return object as T; + } + + Object? toUserCodeError() { + if (this != nullptr) { + final result = toObject(); + realmLib.realm_dart_delete_persistent_handle(this); + return result; + } + + return null; + } +} + +extension PointerUtf8Ex on Pointer { + String? toRealmDartString({bool treatEmptyAsNull = false, int? length, bool freeRealmMemory = false}) { + if (this == nullptr) { + return null; + } + + try { + final result = toDartString(length: length); + + if (treatEmptyAsNull && result == '') { + return null; + } + return result; + } finally { + if (freeRealmMemory) { + realmLib.realm_free(cast()); + } + } + } +} + +extension RealmSyncErrorEx on realm_sync_error { + SyncErrorDetails toDart() { + final message = status.message.cast().toRealmDartString()!; + final userInfoMap = user_info_map.toDart(user_info_length); + final originalFilePathKey = c_original_file_path_key.cast().toRealmDartString(); + final recoveryFilePathKey = c_recovery_file_path_key.cast().toRealmDartString(); + + return SyncErrorDetails( + message, + status.error, + user_code_error.toUserCodeError(), + isFatal: is_fatal, + isClientResetRequested: is_client_reset_requested, + originalFilePath: userInfoMap?[originalFilePathKey], + backupFilePath: userInfoMap?[recoveryFilePathKey], + compensatingWrites: compensating_writes.toList(compensating_writes_length), + ); + } +} + +extension PointerRealmSyncErrorUserInfoEx on Pointer { + Map? toDart(int length) { + if (this == nullptr) { + return null; + } + Map userInfoMap = {}; + for (int i = 0; i < length; i++) { + final userInfoItem = this[i]; + final key = userInfoItem.key.cast().toDartString(); + final value = userInfoItem.value.cast().toDartString(); + userInfoMap[key] = value; + } + return userInfoMap; + } +} + +extension PointerRealmSyncErrorCompensatingWriteInfoEx on Pointer { + List? toList(int length) { + if (this == nullptr || length == 0) { + return null; + } + List compensatingWrites = []; + for (int i = 0; i < length; i++) { + final compensatingWrite = this[i]; + final reason = compensatingWrite.reason.cast().toDartString(); + final objectName = compensatingWrite.object_name.cast().toDartString(); + final primaryKey = compensatingWrite.primary_key.toPrimitiveValue(); + compensatingWrites.add(CompensatingWriteInfo(objectName, reason, RealmValue.from(primaryKey))); + } + return compensatingWrites; + } +} + +extension PointerRealmErrorEx on Pointer { + SyncError toDart() { + final message = ref.message.cast().toDartString(); + final details = SyncErrorDetails(message, ref.error, ref.user_code_error.toUserCodeError()); + return SyncErrorInternal.createSyncError(details); + } +} + +extension ObjectEx on Object { + Pointer toPersistentHandle() { + return realmLib.realm_dart_object_to_persistent_handle(this); + } +} + +extension ListUserStateEx on List { + UserState fromIndex(int index) { + if (!UserState.values.any((value) => value.index == index)) { + throw RealmError("Unknown user state $index"); + } + + return UserState.values[index]; + } +} + +extension RealmPropertyInfoEx on realm_property_info { + SchemaProperty toSchemaProperty() { + final linkTarget = link_target == nullptr ? null : link_target.cast().toDartString(); + return SchemaProperty(name.cast().toDartString(), RealmPropertyType.values[type], + optional: flags & realm_property_flags.RLM_PROPERTY_NULLABLE == realm_property_flags.RLM_PROPERTY_NULLABLE, + primaryKey: flags & realm_property_flags.RLM_PROPERTY_PRIMARY_KEY == realm_property_flags.RLM_PROPERTY_PRIMARY_KEY, + linkTarget: linkTarget == null || linkTarget.isEmpty ? null : linkTarget, + collectionType: RealmCollectionType.values[collection_type]); + } +} + +extension CompleterEx on Completer { + void completeFrom(FutureOr Function() action) { + try { + complete(action()); + } catch (error, stackTrace) { + completeError(error, stackTrace); + } + } + + void completeWithAppError(Pointer error) { + final message = error.ref.message.cast().toRealmDartString()!; + final linkToLogs = error.ref.link_to_server_logs.cast().toRealmDartString(); + completeError(AppInternal.createException(message, linkToLogs, error.ref.http_status_code)); + } +} + +enum CustomErrorCode { + noError(0), + unknownHttp(998), + unknown(999), + timeout(1000); + + final int code; + const CustomErrorCode(this.code); +} + +enum HttpMethod { + get, + post, + patch, + put, + delete, +} + +extension RealmTimestampEx on realm_timestamp_t { + DateTime toDart() { + return DateTime.fromMicrosecondsSinceEpoch(seconds * _microsecondsPerSecond + nanoseconds ~/ 1000, isUtc: true); + } +} + +extension RealmStringEx on realm_string_t { + String? toDart() => data.cast().toRealmDartString(); +} + +extension ObjectIdEx on ObjectId { + Pointer toNative(Allocator allocator) { + final result = allocator(); + for (var i = 0; i < 12; i++) { + result.ref.bytes[i] = bytes[i]; + } + return result; + } +} + +extension RealmObjectIdEx on realm_object_id { + ObjectId toDart() { + final buffer = Uint8List(12); + for (int i = 0; i < 12; ++i) { + buffer[i] = bytes[i]; + } + return ObjectId.fromBytes(buffer); + } +} + +extension RealmAppUserApikeyEx on realm_app_user_apikey { + ApiKey toDart() => UserInternal.createApiKey( + id.toDart(), + name.cast().toDartString(), + key.cast().toRealmDartString(treatEmptyAsNull: true), + !disabled, + ); +} + +extension PlatformEx on Platform { + static String fromEnvironment(String name, {String defaultValue = ""}) { + final result = Platform.environment[name]; + if (result == null) { + return defaultValue; + } + + return result; + } +} + +/// @nodoc +class SyncErrorDetails { + final String message; + final int code; + final String? path; + final bool isFatal; + final bool isClientResetRequested; + final String? originalFilePath; + final String? backupFilePath; + final List? compensatingWrites; + final Object? userError; + + SyncErrorDetails( + this.message, + this.code, + this.userError, { + this.path, + this.isFatal = false, + this.isClientResetRequested = false, + this.originalFilePath, + this.backupFilePath, + this.compensatingWrites, + }); +} + +extension PointerRealmValueEx on Pointer { + Object? toDartValue(Realm realm, Pointer Function()? getList, Pointer Function()? getMap) { + if (this == nullptr) { + throw RealmException("Can not convert nullptr realm_value to Dart value"); + } + return ref.toDartValue(realm: realm, getList: getList, getMap: getMap); + } + + List toStringList(int count) { + final result = List.filled(count, ''); + for (var i = 0; i < count; i++) { + final strValue = (this + i).ref.values.string; + result[i] = strValue.data.cast().toRealmDartString(length: strValue.size)!; + } + + return result; + } +} diff --git a/packages/realm_dart/lib/src/native/handle_base.dart b/packages/realm_dart/lib/src/native/handle_base.dart new file mode 100644 index 000000000..f4a3cbfe5 --- /dev/null +++ b/packages/realm_dart/lib/src/native/handle_base.dart @@ -0,0 +1,93 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'package:realm_dart/src/native/error_handling.dart'; + +import 'realm_library.dart'; + +// Flag to enable trace on finalization. +// +// Be aware that the trace is likely late, and it might in rare case be missing, +// as there are no absolute guarantees with Finalizer. +// +// It is often beneficial to also instrument the native realm_release to +// print the address released to get the exact time. +// +// This is const to allow the compiler to remove the trace code if not enabled. +const _enableFinalizerTrace = false; + +void _traceFinalization(Object o) { + print('Finalizing: $o'); +} + +final _debugFinalizer = Finalizer(_traceFinalization); + +void _setupFinalizationTrace(Object value, Object finalizationToken) { + _debugFinalizer.attach(value, finalizationToken, detach: value); +} + +void _tearDownFinalizationTrace(Object value, Object finalizationToken) { + _debugFinalizer.detach(value); + _traceFinalization(finalizationToken); +} + +abstract class HandleBase implements Finalizable { + late Pointer _finalizableHandle; + Pointer pointer; + bool get released => pointer == nullptr; + final bool isUnowned; + + @pragma('vm:never-inline') + void keepAlive() {} + + HandleBase(this.pointer, int size) : isUnowned = false { + pointer.raiseLastErrorIfNull(); + _finalizableHandle = realmLib.realm_attach_finalizer(this, pointer.cast(), size); + + if (_enableFinalizerTrace) { + _setupFinalizationTrace(this, pointer); + } + } + + HandleBase.unowned(this.pointer) : isUnowned = true { + pointer.raiseLastErrorIfNull(); + } + + @override + String toString() => "${pointer.toString()} value=${pointer.cast().value}${isUnowned ? ' (unowned)' : ''}"; + + /// @nodoc + /// A method that will be invoked just before the handle is released. Allows to cleanup + /// any custom data that inheritors are storing. + void releaseCore() {} + + void release() { + if (released) { + return; + } + + releaseCore(); + + if (!isUnowned) { + realmLib.realm_detach_finalizer(_finalizableHandle, this); + + realmLib.realm_release(pointer.cast()); + } + + pointer = nullptr; + + if (_enableFinalizerTrace) { + _tearDownFinalizationTrace(this, pointer); + } + } + + @override + // ignore: hash_and_equals + bool operator ==(Object other) => other is HandleBase + ? pointer == other.pointer + ? true + : realmLib.realm_equals(pointer.cast(), other.pointer.cast()) + : false; +} diff --git a/packages/realm_dart/lib/src/native/http_transport_handle.dart b/packages/realm_dart/lib/src/native/http_transport_handle.dart new file mode 100644 index 000000000..afb776bfd --- /dev/null +++ b/packages/realm_dart/lib/src/native/http_transport_handle.dart @@ -0,0 +1,153 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; + +import '../logging.dart'; +import '../realm_dart.dart'; +import '../scheduler.dart'; +import 'convert_native.dart'; +import 'ffi.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +class HttpTransportHandle extends HandleBase { + HttpTransportHandle(Pointer pointer) : super(pointer, 24); + + factory HttpTransportHandle.from(HttpClient httpClient) { + final requestCallback = Pointer.fromFunction)>(_requestCallback); + final requestCallbackUserdata = realmLib.realm_dart_userdata_async_new(httpClient, requestCallback.cast(), scheduler.handle.pointer); + return HttpTransportHandle(realmLib.realm_http_transport_new( + realmLib.addresses.realm_dart_http_request_callback, + requestCallbackUserdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + )); + } +} + +void _requestCallback(Object userData, realm_http_request request, Pointer requestContext) { + // + // The request struct only survives until end-of-call, even though + // we explicitly call realm_http_transport_complete_request to + // mark request as completed later. + // + // Therefore we need to copy everything out of request before returning. + // We cannot clone request on the native side with realm_clone, + // since realm_http_request does not inherit from WrapC. + + final client = userData as HttpClient; + + client.connectionTimeout = Duration(milliseconds: request.timeout_ms); + + final url = Uri.parse(request.url.cast().toRealmDartString()!); + + final body = request.body.cast().toRealmDartString(length: request.body_size); + + final headers = {}; + for (int i = 0; i < request.num_headers; ++i) { + final header = request.headers[i]; + final name = header.name.cast().toRealmDartString()!; + final value = header.value.cast().toRealmDartString()!; + headers[name] = value; + } + + _requestCallbackAsync(client, request.method, url, body, headers, requestContext); + // The request struct dies here! +} + +Future _requestCallbackAsync( + HttpClient client, + int requestMethod, + Uri url, + String? body, + Map headers, + Pointer requestContext, +) async { + await using((arena) async { + final responsePointer = arena(); + final responseRef = responsePointer.ref; + final method = HttpMethod.values[requestMethod]; + + try { + // Build request + late HttpClientRequest request; + + switch (method) { + case HttpMethod.delete: + request = await client.deleteUrl(url); + break; + case HttpMethod.put: + request = await client.putUrl(url); + break; + case HttpMethod.patch: + request = await client.patchUrl(url); + break; + case HttpMethod.post: + request = await client.postUrl(url); + break; + case HttpMethod.get: + request = await client.getUrl(url); + break; + } + + for (final header in headers.entries) { + request.headers.add(header.key, header.value); + } + + if (body != null) { + request.add(utf8.encode(body)); + } + + Realm.logger.log(LogLevel.debug, "HTTP Transport: Executing ${method.name} $url"); + + final stopwatch = Stopwatch()..start(); + + // Do the call.. + final response = await request.close(); + + stopwatch.stop(); + Realm.logger.log(LogLevel.debug, "HTTP Transport: Executed ${method.name} $url: ${response.statusCode} in ${stopwatch.elapsedMilliseconds} ms"); + + final responseBody = await response.fold>([], (acc, l) => acc..addAll(l)); // gather response + + // Report back to core + responseRef.status_code = response.statusCode; + responseRef.body = responseBody.toCharPtr(arena); + responseRef.body_size = responseBody.length; + + int headerCnt = 0; + response.headers.forEach((name, values) { + headerCnt += values.length; + }); + + responseRef.headers = arena(headerCnt); + responseRef.num_headers = headerCnt; + + int index = 0; + response.headers.forEach((name, values) { + for (final value in values) { + final headerRef = (responseRef.headers + index).ref; + headerRef.name = name.toCharPtr(arena); + headerRef.value = value.toCharPtr(arena); + index++; + } + }); + + responseRef.custom_status_code = CustomErrorCode.noError.code; + } on SocketException catch (socketEx) { + Realm.logger.log(LogLevel.warn, "HTTP Transport: SocketException executing ${method.name} $url: $socketEx"); + responseRef.custom_status_code = CustomErrorCode.timeout.code; + } on HttpException catch (httpEx) { + Realm.logger.log(LogLevel.warn, "HTTP Transport: HttpException executing ${method.name} $url: $httpEx"); + responseRef.custom_status_code = CustomErrorCode.unknownHttp.code; + } catch (ex) { + Realm.logger.log(LogLevel.error, "HTTP Transport: Exception executing ${method.name} $url: $ex"); + responseRef.custom_status_code = CustomErrorCode.unknown.code; + } finally { + realmLib.realm_http_transport_complete_request(requestContext, responsePointer); + } + }); +} diff --git a/packages/realm_dart/lib/src/native/list_handle.dart b/packages/realm_dart/lib/src/native/list_handle.dart new file mode 100644 index 000000000..5021b5abc --- /dev/null +++ b/packages/realm_dart/lib/src/native/list_handle.dart @@ -0,0 +1,146 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import '../realm_dart.dart'; +import 'collection_handle_base.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'ffi.dart'; +import 'notification_token_handle.dart'; +import 'object_handle.dart'; +import 'query_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; + +class ListHandle extends CollectionHandleBase { + ListHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 88); + + bool get isValid => realmLib.realm_list_is_valid(pointer); + + ResultsHandle asResults() { + return ResultsHandle(realmLib.realm_list_to_results(pointer), root); + } + + int get size { + return using((arena) { + final size = arena(); + realmLib.realm_list_size(pointer, size).raiseLastErrorIfFalse(); + return size.value; + }); + } + + void removeAt(int index) { + realmLib.realm_list_erase(pointer, index).raiseLastErrorIfFalse(); + } + + void move(int from, int to) { + realmLib.realm_list_move(pointer, from, to).raiseLastErrorIfFalse(); + } + + void deleteAll() { + realmLib.realm_list_remove_all(pointer).raiseLastErrorIfFalse(); + } + + int indexOf(Object? value) { + return using((arena) { + final outIndex = arena(); + final outFound = arena(); + + // TODO: how should this behave for collections + final realmValue = value.toNative(arena); + realmLib + .realm_list_find( + pointer, + realmValue, + outIndex, + outFound, + ) + .raiseLastErrorIfFalse(); + return outFound.value ? outIndex.value : -1; + }); + } + + void clear() { + realmLib.realm_list_clear(pointer).raiseLastErrorIfFalse(); + } + + // TODO: avoid taking the [realm] parameter + Object? elementAt(Realm realm, int index) { + return using((arena) { + final realmValue = arena(); + realmLib.realm_list_get(pointer, index, realmValue).raiseLastErrorIfFalse(); + return realmValue.toDartValue( + realm, + () => realmLib.realm_list_get_list(pointer, index), + () => realmLib.realm_list_get_dictionary(pointer, index), + ); + }); + } + + ListHandle? resolveIn(RealmHandle frozenRealm) { + return using((arena) { + final resultPtr = arena>(); + realmLib.realm_list_resolve_in(pointer, frozenRealm.pointer, resultPtr).raiseLastErrorIfFalse(); + return resultPtr == nullptr ? null : ListHandle(resultPtr.value, root); + }); + } + + // TODO: Consider splitting into two methods + void addOrUpdateAt(int index, Object? value, bool insert) { + using((arena) { + final realmValue = value.toNative(arena); + (insert ? realmLib.realm_list_insert : realmLib.realm_list_set)(pointer, index, realmValue.ref).raiseLastErrorIfFalse(); + }); + } + + // TODO: avoid taking the [realm] parameter + void addOrUpdateCollectionAt(Realm realm, int index, RealmValue value, bool insert) { + createCollection(realm, value, () => (insert ? realmLib.realm_list_insert_list : realmLib.realm_list_set_list)(pointer, index), + () => (insert ? realmLib.realm_list_insert_dictionary : realmLib.realm_list_set_dictionary)(pointer, index)); + } + + ObjectHandle setEmbeddedAt(int index) { + return ObjectHandle(realmLib.realm_list_set_embedded(pointer, index), root); + } + + ObjectHandle insertEmbeddedAt(int index) { + return ObjectHandle(realmLib.realm_list_insert_embedded(pointer, index), root); + } + + ResultsHandle query(String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + intoRealmQueryArg(args[i], argsPointer + i, arena); + } + final queryHandle = QueryHandle( + realmLib.realm_query_parse_for_list( + pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + root, + ); + return queryHandle.findAll(); + }); + } + + NotificationTokenHandle subscribeForNotifications(NotificationsController controller) { + return NotificationTokenHandle( + realmLib.realm_list_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + nullptr, + Pointer.fromFunction(collectionChangeCallback), + ), + root, + ); + } +} diff --git a/packages/realm_dart/lib/src/native/map_changes_handle.dart b/packages/realm_dart/lib/src/native/map_changes_handle.dart new file mode 100644 index 000000000..bccfbb909 --- /dev/null +++ b/packages/realm_dart/lib/src/native/map_changes_handle.dart @@ -0,0 +1,55 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'package:realm_dart/src/native/from_native.dart'; + +import '../collections.dart'; +import 'ffi.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +class MapChangesHandle extends HandleBase { + MapChangesHandle(Pointer pointer) : super(pointer, 256); + + MapChanges get changes { + return using((arena) { + final outNumDeletions = arena(); + final outNumInsertions = arena(); + final outNumModifications = arena(); + final outCollectionWasDeleted = arena(); + realmLib.realm_dictionary_get_changes( + pointer, + outNumDeletions, + outNumInsertions, + outNumModifications, + outCollectionWasDeleted, + ); + + final deletionsCount = outNumDeletions != nullptr ? outNumDeletions.value : 0; + final insertionCount = outNumInsertions != nullptr ? outNumInsertions.value : 0; + final modificationCount = outNumModifications != nullptr ? outNumModifications.value : 0; + + final outDeletionIndexes = arena(deletionsCount); + final outInsertionIndexes = arena(insertionCount); + final outModificationIndexes = arena(modificationCount); + final outCollectionWasCleared = arena(); + + realmLib.realm_dictionary_get_changed_keys( + pointer, + outDeletionIndexes, + outNumDeletions, + outInsertionIndexes, + outNumInsertions, + outModificationIndexes, + outNumModifications, + outCollectionWasCleared, + ); + + return MapChanges(outDeletionIndexes.toStringList(deletionsCount), outInsertionIndexes.toStringList(insertionCount), + outModificationIndexes.toStringList(modificationCount), outCollectionWasCleared.value, outCollectionWasDeleted.value); + }); + } +} diff --git a/packages/realm_dart/lib/src/native/map_handle.dart b/packages/realm_dart/lib/src/native/map_handle.dart new file mode 100644 index 000000000..27a96402a --- /dev/null +++ b/packages/realm_dart/lib/src/native/map_handle.dart @@ -0,0 +1,198 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import '../realm_dart.dart'; +import 'collection_handle_base.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'ffi.dart'; +import 'map_changes_handle.dart'; +import 'notification_token_handle.dart'; +import 'object_handle.dart'; +import 'query_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; + +class MapHandle extends CollectionHandleBase { + MapHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 96); // TODO: check size + + int get size { + return using((arena) { + final outSize = arena(); + realmLib.realm_dictionary_size(pointer, outSize).raiseLastErrorIfFalse(); + return outSize.value; + }); + } + + bool remove(String key) { + return using((arena) { + final keyNative = key.toNative(arena); + final outErased = arena(); + realmLib.realm_dictionary_erase(pointer, keyNative.ref, outErased).raiseLastErrorIfFalse(); + return outErased.value; + }); + } + + // TODO: avoid taking the [realm] parameter + Object? find(Realm realm, String key) { + return using((arena) { + final keyNative = key.toNative(arena); + final outValue = arena(); + final outFound = arena(); + realmLib.realm_dictionary_find(pointer, keyNative.ref, outValue, outFound).raiseLastErrorIfFalse(); + if (outFound.value) { + return outValue.toDartValue( + realm, + () => realmLib.realm_dictionary_get_list(pointer, keyNative.ref), + () => realmLib.realm_dictionary_get_dictionary(pointer, keyNative.ref), + ); + } + return null; + }); + } + + bool get isValid { + return realmLib.realm_dictionary_is_valid(pointer); + } + + void clear() { + realmLib.realm_dictionary_clear(pointer).raiseLastErrorIfFalse(); + } + + ResultsHandle get keys { + return using((arena) { + final outSize = arena(); + final outKeys = arena>(); + realmLib.realm_dictionary_get_keys(pointer, outSize, outKeys).raiseLastErrorIfFalse(); + return ResultsHandle(outKeys.value, root); + }); + } + + ResultsHandle get values { + return ResultsHandle(realmLib.realm_dictionary_to_results(pointer), root); + } + + bool containsKey(String key) { + return using((arena) { + final keyNative = key.toNative(arena); + final found = arena(); + realmLib.realm_dictionary_contains_key(pointer, keyNative.ref, found).raiseLastErrorIfFalse(); + return found.value; + }); + } + + int indexOf(Object? value) { + return using((arena) { + // TODO: how should this behave for collections + final valueNative = value.toNative(arena); + final index = arena(); + realmLib.realm_dictionary_contains_value(pointer, valueNative.ref, index).raiseLastErrorIfFalse(); + return index.value; + }); + } + + bool containsValue(Object? value) => indexOf(value) > -1; + + ObjectHandle insertEmbedded(String key) { + return using((arena) { + final keyNative = key.toNative(arena); + return ObjectHandle(realmLib.realm_dictionary_insert_embedded(pointer, keyNative.ref), root); + }); + } + + void insert(String key, Object? value) { + using((arena) { + final keyNative = key.toNative(arena); + final valueNative = value.toNative(arena); + realmLib + .realm_dictionary_insert( + pointer, + keyNative.ref, + valueNative.ref, + nullptr, + nullptr, + ) + .raiseLastErrorIfFalse(); + }); + } + + void insertCollection(Realm realm, String key, RealmValue value) { + using((arena) { + final keyNative = key.toNative(arena); + createCollection( + realm, + value, + () => realmLib.realm_dictionary_insert_list(pointer, keyNative.ref), + () => realmLib.realm_dictionary_insert_dictionary(pointer, keyNative.ref), + ); + }); + } + + ResultsHandle query(String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + intoRealmQueryArg(args[i], argsPointer + i, arena); + } + + final queryHandle = QueryHandle( + realmLib.realm_query_parse_for_results( + values.pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + root, + ); + return queryHandle.findAll(); + }); + } + + MapHandle? resolveIn(RealmHandle frozenRealm) { + return using((arena) { + final resultPtr = arena>(); + realmLib.realm_dictionary_resolve_in(pointer, frozenRealm.pointer, resultPtr).raiseLastErrorIfFalse(); + return resultPtr == nullptr ? null : MapHandle(resultPtr.value, root); + }); + } + + NotificationTokenHandle subscribeForNotifications(NotificationsController controller) { + return NotificationTokenHandle( + realmLib.realm_dictionary_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + nullptr, + Pointer.fromFunction(_mapChangeCallback), + ), + root, + ); + } +} + +void _mapChangeCallback(Pointer userdata, Pointer data) { + final NotificationsController controller = userdata.toObject(); + + if (data == nullptr) { + controller.onError(RealmError("Invalid notifications data received")); + return; + } + + try { + final clonedData = realmLib.realm_clone(data.cast()); + if (clonedData == nullptr) { + controller.onError(RealmError("Error while cloning notifications data")); + return; + } + + final changesHandle = MapChangesHandle(clonedData.cast()); + controller.onChanges(changesHandle); + } catch (e) { + controller.onError(RealmError("Error handling change notifications. Error: $e")); + } +} diff --git a/packages/realm_dart/lib/src/native/mutable_subscription_set_handle.dart b/packages/realm_dart/lib/src/native/mutable_subscription_set_handle.dart new file mode 100644 index 000000000..8e6a4aeab --- /dev/null +++ b/packages/realm_dart/lib/src/native/mutable_subscription_set_handle.dart @@ -0,0 +1,90 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../realm_dart.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; +import 'subscription_handle.dart'; +import 'subscription_set_handle.dart'; + +class MutableSubscriptionSetHandle extends SubscriptionSetHandle { + MutableSubscriptionSetHandle(Pointer pointer, RealmHandle root) : super(pointer.cast(), root); + + Pointer get _mutablePointer => super.pointer.cast(); + + SubscriptionSetHandle commit() => SubscriptionSetHandle(realmLib.realm_sync_subscription_set_commit(_mutablePointer), root); + + SubscriptionHandle insertOrAssignSubscription(ResultsHandle results, String? name, bool update) { + if (!update) { + if (name != null && findByName(name) != null) { + throw RealmException('Duplicate subscription with name: $name'); + } + } + return using((arena) { + final outIndex = arena(); + final outInserted = arena(); + realmLib + .realm_sync_subscription_set_insert_or_assign_results( + _mutablePointer, + results.pointer, + name?.toCharPtr(arena) ?? nullptr, + outIndex, + outInserted, + ) + .raiseLastErrorIfFalse(); + return this[outIndex.value]; + }); + } + + bool erase(SubscriptionHandle subscription) { + return using((arena) { + final outErased = arena(); + realmLib + .realm_sync_subscription_set_erase_by_id( + _mutablePointer, + subscription.id.toNative(arena), + outErased, + ) + .raiseLastErrorIfFalse(); + return outErased.value; + }); + } + + bool eraseByName(String name) { + return using((arena) { + final outErased = arena(); + realmLib + .realm_sync_subscription_set_erase_by_name( + _mutablePointer, + name.toCharPtr(arena), + outErased, + ) + .raiseLastErrorIfFalse(); + return outErased.value; + }); + } + + bool eraseByResults(ResultsHandle results) { + return using((arena) { + final outErased = arena(); + realmLib + .realm_sync_subscription_set_erase_by_results( + _mutablePointer, + results.pointer, + outErased, + ) + .raiseLastErrorIfFalse(); + return outErased.value; + }); + } + + void clear() => realmLib.realm_sync_subscription_set_clear(_mutablePointer).raiseLastErrorIfFalse(); +} diff --git a/packages/realm_dart/lib/src/native/notification_token_handle.dart b/packages/realm_dart/lib/src/native/notification_token_handle.dart new file mode 100644 index 000000000..eebee574e --- /dev/null +++ b/packages/realm_dart/lib/src/native/notification_token_handle.dart @@ -0,0 +1,35 @@ +import 'dart:ffi'; + +import '../realm_dart.dart'; +import 'collection_changes_handle.dart'; +import 'from_native.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'rooted_handle.dart'; + +class NotificationTokenHandle extends RootedHandleBase { + NotificationTokenHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 32); +} + +void collectionChangeCallback(Pointer userdata, Pointer data) { + final NotificationsController controller = userdata.toObject(); + + if (data == nullptr) { + controller.onError(RealmError("Invalid notifications data received")); + return; + } + + try { + final clonedData = realmLib.realm_clone(data.cast()); + if (clonedData == nullptr) { + controller.onError(RealmError("Error while cloning notifications data")); + return; + } + + final changesHandle = CollectionChangesHandle(clonedData.cast()); + controller.onChanges(changesHandle); + } catch (e) { + controller.onError(RealmError("Error handling change notifications. Error: $e")); + } +} diff --git a/packages/realm_dart/lib/src/native/object_handle.dart b/packages/realm_dart/lib/src/native/object_handle.dart new file mode 100644 index 000000000..f561e1815 --- /dev/null +++ b/packages/realm_dart/lib/src/native/object_handle.dart @@ -0,0 +1,206 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import '../realm_dart.dart'; +import 'collection_handle_base.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'ffi.dart'; +import 'handle_base.dart'; +import 'list_handle.dart'; +import 'map_handle.dart'; +import 'notification_token_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; +import 'rooted_handle.dart'; +import 'set_handle.dart'; + +class ObjectHandle extends RootedHandleBase { + ObjectHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 112); + + ObjectHandle createEmbedded(int propertyKey) { + return ObjectHandle(realmLib.realm_set_embedded(pointer, propertyKey), root); + } + + (ObjectHandle, int) get parent { + return using((arena) { + final parentPtr = arena>(); + final classKeyPtr = arena(); + realmLib.realm_object_get_parent(pointer, parentPtr, classKeyPtr).raiseLastErrorIfFalse(); + + final handle = ObjectHandle(parentPtr.value, root); + + return (handle, classKeyPtr.value); + }); + } + + int get classKey => realmLib.realm_object_get_table(pointer); + + bool get isValid => realmLib.realm_object_is_valid(pointer); + + Link get asLink { + final realmLink = realmLib.realm_object_as_link(pointer); + return Link(realmLink); + } + + // TODO: avoid taking the [realm] parameter + Object? getValue(Realm realm, int propertyKey) { + return using((arena) { + final realmValue = arena(); + realmLib.realm_get_value(pointer, propertyKey, realmValue).raiseLastErrorIfFalse(); + return realmValue.toDartValue( + realm, + () => realmLib.realm_get_list(pointer, propertyKey), + () => realmLib.realm_get_dictionary(pointer, propertyKey), + ); + }); + } + + // TODO: value should be RealmValue, and perhaps this method should be combined + // with setCollection? + void setValue(int propertyKey, Object? value, bool isDefault) { + using((arena) { + final realmValue = value.toNative(arena); + realmLib + .realm_set_value( + pointer, + propertyKey, + realmValue.ref, + isDefault, + ) + .raiseLastErrorIfFalse(); + }); + } + + ListHandle getList(int propertyKey) { + return ListHandle(realmLib.realm_get_list(pointer, propertyKey), root); + } + + SetHandle getSet(int propertyKey) { + return SetHandle(realmLib.realm_get_set(pointer, propertyKey), root); + } + + MapHandle getMap(int propertyKey) { + return MapHandle(realmLib.realm_get_dictionary(pointer, propertyKey), root); + } + + ResultsHandle getBacklinks(int sourceTableKey, int propertyKey) { + return ResultsHandle(realmLib.realm_get_backlinks(pointer, sourceTableKey, propertyKey), root); + } + + void setCollection(Realm realm, int propertyKey, RealmValue value) { + createCollection( + realm, + value, + () => realmLib.realm_set_list(pointer, propertyKey), + () => realmLib.realm_set_dictionary(pointer, propertyKey), + ); + } + + String objectToString() { + return realmLib.realm_object_to_string(pointer).cast().toRealmDartString(freeRealmMemory: true)!; + } + + void delete() { + realmLib.realm_object_delete(pointer).raiseLastErrorIfFalse(); + } + + ObjectHandle? resolveIn(RealmHandle frozenRealm) { + return using((arena) { + final resultPtr = arena>(); + realmLib.realm_object_resolve_in(pointer, frozenRealm.pointer, resultPtr).raiseLastErrorIfFalse(); + return resultPtr == nullptr ? null : ObjectHandle(resultPtr.value, frozenRealm); + }); + } + + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List? keyPaths]) { + return using((arena) { + final kpNative = buildAndVerifyKeyPath(keyPaths); + return NotificationTokenHandle( + realmLib.realm_object_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + kpNative, + Pointer.fromFunction(_objectChangeCallback), + ), + root, + ); + }); + } + + Pointer buildAndVerifyKeyPath(List? keyPaths) { + return using((arena) { + if (keyPaths == null) { + return nullptr; + } + + final length = keyPaths.length; + final keypathsNative = arena>(length); + + for (int i = 0; i < length; i++) { + keypathsNative[i] = keyPaths[i].toCharPtr(arena); + } + // TODO(kn): + // call to classKey getter involves a native call, which is not ideal + return realmLib.realm_create_key_path_array(root.pointer, classKey, length, keypathsNative).raiseLastErrorIfNull(); + }); + } + + @override + // equals handled by HandleBase + // ignore: hash_and_equals + int get hashCode => asLink.hash; +} + +extension type Link(realm_link_t link) { + int get targetKey => link.target; + int get classKey => link.target_table; + + int get hash => Object.hash(targetKey, classKey); +} + +void _objectChangeCallback(Pointer userdata, Pointer data) { + final NotificationsController controller = userdata.toObject(); + + if (data == nullptr) { + controller.onError(RealmError("Invalid notifications data received")); + return; + } + + try { + final clonedData = realmLib.realm_clone(data.cast()); + if (clonedData == nullptr) { + controller.onError(RealmError("Error while cloning notifications data")); + return; + } + + final changesHandle = ObjectChangesHandle(clonedData.cast()); + controller.onChanges(changesHandle); + } catch (e) { + controller.onError(RealmError("Error handling change notifications. Error: $e")); + } +} + +class ObjectChangesHandle extends HandleBase { + ObjectChangesHandle(Pointer pointer) : super(pointer, 256); + + bool get isDeleted { + return realmLib.realm_object_changes_is_deleted(pointer); + } + + List get properties { + return using((arena) { + final count = realmLib.realm_object_changes_get_num_modified_properties(pointer); + + final outModified = arena(count); + realmLib.realm_object_changes_get_modified_properties(pointer, outModified, count); + + return outModified.asTypedList(count).toList(); + }); + } +} diff --git a/packages/realm_dart/lib/src/native/query_handle.dart b/packages/realm_dart/lib/src/native/query_handle.dart new file mode 100644 index 000000000..88a2e1856 --- /dev/null +++ b/packages/realm_dart/lib/src/native/query_handle.dart @@ -0,0 +1,22 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; +import 'rooted_handle.dart'; + +class QueryHandle extends RootedHandleBase { + QueryHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 256); + + ResultsHandle findAll() { + try { + return ResultsHandle(realmLib.realm_query_find_all(pointer), root); + } finally { + release(); + } + } +} diff --git a/packages/realm_dart/lib/src/native/realm_bindings.dart b/packages/realm_dart/lib/src/native/realm_bindings.dart index 607c45033..a7fdd06fe 100644 --- a/packages/realm_dart/lib/src/native/realm_bindings.dart +++ b/packages/realm_dart/lib/src/native/realm_bindings.dart @@ -175,6 +175,25 @@ class RealmLibrary { ffi.Pointer, realm_free_userdata_func_t)>(); + ffi.Pointer + realm_app_config_get_sync_client_config( + ffi.Pointer arg0, + ) { + return _realm_app_config_get_sync_client_config( + arg0, + ); + } + + late final _realm_app_config_get_sync_client_configPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>( + 'realm_app_config_get_sync_client_config'); + late final _realm_app_config_get_sync_client_config = + _realm_app_config_get_sync_client_configPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer)>(); + /// Create a new app configuration. /// /// @param app_id The MongoDB Realm app id. @@ -197,6 +216,25 @@ class RealmLibrary { ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>(); + void realm_app_config_set_base_file_path( + ffi.Pointer arg0, + ffi.Pointer arg1, + ) { + return _realm_app_config_set_base_file_path( + arg0, + arg1, + ); + } + + late final _realm_app_config_set_base_file_pathPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('realm_app_config_set_base_file_path'); + late final _realm_app_config_set_base_file_path = + _realm_app_config_set_base_file_pathPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + void realm_app_config_set_base_url( ffi.Pointer arg0, ffi.Pointer arg1, @@ -330,6 +368,44 @@ class RealmLibrary { void Function( ffi.Pointer, ffi.Pointer)>(); + void realm_app_config_set_metadata_encryption_key( + ffi.Pointer arg0, + ffi.Pointer arg1, + ) { + return _realm_app_config_set_metadata_encryption_key( + arg0, + arg1, + ); + } + + late final _realm_app_config_set_metadata_encryption_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>( + 'realm_app_config_set_metadata_encryption_key'); + late final _realm_app_config_set_metadata_encryption_key = + _realm_app_config_set_metadata_encryption_keyPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void realm_app_config_set_metadata_mode( + ffi.Pointer arg0, + int arg1, + ) { + return _realm_app_config_set_metadata_mode( + arg0, + arg1, + ); + } + + late final _realm_app_config_set_metadata_modePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int32)>>('realm_app_config_set_metadata_mode'); + late final _realm_app_config_set_metadata_mode = + _realm_app_config_set_metadata_modePtr + .asFunction, int)>(); + void realm_app_config_set_platform_version( ffi.Pointer arg0, ffi.Pointer arg1, @@ -387,48 +463,61 @@ class RealmLibrary { void Function( ffi.Pointer, ffi.Pointer)>(); - /// Create realm_app_t* instance given a valid realm configuration and sync client configuration. + void realm_app_config_set_security_access_group( + ffi.Pointer arg0, + ffi.Pointer arg1, + ) { + return _realm_app_config_set_security_access_group( + arg0, + arg1, + ); + } + + late final _realm_app_config_set_security_access_groupPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>( + 'realm_app_config_set_security_access_group'); + late final _realm_app_config_set_security_access_group = + _realm_app_config_set_security_access_groupPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + /// Create realm_app_t* instance given a valid realm app configuration. /// /// @return A non-null pointer if no error occurred. ffi.Pointer realm_app_create( ffi.Pointer arg0, - ffi.Pointer arg1, ) { return _realm_app_create( arg0, - arg1, ); } late final _realm_app_createPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer)>>('realm_app_create'); + ffi.Pointer Function( + ffi.Pointer)>>('realm_app_create'); late final _realm_app_create = _realm_app_createPtr.asFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer)>(); + ffi.Pointer Function(ffi.Pointer)>(); - /// Create cached realm_app_t* instance given a valid realm configuration and sync client configuration. + /// Create cached realm_app_t* instance given a valid realm app configuration. /// /// @return A non-null pointer if no error occurred. ffi.Pointer realm_app_create_cached( ffi.Pointer arg0, - ffi.Pointer arg1, ) { return _realm_app_create_cached( arg0, - arg1, ); } late final _realm_app_create_cachedPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer)>>( - 'realm_app_create_cached'); + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('realm_app_create_cached'); late final _realm_app_create_cached = _realm_app_create_cachedPtr.asFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer)>(); + ffi.Pointer Function(ffi.Pointer)>(); ffi.Pointer realm_app_credentials_new_anonymous( bool reuse_credentials, @@ -1413,31 +1502,24 @@ class RealmLibrary { /// Switches the active user with the specified one. The user must exist in the list of all users who have logged into /// this application. /// @param app ptr to realm_app - /// @param user ptr to current user - /// @param new_user ptr to the new user to switch + /// @param user ptr to user to set as current. /// @return True if no error has been recorded, False otherwise bool realm_app_switch_user( ffi.Pointer app, ffi.Pointer user, - ffi.Pointer> new_user, ) { return _realm_app_switch_user( app, user, - new_user, ); } late final _realm_app_switch_userPtr = _lookup< - ffi.NativeFunction< - ffi.Bool Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer>)>>( - 'realm_app_switch_user'); + ffi.NativeFunction< + ffi.Bool Function(ffi.Pointer, + ffi.Pointer)>>('realm_app_switch_user'); late final _realm_app_switch_user = _realm_app_switch_userPtr.asFunction< - bool Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer>)>(); + bool Function(ffi.Pointer, ffi.Pointer)>(); /// Get the default realm file path based on the user and partition value in the config. /// @@ -9280,26 +9362,6 @@ class RealmLibrary { late final _realm_sync_client_config_new = _realm_sync_client_config_newPtr .asFunction Function()>(); - void realm_sync_client_config_set_base_file_path( - ffi.Pointer arg0, - ffi.Pointer arg1, - ) { - return _realm_sync_client_config_set_base_file_path( - arg0, - arg1, - ); - } - - late final _realm_sync_client_config_set_base_file_pathPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>( - 'realm_sync_client_config_set_base_file_path'); - late final _realm_sync_client_config_set_base_file_path = - _realm_sync_client_config_set_base_file_pathPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer)>(); - void realm_sync_client_config_set_connect_timeout( ffi.Pointer arg0, int arg1, @@ -9416,44 +9478,6 @@ class RealmLibrary { _realm_sync_client_config_set_max_resumption_delay_intervalPtr.asFunction< void Function(ffi.Pointer, int)>(); - void realm_sync_client_config_set_metadata_encryption_key( - ffi.Pointer arg0, - ffi.Pointer arg1, - ) { - return _realm_sync_client_config_set_metadata_encryption_key( - arg0, - arg1, - ); - } - - late final _realm_sync_client_config_set_metadata_encryption_keyPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>( - 'realm_sync_client_config_set_metadata_encryption_key'); - late final _realm_sync_client_config_set_metadata_encryption_key = - _realm_sync_client_config_set_metadata_encryption_keyPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer)>(); - - void realm_sync_client_config_set_metadata_mode( - ffi.Pointer arg0, - int arg1, - ) { - return _realm_sync_client_config_set_metadata_mode( - arg0, - arg1, - ); - } - - late final _realm_sync_client_config_set_metadata_modePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int32)>>('realm_sync_client_config_set_metadata_mode'); - late final _realm_sync_client_config_set_metadata_mode = - _realm_sync_client_config_set_metadata_modePtr.asFunction< - void Function(ffi.Pointer, int)>(); - void realm_sync_client_config_set_multiplex_sessions( ffi.Pointer arg0, bool arg1, @@ -11318,7 +11342,7 @@ class RealmLibrary { ffi.Pointer)>(); /// @return a notification token object. Dispose it to stop receiving notifications. - ffi.Pointer + ffi.Pointer realm_sync_user_on_state_change_register_callback( ffi.Pointer arg0, realm_sync_on_user_state_changed_t arg1, @@ -11335,7 +11359,7 @@ class RealmLibrary { late final _realm_sync_user_on_state_change_register_callbackPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function( + ffi.Pointer Function( ffi.Pointer, realm_sync_on_user_state_changed_t, ffi.Pointer, @@ -11343,7 +11367,7 @@ class RealmLibrary { 'realm_sync_user_on_state_change_register_callback'); late final _realm_sync_user_on_state_change_register_callback = _realm_sync_user_on_state_change_register_callbackPtr.asFunction< - ffi.Pointer Function( + ffi.Pointer Function( ffi.Pointer, realm_sync_on_user_state_changed_t, ffi.Pointer, @@ -11543,7 +11567,7 @@ class RealmLibrary { /// Return the identiy for the user passed as argument /// @param user ptr to the user for which the identiy has to be retrieved - /// @return a ptr to the identity string + /// @return a ptr to the identity string. This must be manually released with realm_free(). ffi.Pointer realm_user_get_identity( ffi.Pointer user, ) { @@ -11953,8 +11977,6 @@ final class realm_app_error extends ffi.Struct { /// Pointers to this struct and its pointer members are only valid inside the scope /// of the callback they were passed to. typedef realm_app_error_t = realm_app_error; - -/// App typedef realm_app_t = realm_app; final class realm_app_user_apikey extends ffi.Struct { @@ -11987,6 +12009,10 @@ typedef Dartrealm_app_user_completion_func_tFunction = void Function( ffi.Pointer user, ffi.Pointer error); +final class realm_app_user_subscription_token extends ffi.Opaque {} + +typedef realm_app_user_subscription_token_t = realm_app_user_subscription_token; + /// Generic completion callback for asynchronous Realm App operations. /// /// @param error Pointer to an error object if the operation failed, otherwise null if it completed successfully. @@ -12265,9 +12291,9 @@ abstract class realm_errno { static const int RLM_ERR_CUSTOM_ERROR = 4000; static const int RLM_ERR_CLIENT_USER_NOT_FOUND = 4100; static const int RLM_ERR_CLIENT_USER_NOT_LOGGED_IN = 4101; - static const int RLM_ERR_CLIENT_APP_DEALLOCATED = 4102; static const int RLM_ERR_CLIENT_REDIRECT_ERROR = 4103; static const int RLM_ERR_CLIENT_TOO_MANY_REDIRECTS = 4104; + static const int RLM_ERR_CLIENT_USER_ALREADY_NAMED = 4105; static const int RLM_ERR_BAD_TOKEN = 4200; static const int RLM_ERR_MALFORMED_JSON = 4201; static const int RLM_ERR_MISSING_JSON_KEY = 4202; @@ -12894,13 +12920,13 @@ final class realm_sync_client_config extends ffi.Opaque {} typedef realm_sync_client_config_t = realm_sync_client_config; -/// Sync abstract class realm_sync_client_metadata_mode { static const int RLM_SYNC_CLIENT_METADATA_MODE_PLAINTEXT = 0; static const int RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED = 1; static const int RLM_SYNC_CLIENT_METADATA_MODE_DISABLED = 2; } +/// Sync abstract class realm_sync_client_reconnect_mode { static const int RLM_SYNC_CLIENT_RECONNECT_MODE_NORMAL = 0; static const int RLM_SYNC_CLIENT_RECONNECT_MODE_TESTING = 1; @@ -13007,6 +13033,14 @@ final class realm_sync_error_user_info extends ffi.Struct { } typedef realm_sync_error_user_info_t = realm_sync_error_user_info; + +abstract class realm_sync_file_action { + static const int RLM_SYNC_FILE_ACTION_DELETE_REALM = 0; + static const int RLM_SYNC_FILE_ACTION_BACK_UP_THEN_DELETE_REALM = 1; +} + +final class realm_sync_manager extends ffi.Opaque {} + typedef realm_sync_on_subscription_state_changed_t = ffi.Pointer< ffi.NativeFunction>; typedef realm_sync_on_subscription_state_changed_tFunction = ffi.Void Function( @@ -13212,11 +13246,6 @@ typedef Dartrealm_sync_ssl_verify_func_tFunction = bool Function( int preverify_ok, int depth); -final class realm_sync_user_subscription_token extends ffi.Opaque {} - -typedef realm_sync_user_subscription_token_t - = realm_sync_user_subscription_token; - /// Callback function invoked by the sync session once it has uploaded or download /// all available changesets. See @a realm_sync_session_wait_for_upload and /// @a realm_sync_session_wait_for_download. @@ -13265,6 +13294,7 @@ abstract class realm_user_state { static const int RLM_USER_STATE_REMOVED = 2; } +/// App typedef realm_user_t = realm_user; final class realm_uuid extends ffi.Struct { diff --git a/packages/realm_dart/lib/src/native/realm_core.dart b/packages/realm_dart/lib/src/native/realm_core.dart index eba4d5d8d..a05b4d1ab 100644 --- a/packages/realm_dart/lib/src/native/realm_core.dart +++ b/packages/realm_dart/lib/src/native/realm_core.dart @@ -1,58 +1,23 @@ // Copyright 2021 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 -// ignore_for_file: non_constant_identifier_names - -import 'dart:async'; import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; -import 'dart:isolate'; -import 'dart:typed_data'; -import 'package:cancellation_token/cancellation_token.dart'; import 'package:crypto/crypto.dart'; -// Hide StringUtf8Pointer.toNativeUtf8 and StringUtf16Pointer since these allows silently allocating memory. Use toUtf8Ptr instead -import 'package:ffi/ffi.dart' hide StringUtf8Pointer, StringUtf16Pointer; import 'package:path/path.dart' as path; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:realm_common/realm_common.dart' as common show Decimal128; -import 'package:realm_common/realm_common.dart' hide Decimal128; -import 'package:realm_dart/src/logging.dart'; -import '../app.dart'; -import '../collections.dart'; -import '../configuration.dart'; -import '../credentials.dart'; import '../init.dart'; -import '../list.dart'; -import '../map.dart'; -import '../migration.dart'; import '../realm_class.dart'; -import '../realm_object.dart'; -import '../results.dart'; import '../scheduler.dart'; -import '../session.dart'; -import '../set.dart'; -import '../subscription.dart'; -import '../user.dart'; -import 'realm_bindings.dart'; - -part 'decimal128.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'ffi.dart'; +import 'realm_library.dart'; -const bugInTheSdkMessage = "This is likely a bug in the Realm SDK - please file an issue at https://github.com/realm/realm-dart/issues"; - -// This variable allows access to realm native library even before RealmCore is created. For Decimal128 for example -final _realmLib = () { - final result = RealmLibrary(initRealm()); - final nativeLibraryVersion = result.realm_dart_library_version().cast().toDartString(); - if (libraryVersion != nativeLibraryVersion) { - final additionMessage = - isFlutterPlatform ? bugInTheSdkMessage : "Did you forget to run `dart run realm_dart install` after upgrading the realm_dart package?"; - throw RealmException('Realm SDK package version does not match the native library version ($libraryVersion != $nativeLibraryVersion). $additionMessage'); - } - return result; -}(); +final realmCore = RealmCore._(); final _pluginLib = () { if (!isFlutterPlatform) { @@ -75,730 +40,108 @@ final _pluginLib = () { return pluginLib; }(); -// stamped into the library by the build system (see prepare-release.yml) -const libraryVersion = '2.0.0'; - -_RealmCore realmCore = _RealmCore(); - -// All access to Realm Core functionality goes through this class -class _RealmCore { - // From realm.h. Currently not exported from the shared library - // ignore: unused_field, constant_identifier_names - static const int RLM_INVALID_CLASS_KEY = 0x7FFFFFFF; - // ignore: unused_field, constant_identifier_names - static const int RLM_INVALID_PROPERTY_KEY = -1; - // ignore: unused_field, constant_identifier_names - static const int RLM_INVALID_OBJECT_KEY = -1; - - final encryptionKeySize = 64; - // ignore: unused_field - static late final _RealmCore _instance; +class RealmCore { + RealmCore._(); - _RealmCore() { - // This disables creation of a second _RealmCore instance effectivelly making `realmCore` global variable readonly - _instance = this; - - // This prevents reentrance if `realmCore` global variable is accessed during _RealmCore construction - realmCore = this; - - _realmLib.realm_dart_init_debug_logger(); - } - - void loggerAttach() { - _realmLib.realm_dart_attach_logger(scheduler.nativePort); - } + // For debugging + int get threadId => realmLib.realm_dart_get_thread_id(); - void loggerDetach() { - _realmLib.realm_dart_detach_logger(scheduler.nativePort); + void clearCachedApps() { + realmLib.realm_clear_cached_apps(); } // for debugging only. Enable in realm_dart.cpp // void invokeGC() { - // _realmLib.realm_dart_gc(); + // realmLib.realm_dart_gc(); // } - LastError? getLastError(Allocator allocator) { - final error = allocator(); - final success = _realmLib.realm_get_last_error(error); - if (!success) { - return null; - } - - return error.ref.toLastError(); - } - - void throwLastError([String? errorMessage]) { - using((Arena arena) { - final lastError = getLastError(arena); - if (lastError?.userError != null) { - throw UserCallbackException(lastError!.userError!); - } - - final message = '${errorMessage != null ? "$errorMessage. " : ""}${lastError ?? ""}'; - switch (lastError?.code) { - case realm_errno.RLM_ERR_SCHEMA_MISMATCH: - throw MigrationRequiredException(message); - default: - throw RealmException(message); - } + void deleteRealmFiles(String path) { + using((arena) { + final realmDeleted = arena(); + realmLib.realm_delete_files(path.toCharPtr(arena), realmDeleted).raiseLastErrorIfFalse(); }); } - SchemaHandle _createSchema(Iterable schema) { - return using((Arena arena) { - final classCount = schema.length; - - final schemaClasses = arena(classCount); - final schemaProperties = arena>(classCount); - - for (var i = 0; i < classCount; i++) { - final schemaObject = schema.elementAt(i); - final classInfo = schemaClasses.elementAt(i).ref; - final propertiesCount = schemaObject.length; - final computedCount = schemaObject.where((p) => p.isComputed).length; - final persistedCount = propertiesCount - computedCount; - - classInfo.name = schemaObject.name.toCharPtr(arena); - classInfo.primary_key = "".toCharPtr(arena); - classInfo.num_properties = persistedCount; - classInfo.num_computed_properties = computedCount; - classInfo.key = RLM_INVALID_CLASS_KEY; - classInfo.flags = schemaObject.baseType.flags; - - final properties = arena(propertiesCount); - - for (var j = 0; j < propertiesCount; j++) { - final schemaProperty = schemaObject[j]; - final propInfo = properties.elementAt(j).ref; - propInfo.name = schemaProperty.mapTo.toCharPtr(arena); - propInfo.public_name = (schemaProperty.mapTo != schemaProperty.name ? schemaProperty.name : '').toCharPtr(arena); - propInfo.link_target = (schemaProperty.linkTarget ?? "").toCharPtr(arena); - propInfo.link_origin_property_name = (schemaProperty.linkOriginProperty ?? "").toCharPtr(arena); - propInfo.type = schemaProperty.propertyType.index; - propInfo.collection_type = schemaProperty.collectionType.index; - propInfo.flags = realm_property_flags.RLM_PROPERTY_NORMAL; - - if (schemaProperty.optional) { - propInfo.flags |= realm_property_flags.RLM_PROPERTY_NULLABLE; - } - - switch (schemaProperty.indexType) { - case RealmIndexType.regular: - propInfo.flags |= realm_property_flags.RLM_PROPERTY_INDEXED; - break; - case RealmIndexType.fullText: - propInfo.flags |= realm_property_flags.RLM_PROPERTY_FULLTEXT_INDEXED; - break; - default: - break; - } - - if (schemaProperty.primaryKey) { - classInfo.primary_key = schemaProperty.mapTo.toCharPtr(arena); - propInfo.flags |= realm_property_flags.RLM_PROPERTY_PRIMARY_KEY; - } - } - - schemaProperties[i] = properties; - schemaProperties.elementAt(i).value = properties; - } - - final schemaPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_schema_new(schemaClasses, classCount, schemaProperties)); - return SchemaHandle._(schemaPtr); + List getAllCategoryNames() { + return using((arena) { + final count = realmLib.realm_get_category_names(0, nullptr); + final outValues = arena>(count); + realmLib.realm_get_category_names(count, outValues); + return [for (int i = 0; i < count; i++) outValues[i].cast().toDartString()]; }); } - ConfigHandle _createConfig(Configuration config) { - return using((Arena arena) { - final configPtr = _realmLib.realm_config_new(); - final configHandle = ConfigHandle._(configPtr); - - if (config.schemaObjects.isNotEmpty) { - final schemaHandle = _createSchema(config.schemaObjects); - _realmLib.realm_config_set_schema(configHandle._pointer, schemaHandle._pointer); - } - - _realmLib.realm_config_set_path(configHandle._pointer, config.path.toCharPtr(arena)); - _realmLib.realm_config_set_scheduler(configHandle._pointer, scheduler.handle._pointer); - - if (config.fifoFilesFallbackPath != null) { - _realmLib.realm_config_set_fifo_path(configHandle._pointer, config.fifoFilesFallbackPath!.toCharPtr(arena)); - } - - // Setting schema version only makes sense for local realms, but core insists it is always set, - // hence we set it to 0 in those cases. - - final schemaVersion = switch (config) { - (LocalConfiguration lc) => lc.schemaVersion, - (FlexibleSyncConfiguration fsc) => fsc.schemaVersion, - _ => 0, - }; - _realmLib.realm_config_set_schema_version(configHandle._pointer, schemaVersion); - if (config.maxNumberOfActiveVersions != null) { - _realmLib.realm_config_set_max_number_of_active_versions(configHandle._pointer, config.maxNumberOfActiveVersions!); + String getAppDirectory() { + try { + if (!isFlutterPlatform || Platform.environment.containsKey('FLUTTER_TEST')) { + return Directory.current.absolute.path; // dart or flutter test } - if (config is LocalConfiguration) { - //_realmLib.realm_config_set_schema_mode(configHandle._pointer, realm_schema_mode.RLM_SCHEMA_MODE_ADDITIVE_DISCOVERED); - if (config.initialDataCallback != null) { - _realmLib.realm_config_set_data_initialization_function( - configHandle._pointer, - Pointer.fromFunction(initial_data_callback, false), - config.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - ); - } - if (config.isReadOnly) { - _realmLib.realm_config_set_schema_mode(configHandle._pointer, realm_schema_mode.RLM_SCHEMA_MODE_IMMUTABLE); - } else if (config.shouldDeleteIfMigrationNeeded) { - _realmLib.realm_config_set_schema_mode(configHandle._pointer, realm_schema_mode.RLM_SCHEMA_MODE_SOFT_RESET_FILE); - } - if (config.disableFormatUpgrade) { - _realmLib.realm_config_set_disable_format_upgrade(configHandle._pointer, config.disableFormatUpgrade); - } - if (config.shouldCompactCallback != null) { - _realmLib.realm_config_set_should_compact_on_launch_function( - configHandle._pointer, - Pointer.fromFunction(should_compact_callback, false), - config.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - ); - } - if (config.migrationCallback != null) { - _realmLib.realm_config_set_migration_function( - configHandle._pointer, - Pointer.fromFunction(migration_callback, false), - config.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - ); - } - } else if (config is InMemoryConfiguration) { - _realmLib.realm_config_set_in_memory(configHandle._pointer, true); - } else if (config is FlexibleSyncConfiguration) { - _realmLib.realm_config_set_schema_mode(configHandle._pointer, realm_schema_mode.RLM_SCHEMA_MODE_ADDITIVE_DISCOVERED); - final syncConfigPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_flx_sync_config_new(config.user.handle._pointer)); - try { - _realmLib.realm_sync_config_set_session_stop_policy(syncConfigPtr, config.sessionStopPolicy.index); - _realmLib.realm_sync_config_set_resync_mode(syncConfigPtr, config.clientResetHandler.clientResyncMode.index); - final errorHandlerCallback = - Pointer.fromFunction, realm_sync_error_t)>(_syncErrorHandlerCallback); - final errorHandlerUserdata = _realmLib.realm_dart_userdata_async_new(config, errorHandlerCallback.cast(), scheduler.handle._pointer); - _realmLib.realm_sync_config_set_error_handler(syncConfigPtr, _realmLib.addresses.realm_dart_sync_error_handler_callback, errorHandlerUserdata.cast(), - _realmLib.addresses.realm_dart_userdata_async_free); - - if (config.clientResetHandler.onBeforeReset != null) { - final syncBeforeResetCallback = Pointer.fromFunction, Pointer)>(_syncBeforeResetCallback); - final beforeResetUserdata = _realmLib.realm_dart_userdata_async_new(config, syncBeforeResetCallback.cast(), scheduler.handle._pointer); - - _realmLib.realm_sync_config_set_before_client_reset_handler(syncConfigPtr, _realmLib.addresses.realm_dart_sync_before_reset_handler_callback, - beforeResetUserdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); - } - if (config.clientResetHandler.onAfterRecovery != null || config.clientResetHandler.onAfterDiscard != null) { - final syncAfterResetCallback = - Pointer.fromFunction, Pointer, Bool, Pointer)>( - _syncAfterResetCallback); - final afterResetUserdata = _realmLib.realm_dart_userdata_async_new(config, syncAfterResetCallback.cast(), scheduler.handle._pointer); - - _realmLib.realm_sync_config_set_after_client_reset_handler(syncConfigPtr, _realmLib.addresses.realm_dart_sync_after_reset_handler_callback, - afterResetUserdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); - } - - if (config.shouldCompactCallback != null) { - _realmLib.realm_config_set_should_compact_on_launch_function( - configHandle._pointer, - Pointer.fromFunction(should_compact_callback, false), - config.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - ); - } + // Flutter from here on.. - _realmLib.realm_config_set_sync_config(configPtr, syncConfigPtr); - } finally { - _realmLib.realm_release(syncConfigPtr.cast()); - } - } else if (config is DisconnectedSyncConfiguration) { - _realmLib.realm_config_set_schema_mode(configHandle._pointer, realm_schema_mode.RLM_SCHEMA_MODE_ADDITIVE_EXPLICIT); - _realmLib.realm_config_set_force_sync_history(configPtr, true); + if (Platform.isAndroid || Platform.isIOS) { + return _getFilesPath(); } - if (config.encryptionKey != null) { - _realmLib.realm_config_set_encryption_key(configPtr, config.encryptionKey!.toUint8Ptr(arena), encryptionKeySize); + if (Platform.isLinux) { + final appSupportDir = Platform.environment['XDG_DATA_HOME'] ?? Platform.environment['HOME'] ?? Directory.current.absolute.path; + return path.join(appSupportDir, ".local/share", _getAppDirectoryFromPlugin()); } - // For sync and for dynamic Realms, we need to have a complete view of the schema in Core. - if (config.schemaObjects.isEmpty || config is FlexibleSyncConfiguration) { - _realmLib.realm_config_set_schema_subset_mode(configHandle._pointer, realm_schema_subset_mode.RLM_SCHEMA_SUBSET_MODE_COMPLETE); + if (Platform.isMacOS) { + return _getAppDirectoryFromPlugin(); } - return configHandle; - }); - } - - String getPathForUser(User user) { - final syncConfigPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_flx_sync_config_new(user.handle._pointer)); - try { - final path = _realmLib.realm_app_sync_client_get_default_file_path_for_realm(syncConfigPtr, nullptr); - return path.cast().toRealmDartString(freeRealmMemory: true)!; - } finally { - _realmLib.realm_release(syncConfigPtr.cast()); - } - } - - ObjectId subscriptionId(Subscription subscription) { - final id = _realmLib.realm_sync_subscription_id(subscription.handle._pointer); - return id.toDart(); - } - - String? subscriptionName(Subscription subscription) { - final name = _realmLib.realm_sync_subscription_name(subscription.handle._pointer); - return name.toDart(); - } - - String subscriptionObjectClassName(Subscription subscription) { - final objectClassName = _realmLib.realm_sync_subscription_object_class_name(subscription.handle._pointer); - return objectClassName.toDart()!; - } - - String subscriptionQueryString(Subscription subscription) { - final queryString = _realmLib.realm_sync_subscription_query_string(subscription.handle._pointer); - return queryString.toDart()!; - } - - DateTime subscriptionCreatedAt(Subscription subscription) { - final createdAt = _realmLib.realm_sync_subscription_created_at(subscription.handle._pointer); - return createdAt.toDart(); - } - - DateTime subscriptionUpdatedAt(Subscription subscription) { - final updatedAt = _realmLib.realm_sync_subscription_updated_at(subscription.handle._pointer); - return updatedAt.toDart(); - } - - SubscriptionSetHandle getSubscriptions(Realm realm) { - return SubscriptionSetHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_get_active_subscription_set(realm.handle._pointer)), realm.handle); - } - - void refreshSubscriptions(SubscriptionSet subscriptions) { - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_refresh(subscriptions.handle._pointer)); - } - - int getSubscriptionSetSize(SubscriptionSet subscriptions) { - return _realmLib.realm_sync_subscription_set_size(subscriptions.handle._pointer); - } - - Exception? getSubscriptionSetError(SubscriptionSet subscriptions) { - final error = _realmLib.realm_sync_subscription_set_error_str(subscriptions.handle._pointer); - final message = error.cast().toRealmDartString(treatEmptyAsNull: true); - return message == null ? null : RealmException(message); - } - - SubscriptionHandle subscriptionAt(SubscriptionSet subscriptions, int index) { - return SubscriptionHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_subscription_at( - subscriptions.handle._pointer, - index, - ))); - } - - SubscriptionHandle? findSubscriptionByName(SubscriptionSet subscriptions, String name) { - return using((arena) { - final result = _realmLib.realm_sync_find_subscription_by_name( - subscriptions.handle._pointer, - name.toCharPtr(arena), - ); - return result == nullptr ? null : SubscriptionHandle._(result); - }); - } - - SubscriptionHandle? findSubscriptionByResults(SubscriptionSet subscriptions, RealmResults results) { - final result = _realmLib.realm_sync_find_subscription_by_results( - subscriptions.handle._pointer, - results.handle._pointer, - ); - return result == nullptr ? null : SubscriptionHandle._(result); - } - - static void _stateChangeCallback(Object userdata, int state) { - final completer = userdata as CancellableCompleter; - if (!completer.isCancelled) { - completer.complete(SubscriptionSetState.values[state]); - } - } - - Future waitForSubscriptionSetStateChange(SubscriptionSet subscriptions, SubscriptionSetState notifyWhen, - [CancellationToken? cancellationToken]) { - final completer = CancellableCompleter(cancellationToken); - if (!completer.isCancelled) { - final callback = Pointer.fromFunction(_stateChangeCallback); - final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); - _realmLib.realm_sync_on_subscription_set_state_change_async(subscriptions.handle._pointer, notifyWhen.index, - _realmLib.addresses.realm_dart_sync_on_subscription_state_changed_callback, userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); - } - return completer.future; - } - - int subscriptionSetGetVersion(SubscriptionSet subscriptions) { - return _realmLib.realm_sync_subscription_set_version(subscriptions.handle._pointer); - } - - SubscriptionSetState subscriptionSetGetState(SubscriptionSet subscriptions) { - return SubscriptionSetState.values[_realmLib.realm_sync_subscription_set_state(subscriptions.handle._pointer)]; - } - - MutableSubscriptionSetHandle subscriptionSetMakeMutable(SubscriptionSet subscriptions) { - return MutableSubscriptionSetHandle._( - _realmLib.invokeGetPointer(() => _realmLib.realm_sync_make_subscription_set_mutable(subscriptions.handle._pointer)), subscriptions.realm.handle); - } - - SubscriptionSetHandle subscriptionSetCommit(MutableSubscriptionSet subscriptions) { - return SubscriptionSetHandle._( - _realmLib.invokeGetPointer(() => _realmLib.realm_sync_subscription_set_commit(subscriptions.handle._mutablePointer)), subscriptions.realm.handle); - } - - SubscriptionHandle insertOrAssignSubscription(MutableSubscriptionSet subscriptions, RealmResults results, String? name, bool update) { - if (!update) { - if (name != null && findSubscriptionByName(subscriptions, name) != null) { - throw RealmException('Duplicate subscription with name: $name'); + if (Platform.isWindows) { + return _getAppDirectoryFromPlugin(); } - } - return using((arena) { - final out_index = arena(); - final out_inserted = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_insert_or_assign_results( - subscriptions.handle._mutablePointer, - results.handle._pointer, - name?.toCharPtr(arena) ?? nullptr, - out_index, - out_inserted, - )); - return subscriptionAt(subscriptions, out_index.value); - }); - } - - bool eraseSubscriptionById(MutableSubscriptionSet subscriptions, Subscription subscription) { - return using((arena) { - final out_found = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_erase_by_id( - subscriptions.handle._mutablePointer, - subscription.id.toNative(arena), - out_found, - )); - return out_found.value; - }); - } - - bool eraseSubscriptionByName(MutableSubscriptionSet subscriptions, String name) { - return using((arena) { - final out_found = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_erase_by_name( - subscriptions.handle._mutablePointer, - name.toCharPtr(arena), - out_found, - )); - return out_found.value; - }); - } - - bool eraseSubscriptionByResults(MutableSubscriptionSet subscriptions, RealmResults results) { - return using((arena) { - final out_found = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_erase_by_results( - subscriptions.handle._mutablePointer, - results.handle._pointer, - out_found, - )); - return out_found.value; - }); - } - - void clearSubscriptionSet(MutableSubscriptionSet subscriptions) { - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_clear(subscriptions.handle._mutablePointer)); - } - - void refreshSubscriptionSet(SubscriptionSet subscriptions) { - _realmLib.invokeGetBool(() => _realmLib.realm_sync_subscription_set_refresh(subscriptions.handle._pointer)); - } - - static bool initial_data_callback(Pointer userdata, Pointer realmPtr) { - final realmHandle = RealmHandle._unowned(realmPtr); - try { - final LocalConfiguration config = userdata.toObject(); - final realm = RealmInternal.getUnowned(config, realmHandle); - config.initialDataCallback!(realm); - return true; - } catch (ex) { - _realmLib.realm_register_user_code_callback_error(ex.toPersistentHandle()); - } finally { - realmHandle.release(); - } - - return false; - } - - static bool should_compact_callback(Pointer userdata, int totalSize, int usedSize) { - final config = userdata.toObject(); - - if (config is LocalConfiguration) { - return config.shouldCompactCallback!(totalSize, usedSize); - } else if (config is FlexibleSyncConfiguration) { - return config.shouldCompactCallback!(totalSize, usedSize); - } - - return false; - } - - static bool migration_callback( - Pointer userdata, Pointer oldRealmHandle, Pointer newRealmHandle, Pointer schema) { - final oldHandle = RealmHandle._unowned(oldRealmHandle); - final newHandle = RealmHandle._unowned(newRealmHandle); - try { - final LocalConfiguration config = userdata.toObject(); - - final oldSchemaVersion = _realmLib.realm_get_schema_version(oldRealmHandle); - final oldConfig = Configuration.local([], path: config.path, isReadOnly: true, schemaVersion: oldSchemaVersion); - final oldRealm = RealmInternal.getUnowned(oldConfig, oldHandle, isInMigration: true); - - final newRealm = RealmInternal.getUnowned(config, newHandle, isInMigration: true); - - final migration = MigrationInternal.create(RealmInternal.getMigrationRealm(oldRealm), newRealm, SchemaHandle.unowned(schema)); - config.migrationCallback!(migration, oldSchemaVersion); - return true; - } catch (ex) { - _realmLib.realm_register_user_code_callback_error(ex.toPersistentHandle()); - } finally { - oldHandle.release(); - newHandle.release(); - } - - return false; - } - static void _syncErrorHandlerCallback(Object userdata, Pointer session, realm_sync_error error) { - final syncConfig = userdata as FlexibleSyncConfiguration; - // TODO: Take the app from the session instead of from syncConfig after fixing issue https://github.com/realm/realm-dart/issues/633 - final syncError = SyncErrorInternal.createSyncError(error.toSyncErrorDetails(), app: syncConfig.user.app); - - if (syncError is ClientResetError) { - syncConfig.clientResetHandler.onManualReset?.call(syncError); - return; - } - - syncConfig.syncErrorHandler(syncError); - } - - static void _guardSynchronousCallback(FutureOr Function() callback, Pointer unlockCallbackFunc) async { - Pointer user_error = nullptr; - try { - await callback(); - } catch (error) { - user_error = error.toPersistentHandle(); - } finally { - _realmLib.realm_dart_invoke_unlock_callback(user_error, unlockCallbackFunc); + throw UnsupportedError("Platform ${Platform.operatingSystem} is not supported"); + } catch (e) { + throw RealmException('Cannot get app directory. Error: $e'); } } - static void _syncBeforeResetCallback(Object userdata, Pointer realmHandle, Pointer unlockCallbackFunc) { - _guardSynchronousCallback(() async { - final syncConfig = userdata as FlexibleSyncConfiguration; - var beforeResetCallback = syncConfig.clientResetHandler.onBeforeReset!; - - final realm = RealmInternal.getUnowned(syncConfig, RealmHandle._unowned(realmHandle)); - try { - await beforeResetCallback(realm); - } finally { - realm.handle.release(); - } - }, unlockCallbackFunc); - } - - static void _syncAfterResetCallback(Object userdata, Pointer beforeHandle, Pointer afterReference, bool didRecover, - Pointer unlockCallbackFunc) { - _guardSynchronousCallback(() async { - final syncConfig = userdata as FlexibleSyncConfiguration; - final afterResetCallback = didRecover ? syncConfig.clientResetHandler.onAfterRecovery : syncConfig.clientResetHandler.onAfterDiscard; - - if (afterResetCallback == null) { - return; - } - - final beforeRealm = RealmInternal.getUnowned(syncConfig, RealmHandle._unowned(beforeHandle)); - final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_from_thread_safe_reference(afterReference, scheduler.handle._pointer)); - final afterRealm = RealmInternal.getUnowned(syncConfig, RealmHandle._unowned(realmPtr)); - + String getBundleId() { + readBundleId() { try { - return await afterResetCallback(beforeRealm, afterRealm); - } finally { - beforeRealm.handle.release(); - afterRealm.handle.release(); - } - }, unlockCallbackFunc); - } - - void raiseError(Session session, int errorCode, bool isFatal) { - using((arena) { - final message = "Simulated session error".toCharPtr(arena); - _realmLib.realm_sync_session_handle_error_for_testing(session.handle._pointer, errorCode, message, isFatal); - }); - } - - void realmDisableAutoRefreshForTesting(Realm realm) { - _realmLib.realm_set_auto_refresh(realm.handle._pointer, false); - } - - SchedulerHandle createScheduler(int isolateId, int sendPort) { - final schedulerPtr = _realmLib.realm_dart_create_scheduler(isolateId, sendPort); - return SchedulerHandle._(schedulerPtr); - } - - void invokeScheduler(int workQueue) { - final queuePointer = Pointer.fromAddress(workQueue); - _realmLib.realm_scheduler_perform_work(queuePointer); - } - - RealmHandle openRealm(Configuration config) { - final configHandle = _createConfig(config); - final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_open(configHandle._pointer), "Error opening realm at path ${config.path}"); - return RealmHandle._(realmPtr); - } + if (!isFlutterPlatform || Platform.environment.containsKey('FLUTTER_TEST')) { + var pubspecPath = path.join(path.current, 'pubspec.yaml'); + var pubspecFile = File(pubspecPath); - RealmAsyncOpenTaskHandle createRealmAsyncOpenTask(FlexibleSyncConfiguration config) { - final configHandle = _createConfig(config); - final asyncOpenTaskPtr = - _realmLib.invokeGetPointer(() => _realmLib.realm_open_synchronized(configHandle._pointer), "Error opening realm at path ${config.path}"); - return RealmAsyncOpenTaskHandle._(asyncOpenTaskPtr); - } + if (pubspecFile.existsSync()) { + final pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + return pubspec.name; + } + } - Future openRealmAsync(RealmAsyncOpenTaskHandle handle, CancellationToken? cancellationToken) { - final completer = CancellableCompleter(cancellationToken); - if (!completer.isCancelled) { - final callback = - Pointer.fromFunction realm, Pointer error)>(_openRealmAsyncCallback); - final userData = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); - _realmLib.realm_async_open_task_start( - handle._pointer, - _realmLib.addresses.realm_dart_async_open_task_callback, - userData.cast(), - _realmLib.addresses.realm_dart_userdata_async_free, - ); - } - return completer.future; - } + if (Platform.isAndroid) { + return realmLib.realm_dart_get_bundle_id().cast().toDartString(); + } - static void _openRealmAsyncCallback(Object userData, Pointer realmSafePtr, Pointer error) { - return using((Arena arena) { - final completer = userData as CancellableCompleter; - if (completer.isCancelled) { - return; - } - if (error != nullptr) { - final err = arena(); - final lastError = _realmLib.realm_get_async_error(error, err) ? err.ref.toLastError() : null; - completer.completeError(RealmException("Failed to open realm: ${lastError?.message ?? 'Error details missing.'}")); - return; + final getBundleIdFunc = _pluginLib.lookupFunction Function(), Pointer Function()>("realm_dart_get_bundle_id"); + final bundleIdPtr = getBundleIdFunc(); + return bundleIdPtr.cast().toDartString(); + } on Exception catch (_) { + //Never fail on bundleId. Use fallback value. } - final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_from_thread_safe_reference(realmSafePtr, scheduler.handle._pointer)); - completer.complete(RealmHandle._(realmPtr)); - }); - } - - void cancelOpenRealmAsync(RealmAsyncOpenTaskHandle handle) { - _realmLib.realm_async_open_task_cancel(handle._pointer); - } - - RealmAsyncOpenTaskProgressNotificationTokenHandle realmAsyncOpenRegisterAsyncOpenProgressNotifier( - RealmAsyncOpenTaskHandle handle, RealmAsyncOpenProgressNotificationsController controller) { - final callback = Pointer.fromFunction(_syncProgressCallback); - final userdata = _realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle._pointer); - final tokenPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_async_open_task_register_download_progress_notifier( - handle._pointer, - _realmLib.addresses.realm_dart_sync_progress_callback, - userdata.cast(), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - return RealmAsyncOpenTaskProgressNotificationTokenHandle._(tokenPtr); - } - - RealmSchema readSchema(Realm realm) { - return using((Arena arena) { - return _readSchema(realm, arena); - }); - } - - RealmSchema _readSchema(Realm realm, Arena arena, {int expectedSize = 10}) { - final classesPtr = arena(expectedSize); - final actualCount = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_get_class_keys(realm.handle._pointer, classesPtr, expectedSize, actualCount)); - if (expectedSize < actualCount.value) { - arena.free(classesPtr); - return _readSchema(realm, arena, expectedSize: actualCount.value); - } - - final schemas = []; - for (var i = 0; i < actualCount.value; i++) { - final classInfo = arena(); - final classKey = classesPtr.elementAt(i).value; - _realmLib.invokeGetBool(() => _realmLib.realm_get_class(realm.handle._pointer, classKey, classInfo)); - - final name = classInfo.ref.name.cast().toDartString(); - final baseType = ObjectType.values.firstWhere((element) => element.flags == classInfo.ref.flags, - orElse: () => throw RealmError('No object type found for flags ${classInfo.ref.flags}')); - final schema = - _getSchemaForClassKey(realm, classKey, name, baseType, arena, expectedSize: classInfo.ref.num_properties + classInfo.ref.num_computed_properties); - schemas.add(schema); - } - - return RealmSchema(schemas); - } - - SchemaObject _getSchemaForClassKey(Realm realm, int classKey, String name, ObjectType baseType, Arena arena, {int expectedSize = 10}) { - final actualCount = arena(); - final propertiesPtr = arena(expectedSize); - _realmLib.invokeGetBool(() => _realmLib.realm_get_class_properties(realm.handle._pointer, classKey, propertiesPtr, expectedSize, actualCount)); - - if (expectedSize < actualCount.value) { - // The supplied array was too small - resize it - arena.free(propertiesPtr); - return _getSchemaForClassKey(realm, classKey, name, baseType, arena, expectedSize: actualCount.value); - } - - final result = []; - for (var i = 0; i < actualCount.value; i++) { - final property = propertiesPtr.elementAt(i).ref.toSchemaProperty(); - result.add(property); - } - - late Type type; - switch (baseType) { - case ObjectType.realmObject: - type = RealmObject; - break; - case ObjectType.embeddedObject: - type = EmbeddedObject; - break; - case ObjectType.asymmetricObject: - type = AsymmetricObject; - break; - default: - throw RealmError('$baseType is not supported yet'); + //Fallback value + return "realm_bundle_id"; } - return SchemaObject(baseType, type, name, result); - } - - void deleteRealmFiles(String path) { - using((Arena arena) { - final realm_deleted = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_delete_files(path.toCharPtr(arena), realm_deleted), "Error deleting realm at path $path"); - }); + String bundleId = readBundleId(); + const salt = [82, 101, 97, 108, 109, 32, 105, 115, 32, 103, 114, 101, 97, 116]; + return base64Encode(sha256.convert([...salt, ...utf8.encode(bundleId)]).bytes); } - String getFilesPath() { - return _realmLib.realm_dart_get_files_path().cast().toRealmDartString()!; + String getDefaultBaseUrl() { + return realmLib.realm_app_get_default_base_url().cast().toRealmDartString()!; } String getDeviceName() { if (Platform.isAndroid || Platform.isIOS) { - return _realmLib.realm_dart_get_device_name().cast().toRealmDartString()!; + return realmLib.realm_dart_get_device_name().cast().toRealmDartString()!; } return ""; @@ -806,3095 +149,43 @@ class _RealmCore { String getDeviceVersion() { if (Platform.isAndroid || Platform.isIOS) { - return _realmLib.realm_dart_get_device_version().cast().toRealmDartString()!; + return realmLib.realm_dart_get_device_version().cast().toRealmDartString()!; } return ""; } String getRealmLibraryCpuArchitecture() { - return _realmLib.realm_get_library_cpu_arch().cast().toDartString(); - } - - void closeRealm(Realm realm) { - _realmLib.invokeGetBool(() => _realmLib.realm_close(realm.handle._pointer), "Realm close failed"); + return realmLib.realm_get_library_cpu_arch().cast().toDartString(); } - bool isRealmClosed(Realm realm) { - return _realmLib.realm_is_closed(realm.handle._pointer); - } + void loggerAttach() => realmLib.realm_dart_attach_logger(scheduler.nativePort); - void beginWrite(Realm realm) { - _realmLib.invokeGetBool(() => _realmLib.realm_begin_write(realm.handle._pointer), "Could not begin write"); - } + void loggerDetach() => realmLib.realm_dart_detach_logger(scheduler.nativePort); - void commitWrite(Realm realm) { - _realmLib.invokeGetBool(() => _realmLib.realm_commit(realm.handle._pointer), "Could not commit write"); + void logMessage(LogCategory category, LogLevel logLevel, String message) { + return using((arena) { + realmLib.realm_dart_log(logLevel.index, category.toString().toCharPtr(arena), message.toCharPtr(arena)); + }); } - Future beginWriteAsync(Realm realm, CancellationToken? ct) { - int? id; - final completer = CancellableCompleter(ct, onCancel: () { - if (id != null) { - realmCore._cancelAsync(realm, id!); - } + void setLogLevel(LogLevel level, {required LogCategory category}) { + using((arena) { + realmLib.realm_set_log_level_category(category.toString().toCharPtr(arena), level.index); }); - if (ct?.isCancelled != true) { - using((arena) { - final transaction_id = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_async_begin_write( - realm.handle._pointer, - Pointer.fromFunction(_completeAsyncBeginWrite), - completer.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - true, - transaction_id, - )); - id = transaction_id.value; - }); - } - return completer.future; } - Future commitWriteAsync(Realm realm, CancellationToken? ct) { - int? id; - final completer = CancellableCompleter(ct, onCancel: () { - if (id != null) { - realmCore._cancelAsync(realm, id!); - } - }); - if (ct?.isCancelled != true) { - using((arena) { - final transaction_id = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_async_commit( - realm.handle._pointer, - Pointer.fromFunction(_completeAsyncCommit), - completer.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - false, - transaction_id, - )); - id = transaction_id.value; - }); - } - return completer.future; - } - - bool _cancelAsync(Realm realm, int cancellationId) { - return using((Arena arena) { - final didCancel = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_async_cancel(realm.handle._pointer, cancellationId, didCancel)); - return didCancel.value; - }); - } - - static void _completeAsyncBeginWrite(Pointer userdata) { - final Completer completer = userdata.toObject(); - completer.complete(); - } - - static void _completeAsyncCommit(Pointer userdata, bool error, Pointer description) { - final Completer completer = userdata.toObject(); - if (error) { - completer.completeError(RealmException(description.cast().toDartString())); - } else { - completer.complete(); - } - } - - bool getIsWritable(Realm realm) { - return _realmLib.realm_is_writable(realm.handle._pointer); - } - - void rollbackWrite(Realm realm) { - _realmLib.invokeGetBool(() => _realmLib.realm_rollback(realm.handle._pointer), "Could not rollback write"); - } - - bool realmRefresh(Realm realm) { - return using((Arena arena) { - final did_refresh = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_refresh(realm.handle._pointer, did_refresh), "Could not refresh"); - return did_refresh.value; - }); - } - - Future realmRefreshAsync(Realm realm) async { - final completer = Completer(); - final callback = Pointer.fromFunction)>(_realmRefreshAsyncCallback); - Pointer completerPtr = _realmLib.realm_dart_object_to_persistent_handle(completer); - Pointer result = _realmLib.realm_add_realm_refresh_callback( - realm.handle._pointer, callback.cast(), completerPtr, _realmLib.addresses.realm_dart_delete_persistent_handle); - - if (result == nullptr) { - return Future.value(false); - } - - return completer.future; - } - - static void _realmRefreshAsyncCallback(Pointer userdata) { - if (userdata == nullptr) { - return; - } - - final completer = _realmLib.realm_dart_persistent_handle_to_object(userdata) as Completer; - completer.complete(true); - } - - RealmObjectMetadata getObjectMetadata(Realm realm, SchemaObject schema) { - return using((Arena arena) { - final found = arena(); - final classInfo = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_find_class(realm.handle._pointer, schema.name.toCharPtr(arena), found, classInfo), - "Error getting class ${schema.name} from realm at ${realm.config.path}"); - - if (!found.value) { - throwLastError("Class ${schema.name} not found in ${realm.config.path}"); - } - - final primaryKey = classInfo.ref.primary_key.cast().toRealmDartString(treatEmptyAsNull: true); - return RealmObjectMetadata(schema, classInfo.ref.key, _getPropertiesMetadata(realm, classInfo.ref.key, primaryKey, arena)); - }); - } - - Map getPropertiesMetadata(Realm realm, int classKey, String? primaryKeyName) { - return using((Arena arena) { - return _getPropertiesMetadata(realm, classKey, primaryKeyName, arena); - }); - } - - Map _getPropertiesMetadata(Realm realm, int classKey, String? primaryKeyName, Arena arena) { - final propertyCountPtr = arena(); - _realmLib.invokeGetBool( - () => _realmLib.realm_get_property_keys(realm.handle._pointer, classKey, nullptr, 0, propertyCountPtr), "Error getting property count"); - - var propertyCount = propertyCountPtr.value; - final propertiesPtr = arena(propertyCount); - _realmLib.invokeGetBool(() => _realmLib.realm_get_class_properties(realm.handle._pointer, classKey, propertiesPtr, propertyCount, propertyCountPtr), - "Error getting class properties."); - - propertyCount = propertyCountPtr.value; - Map result = {}; - for (var i = 0; i < propertyCount; i++) { - final property = propertiesPtr.elementAt(i); - final propertyName = property.ref.name.cast().toRealmDartString()!; - final objectType = property.ref.link_target.cast().toRealmDartString(treatEmptyAsNull: true); - final linkOriginProperty = property.ref.link_origin_property_name.cast().toRealmDartString(treatEmptyAsNull: true); - final isNullable = property.ref.flags & realm_property_flags.RLM_PROPERTY_NULLABLE != 0; - final isPrimaryKey = propertyName == primaryKeyName; - final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, linkOriginProperty, RealmPropertyType.values.elementAt(property.ref.type), - isNullable, isPrimaryKey, RealmCollectionType.values.elementAt(property.ref.collection_type)); - result[propertyName] = propertyMeta; - } - return result; - } - - RealmObjectHandle createRealmObject(Realm realm, int classKey) { - final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_object_create(realm.handle._pointer, classKey)); - return RealmObjectHandle._(realmPtr, realm.handle); - } - - RealmObjectHandle createEmbeddedObject(RealmObjectBase obj, int propertyKey) { - final objectPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_set_embedded(obj.handle._pointer, propertyKey)); - return RealmObjectHandle._(objectPtr, obj.realm.handle); - } - - Tuple getEmbeddedParent(EmbeddedObject obj) { - return using((Arena arena) { - final parentPtr = arena>(); - final classKeyPtr = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_object_get_parent(obj.handle._pointer, parentPtr, classKeyPtr)); - - final handle = RealmObjectHandle._(parentPtr.value, obj.realm.handle); - - return Tuple(handle, classKeyPtr.value); - }); - } - - int getClassKey(RealmObjectHandle handle) { - return _realmLib.realm_object_get_table(handle._pointer); - } - - RealmObjectHandle getOrCreateRealmObjectWithPrimaryKey(Realm realm, int classKey, Object? primaryKey) { - return using((Arena arena) { - final realm_value = _toRealmValue(primaryKey, arena); - final didCreate = arena(); - final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_object_get_or_create_with_primary_key( - realm.handle._pointer, - classKey, - realm_value.ref, - didCreate, - )); - return RealmObjectHandle._(realmPtr, realm.handle); - }); - } - - RealmObjectHandle createRealmObjectWithPrimaryKey(Realm realm, int classKey, Object? primaryKey) { - return using((Arena arena) { - final realm_value = _toRealmValue(primaryKey, arena); - final realmPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_object_create_with_primary_key(realm.handle._pointer, classKey, realm_value.ref)); - return RealmObjectHandle._(realmPtr, realm.handle); - }); - } - - Object? getProperty(RealmObjectBase object, int propertyKey) { - return using((Arena arena) { - final realm_value = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_get_value(object.handle._pointer, propertyKey, realm_value)); - return realm_value.toDartValue(object.realm, () => _realmLib.realm_get_list(object.handle._pointer, propertyKey), - () => _realmLib.realm_get_dictionary(object.handle._pointer, propertyKey)); - }); - } - - void setProperty(RealmObjectBase object, int propertyKey, Object? value, bool isDefault) { - using((Arena arena) { - final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool(() => _realmLib.realm_set_value(object.handle._pointer, propertyKey, realm_value.ref, isDefault)); - }); - } - - void objectSetCollection(RealmObjectBase object, int propertyKey, RealmValue value) { - _createCollection(object.realm, value, () => _realmLib.realm_set_list(object.handle._pointer, propertyKey), - () => _realmLib.realm_set_dictionary(object.handle._pointer, propertyKey)); - } - - String objectToString(RealmObjectBase object) { - return _realmLib.realm_object_to_string(object.handle._pointer).cast().toRealmDartString(freeRealmMemory: true)!; - } - - // For debugging - // ignore: unused_element - int get _threadId => _realmLib.realm_dart_get_thread_id(); - - RealmObjectHandle? find(Realm realm, int classKey, Object? primaryKey) { - return using((Arena arena) { - final realm_value = _toRealmValue(primaryKey, arena); - final pointer = _realmLib.realm_object_find_with_primary_key(realm.handle._pointer, classKey, realm_value.ref, nullptr); - if (pointer == nullptr) { - return null; - } - - return RealmObjectHandle._(pointer, realm.handle); - }); - } - - RealmObjectHandle? findExisting(Realm realm, int classKey, RealmObjectHandle other) { - final key = _realmLib.realm_object_get_key(other._pointer); - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_object(realm.handle._pointer, classKey, key)); - return RealmObjectHandle._(pointer, realm.handle); - } - - void renameProperty(Realm realm, String objectType, String oldName, String newName, SchemaHandle schema) { - using((Arena arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_schema_rename_property( - realm.handle._pointer, schema._pointer, objectType.toCharPtr(arena), oldName.toCharPtr(arena), newName.toCharPtr(arena))); - }); - } - - bool deleteType(Realm realm, String objectType) { - return using((Arena arena) { - final deletedPtr = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_remove_table(realm.handle._pointer, objectType.toCharPtr(arena), deletedPtr)); - return deletedPtr.value; - }); - } - - void deleteRealmObject(RealmObjectBase object) { - _realmLib.invokeGetBool(() => _realmLib.realm_object_delete(object.handle._pointer)); - } - - RealmResultsHandle findAll(Realm realm, int classKey) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_object_find_all(realm.handle._pointer, classKey)); - return RealmResultsHandle._(pointer, realm.handle); - } - - RealmResultsHandle queryClass(Realm realm, int classKey, String query, List args) { - return using((arena) { - final length = args.length; - final argsPointer = arena(length); - for (var i = 0; i < length; ++i) { - _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); - } - final queryHandle = _RealmQueryHandle._( - _realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse( - realm.handle._pointer, - classKey, - query.toCharPtr(arena), - length, - argsPointer, - ), - ), - realm.handle); - return _queryFindAll(queryHandle); - }); - } - - RealmResultsHandle queryResults(RealmResults target, String query, List args) { - return using((arena) { - final length = args.length; - final argsPointer = arena(length); - for (var i = 0; i < length; ++i) { - _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); - } - final queryHandle = _RealmQueryHandle._( - _realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse_for_results( - target.handle._pointer, - query.toCharPtr(arena), - length, - argsPointer, - ), - ), - target.realm.handle); - return _queryFindAll(queryHandle); - }); - } - - RealmResultsHandle _queryFindAll(_RealmQueryHandle queryHandle) { - try { - final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_query_find_all(queryHandle._pointer)); - return RealmResultsHandle._(resultsPointer, queryHandle._root); - } finally { - queryHandle.release(); - } - } - - RealmResultsHandle queryList(RealmList target, String query, List args) { - return using((arena) { - final length = args.length; - final argsPointer = arena(length); - for (var i = 0; i < length; ++i) { - _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); - } - final queryHandle = _RealmQueryHandle._( - _realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse_for_list( - target.handle._pointer, - query.toCharPtr(arena), - length, - argsPointer, - ), - ), - target.realm.handle); - return _queryFindAll(queryHandle); - }); - } - - RealmResultsHandle querySet(RealmSet target, String query, List args) { - return using((arena) { - final length = args.length; - final argsPointer = arena(length); - for (var i = 0; i < length; ++i) { - _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); - } - final queryHandle = _RealmQueryHandle._( - _realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse_for_set( - target.handle._pointer, - query.toCharPtr(arena), - length, - argsPointer, - ), - ), - target.realm.handle); - return _queryFindAll(queryHandle); - }); - } - - RealmResultsHandle queryMap(ManagedRealmMap target, String query, List args) { - return using((arena) { - final length = args.length; - final argsPointer = arena(length); - for (var i = 0; i < length; ++i) { - _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); - } - - final results = mapGetValues(target); - final queryHandle = _RealmQueryHandle._( - _realmLib.invokeGetPointer( - () => _realmLib.realm_query_parse_for_results( - results._pointer, - query.toCharPtr(arena), - length, - argsPointer, - ), - ), - target.realm.handle); - return _queryFindAll(queryHandle); - }); - } - - RealmResultsHandle resultsFromList(RealmList list) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_list_to_results(list.handle._pointer)); - return RealmResultsHandle._(pointer, list.realm.handle); - } - - RealmResultsHandle resultsFromSet(RealmSet set) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_set_to_results(set.handle._pointer)); - return RealmResultsHandle._(pointer, set.realm.handle); - } - - Object? resultsGetElementAt(RealmResults results, int index) { - return using((Arena arena) { - final realm_value = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_results_get(results.handle._pointer, index, realm_value)); - return realm_value.toDartValue(results.realm, () => _realmLib.realm_results_get_list(results.handle._pointer, index), - () => _realmLib.realm_results_get_dictionary(results.handle._pointer, index)); - }); - } - - int resultsFind(RealmResults results, Object? value) { - return using((Arena arena) { - final out_index = arena(); - final out_found = arena(); - - // TODO: how should this behave for collections - final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool( - () => _realmLib.realm_results_find( - results.handle._pointer, - realm_value, - out_index, - out_found, - ), - ); - return out_found.value ? out_index.value : -1; - }); - } - - RealmObjectHandle resultsGetObjectAt(RealmResults results, int index) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_get_object(results.handle._pointer, index)); - return RealmObjectHandle._(pointer, results.realm.handle); - } - - int getResultsCount(RealmResults results) { - return using((Arena arena) { - final countPtr = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_results_count(results.handle._pointer, countPtr)); - return countPtr.value; - }); - } - - bool resultsIsValid(RealmResults results) { - return using((arena) { - final is_valid = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_results_is_valid(results.handle._pointer, is_valid)); - return is_valid.value; - }); - } - - CollectionChanges getCollectionChanges(RealmCollectionChangesHandle changes) { - return using((arena) { - final out_num_deletions = arena(); - final out_num_insertions = arena(); - final out_num_modifications = arena(); - final out_num_moves = arena(); - final out_collection_cleared = arena(); - final out_collection_was_deleted = arena(); - _realmLib.realm_collection_changes_get_num_changes( - changes._pointer, - out_num_deletions, - out_num_insertions, - out_num_modifications, - out_num_moves, - out_collection_cleared, - out_collection_was_deleted, - ); - - final deletionsCount = out_num_deletions != nullptr ? out_num_deletions.value : 0; - final insertionCount = out_num_insertions != nullptr ? out_num_insertions.value : 0; - final modificationCount = out_num_modifications != nullptr ? out_num_modifications.value : 0; - var moveCount = out_num_moves != nullptr ? out_num_moves.value : 0; - - final out_deletion_indexes = arena(deletionsCount); - final out_insertion_indexes = arena(insertionCount); - final out_modification_indexes = arena(modificationCount); - final out_modification_indexes_after = arena(modificationCount); - final out_moves = arena(moveCount); - - _realmLib.realm_collection_changes_get_changes( - changes._pointer, - out_deletion_indexes, - deletionsCount, - out_insertion_indexes, - insertionCount, - out_modification_indexes, - modificationCount, - out_modification_indexes_after, - modificationCount, - out_moves, - moveCount, - ); - - var elementZero = out_moves.elementAt(0); - List moves = List.filled(moveCount, Move(elementZero.ref.from, elementZero.ref.to)); - for (var i = 1; i < moveCount; i++) { - final movePtr = out_moves.elementAt(i); - moves[i] = Move(movePtr.ref.from, movePtr.ref.to); - } - - return CollectionChanges( - out_deletion_indexes.toIntList(deletionsCount), - out_insertion_indexes.toIntList(insertionCount), - out_modification_indexes.toIntList(modificationCount), - out_modification_indexes_after.toIntList(modificationCount), - moves, - out_collection_cleared.value, - out_collection_was_deleted.value, - ); - }); - } - - MapChanges getMapChanges(RealmMapChangesHandle changes) { - return using((arena) { - final out_num_deletions = arena(); - final out_num_insertions = arena(); - final out_num_modifications = arena(); - final out_collection_was_deleted = arena(); - _realmLib.realm_dictionary_get_changes( - changes._pointer, - out_num_deletions, - out_num_insertions, - out_num_modifications, - out_collection_was_deleted, - ); - - final deletionsCount = out_num_deletions != nullptr ? out_num_deletions.value : 0; - final insertionCount = out_num_insertions != nullptr ? out_num_insertions.value : 0; - final modificationCount = out_num_modifications != nullptr ? out_num_modifications.value : 0; - - final out_deletion_indexes = arena(deletionsCount); - final out_insertion_indexes = arena(insertionCount); - final out_modification_indexes = arena(modificationCount); - final out_collection_was_cleared = arena(); - - _realmLib.realm_dictionary_get_changed_keys( - changes._pointer, - out_deletion_indexes, - out_num_deletions, - out_insertion_indexes, - out_num_insertions, - out_modification_indexes, - out_num_modifications, - out_collection_was_cleared, - ); - - return MapChanges(out_deletion_indexes.toStringList(deletionsCount), out_insertion_indexes.toStringList(insertionCount), - out_modification_indexes.toStringList(modificationCount), out_collection_was_cleared.value, out_collection_was_deleted.value); - }); - } - - _RealmLinkHandle _getObjectAsLink(RealmObjectBase object) { - final realmLink = _realmLib.realm_object_as_link(object.handle._pointer); - return _RealmLinkHandle._(realmLink); - } - - RealmObjectHandle _getObject(Realm realm, int classKey, int objectKey) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_object(realm.handle._pointer, classKey, objectKey)); - return RealmObjectHandle._(pointer, realm.handle); - } - - RealmListHandle getListProperty(RealmObjectBase object, int propertyKey) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_list(object.handle._pointer, propertyKey)); - return RealmListHandle._(pointer, object.realm.handle); - } - - RealmResultsHandle getBacklinks(RealmObjectBase object, int sourceTableKey, int propertyKey) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_backlinks(object.handle._pointer, sourceTableKey, propertyKey)); - return RealmResultsHandle._(pointer, object.realm.handle); - } - - int getListSize(RealmListHandle handle) { - return using((Arena arena) { - final size = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_list_size(handle._pointer, size)); - return size.value; - }); - } - - Object? listGetElementAt(RealmList list, int index) { - return using((Arena arena) { - final realm_value = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_list_get(list.handle._pointer, index, realm_value)); - return realm_value.toDartValue( - list.realm, () => _realmLib.realm_list_get_list(list.handle._pointer, index), () => _realmLib.realm_list_get_dictionary(list.handle._pointer, index)); - }); - } - - void listAddElementAt(RealmListHandle handle, int index, Object? value, bool insert) { - using((Arena arena) { - final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool(() => (insert ? _realmLib.realm_list_insert : _realmLib.realm_list_set)(handle._pointer, index, realm_value.ref)); - }); - } - - void listAddCollectionAt(RealmListHandle handle, Realm realm, int index, RealmValue value, bool insert) { - _createCollection(realm, value, () => (insert ? _realmLib.realm_list_insert_list : _realmLib.realm_list_set_list)(handle._pointer, index), - () => (insert ? _realmLib.realm_list_insert_dictionary : _realmLib.realm_list_set_dictionary)(handle._pointer, index)); - } - - RealmObjectHandle listSetEmbeddedObjectAt(Realm realm, RealmListHandle handle, int index) { - final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_list_set_embedded(handle._pointer, index)); - return RealmObjectHandle._(ptr, realm.handle); - } - - RealmObjectHandle listInsertEmbeddedObjectAt(Realm realm, RealmListHandle handle, int index) { - final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_list_insert_embedded(handle._pointer, index)); - return RealmObjectHandle._(ptr, realm.handle); - } - - void listRemoveElementAt(RealmListHandle handle, int index) { - _realmLib.invokeGetBool(() => _realmLib.realm_list_erase(handle._pointer, index)); - } - - void listMoveElement(RealmListHandle handle, int from, int to) { - _realmLib.invokeGetBool(() => _realmLib.realm_list_move(handle._pointer, from, to)); - } - - void listDeleteAll(RealmList list) { - _realmLib.invokeGetBool(() => _realmLib.realm_list_remove_all(list.handle._pointer)); - } - - int listFind(RealmList list, Object? value) { - return using((Arena arena) { - final out_index = arena(); - final out_found = arena(); - - // TODO: how should this behave for collections - final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool( - () => _realmLib.realm_list_find( - list.handle._pointer, - realm_value, - out_index, - out_found, - ), - ); - return out_found.value ? out_index.value : -1; - }); - } - - void resultsDeleteAll(RealmResults results) { - _realmLib.invokeGetBool(() => _realmLib.realm_results_delete_all(results.handle._pointer)); - } - - void listClear(RealmListHandle listHandle) { - _realmLib.invokeGetBool(() => _realmLib.realm_list_clear(listHandle._pointer)); - } - - RealmSetHandle getSetProperty(RealmObjectBase object, int propertyKey) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_set(object.handle._pointer, propertyKey)); - return RealmSetHandle._(pointer, object.realm.handle); - } - - bool realmSetInsert(RealmSetHandle handle, Object? value) { - return using((Arena arena) { - final realm_value = _toRealmValue(value, arena); - final out_index = arena(); - final out_inserted = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_set_insert(handle._pointer, realm_value.ref, out_index, out_inserted)); - return out_inserted.value; - }); - } - - Object? realmSetGetElementAt(RealmSet realmSet, int index) { - return using((Arena arena) { - final realm_value = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_set_get(realmSet.handle._pointer, index, realm_value)); - final result = realm_value.toDartValue( - realmSet.realm, () => throw RealmException('Sets cannot contain collections'), () => throw RealmException('Sets cannot contain collections')); - return result; - }); - } - - bool realmSetFind(RealmSet realmSet, Object? value) { - return using((Arena arena) { - // TODO: how should this behave for collections - final realm_value = _toRealmValue(value, arena); - final out_index = arena(); - final out_found = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_set_find(realmSet.handle._pointer, realm_value.ref, out_index, out_found)); - return out_found.value; - }); - } - - bool realmSetErase(RealmSet realmSet, Object? value) { - return using((Arena arena) { - // TODO: do we support sets containing mixed collections - final realm_value = _toRealmValue(value, arena); - final out_erased = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_set_erase(realmSet.handle._pointer, realm_value.ref, out_erased)); - return out_erased.value; - }); - } - - void realmSetClear(RealmSetHandle handle) { - _realmLib.invokeGetBool(() => _realmLib.realm_set_clear(handle._pointer)); - } - - int realmSetSize(RealmSet realmSet) { - return using((Arena arena) { - final out_size = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_set_size(realmSet.handle._pointer, out_size)); - return out_size.value; - }); - } - - bool realmSetIsValid(RealmSet realmSet) { - return _realmLib.realm_set_is_valid(realmSet.handle._pointer); - } - - void realmSetRemoveAll(RealmSet realmSet) { - _realmLib.invokeGetBool(() => _realmLib.realm_set_remove_all(realmSet.handle._pointer)); - } - - RealmNotificationTokenHandle subscribeSetNotifications(RealmSet realmSet, NotificationsController controller) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_set_add_notification_callback( - realmSet.handle._pointer, - controller.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - nullptr, - Pointer.fromFunction(collection_change_callback), - )); - - return RealmNotificationTokenHandle._(pointer, realmSet.realm.handle); - } - - int mapGetSize(RealmMapHandle handle) { - return using((Arena arena) { - final size = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_size(handle._pointer, size)); - return size.value; - }); - } - - bool mapRemoveKey(RealmMapHandle handle, String key) { - return using((Arena arena) { - final keyValue = _toRealmValue(key, arena); - final out_erased = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_erase(handle._pointer, keyValue.ref, out_erased)); - return out_erased.value; - }); - } - - Object? mapGetElement(RealmMap map, String key) { - return using((Arena arena) { - final realm_value = arena(); - final key_value = _toRealmValue(key, arena); - final out_found = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_find(map.handle._pointer, key_value.ref, realm_value, out_found)); - if (out_found.value) { - return realm_value.toDartValue(map.realm, () => _realmLib.realm_dictionary_get_list(map.handle._pointer, key_value.ref), - () => _realmLib.realm_dictionary_get_dictionary(map.handle._pointer, key_value.ref)); - } - - return null; - }); - } - - bool mapIsValid(RealmMap map) { - return _realmLib.realm_dictionary_is_valid(map.handle._pointer); - } - - void mapClear(RealmMapHandle mapHandle) { - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_clear(mapHandle._pointer)); - } - - RealmResultsHandle mapGetKeys(ManagedRealmMap map) { - return using((Arena arena) { - final out_size = arena(); - final out_keys = arena>(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_get_keys(map.handle._pointer, out_size, out_keys)); - return RealmResultsHandle._(out_keys.value, map.realm.handle); - }); - } - - RealmResultsHandle mapGetValues(ManagedRealmMap map) { - final result = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_to_results(map.handle._pointer)); - return RealmResultsHandle._(result, map.realm.handle); - } - - bool mapContainsKey(ManagedRealmMap map, String key) { - return using((Arena arena) { - final key_value = _toRealmValue(key, arena); - final out_found = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_key(map.handle._pointer, key_value.ref, out_found)); - return out_found.value; - }); - } - - bool mapContainsValue(ManagedRealmMap map, Object? value) { - return using((Arena arena) { - // TODO: how should this behave for collections - final value_value = _toRealmValue(value, arena); - final out_index = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_value(map.handle._pointer, value_value.ref, out_index)); - return out_index.value > -1; - }); - } - - RealmObjectHandle mapInsertEmbeddedObject(Realm realm, RealmMapHandle handle, String key) { - return using((Arena arena) { - final realm_value = _toRealmValue(key, arena); - final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_insert_embedded(handle._pointer, realm_value.ref)); - return RealmObjectHandle._(ptr, realm.handle); - }); - } - - void mapInsertValue(RealmMapHandle handle, String key, Object? value) { - using((Arena arena) { - final key_value = _toRealmValue(key, arena); - final value_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_insert(handle._pointer, key_value.ref, value_value.ref, nullptr, nullptr)); - }); - } - - void mapInsertCollection(RealmMapHandle handle, Realm realm, String key, RealmValue value) { - using((Arena arena) { - final key_value = _toRealmValue(key, arena); - _createCollection(realm, value, () => _realmLib.realm_dictionary_insert_list(handle._pointer, key_value.ref), - () => _realmLib.realm_dictionary_insert_dictionary(handle._pointer, key_value.ref)); - }); - } - - RealmMapHandle getMapProperty(RealmObjectBase object, int propertyKey) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_dictionary(object.handle._pointer, propertyKey)); - return RealmMapHandle._(pointer, object.realm.handle); - } - - bool _equals(HandleBase first, HandleBase second) { - return _realmLib.realm_equals(first._pointer.cast(), second._pointer.cast()); - } - - bool objectEquals(RealmObjectBase first, RealmObjectBase second) => _equals(first.handle, second.handle); - bool realmEquals(Realm first, Realm second) => _equals(first.handle, second.handle); - bool userEquals(User first, User second) => _equals(first.handle, second.handle); - bool subscriptionEquals(Subscription first, Subscription second) => _equals(first.handle, second.handle); - - int objectGetHashCode(RealmObjectBase value) { - final link = realmCore._getObjectAsLink(value); - - var hashCode = -986587137; - hashCode = (hashCode * -1521134295) + link.classKey; - hashCode = (hashCode * -1521134295) + link.targetKey; - return hashCode; - } - - RealmResultsHandle resultsSnapshot(RealmResults results) { - final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_snapshot(results.handle._pointer)); - return RealmResultsHandle._(resultsPointer, results.realm.handle); - } - - bool objectIsValid(RealmObjectBase object) { - return _realmLib.realm_object_is_valid(object.handle._pointer); - } - - bool listIsValid(RealmList list) { - return _realmLib.realm_list_is_valid(list.handle._pointer); - } - - static void collection_change_callback(Pointer userdata, Pointer data) { - final NotificationsController controller = userdata.toObject(); - - if (data == nullptr) { - controller.onError(RealmError("Invalid notifications data received")); - return; - } - - try { - final clonedData = _realmLib.realm_clone(data.cast()); - if (clonedData == nullptr) { - controller.onError(RealmError("Error while cloning notifications data")); - return; - } - - final changesHandle = RealmCollectionChangesHandle._(clonedData.cast()); - controller.onChanges(changesHandle); - } catch (e) { - controller.onError(RealmError("Error handling change notifications. Error: $e")); - } - } - - static void object_change_callback(Pointer userdata, Pointer data) { - final NotificationsController controller = userdata.toObject(); - - if (data == nullptr) { - controller.onError(RealmError("Invalid notifications data received")); - return; - } - - try { - final clonedData = _realmLib.realm_clone(data.cast()); - if (clonedData == nullptr) { - controller.onError(RealmError("Error while cloning notifications data")); - return; - } - - final changesHandle = RealmObjectChangesHandle._(clonedData.cast()); - controller.onChanges(changesHandle); - } catch (e) { - controller.onError(RealmError("Error handling change notifications. Error: $e")); - } - } - - static void map_change_callback(Pointer userdata, Pointer data) { - final NotificationsController controller = userdata.toObject(); - - if (data == nullptr) { - controller.onError(RealmError("Invalid notifications data received")); - return; - } - - try { - final clonedData = _realmLib.realm_clone(data.cast()); - if (clonedData == nullptr) { - controller.onError(RealmError("Error while cloning notifications data")); - return; - } - - final changesHandle = RealmMapChangesHandle._(clonedData.cast()); - controller.onChanges(changesHandle); - } catch (e) { - controller.onError(RealmError("Error handling change notifications. Error: $e")); - } - } - - static void user_change_callback(Object userdata, int data) { - final controller = userdata as UserNotificationsController; - - controller.onUserChanged(); - } - - static void schema_change_callback(Pointer userdata, Pointer data) { - final Realm realm = userdata.toObject(); - try { - realm.updateSchema(); - } catch (e) { - Realm.logger.log(LogLevel.error, 'Failed to update Realm schema: $e'); - } - } - - RealmNotificationTokenHandle subscribeResultsNotifications(RealmResults results, NotificationsController controller) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_add_notification_callback( - results.handle._pointer, - controller.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - nullptr, - Pointer.fromFunction(collection_change_callback), - )); - - return RealmNotificationTokenHandle._(pointer, results.realm.handle); - } - - RealmNotificationTokenHandle subscribeListNotifications(RealmList list, NotificationsController controller) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_list_add_notification_callback( - list.handle._pointer, - controller.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - nullptr, - Pointer.fromFunction(collection_change_callback), - )); - - return RealmNotificationTokenHandle._(pointer, list.realm.handle); - } - - RealmNotificationTokenHandle subscribeObjectNotifications(RealmObjectBase object, NotificationsController controller) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_object_add_notification_callback( - object.handle._pointer, - controller.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - nullptr, - Pointer.fromFunction(object_change_callback), - )); - - return RealmNotificationTokenHandle._(pointer, object.realm.handle); - } - - RealmNotificationTokenHandle subscribeMapNotifications(RealmMap map, NotificationsController controller) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_add_notification_callback( - map.handle._pointer, - controller.toPersistentHandle(), - _realmLib.addresses.realm_dart_delete_persistent_handle, - nullptr, - Pointer.fromFunction(map_change_callback), - )); - - return RealmNotificationTokenHandle._(pointer, map.realm.handle); - } - - UserNotificationTokenHandle subscribeUserNotifications(UserNotificationsController controller) { - final callback = Pointer.fromFunction(user_change_callback); - final userdata = _realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle._pointer); - final notification_token = _realmLib.realm_sync_user_on_state_change_register_callback( - controller.user.handle._pointer, - _realmLib.addresses.realm_dart_user_change_callback, - userdata.cast(), - _realmLib.addresses.realm_dart_userdata_async_free, - ); - return UserNotificationTokenHandle._(notification_token); - } - - RealmCallbackTokenHandle subscribeForSchemaNotifications(Realm realm) { - final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_add_schema_changed_callback(realm.handle._pointer, - Pointer.fromFunction(schema_change_callback), realm.toPersistentHandle(), _realmLib.addresses.realm_dart_delete_persistent_handle)); - - return RealmCallbackTokenHandle._(pointer, realm.handle); - } - - bool getObjectChangesIsDeleted(RealmObjectChangesHandle handle) { - return _realmLib.realm_object_changes_is_deleted(handle._pointer); - } - - List getObjectChangesProperties(RealmObjectChangesHandle handle) { - return using((arena) { - final count = _realmLib.realm_object_changes_get_num_modified_properties(handle._pointer); - - final out_modified = arena(count); - _realmLib.realm_object_changes_get_modified_properties(handle._pointer, out_modified, count); - - return out_modified.asTypedList(count).toList(); - }); - } - - AppConfigHandle _createAppConfig(AppConfiguration configuration, RealmHttpTransportHandle httpTransport) { - return using((arena) { - final app_id = configuration.appId.toCharPtr(arena); - final handle = AppConfigHandle._(_realmLib.realm_app_config_new(app_id, httpTransport._pointer)); - - _realmLib.realm_app_config_set_platform_version(handle._pointer, Platform.operatingSystemVersion.toCharPtr(arena)); - - _realmLib.realm_app_config_set_sdk(handle._pointer, 'Dart'.toCharPtr(arena)); - _realmLib.realm_app_config_set_sdk_version(handle._pointer, libraryVersion.toCharPtr(arena)); - - final deviceName = getDeviceName(); - _realmLib.realm_app_config_set_device_name(handle._pointer, deviceName.toCharPtr(arena)); - - final deviceVersion = getDeviceVersion(); - _realmLib.realm_app_config_set_device_version(handle._pointer, deviceVersion.toCharPtr(arena)); - - _realmLib.realm_app_config_set_framework_name(handle._pointer, (isFlutterPlatform ? 'Flutter' : 'Dart VM').toCharPtr(arena)); - _realmLib.realm_app_config_set_framework_version(handle._pointer, Platform.version.toCharPtr(arena)); - - _realmLib.realm_app_config_set_base_url(handle._pointer, configuration.baseUrl.toString().toCharPtr(arena)); - - _realmLib.realm_app_config_set_default_request_timeout(handle._pointer, configuration.defaultRequestTimeout.inMilliseconds); - - _realmLib.realm_app_config_set_bundle_id(handle._pointer, getBundleId().toCharPtr(arena)); - - return handle; - }); - } - - RealmAppCredentialsHandle createAppCredentialsAnonymous(bool reuseCredentials) { - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_anonymous(reuseCredentials)); - } - - RealmAppCredentialsHandle createAppCredentialsEmailPassword(String email, String password) { - return using((arena) { - final emailPtr = email.toCharPtr(arena); - final passwordPtr = password.toRealmString(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_email_password(emailPtr, passwordPtr.ref)); - }); - } - - RealmAppCredentialsHandle createAppCredentialsJwt(String token) { - return using((arena) { - final tokenPtr = token.toCharPtr(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_jwt(tokenPtr)); - }); - } - - RealmAppCredentialsHandle createAppCredentialsApple(String idToken) { - return using((arena) { - final idTokenPtr = idToken.toCharPtr(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_apple(idTokenPtr)); - }); - } - - RealmAppCredentialsHandle createAppCredentialsFacebook(String accessToken) { - return using((arena) { - final accessTokenPtr = accessToken.toCharPtr(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_facebook(accessTokenPtr)); - }); - } - - RealmAppCredentialsHandle createAppCredentialsGoogleIdToken(String idToken) { - return using((arena) { - final idTokenPtr = idToken.toCharPtr(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_google_id_token(idTokenPtr)); - }); - } - - RealmAppCredentialsHandle createAppCredentialsGoogleAuthCode(String authCode) { - return using((arena) { - final authCodePtr = authCode.toCharPtr(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_google_auth_code(authCodePtr)); - }); - } - - RealmAppCredentialsHandle createAppCredentialsFunction(String payload) { - return using((arena) { - final payloadPtr = payload.toCharPtr(arena); - final credentialsPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_app_credentials_new_function(payloadPtr)); - return RealmAppCredentialsHandle._(credentialsPtr); - }); - } - - RealmAppCredentialsHandle createAppCredentialsApiKey(String key) { - return using((arena) { - final keyPtr = key.toCharPtr(arena); - return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_api_key(keyPtr)); - }); - } - - RealmHttpTransportHandle _createHttpTransport(HttpClient httpClient) { - final requestCallback = Pointer.fromFunction)>(_request_callback); - final requestCallbackUserdata = _realmLib.realm_dart_userdata_async_new(httpClient, requestCallback.cast(), scheduler.handle._pointer); - return RealmHttpTransportHandle._(_realmLib.realm_http_transport_new( - _realmLib.addresses.realm_dart_http_request_callback, - requestCallbackUserdata.cast(), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - } - - static void _request_callback(Object userData, realm_http_request request, Pointer request_context) { - // - // The request struct only survives until end-of-call, even though - // we explicitly call realm_http_transport_complete_request to - // mark request as completed later. - // - // Therefore we need to copy everything out of request before returning. - // We cannot clone request on the native side with realm_clone, - // since realm_http_request does not inherit from WrapC. - - final client = userData as HttpClient; - - client.connectionTimeout = Duration(milliseconds: request.timeout_ms); - - final url = Uri.parse(request.url.cast().toRealmDartString()!); - - final body = request.body.cast().toRealmDartString(length: request.body_size); - - final headers = {}; - for (int i = 0; i < request.num_headers; ++i) { - final header = request.headers[i]; - final name = header.name.cast().toRealmDartString()!; - final value = header.value.cast().toRealmDartString()!; - headers[name] = value; - } - - _request_callback_async(client, request.method, url, body, headers, request_context); - // The request struct dies here! - } - - static Future _request_callback_async( - HttpClient client, - int requestMethod, - Uri url, - String? body, - Map headers, - Pointer request_context, - ) async { - await using((arena) async { - final response_pointer = arena(); - final responseRef = response_pointer.ref; - final method = _HttpMethod.values[requestMethod]; - - try { - // Build request - late HttpClientRequest request; - - switch (method) { - case _HttpMethod.delete: - request = await client.deleteUrl(url); - break; - case _HttpMethod.put: - request = await client.putUrl(url); - break; - case _HttpMethod.patch: - request = await client.patchUrl(url); - break; - case _HttpMethod.post: - request = await client.postUrl(url); - break; - case _HttpMethod.get: - request = await client.getUrl(url); - break; - } - - for (final header in headers.entries) { - request.headers.add(header.key, header.value); - } - - if (body != null) { - request.add(utf8.encode(body)); - } - - Realm.logger.log(LogLevel.debug, "HTTP Transport: Executing ${method.name} $url"); - - final stopwatch = Stopwatch()..start(); - - // Do the call.. - final response = await request.close(); - - stopwatch.stop(); - Realm.logger.log(LogLevel.debug, "HTTP Transport: Executed ${method.name} $url: ${response.statusCode} in ${stopwatch.elapsedMilliseconds} ms"); - - final responseBody = await response.fold>([], (acc, l) => acc..addAll(l)); // gather response - - // Report back to core - responseRef.status_code = response.statusCode; - responseRef.body = responseBody.toCharPtr(arena); - responseRef.body_size = responseBody.length; - - int headerCnt = 0; - response.headers.forEach((name, values) { - headerCnt += values.length; - }); - - responseRef.headers = arena(headerCnt); - responseRef.num_headers = headerCnt; - - int index = 0; - response.headers.forEach((name, values) { - for (final value in values) { - final headerRef = responseRef.headers.elementAt(index).ref; - headerRef.name = name.toCharPtr(arena); - headerRef.value = value.toCharPtr(arena); - index++; - } - }); - - responseRef.custom_status_code = _CustomErrorCode.noError.code; - } on SocketException catch (socketEx) { - Realm.logger.log(LogLevel.warn, "HTTP Transport: SocketException executing ${method.name} $url: $socketEx"); - responseRef.custom_status_code = _CustomErrorCode.timeout.code; - } on HttpException catch (httpEx) { - Realm.logger.log(LogLevel.warn, "HTTP Transport: HttpException executing ${method.name} $url: $httpEx"); - responseRef.custom_status_code = _CustomErrorCode.unknownHttp.code; - } catch (ex) { - Realm.logger.log(LogLevel.error, "HTTP Transport: Exception executing ${method.name} $url: $ex"); - responseRef.custom_status_code = _CustomErrorCode.unknown.code; - } finally { - _realmLib.realm_http_transport_complete_request(request_context, response_pointer); - } - }); - } - - void logMessage(LogCategory category, LogLevel logLevel, String message) { - return using((arena) { - _realmLib.realm_dart_log(logLevel.index, category.toString().toCharPtr(arena), message.toCharPtr(arena)); - }); - } - - SyncClientConfigHandle _createSyncClientConfig(AppConfiguration configuration) { - return using((arena) { - final handle = SyncClientConfigHandle._(_realmLib.realm_sync_client_config_new()); - - _realmLib.realm_sync_client_config_set_base_file_path(handle._pointer, configuration.baseFilePath.path.toCharPtr(arena)); - _realmLib.realm_sync_client_config_set_metadata_mode(handle._pointer, configuration.metadataPersistenceMode.index); - _realmLib.realm_sync_client_config_set_connect_timeout(handle._pointer, configuration.maxConnectionTimeout.inMilliseconds); - if (configuration.metadataEncryptionKey != null && configuration.metadataPersistenceMode == MetadataPersistenceMode.encrypted) { - _realmLib.realm_sync_client_config_set_metadata_encryption_key(handle._pointer, configuration.metadataEncryptionKey!.toUint8Ptr(arena)); - } - return handle; - }); - } - - // TODO: - // We need a pure Dart equivalent of: - // `ServiceBinding.rootIsolateToken != null` - // to get rid of this hack. - final bool _isRootIsolate = Isolate.current.debugName == 'main'; - - static bool _firstTime = true; - AppHandle createApp(AppConfiguration configuration) { - // to avoid caching apps across hot restarts we clear the cache on the first - // call to createApp in the root isolate. - if (_firstTime && _isRootIsolate) { - _firstTime = false; - _realmLib.realm_clear_cached_apps(); - } - final httpTransportHandle = _createHttpTransport(configuration.httpClient); - final appConfigHandle = _createAppConfig(configuration, httpTransportHandle); - final syncClientConfigHandle = _createSyncClientConfig(configuration); - final realmAppPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_app_create_cached(appConfigHandle._pointer, syncClientConfigHandle._pointer)); - return AppHandle._(realmAppPtr); - } - - String getDefaultBaseUrl() { - return _realmLib.realm_app_get_default_base_url().cast().toRealmDartString()!; - } - - AppHandle? getApp(String id, String? baseUrl) { - return using((arena) { - final out_app = arena>(); - _realmLib.invokeGetBool(() => _realmLib.realm_app_get_cached(id.toCharPtr(arena), baseUrl == null ? nullptr : baseUrl.toCharPtr(arena), out_app)); - return out_app.value == nullptr ? null : AppHandle._(out_app.value); - }); - } - - String appGetId(App app) { - return _realmLib.realm_app_get_app_id(app.handle._pointer).cast().toRealmDartString()!; - } - - static void _app_user_completion_callback(Pointer userdata, Pointer user, Pointer error) { - final Completer completer = userdata.toObject(); - - if (error != nullptr) { - completer.completeWithAppError(error); - return; - } - - user = _realmLib.realm_clone(user.cast()).cast(); // take an extra reference to the user object - if (user == nullptr) { - completer.completeError(RealmException("Error while cloning user object.")); - return; - } - - completer.complete(UserHandle._(user.cast())); - } - - Pointer _createAsyncUserCallbackUserdata(Completer completer) { - final callback = Pointer.fromFunction< - Void Function( - Pointer, - Pointer, - Pointer, - )>(_app_user_completion_callback); - - final userdata = _realmLib.realm_dart_userdata_async_new( - completer, - callback.cast(), - scheduler.handle._pointer, - ); - - return userdata.cast(); - } - - Future logIn(App app, Credentials credentials) async { - final completer = Completer(); - final userdata = _createAsyncUserCallbackUserdata(completer); - - _realmLib.invokeGetBool( - () => _realmLib.realm_app_log_in_with_credentials( - app.handle._pointer, - credentials.handle._pointer, - _realmLib.addresses.realm_dart_user_completion_callback, - userdata, - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Login failed"); - - return await completer.future; - } - - static void _void_completion_callback(Pointer userdata, Pointer error) { - final Completer completer = userdata.toObject(); - - if (error != nullptr) { - completer.completeWithAppError(error); - return; - } - - completer.complete(); - } - - Future appEmailPasswordRegisterUser(App app, String email, String password) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_register_email( - app.handle._pointer, - email.toCharPtr(arena), - password.toRealmString(arena).ref, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return completer.future; - } - - Future emailPasswordConfirmUser(App app, String token, String tokenId) async { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_confirm_user( - app.handle._pointer, - token.toCharPtr(arena), - tokenId.toCharPtr(arena), - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return await completer.future; - } - - Future emailPasswordResendUserConfirmation(App app, String email) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_resend_confirmation_email( - app.handle._pointer, - email.toCharPtr(arena), - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return completer.future; - } - - Future emailPasswordCompleteResetPassword(App app, String password, String token, String tokenId) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_reset_password( - app.handle._pointer, - password.toRealmString(arena).ref, - token.toCharPtr(arena), - tokenId.toCharPtr(arena), - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return completer.future; - } - - Future emailPasswordResetPassword(App app, String email) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_send_reset_password_email( - app.handle._pointer, - email.toCharPtr(arena), - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return completer.future; - } - - Future emailPasswordCallResetPasswordFunction(App app, String email, String password, String? argsAsJSON) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_call_reset_password_function( - app.handle._pointer, - email.toCharPtr(arena), - password.toRealmString(arena).ref, - argsAsJSON != null ? argsAsJSON.toCharPtr(arena) : nullptr, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return completer.future; - } - - Future emailPasswordRetryCustomConfirmationFunction(App app, String email) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool(() => _realmLib.realm_app_email_password_provider_client_retry_custom_confirmation( - app.handle._pointer, - email.toCharPtr(arena), - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - }); - return completer.future; - } - - UserHandle? getCurrentUser(AppHandle appHandle) { - final userPtr = _realmLib.realm_app_get_current_user(appHandle._pointer); - if (userPtr == nullptr) { - return null; - } - return UserHandle._(userPtr); - } - - Future logOut(App application, User? user) { - final completer = Completer(); - if (user == null) { - _realmLib.invokeGetBool( - () => _realmLib.realm_app_log_out_current_user( - application.handle._pointer, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Logout failed"); - } else { - _realmLib.invokeGetBool( - () => _realmLib.realm_app_log_out( - application.handle._pointer, - user.handle._pointer, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Logout failed"); - } - return completer.future; - } - - void clearCachedApps() { - _realmLib.realm_clear_cached_apps(); - } - - List getUsers(App app) { - return using((arena) { - return _getUsers(app, arena); - }); - } - - List _getUsers(App app, Arena arena, {int expectedSize = 2}) { - final actualCount = arena(); - final usersPtr = arena>(expectedSize); - _realmLib.invokeGetBool(() => _realmLib.realm_app_get_all_users(app.handle._pointer, usersPtr, expectedSize, actualCount)); - - if (expectedSize < actualCount.value) { - // The supplied array was too small - resize it - arena.free(usersPtr); - return _getUsers(app, arena, expectedSize: actualCount.value); - } - - final result = []; - for (var i = 0; i < actualCount.value; i++) { - result.add(UserHandle._(usersPtr.elementAt(i).value)); - } - - return result; - } - - Future removeUser(App app, User user) { - final completer = Completer(); - _realmLib.invokeGetBool( - () => _realmLib.realm_app_remove_user( - app.handle._pointer, - user.handle._pointer, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Remove user failed"); - return completer.future; - } - - void switchUser(App application, User user) { - return using((arena) { - _realmLib.invokeGetBool( - () => _realmLib.realm_app_switch_user( - application.handle._pointer, - user.handle._pointer, - nullptr, - ), - "Switch user failed"); - }); - } - - void reconnect(App application) { - _realmLib.realm_app_sync_client_reconnect( - application.handle._pointer, - ); - } - - String? getBaseUrl(App app) { - final customDataPtr = _realmLib.realm_app_get_base_url(app.handle._pointer); - return customDataPtr.cast().toRealmDartString(freeRealmMemory: true); - } - - Future updateBaseUrl(App app, Uri baseUrl) { - final completer = Completer(); - using((arena) { - _realmLib.invokeGetBool( - () => _realmLib.realm_app_update_base_url( - app.handle._pointer, - baseUrl.toString().toCharPtr(arena), - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Update base URL failed"); - }); - return completer.future; - } - - String? userGetCustomData(User user) { - final customDataPtr = _realmLib.realm_user_get_custom_data(user.handle._pointer); - return customDataPtr.cast().toRealmDartString(freeRealmMemory: true, treatEmptyAsNull: true); - } - - Future userRefreshCustomData(App app, User user) { - final completer = Completer(); - _realmLib.invokeGetBool( - () => _realmLib.realm_app_refresh_custom_data( - app.handle._pointer, - user.handle._pointer, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Refresh custom data failed"); - return completer.future; - } - - Future userLinkCredentials(App app, User user, Credentials credentials) { - final completer = Completer(); - _realmLib.invokeGetBool( - () => _realmLib.realm_app_link_user( - app.handle._pointer, - user.handle._pointer, - credentials.handle._pointer, - _realmLib.addresses.realm_dart_user_completion_callback, - _createAsyncUserCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Link credentials failed"); - return completer.future; - } - - UserState userGetState(User user) { - final nativeUserState = _realmLib.realm_user_get_state(user.handle._pointer); - return UserState.values.fromIndex(nativeUserState); - } - - String userGetId(User user) { - final idPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_identity(user.handle._pointer), "Error while getting user id"); - final userId = idPtr.cast().toDartString(); - return userId; - } - - AppHandle userGetApp(UserHandle userHandle) { - final appPtr = _realmLib.realm_user_get_app(userHandle._pointer); - if (appPtr == nullptr) { - throw RealmException('User does not have an associated app. This is likely due to the user being logged out.'); - } - - return AppHandle._(appPtr); - } - - List userGetIdentities(User user) { - return using((arena) { - return _userGetIdentities(user, arena); - }); - } - - List _userGetIdentities(User user, Arena arena, {int expectedSize = 2}) { - final actualCount = arena(); - final identitiesPtr = arena(expectedSize); - _realmLib.invokeGetBool(() => _realmLib.realm_user_get_all_identities(user.handle._pointer, identitiesPtr, expectedSize, actualCount)); - - if (expectedSize < actualCount.value) { - // The supplied array was too small - resize it - arena.free(identitiesPtr); - return _userGetIdentities(user, arena, expectedSize: actualCount.value); - } - - final result = []; - for (var i = 0; i < actualCount.value; i++) { - final identity = identitiesPtr.elementAt(i).ref; - - result.add(UserIdentityInternal.create( - identity.id.cast().toRealmDartString(freeRealmMemory: true)!, AuthProviderTypeInternal.getByValue(identity.provider_type))); - } - - return result; - } - - Future userLogOut(User user) { - _realmLib.invokeGetBool(() => _realmLib.realm_user_log_out(user.handle._pointer), "Logout failed"); - return Future.value(); - } - - String? userGetDeviceId(User user) { - final deviceId = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_device_id(user.handle._pointer)); - return deviceId.cast().toRealmDartString(treatEmptyAsNull: true, freeRealmMemory: true); - } - - AuthProviderType userGetCredentialsProviderType(Credentials credentials) { - final provider = _realmLib.realm_auth_credentials_get_provider(credentials.handle._pointer); - return AuthProviderTypeInternal.getByValue(provider); - } - - UserProfile userGetProfileData(User user) { - final data = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_profile_data(user.handle._pointer)); - final dynamic profileData = jsonDecode(data.cast().toRealmDartString(freeRealmMemory: true)!); - return UserProfile(profileData as Map); - } - - String userGetRefreshToken(User user) { - final token = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_refresh_token(user.handle._pointer)); - return token.cast().toRealmDartString(freeRealmMemory: true)!; - } - - String userGetAccessToken(User user) { - final token = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_access_token(user.handle._pointer)); - return token.cast().toRealmDartString(freeRealmMemory: true)!; - } - - SessionHandle realmGetSession(Realm realm) { - return SessionHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_session_get(realm.handle._pointer)), realm.handle); - } - - String sessionGetPath(Session session) { - return _realmLib.realm_sync_session_get_file_path(session.handle._pointer).cast().toRealmDartString()!; - } - - SessionState sessionGetState(Session session) { - final value = _realmLib.realm_sync_session_get_state(session.handle._pointer); - return _convertCoreSessionState(value); - } - - ConnectionState sessionGetConnectionState(Session session) { - final value = _realmLib.realm_sync_session_get_connection_state(session.handle._pointer); - return ConnectionState.values[value]; - } - - UserHandle sessionGetUser(Session session) { - return UserHandle._(_realmLib.realm_sync_session_get_user(session.handle._pointer)); - } - - SessionState _convertCoreSessionState(int value) { - switch (value) { - case 0: // RLM_SYNC_SESSION_STATE_ACTIVE - case 1: // RLM_SYNC_SESSION_STATE_DYING - return SessionState.active; - case 2: // RLM_SYNC_SESSION_STATE_INACTIVE - case 3: // RLM_SYNC_SESSION_STATE_WAITING_FOR_ACCESS_TOKEN - case 4: // RLM_SYNC_SESSION_STATE_PAUSED - return SessionState.inactive; - default: - throw Exception("Unexpected SessionState: $value"); - } - } - - void sessionPause(Session session) { - _realmLib.realm_sync_session_pause(session.handle._pointer); - } - - void sessionResume(Session session) { - _realmLib.realm_sync_session_resume(session.handle._pointer); - } - - RealmSyncSessionConnectionStateNotificationTokenHandle sessionRegisterProgressNotifier( - Session session, ProgressDirection direction, ProgressMode mode, SessionProgressNotificationsController controller) { - final isStreaming = mode == ProgressMode.reportIndefinitely; - final callback = Pointer.fromFunction(_syncProgressCallback); - final userdata = _realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle._pointer); - final tokenPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_sync_session_register_progress_notifier( - session.handle._pointer, - _realmLib.addresses.realm_dart_sync_progress_callback, - direction.index, - isStreaming, - userdata.cast(), - _realmLib.addresses.realm_dart_userdata_async_free)); - return RealmSyncSessionConnectionStateNotificationTokenHandle._(tokenPtr); - } - - static void _syncProgressCallback(Object userdata, int transferred, int transferable, double estimate) { - final controller = userdata as ProgressNotificationsController; - - controller.onProgress(estimate); - } - - RealmSyncSessionConnectionStateNotificationTokenHandle sessionRegisterConnectionStateNotifier(Session session, SessionConnectionStateController controller) { - final callback = Pointer.fromFunction(_onConnectionStateChange); - final userdata = _realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle._pointer); - final notification_token = _realmLib.realm_sync_session_register_connection_state_change_callback( - session.handle._pointer, - _realmLib.addresses.realm_dart_sync_connection_state_changed_callback, - userdata.cast(), - _realmLib.addresses.realm_dart_userdata_async_free, - ); - return RealmSyncSessionConnectionStateNotificationTokenHandle._(notification_token); - } - - static void _onConnectionStateChange(Object userdata, int oldState, int newState) { - final controller = userdata as SessionConnectionStateController; - - controller.onConnectionStateChange(ConnectionState.values[oldState], ConnectionState.values[newState]); - } - - Future sessionWaitForUpload(Session session, [CancellationToken? cancellationToken]) { - final completer = CancellableCompleter(cancellationToken); - if (!completer.isCancelled) { - final callback = Pointer.fromFunction)>(_sessionWaitCompletionCallback); - final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); - _realmLib.realm_sync_session_wait_for_upload_completion(session.handle._pointer, _realmLib.addresses.realm_dart_sync_wait_for_completion_callback, - userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); - } - return completer.future; - } - - Future sessionWaitForDownload(Session session, [CancellationToken? cancellationToken]) { - final completer = CancellableCompleter(cancellationToken); - if (!completer.isCancelled) { - final callback = Pointer.fromFunction)>(_sessionWaitCompletionCallback); - final userdata = _realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle._pointer); - _realmLib.realm_sync_session_wait_for_download_completion(session.handle._pointer, _realmLib.addresses.realm_dart_sync_wait_for_completion_callback, - userdata.cast(), _realmLib.addresses.realm_dart_userdata_async_free); - } - return completer.future; - } - - static void _sessionWaitCompletionCallback(Object userdata, Pointer errorCode) { - final completer = userdata as CancellableCompleter; - if (completer.isCancelled) { - return; - } - if (errorCode != nullptr) { - // Throw RealmException instead of RealmError to be recoverable by the user. - completer.completeError(RealmException(errorCode.toSyncError().toString())); - } else { - completer.complete(); - } - } - - String getBundleId() { - readBundleId() { - try { - if (!isFlutterPlatform || Platform.environment.containsKey('FLUTTER_TEST')) { - var pubspecPath = path.join(path.current, 'pubspec.yaml'); - var pubspecFile = File(pubspecPath); - - if (pubspecFile.existsSync()) { - final pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - return pubspec.name; - } - } - - if (Platform.isAndroid) { - return _realmLib.realm_dart_get_bundle_id().cast().toDartString(); - } - - final getBundleIdFunc = _pluginLib.lookupFunction Function(), Pointer Function()>("realm_dart_get_bundle_id"); - final bundleIdPtr = getBundleIdFunc(); - return bundleIdPtr.cast().toDartString(); - } on Exception catch (_) { - //Never fail on bundleId. Use fallback value. - } - - //Fallback value - return "realm_bundle_id"; - } - - String bundleId = readBundleId(); - const salt = [82, 101, 97, 108, 109, 32, 105, 115, 32, 103, 114, 101, 97, 116]; - return base64Encode(sha256.convert([...salt, ...utf8.encode(bundleId)]).bytes); - } - - String _getAppDirectoryFromPlugin() { - assert(isFlutterPlatform); - - final getAppDirFunc = _pluginLib.lookupFunction Function(), Pointer Function()>("realm_dart_get_app_directory"); - final dirNamePtr = getAppDirFunc(); - final dirName = Platform.isWindows ? dirNamePtr.cast().toDartString() : dirNamePtr.cast().toDartString(); - - return dirName; - } - - String getAppDirectory() { - try { - if (!isFlutterPlatform || Platform.environment.containsKey('FLUTTER_TEST')) { - return Directory.current.absolute.path; // dart or flutter test - } - - // Flutter from here on.. - - if (Platform.isAndroid || Platform.isIOS) { - return getFilesPath(); - } - - if (Platform.isLinux) { - String appSupportDir = PlatformEx.fromEnvironment( - "XDG_DATA_HOME", - defaultValue: PlatformEx.fromEnvironment( - "HOME", - defaultValue: Directory.current.absolute.path, - ), - ); - return path.join(appSupportDir, ".local/share", _getAppDirectoryFromPlugin()); - } - - if (Platform.isMacOS) { - return _getAppDirectoryFromPlugin(); - } - - if (Platform.isWindows) { - return _getAppDirectoryFromPlugin(); - } - - throw UnsupportedError("Platform ${Platform.operatingSystem} is not supported"); - } catch (e) { - throw RealmException('Cannot get app directory. Error: $e'); - } - } - - Future deleteUser(App app, User user) { - final completer = Completer(); - _realmLib.invokeGetBool( - () => _realmLib.realm_app_delete_user( - app.handle._pointer, - user.handle._pointer, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - ), - "Delete user failed"); - return completer.future; - } - - bool isFrozen(Realm realm) { - return _realmLib.realm_is_frozen(realm.handle._pointer.cast()); - } - - RealmHandle freeze(Realm realm) { - final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_freeze(realm.handle._pointer)); - return RealmHandle._(ptr); - } - - RealmResultsHandle resolveResults(RealmResults realmResults, Realm frozenRealm) { - final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_results_resolve_in(realmResults.handle._pointer, frozenRealm.handle._pointer)); - return RealmResultsHandle._(ptr, frozenRealm.handle); - } - - RealmObjectHandle? resolveObject(RealmObjectBase object, Realm frozenRealm) { - return using((Arena arena) { - final resultPtr = arena>(); - _realmLib.invokeGetBool(() => _realmLib.realm_object_resolve_in(object.handle._pointer, frozenRealm.handle._pointer, resultPtr)); - return resultPtr == nullptr ? null : RealmObjectHandle._(resultPtr.value, frozenRealm.handle); - }); - } - - RealmListHandle? resolveList(ManagedRealmList list, Realm frozenRealm) { - return using((Arena arena) { - final resultPtr = arena>(); - _realmLib.invokeGetBool(() => _realmLib.realm_list_resolve_in(list.handle._pointer, frozenRealm.handle._pointer, resultPtr)); - return resultPtr == nullptr ? null : RealmListHandle._(resultPtr.value, frozenRealm.handle); - }); - } - - RealmSetHandle? resolveSet(ManagedRealmSet set, Realm frozenRealm) { - return using((Arena arena) { - final resultPtr = arena>(); - _realmLib.invokeGetBool(() => _realmLib.realm_set_resolve_in(set.handle._pointer, frozenRealm.handle._pointer, resultPtr)); - return resultPtr == nullptr ? null : RealmSetHandle._(resultPtr.value, frozenRealm.handle); - }); - } - - RealmMapHandle? resolveMap(ManagedRealmMap map, Realm frozenRealm) { - return using((Arena arena) { - final resultPtr = arena>(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_resolve_in(map.handle._pointer, frozenRealm.handle._pointer, resultPtr)); - return resultPtr == nullptr ? null : RealmMapHandle._(resultPtr.value, frozenRealm.handle); - }); - } - - static void _app_api_key_completion_callback(Pointer userdata, Pointer apiKey, Pointer error) { - final Completer completer = userdata.toObject(); - if (error != nullptr) { - completer.completeWithAppError(error); - return; - } - completer.complete(apiKey.ref.toDart()); - } - - static void _app_api_key_array_completion_callback(Pointer userdata, Pointer apiKey, int size, Pointer error) { - final Completer> completer = userdata.toObject(); - - if (error != nullptr) { - completer.completeWithAppError(error); - return; - } - - final result = []; - for (var i = 0; i < size; i++) { - result.add(apiKey[i].toDart()); - } - - completer.complete(result); - } - - Future createApiKey(User user, String name) { - return using((Arena arena) { - final namePtr = name.toCharPtr(arena); - final completer = Completer(); - _realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_create_apikey( - user.app.handle._pointer, - user.handle._pointer, - namePtr, - _realmLib.addresses.realm_dart_apikey_callback, - _createAsyncApikeyCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - - return completer.future; - }); - } - - Future fetchApiKey(User user, ObjectId id) { - return using((Arena arena) { - final completer = Completer(); - final native_id = id.toNative(arena); - _realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_fetch_apikey( - user.app.handle._pointer, - user.handle._pointer, - native_id.ref, - _realmLib.addresses.realm_dart_apikey_callback, - _createAsyncApikeyCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - - return completer.future; - }); - } - - Future> fetchAllApiKeys(User user) { - return using((Arena arena) { - final completer = Completer>(); - _realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_fetch_apikeys( - user.app.handle._pointer, - user.handle._pointer, - _realmLib.addresses.realm_dart_apikey_list_callback, - _createAsyncApikeyListCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - - return completer.future; - }); - } - - Future deleteApiKey(User user, ObjectId id) { - return using((Arena arena) { - final completer = Completer(); - final native_id = id.toNative(arena); - _realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_delete_apikey( - user.app.handle._pointer, - user.handle._pointer, - native_id.ref, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - - return completer.future; - }); - } - - Pointer _createAsyncCallbackUserdata(Completer completer) { - final callback = Pointer.fromFunction< - Void Function( - Pointer, - Pointer, - )>(_void_completion_callback); - - final userdata = _realmLib.realm_dart_userdata_async_new( - completer, - callback.cast(), - scheduler.handle._pointer, - ); - - return userdata.cast(); - } - - Pointer _createAsyncApikeyCallbackUserdata(Completer completer) { - final callback = Pointer.fromFunction< - Void Function( - Pointer, - Pointer, - Pointer, - )>(_app_api_key_completion_callback); - - final userdata = _realmLib.realm_dart_userdata_async_new( - completer, - callback.cast(), - scheduler.handle._pointer, - ); - - return userdata.cast(); - } - - Pointer _createAsyncApikeyListCallbackUserdata(Completer> completer) { - final callback = Pointer.fromFunction< - Void Function( - Pointer, - Pointer, - Size count, - Pointer, - )>(_app_api_key_array_completion_callback); - - final userdata = _realmLib.realm_dart_userdata_async_new( - completer, - callback.cast(), - scheduler.handle._pointer, - ); - - return userdata.cast(); - } - - Future disableApiKey(User user, ObjectId objectId) { - return using((Arena arena) { - final completer = Completer(); - final native_id = objectId.toNative(arena); - - _realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_disable_apikey( - user.app.handle._pointer, - user.handle._pointer, - native_id.ref, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - - return completer.future; - }); - } - - Future enableApiKey(User user, ObjectId objectId) { - return using((Arena arena) { - final completer = Completer(); - final native_id = objectId.toNative(arena); - _realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_enable_apikey( - user.app.handle._pointer, - user.handle._pointer, - native_id.ref, - _realmLib.addresses.realm_dart_void_completion_callback, - _createAsyncCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - - return completer.future; - }); - } - - static void _call_app_function_callback(Pointer userdata, Pointer response, Pointer error) { - final Completer completer = userdata.toObject(); - - if (error != nullptr) { - completer.completeWithAppError(error); - return; - } - - final stringResponse = response.cast().toRealmDartString()!; - completer.complete(stringResponse); - } - - Pointer _createAsyncFunctionCallbackUserdata(Completer completer) { - final callback = Pointer.fromFunction< - Void Function( - Pointer, - Pointer, - Pointer, - )>(_call_app_function_callback); - - final userdata = _realmLib.realm_dart_userdata_async_new( - completer, - callback.cast(), - scheduler.handle._pointer, - ); - - return userdata.cast(); - } - - Future callAppFunction(App app, User user, String functionName, String? argsAsJSON) { - return using((arena) { - final completer = Completer(); - _realmLib.invokeGetBool(() => _realmLib.realm_app_call_function( - app.handle._pointer, - user.handle._pointer, - functionName.toCharPtr(arena), - argsAsJSON?.toCharPtr(arena) ?? nullptr, - nullptr, - _realmLib.addresses.realm_dart_return_string_callback, - _createAsyncFunctionCallbackUserdata(completer), - _realmLib.addresses.realm_dart_userdata_async_free, - )); - return completer.future; - }); - } - - bool compact(Realm realm) { - return using((arena) { - final out_did_compact = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_compact(realm.handle._pointer, out_did_compact)); - return out_did_compact.value; - }); - } - - bool immediatelyRunFileActions(App app, String realmPath) { - return using((arena) { - final out_did_run = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_sync_immediately_run_file_actions(app.handle._pointer, realmPath.toCharPtr(arena), out_did_run), - "An error occurred while resetting the Realm. Check if the file is in use: '$realmPath'"); - return out_did_run.value; - }); - } - - void writeCopy(Realm realm, Configuration config) { - final configHandle = _createConfig(config); - _realmLib.invokeGetBool(() => _realmLib.realm_convert_with_config(realm.handle._pointer, configHandle._pointer, false)); - } - - void _createCollection(Realm realm, RealmValue value, Pointer Function() createList, Pointer Function() createMap) { - CollectionHandleBase? collectionHandle; - try { - switch (value.collectionType) { - case RealmCollectionType.list: - final listPointer = _realmLib.invokeGetPointer(createList); - final listHandle = RealmListHandle._(listPointer, realm.handle); - collectionHandle = listHandle; - - final list = realm.createList(listHandle, null); - - // Necessary since Core will not clear the collection if the value was already a collection - list.clear(); - - for (final item in value.value as List) { - list.add(item); - } - case RealmCollectionType.map: - final mapPointer = _realmLib.invokeGetPointer(createMap); - final mapHandle = RealmMapHandle._(mapPointer, realm.handle); - collectionHandle = mapHandle; - - final map = realm.createMap(mapHandle, null); - - // Necessary since Core will not clear the collection if the value was already a collection - map.clear(); - - for (final kvp in (value.value as Map).entries) { - map[kvp.key] = kvp.value; - } - default: - throw RealmStateError('_createCollection invoked with type that is not list or map.'); - } - } finally { - collectionHandle?.release(); - } - } - - void setLogLevel(LogLevel level, {required LogCategory category}) { - using((arena) { - _realmLib.realm_set_log_level_category(category.toString().toCharPtr(arena), level.index); - }); - } - - List getAllCategoryNames() { - return using((arena) { - final count = _realmLib.realm_get_category_names(0, nullptr); - final out_values = arena>(count); - _realmLib.realm_get_category_names(count, out_values); - return [for (int i = 0; i < count; i++) out_values[i].cast().toDartString()]; - }); - } -} - -class LastError { - final int code; - final String? message; - final Object? userError; - - LastError(this.code, [this.message, this.userError]); - - @override - String toString() => "${message ?? 'No message'}. Error code: $code."; -} - -// Flag to enable trace on finalization. -// -// Be aware that the trace is likely late, and it might in rare case be missing, -// as there are no absolute guarantees with Finalizer. -// -// It is often beneficial to also instrument the native realm_release to -// print the address released to get the exact time. -const _enableFinalizerTrace = false; - -// Level used for finalization trace, if enabled. -const _finalizerTraceLevel = LogLevel.trace; - -void _traceFinalization(Object o) { - Realm.logger.log(_finalizerTraceLevel, 'Finalizing: $o'); -} - -final _debugFinalizer = Finalizer(_traceFinalization); - -void _setupFinalizationTrace(Object value, Object finalizationToken) { - _debugFinalizer.attach(value, finalizationToken, detach: value); -} - -void _tearDownFinalizationTrace(Object value, Object finalizationToken) { - _debugFinalizer.detach(value); - _traceFinalization(finalizationToken); -} - -abstract class HandleBase implements Finalizable { - late Pointer _finalizableHandle; - Pointer _pointer; - bool get released => _pointer == nullptr; - final bool isUnowned; - - @pragma('vm:never-inline') - void keepAlive() {} - - HandleBase(this._pointer, int size) : isUnowned = false { - _finalizableHandle = _realmLib.realm_attach_finalizer(this, _pointer.cast(), size); - - if (_enableFinalizerTrace) { - _setupFinalizationTrace(this, _pointer); - } - } - - HandleBase.unowned(this._pointer) : isUnowned = true; - - @override - String toString() => "${_pointer.toString()} value=${_pointer.cast().value}${isUnowned ? ' (unowned)' : ''}"; - - /// @nodoc - /// A method that will be invoked just before the handle is released. Allows to cleanup - /// any custom data that inheritors are storing. - void _releaseCore() {} - - void release() { - if (released) { - return; - } - - _releaseCore(); - - if (!isUnowned) { - _realmLib.realm_detach_finalizer(_finalizableHandle, this); - - _realmLib.realm_release(_pointer.cast()); - } - - _pointer = nullptr; - - if (_enableFinalizerTrace) { - _tearDownFinalizationTrace(this, _pointer); - } - } -} - -class FinalizationToken { - final WeakReference root; - final int id; - - FinalizationToken(RealmHandle handle, this.id) : root = WeakReference(handle); -} - -// This finalizer is intended to prevent the list of children in the RealmHandle -// from growing endlessly. It's not intended to replace the native finalizer which -// will free the actual resources owned by the handle. -final _rootedHandleFinalizer = Finalizer((token) { - token.root.target?.removeChild(token.id); -}); - -abstract class RootedHandleBase extends HandleBase { - final RealmHandle _root; - int? _id; - - bool get shouldRoot => _root.isUnowned; - - RootedHandleBase(this._root, Pointer pointer, int size) : super(pointer, size) { - if (shouldRoot) { - _id = _root.addChild(this); - } - } - - @override - void _releaseCore() { - if (_id != null) { - _root.removeChild(_id!); - } - } -} - -abstract class CollectionHandleBase extends RootedHandleBase { - CollectionHandleBase(super.root, super.pointer, super.size); -} - -class SchemaHandle extends HandleBase { - SchemaHandle._(Pointer pointer) : super(pointer, 24); - - SchemaHandle.unowned(super.pointer) : super.unowned(); -} - -class ConfigHandle extends HandleBase { - ConfigHandle._(Pointer pointer) : super(pointer, 512); -} - -class RealmHandle extends HandleBase { - int _counter = 0; - - final Map> _children = {}; - - RealmHandle._(Pointer pointer) : super(pointer, 24); - - RealmHandle._unowned(super.pointer) : super.unowned(); - - int addChild(RootedHandleBase child) { - final id = _counter++; - _children[id] = WeakReference(child); - _rootedHandleFinalizer.attach(this, FinalizationToken(this, id), detach: this); - return id; - } - - void removeChild(int id) { - final child = _children.remove(id); - if (child != null) { - final target = child.target; - if (target != null) { - _rootedHandleFinalizer.detach(target); - } - } - } - - @override - void _releaseCore() { - final keys = _children.keys.toList(); - - for (final key in keys) { - _children[key]?.target?.release(); - } - } -} - -class SchedulerHandle extends HandleBase { - SchedulerHandle._(Pointer pointer) : super(pointer, 24); -} - -class RealmObjectHandle extends RootedHandleBase { - RealmObjectHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 112); -} - -class _RealmLinkHandle { - final int targetKey; - final int classKey; - _RealmLinkHandle._(realm_link_t link) - : targetKey = link.target, - classKey = link.target_table; -} - -class RealmResultsHandle extends RootedHandleBase { - RealmResultsHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 872); -} - -class RealmListHandle extends CollectionHandleBase { - RealmListHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 88); -} - -class RealmSetHandle extends RootedHandleBase { - RealmSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); -} - -class RealmMapHandle extends CollectionHandleBase { - RealmMapHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); // TODO: check size -} - -class _RealmQueryHandle extends RootedHandleBase { - _RealmQueryHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 256); -} - -class RealmCallbackTokenHandle extends RootedHandleBase { - RealmCallbackTokenHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 32); -} - -class RealmNotificationTokenHandle extends RootedHandleBase { - RealmNotificationTokenHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 32); -} - -class UserNotificationTokenHandle extends HandleBase { - UserNotificationTokenHandle._(Pointer pointer) : super(pointer, 32); -} - -class RealmSyncSessionConnectionStateNotificationTokenHandle extends HandleBase { - RealmSyncSessionConnectionStateNotificationTokenHandle._(Pointer pointer) : super(pointer, 32); -} - -class RealmCollectionChangesHandle extends HandleBase { - RealmCollectionChangesHandle._(Pointer pointer) : super(pointer, 256); -} - -class RealmMapChangesHandle extends HandleBase { - RealmMapChangesHandle._(Pointer pointer) : super(pointer, 256); -} - -class RealmObjectChangesHandle extends HandleBase { - RealmObjectChangesHandle._(Pointer pointer) : super(pointer, 256); -} - -class RealmAppCredentialsHandle extends HandleBase { - RealmAppCredentialsHandle._(Pointer pointer) : super(pointer, 16); -} - -class RealmHttpTransportHandle extends HandleBase { - RealmHttpTransportHandle._(Pointer pointer) : super(pointer, 24); -} - -class AppConfigHandle extends HandleBase { - AppConfigHandle._(Pointer pointer) : super(pointer, 8); -} - -class SyncClientConfigHandle extends HandleBase { - SyncClientConfigHandle._(Pointer pointer) : super(pointer, 8); -} - -class AppHandle extends HandleBase { - AppHandle._(Pointer pointer) : super(pointer, 16); -} - -class UserHandle extends HandleBase { - UserHandle._(Pointer pointer) : super(pointer, 24); -} - -class SubscriptionHandle extends HandleBase { - SubscriptionHandle._(Pointer pointer) : super(pointer, 184); -} - -class RealmAsyncOpenTaskHandle extends HandleBase { - RealmAsyncOpenTaskHandle._(Pointer pointer) : super(pointer, 32); -} - -class RealmAsyncOpenTaskProgressNotificationTokenHandle extends HandleBase { - RealmAsyncOpenTaskProgressNotificationTokenHandle._(Pointer pointer) : super(pointer, 40); -} - -class SubscriptionSetHandle extends RootedHandleBase { - @override - bool get shouldRoot => true; - - SubscriptionSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 128); -} - -class MutableSubscriptionSetHandle extends SubscriptionSetHandle { - MutableSubscriptionSetHandle._(Pointer pointer, RealmHandle root) : super._(pointer.cast(), root); - - Pointer get _mutablePointer => super._pointer.cast(); -} - -class SessionHandle extends RootedHandleBase { - @override - bool get shouldRoot => true; - - SessionHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 24); -} - -extension on List { - Pointer toCharPtr(Allocator allocator) { - return toUint8Ptr(allocator).cast(); - } - - Pointer toUint8Ptr(Allocator allocator) { - final nativeSize = length + 1; - final result = allocator(nativeSize); - final Uint8List native = result.asTypedList(nativeSize); - native.setAll(0, this); // copy - native.last = 0; // zero terminate - return result.cast(); - } -} - -extension _StringEx on String { - Pointer toCharPtr(Allocator allocator) { - final units = utf8.encode(this); - return units.toCharPtr(allocator).cast(); - } - - Pointer toRealmString(Allocator allocator) { - final realm_string = allocator(); - final units = utf8.encode(this); - realm_string.ref.data = units.toCharPtr(allocator).cast(); - realm_string.ref.size = units.length; - return realm_string; - } -} - -extension _RealmLibraryEx on RealmLibrary { - void invokeGetBool(bool Function() callback, [String? errorMessage]) { - bool success = callback(); - if (!success) { - realmCore.throwLastError(errorMessage); - } - } - - Pointer invokeGetPointer(Pointer Function() callback, [String? errorMessage]) { - final result = callback(); - if (result == nullptr) { - realmCore.throwLastError(errorMessage); - } - return result; - } -} - -Pointer _toRealmValue(Object? value, Allocator allocator) { - final realm_value = allocator(); - if (value is RealmValue && value.type.isCollection) { - throw RealmError( - "Don't use _toPrimitiveValue if the value may contain collections. Use storeValue instead. This is a bug in the Realm Flutter SDK and should be reported to https://github.com/realm/realm-dart/issues/new"); - } - _intoRealmValue(value, realm_value.ref, allocator); - return realm_value; -} - -const int _microsecondsPerSecond = 1000 * 1000; -const int _nanosecondsPerMicrosecond = 1000; - -void _intoRealmQueryArg(Object? value, Pointer realm_query_arg, Allocator allocator) { - if (value is Iterable) { - realm_query_arg.ref.nb_args = value.length; - realm_query_arg.ref.is_list = true; - realm_query_arg.ref.arg = allocator(value.length); - int i = 0; - for (var item in value) { - _intoRealmValue(item, realm_query_arg.ref.arg[i], allocator); - i++; - } - } else { - realm_query_arg.ref.arg = allocator(); - realm_query_arg.ref.nb_args = 1; - realm_query_arg.ref.is_list = false; - _intoRealmValueHack(value, realm_query_arg.ref.arg.ref, allocator); - } -} - -void _intoRealmValueHack(Object? value, realm_value realm_value, Allocator allocator) { - if (value is GeoShape) { - _intoRealmValue(value.toString(), realm_value, allocator); - } else if (value is RealmValueType) { - _intoRealmValue(value.toQueryArgString(), realm_value, allocator); - } else { - _intoRealmValue(value, realm_value, allocator); - } -} - -void _intoRealmValue(Object? value, realm_value realm_value, Allocator allocator) { - if (value == null) { - realm_value.type = realm_value_type.RLM_TYPE_NULL; - } else if (value is RealmObjectBase) { - // when converting a RealmObjectBase to realm_value.link we assume the object is managed - final link = realmCore._getObjectAsLink(value); - realm_value.values.link.target = link.targetKey; - realm_value.values.link.target_table = link.classKey; - realm_value.type = realm_value_type.RLM_TYPE_LINK; - } else if (value is int) { - realm_value.values.integer = value; - realm_value.type = realm_value_type.RLM_TYPE_INT; - } else if (value is bool) { - realm_value.values.boolean = value; - realm_value.type = realm_value_type.RLM_TYPE_BOOL; - } else if (value is String) { - String string = value; - final units = utf8.encode(string); - final result = allocator(units.length); - final Uint8List nativeString = result.asTypedList(units.length); - nativeString.setAll(0, units); - realm_value.values.string.data = result.cast(); - realm_value.values.string.size = units.length; - realm_value.type = realm_value_type.RLM_TYPE_STRING; - } else if (value is double) { - realm_value.values.dnum = value; - realm_value.type = realm_value_type.RLM_TYPE_DOUBLE; - } else if (value is ObjectId) { - final bytes = value.bytes; - for (var i = 0; i < 12; i++) { - realm_value.values.object_id.bytes[i] = bytes[i]; - } - realm_value.type = realm_value_type.RLM_TYPE_OBJECT_ID; - } else if (value is Uuid) { - final bytes = value.bytes.asUint8List(); - for (var i = 0; i < 16; i++) { - realm_value.values.uuid.bytes[i] = bytes[i]; - } - realm_value.type = realm_value_type.RLM_TYPE_UUID; - } else if (value is DateTime) { - final microseconds = value.toUtc().microsecondsSinceEpoch; - final seconds = microseconds ~/ _microsecondsPerSecond; - int nanoseconds = _nanosecondsPerMicrosecond * (microseconds % _microsecondsPerSecond); - if (microseconds < 0 && nanoseconds != 0) { - nanoseconds = nanoseconds - _nanosecondsPerMicrosecond * _microsecondsPerSecond; - } - realm_value.values.timestamp.seconds = seconds; - realm_value.values.timestamp.nanoseconds = nanoseconds; - realm_value.type = realm_value_type.RLM_TYPE_TIMESTAMP; - } else if (value is Decimal128) { - realm_value.values.decimal128 = value.value; - realm_value.type = realm_value_type.RLM_TYPE_DECIMAL128; - } else if (value is Uint8List) { - realm_value.type = realm_value_type.RLM_TYPE_BINARY; - realm_value.values.binary.size = value.length; - realm_value.values.binary.data = allocator(value.length); - realm_value.values.binary.data.asTypedList(value.length).setAll(0, value); - } else if (value is RealmValue) { - if (value.type == List) { - realm_value.type = realm_value_type.RLM_TYPE_LIST; - } else if (value.type == Map) { - realm_value.type = realm_value_type.RLM_TYPE_DICTIONARY; - } else { - return _intoRealmValue(value.value, realm_value, allocator); - } - } else { - throw RealmException("Property type ${value.runtimeType} not supported"); - } -} - -extension on Pointer { - Object? toDartValue(Realm realm, Pointer Function()? getList, Pointer Function()? getMap) { - if (this == nullptr) { - throw RealmException("Can not convert nullptr realm_value to Dart value"); - } - return ref.toDartValue(realm: realm, getList: getList, getMap: getMap); - } -} - -extension on realm_value_t { - Object? toPrimitiveValue() => toDartValue(realm: null, getList: null, getMap: null); - - Object? toDartValue({required Realm? realm, required Pointer Function()? getList, required Pointer Function()? getMap}) { - switch (type) { - case realm_value_type.RLM_TYPE_NULL: - return null; - case realm_value_type.RLM_TYPE_INT: - return values.integer; - case realm_value_type.RLM_TYPE_BOOL: - return values.boolean; - case realm_value_type.RLM_TYPE_STRING: - return values.string.data.cast().toRealmDartString(length: values.string.size)!; - case realm_value_type.RLM_TYPE_FLOAT: - return values.fnum; - case realm_value_type.RLM_TYPE_DOUBLE: - return values.dnum; - case realm_value_type.RLM_TYPE_LINK: - if (realm == null) { - throw RealmError("A realm instance is required to resolve Backlinks"); - } - final objectKey = values.link.target; - final classKey = values.link.target_table; - if (realm.metadata.getByClassKeyIfExists(classKey) == null) return null; // temprorary workaround to avoid crash on assertion - return realmCore._getObject(realm, classKey, objectKey); - case realm_value_type.RLM_TYPE_BINARY: - return Uint8List.fromList(values.binary.data.asTypedList(values.binary.size)); - case realm_value_type.RLM_TYPE_TIMESTAMP: - final seconds = values.timestamp.seconds; - final nanoseconds = values.timestamp.nanoseconds; - return DateTime.fromMicrosecondsSinceEpoch(seconds * _microsecondsPerSecond + nanoseconds ~/ _nanosecondsPerMicrosecond, isUtc: true); - case realm_value_type.RLM_TYPE_DECIMAL128: - var decimal = values.decimal128; // NOTE: Does not copy the struct! - decimal = _realmLib.realm_dart_decimal128_copy(decimal); // This is a workaround to that - return Decimal128Internal.fromNative(decimal); - case realm_value_type.RLM_TYPE_OBJECT_ID: - return ObjectId.fromBytes(values.object_id.bytes.toList(12)); - case realm_value_type.RLM_TYPE_UUID: - final listInt = values.uuid.bytes.toList(16); - return Uuid.fromBytes(Uint8List.fromList(listInt).buffer); - case realm_value_type.RLM_TYPE_LIST: - if (getList == null || realm == null) { - throw RealmException('toDartValue called with a list argument but without a list getter'); - } - - final listPointer = _realmLib.invokeGetPointer(() => getList()); - final listHandle = RealmListHandle._(listPointer, realm.handle); - return realm.createList(listHandle, null); - case realm_value_type.RLM_TYPE_DICTIONARY: - if (getMap == null || realm == null) { - throw RealmException('toDartValue called with a list argument but without a list getter'); - } - - final mapPointer = _realmLib.invokeGetPointer(() => getMap()); - final mapHandle = RealmMapHandle._(mapPointer, realm.handle); - return realm.createMap(mapHandle, null); - default: - throw RealmException("realm_value_type $type not supported"); - } - } -} - -extension on Array { - List toList(int count) { - final result = []; - for (var i = 0; i < count; i++) { - result.add(this[i]); - } - return result; - } -} - -extension on Pointer { - List toIntList(int count) { - List result = List.filled(count, elementAt(0).value); - for (var i = 1; i < count; i++) { - result[i] = elementAt(i).value; - } - return result; - } -} - -extension on Pointer { - List toStringList(int count) { - final result = List.filled(count, ''); - for (var i = 0; i < count; i++) { - final str_value = elementAt(i).ref.values.string; - result[i] = str_value.data.cast().toRealmDartString(length: str_value.size)!; - } - - return result; - } -} - -extension on Pointer { - T toObject() { - assert(this != nullptr, "Pointer is null"); - - Object object = _realmLib.realm_dart_persistent_handle_to_object(this); - - assert(object is T, "$T expected"); - return object as T; - } - - Object? toUserCodeError() { - if (this != nullptr) { - final result = toObject(); - _realmLib.realm_dart_delete_persistent_handle(this); - return result; - } - - return null; - } -} - -extension on Pointer { - String? toRealmDartString({bool treatEmptyAsNull = false, int? length, bool freeRealmMemory = false}) { - if (this == nullptr) { - return null; - } - - try { - final result = toDartString(length: length); - - if (treatEmptyAsNull && result == '') { - return null; - } - return result; - } finally { - if (freeRealmMemory) { - _realmLib.realm_free(cast()); - } - } - } -} - -extension on realm_sync_error { - SyncErrorDetails toSyncErrorDetails() { - final message = status.message.cast().toRealmDartString()!; - final userInfoMap = user_info_map.toMap(user_info_length); - final originalFilePathKey = c_original_file_path_key.cast().toRealmDartString(); - final recoveryFilePathKey = c_recovery_file_path_key.cast().toRealmDartString(); - - return SyncErrorDetails( - message, - status.error, - user_code_error.toUserCodeError(), - isFatal: is_fatal, - isClientResetRequested: is_client_reset_requested, - originalFilePath: userInfoMap?[originalFilePathKey], - backupFilePath: userInfoMap?[recoveryFilePathKey], - compensatingWrites: compensating_writes.toList(compensating_writes_length), - ); - } -} - -extension on Pointer { - Map? toMap(int length) { - if (this == nullptr) { - return null; - } - Map userInfoMap = {}; - for (int i = 0; i < length; i++) { - final userInfoItem = this[i]; - final key = userInfoItem.key.cast().toDartString(); - final value = userInfoItem.value.cast().toDartString(); - userInfoMap[key] = value; - } - return userInfoMap; - } -} - -extension on Pointer { - List? toList(int length) { - if (this == nullptr || length == 0) { - return null; - } - List compensatingWrites = []; - for (int i = 0; i < length; i++) { - final compensatingWrite = this[i]; - final reason = compensatingWrite.reason.cast().toDartString(); - final object_name = compensatingWrite.object_name.cast().toDartString(); - final primary_key = compensatingWrite.primary_key.toPrimitiveValue(); - compensatingWrites.add(CompensatingWriteInfo(object_name, reason, RealmValue.from(primary_key))); - } - return compensatingWrites; - } -} - -extension on Pointer { - SyncError toSyncError() { - final message = ref.message.cast().toDartString(); - final details = SyncErrorDetails(message, ref.error, ref.user_code_error.toUserCodeError()); - return SyncErrorInternal.createSyncError(details); - } -} - -extension on Object { - Pointer toPersistentHandle() { - return _realmLib.realm_dart_object_to_persistent_handle(this); - } -} - -extension on List { - UserState fromIndex(int index) { - if (!UserState.values.any((value) => value.index == index)) { - throw RealmError("Unknown user state $index"); - } - - return UserState.values[index]; - } -} - -extension on realm_property_info { - SchemaProperty toSchemaProperty() { - final linkTarget = link_target == nullptr ? null : link_target.cast().toDartString(); - return SchemaProperty(name.cast().toDartString(), RealmPropertyType.values[type], - optional: flags & realm_property_flags.RLM_PROPERTY_NULLABLE == realm_property_flags.RLM_PROPERTY_NULLABLE, - primaryKey: flags & realm_property_flags.RLM_PROPERTY_PRIMARY_KEY == realm_property_flags.RLM_PROPERTY_PRIMARY_KEY, - linkTarget: linkTarget == null || linkTarget.isEmpty ? null : linkTarget, - collectionType: RealmCollectionType.values[collection_type]); - } -} - -extension on Completer { - void completeWithAppError(Pointer error) { - final message = error.ref.message.cast().toRealmDartString()!; - final linkToLogs = error.ref.link_to_server_logs.cast().toRealmDartString(); - completeError(AppInternal.createException(message, linkToLogs, error.ref.http_status_code)); - } -} - -enum _CustomErrorCode { - noError(0), - unknownHttp(998), - unknown(999), - timeout(1000); - - final int code; - const _CustomErrorCode(this.code); -} - -enum _HttpMethod { - get, - post, - patch, - put, - delete, -} - -extension on realm_timestamp_t { - DateTime toDart() { - return DateTime.fromMicrosecondsSinceEpoch(seconds * 1000000 + nanoseconds ~/ 1000, isUtc: true); - } -} - -extension on realm_string_t { - String? toDart() => data.cast().toRealmDartString(); -} - -extension on ObjectId { - Pointer toNative(Allocator allocator) { - final result = allocator(); - for (var i = 0; i < 12; i++) { - result.ref.bytes[i] = bytes[i]; - } - return result; - } -} - -extension on realm_object_id { - ObjectId toDart() { - final buffer = Uint8List(12); - for (int i = 0; i < 12; ++i) { - buffer[i] = bytes[i]; - } - return ObjectId.fromBytes(buffer); - } -} - -extension on realm_app_user_apikey { - ApiKey toDart() => UserInternal.createApiKey( - id.toDart(), - name.cast().toDartString(), - key.cast().toRealmDartString(treatEmptyAsNull: true), - !disabled, - ); -} - -extension PlatformEx on Platform { - static String fromEnvironment(String name, {String defaultValue = ""}) { - final result = Platform.environment[name]; - if (result == null) { - return defaultValue; - } - - return result; - } -} - -/// @nodoc -class SyncErrorDetails { - final String message; - final int code; - final String? path; - final bool isFatal; - final bool isClientResetRequested; - final String? originalFilePath; - final String? backupFilePath; - final List? compensatingWrites; - final Object? userError; + String _getAppDirectoryFromPlugin() { + assert(isFlutterPlatform); - SyncErrorDetails( - this.message, - this.code, - this.userError, { - this.path, - this.isFatal = false, - this.isClientResetRequested = false, - this.originalFilePath, - this.backupFilePath, - this.compensatingWrites, - }); -} + final getAppDirFunc = _pluginLib.lookupFunction Function(), Pointer Function()>("realm_dart_get_app_directory"); + final dirNamePtr = getAppDirFunc(); + final dirName = Platform.isWindows ? dirNamePtr.cast().toDartString() : dirNamePtr.cast().toDartString(); -extension on realm_error { - LastError toLastError() { - final message = this.message.cast().toRealmDartString(); - return LastError(error, message, user_code_error.toUserCodeError()); + return dirName; } -} -extension on RealmValueType { - String toQueryArgString() { - return switch (this) { - RealmValueType.nullValue => 'null', - RealmValueType.boolean => 'bool', - RealmValueType.string => 'string', - RealmValueType.int => 'int', - RealmValueType.double => 'double', - RealmValueType.object => 'link', - RealmValueType.objectId => 'objectid', - RealmValueType.dateTime => 'date', - RealmValueType.decimal => 'decimal', - RealmValueType.uuid => 'uuid', - RealmValueType.binary => 'binary', - RealmValueType.list => 'array', - RealmValueType.map => 'object', - }; + String _getFilesPath() { + return realmLib.realm_dart_get_files_path().cast().toRealmDartString()!; } } diff --git a/packages/realm_dart/lib/src/native/realm_handle.dart b/packages/realm_dart/lib/src/native/realm_handle.dart new file mode 100644 index 000000000..91a4ac925 --- /dev/null +++ b/packages/realm_dart/lib/src/native/realm_handle.dart @@ -0,0 +1,452 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; +import 'dart:ffi'; + +import 'package:cancellation_token/cancellation_token.dart'; +import 'ffi.dart'; +import 'package:realm_common/realm_common.dart'; + +import '../logging.dart'; +import '../realm_class.dart'; +import '../realm_object.dart'; +import 'config_handle.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'handle_base.dart'; +import 'object_handle.dart'; +import 'query_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; +import 'rooted_handle.dart'; +import 'schema_handle.dart'; +import 'session_handle.dart'; +import 'subscription_set_handle.dart'; + +class RealmHandle extends HandleBase { + int _counter = 0; + + final Map> _children = {}; + + RealmHandle(Pointer pointer) : super(pointer, 24); + + RealmHandle.unowned(super.pointer) : super.unowned(); + + factory RealmHandle.open(Configuration config) { + final configHandle = ConfigHandle.from(config); + return RealmHandle(realmLib + .realm_open(configHandle.pointer) // + .raiseLastErrorIfNull()); + } + + int addChild(RootedHandleBase child) { + final id = _counter++; + _children[id] = WeakReference(child); + rootedHandleFinalizer.attach(this, FinalizationToken(this, id), detach: this); + return id; + } + + void removeChild(int id) { + final child = _children.remove(id); + if (child != null) { + final target = child.target; + if (target != null) { + rootedHandleFinalizer.detach(target); + } + } + } + + @override + void releaseCore() { + for (final child in _children.values.toList()) { + child.target?.release(); + } + } + + ObjectHandle createWithPrimaryKey(int classKey, Object? primaryKey) { + return using((arena) { + final realmValue = primaryKey.toNative(arena); + return ObjectHandle(realmLib.realm_object_create_with_primary_key(pointer, classKey, realmValue.ref), this); + }); + } + + ObjectHandle create(int classKey) { + return ObjectHandle(realmLib.realm_object_create(pointer, classKey), this); + } + + ObjectHandle getOrCreateWithPrimaryKey(int classKey, Object? primaryKey) { + return using((arena) { + final realmValue = primaryKey.toNative(arena); + final didCreate = arena(); + return ObjectHandle( + realmLib.realm_object_get_or_create_with_primary_key( + pointer, + classKey, + realmValue.ref, + didCreate, + ), + this, + ); + }); + } + + bool compact() { + return using((arena) { + final outDidCompact = arena(); + realmLib.realm_compact(pointer, outDidCompact).raiseLastErrorIfFalse(); + return outDidCompact.value; + }); + } + + void writeCopy(Configuration config) { + final configHandle = ConfigHandle.from(config); + realmLib.realm_convert_with_config(pointer, configHandle.pointer, false).raiseLastErrorIfFalse(); + } + + ResultsHandle queryClass(int classKey, String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + intoRealmQueryArg(args[i], argsPointer + i, arena); + } + final queryHandle = QueryHandle( + realmLib.realm_query_parse( + pointer, + classKey, + query.toCharPtr(arena), + length, + argsPointer, + ), + this, + ); + return queryHandle.findAll(); + }); + } + + RealmHandle freeze() => RealmHandle(realmLib.realm_freeze(pointer)); + + SessionHandle getSession() { + return SessionHandle(realmLib.realm_sync_session_get(pointer), this); + } + + bool get isFrozen { + return realmLib.realm_is_frozen(pointer.cast()); + } + + SubscriptionSetHandle get subscriptions { + return SubscriptionSetHandle(realmLib.realm_sync_get_active_subscription_set(pointer), this); + } + + void disableAutoRefreshForTesting() { + realmLib.realm_set_auto_refresh(pointer, false); + } + + void close() { + realmLib.realm_close(pointer).raiseLastErrorIfFalse(); + } + + bool get isClosed { + return realmLib.realm_is_closed(pointer); + } + + void beginWrite() { + realmLib.realm_begin_write(pointer).raiseLastErrorIfFalse(); + } + + void commitWrite() { + realmLib.realm_commit(pointer).raiseLastErrorIfFalse(); + } + + Future beginWriteAsync(CancellationToken? ct) { + int? id; + final completer = CancellableCompleter(ct, onCancel: () { + if (id != null) { + _cancelAsync(id!); + } + }); + if (ct?.isCancelled != true) { + using((arena) { + final transactionId = arena(); + realmLib + .realm_async_begin_write( + pointer, + Pointer.fromFunction(_completeAsyncBeginWrite), + completer.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + true, + transactionId, + ) + .raiseLastErrorIfFalse(); + id = transactionId.value; + }); + } + return completer.future; + } + + Future commitWriteAsync(CancellationToken? ct) { + int? id; + final completer = CancellableCompleter(ct, onCancel: () { + if (id != null) { + _cancelAsync(id!); + } + }); + if (ct?.isCancelled != true) { + using((arena) { + final transactionId = arena(); + realmLib + .realm_async_commit( + pointer, + Pointer.fromFunction(_completeAsyncCommit), + completer.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + false, + transactionId, + ) + .raiseLastErrorIfFalse(); + id = transactionId.value; + }); + } + return completer.future; + } + + bool _cancelAsync(int cancellationId) { + return using((arena) { + final didCancel = arena(); + realmLib.realm_async_cancel(pointer, cancellationId, didCancel).raiseLastErrorIfFalse(); + return didCancel.value; + }); + } + + static void _completeAsyncBeginWrite(Pointer userdata) { + final Completer completer = userdata.toObject(); + completer.complete(); + } + + static void _completeAsyncCommit(Pointer userdata, bool error, Pointer description) { + final Completer completer = userdata.toObject(); + if (error) { + completer.completeError(RealmException(description.cast().toDartString())); + } else { + completer.complete(); + } + } + + bool get isWritable { + return realmLib.realm_is_writable(pointer); + } + + void rollbackWrite() { + realmLib.realm_rollback(pointer).raiseLastErrorIfFalse(); + } + + bool refresh() { + return using((arena) { + final didRefresh = arena(); + realmLib.realm_refresh(pointer, didRefresh).raiseLastErrorIfFalse(); + return didRefresh.value; + }); + } + + Future refreshAsync() async { + final completer = Completer(); + final callback = Pointer.fromFunction)>(_realmRefreshAsyncCallback); + final completerPtr = realmLib.realm_dart_object_to_persistent_handle(completer); + final result = realmLib.realm_add_realm_refresh_callback(pointer, callback.cast(), completerPtr, realmLib.addresses.realm_dart_delete_persistent_handle); + + if (result == nullptr) { + return false; + } + return await completer.future; + } + + static void _realmRefreshAsyncCallback(Pointer userdata) { + if (userdata == nullptr) { + return; + } + + final completer = realmLib.realm_dart_persistent_handle_to_object(userdata) as Completer; + completer.complete(true); + } + + ResultsHandle findAll(int classKey) { + return ResultsHandle(realmLib.realm_object_find_all(pointer, classKey), this); + } + + ObjectHandle? find(int classKey, Object? primaryKey) { + return using((arena) { + final realmValue = primaryKey.toNative(arena); + final found = arena(); + final ptr = realmLib.realm_object_find_with_primary_key(pointer, classKey, realmValue.ref, found); + if (!found.value) { + assert(ptr == nullptr); // If not found, the pointer should be null. Otherwise we have a leak + return null; + } + return ObjectHandle(ptr, this); + }); + } + + ObjectHandle? findExisting(int classKey, ObjectHandle other) { + final key = realmLib.realm_object_get_key(other.pointer); + return ObjectHandle(realmLib.realm_get_object(pointer, classKey, key), this); + } + + void renameProperty(String objectType, String oldName, String newName, SchemaHandle schema) { + using((arena) { + realmLib + .realm_schema_rename_property( + pointer, + schema.pointer, + objectType.toCharPtr(arena), + oldName.toCharPtr(arena), + newName.toCharPtr(arena), + ) + .raiseLastErrorIfFalse(); + }); + } + + bool deleteType(String objectType) { + return using((arena) { + final tableDeleted = arena(); + realmLib.realm_remove_table(pointer, objectType.toCharPtr(arena), tableDeleted).raiseLastErrorIfFalse(); + return tableDeleted.value; + }); + } + + ObjectHandle getObject(int classKey, int objectKey) { + return ObjectHandle(realmLib.realm_get_object(pointer, classKey, objectKey), this); + } + + CallbackTokenHandle subscribeForSchemaNotifications(Realm realm) { + return CallbackTokenHandle( + realmLib.realm_add_schema_changed_callback( + pointer, + Pointer.fromFunction(_schemaChangeCallback), + realm.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + ), + this, + ); + } + + RealmSchema readSchema() { + return using((arena) { + return _readSchema(arena); + }); + } + + RealmSchema _readSchema(Arena arena, {int expectedSize = 10}) { + final classesPtr = arena(expectedSize); + final actualCount = arena(); + realmLib.realm_get_class_keys(pointer, classesPtr, expectedSize, actualCount).raiseLastErrorIfFalse(); + if (expectedSize < actualCount.value) { + arena.free(classesPtr); + return _readSchema(arena, expectedSize: actualCount.value); + } + + final schemas = []; + for (var i = 0; i < actualCount.value; i++) { + final classInfo = arena(); + final classKey = (classesPtr + i).value; + realmLib.realm_get_class(pointer, classKey, classInfo).raiseLastErrorIfFalse(); + + final name = classInfo.ref.name.cast().toDartString(); + final baseType = ObjectType.values.firstWhere((element) => element.flags == classInfo.ref.flags, + orElse: () => throw RealmError('No object type found for flags ${classInfo.ref.flags}')); + final schema = _getSchemaForClassKey(classKey, name, baseType, arena, expectedSize: classInfo.ref.num_properties + classInfo.ref.num_computed_properties); + schemas.add(schema); + } + + return RealmSchema(schemas); + } + + SchemaObject _getSchemaForClassKey(int classKey, String name, ObjectType baseType, Arena arena, {int expectedSize = 10}) { + final actualCount = arena(); + final propertiesPtr = arena(expectedSize); + realmLib.realm_get_class_properties(pointer, classKey, propertiesPtr, expectedSize, actualCount).raiseLastErrorIfFalse(); + + if (expectedSize < actualCount.value) { + // The supplied array was too small - resize it + arena.free(propertiesPtr); + return _getSchemaForClassKey(classKey, name, baseType, arena, expectedSize: actualCount.value); + } + + final result = []; + for (var i = 0; i < actualCount.value; i++) { + final property = (propertiesPtr + i).ref.toSchemaProperty(); + result.add(property); + } + + late Type type; + switch (baseType) { + case ObjectType.realmObject: + type = RealmObject; + break; + case ObjectType.embeddedObject: + type = EmbeddedObject; + break; + case ObjectType.asymmetricObject: + type = AsymmetricObject; + break; + default: + throw RealmError('$baseType is not supported yet'); + } + + return SchemaObject(baseType, type, name, result); + } + + Map getPropertiesMetadata(int classKey, String? primaryKeyName) { + return using((arena) { + return _getPropertiesMetadata(classKey, primaryKeyName, arena); + }); + } + + RealmObjectMetadata getObjectMetadata(SchemaObject schema) { + return using((arena) { + final found = arena(); + final classInfo = arena(); + realmLib.realm_find_class(pointer, schema.name.toCharPtr(arena), found, classInfo).raiseLastErrorIfFalse(); + final primaryKey = classInfo.ref.primary_key.cast().toRealmDartString(treatEmptyAsNull: true); + return RealmObjectMetadata(schema, classInfo.ref.key, _getPropertiesMetadata(classInfo.ref.key, primaryKey, arena)); + }); + } + + Map _getPropertiesMetadata(int classKey, String? primaryKeyName, Arena arena) { + final propertyCountPtr = arena(); + realmLib.realm_get_property_keys(pointer, classKey, nullptr, 0, propertyCountPtr).raiseLastErrorIfFalse(); + + var propertyCount = propertyCountPtr.value; + final propertiesPtr = arena(propertyCount); + realmLib.realm_get_class_properties(pointer, classKey, propertiesPtr, propertyCount, propertyCountPtr).raiseLastErrorIfFalse(); + + propertyCount = propertyCountPtr.value; + Map result = {}; + for (var i = 0; i < propertyCount; i++) { + final property = propertiesPtr + i; + final propertyName = property.ref.name.cast().toRealmDartString()!; + final objectType = property.ref.link_target.cast().toRealmDartString(treatEmptyAsNull: true); + final linkOriginProperty = property.ref.link_origin_property_name.cast().toRealmDartString(treatEmptyAsNull: true); + final isNullable = property.ref.flags & realm_property_flags.RLM_PROPERTY_NULLABLE != 0; + final isPrimaryKey = propertyName == primaryKeyName; + final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, linkOriginProperty, RealmPropertyType.values.elementAt(property.ref.type), + isNullable, isPrimaryKey, RealmCollectionType.values.elementAt(property.ref.collection_type)); + result[propertyName] = propertyMeta; + } + return result; + } +} + +class CallbackTokenHandle extends RootedHandleBase { + CallbackTokenHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 32); +} + +void _schemaChangeCallback(Pointer userdata, Pointer data) { + final Realm realm = userdata.toObject(); + try { + realm.updateSchema(); + } catch (e) { + Realm.logger.log(LogLevel.error, 'Failed to update Realm schema: $e'); + } +} diff --git a/packages/realm_dart/lib/src/native/realm_library.dart b/packages/realm_dart/lib/src/native/realm_library.dart new file mode 100644 index 000000000..678c395b1 --- /dev/null +++ b/packages/realm_dart/lib/src/native/realm_library.dart @@ -0,0 +1,24 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'ffi.dart'; +import 'package:realm_common/realm_common.dart'; +import 'package:realm_dart/src/init.dart'; +import 'package:realm_dart/src/native/realm_bindings.dart'; + +const bugInTheSdkMessage = "This is likely a bug in the Realm SDK - please file an issue at https://github.com/realm/realm-dart/issues"; + +// stamped into the library by the build system (see prepare-release.yml) +const libraryVersion = '2.3.0'; + +final realmLib = () { + final result = RealmLibrary(initRealm()); + final nativeLibraryVersion = result.realm_dart_library_version().cast().toDartString(); + if (libraryVersion != nativeLibraryVersion) { + final additionMessage = + isFlutterPlatform ? bugInTheSdkMessage : "Did you forget to run `dart run realm_dart install` after upgrading the realm_dart package?"; + throw RealmError('Realm SDK package version does not match the native library version ($libraryVersion != $nativeLibraryVersion). $additionMessage'); + } + result.realm_dart_init_debug_logger(); + return result; +}(); diff --git a/packages/realm_dart/lib/src/native/results_handle.dart b/packages/realm_dart/lib/src/native/results_handle.dart new file mode 100644 index 000000000..0f4277776 --- /dev/null +++ b/packages/realm_dart/lib/src/native/results_handle.dart @@ -0,0 +1,117 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../realm_dart.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'notification_token_handle.dart'; +import 'object_handle.dart'; +import 'query_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'rooted_handle.dart'; + +class ResultsHandle extends RootedHandleBase { + ResultsHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 872); + + ResultsHandle queryResults(String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + intoRealmQueryArg(args[i], argsPointer + i, arena); + } + final queryHandle = QueryHandle( + realmLib.realm_query_parse_for_results( + pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + root, + ); + return queryHandle.findAll(); + }); + } + + int find(Object? value) { + return using((arena) { + final outIndex = arena(); + final outFound = arena(); + + // TODO: how should this behave for collections + final realmValue = value.toNative(arena); + realmLib + .realm_results_find( + pointer, + realmValue, + outIndex, + outFound, + ) + .raiseLastErrorIfFalse(); + return outFound.value ? outIndex.value : -1; + }); + } + + ObjectHandle getObjectAt(int index) { + return ObjectHandle(realmLib.realm_results_get_object(pointer, index), root); + } + + int get count { + return using((arena) { + final countPtr = arena(); + realmLib.realm_results_count(pointer, countPtr).raiseLastErrorIfFalse(); + return countPtr.value; + }); + } + + bool isValid() { + return using((arena) { + final isValid = arena(); + realmLib.realm_results_is_valid(pointer, isValid).raiseLastErrorIfFalse(); + return isValid.value; + }); + } + + void deleteAll() { + realmLib.realm_results_delete_all(pointer).raiseLastErrorIfFalse(); + } + + ResultsHandle snapshot() { + return ResultsHandle(realmLib.realm_results_snapshot(pointer), root); + } + + ResultsHandle resolveIn(RealmHandle realmHandle) { + return ResultsHandle(realmLib.realm_results_resolve_in(pointer, realmHandle.pointer), realmHandle); + } + + Object? elementAt(Realm realm, int index) { + return using((arena) { + final realmValue = arena(); + realmLib.realm_results_get(pointer, index, realmValue).raiseLastErrorIfFalse(); + return realmValue.toDartValue( + realm, + () => realmLib.realm_results_get_list(pointer, index), + () => realmLib.realm_results_get_dictionary(pointer, index), + ); + }); + } + + NotificationTokenHandle subscribeForNotifications(NotificationsController controller) { + return NotificationTokenHandle( + realmLib.realm_results_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + nullptr, + Pointer.fromFunction(collectionChangeCallback), + ), + root, + ); + } +} diff --git a/packages/realm_dart/lib/src/native/rooted_handle.dart b/packages/realm_dart/lib/src/native/rooted_handle.dart new file mode 100644 index 000000000..810961885 --- /dev/null +++ b/packages/realm_dart/lib/src/native/rooted_handle.dart @@ -0,0 +1,41 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'handle_base.dart'; +import 'realm_handle.dart'; + +class FinalizationToken { + final WeakReference root; + final int id; + + FinalizationToken(RealmHandle handle, this.id) : root = WeakReference(handle); +} + +// This finalizer is intended to prevent the list of children in the RealmHandle +// from growing endlessly. It's not intended to replace the native finalizer which +// will free the actual resources owned by the handle. +final rootedHandleFinalizer = Finalizer((token) { + token.root.target?.removeChild(token.id); +}); + +abstract class RootedHandleBase extends HandleBase { + final RealmHandle root; + int? _id; + + bool get shouldRoot => root.isUnowned; + + RootedHandleBase(this.root, Pointer pointer, int size) : super(pointer, size) { + if (shouldRoot) { + _id = root.addChild(this); + } + } + + @override + void releaseCore() { + if (_id != null) { + root.removeChild(_id!); + } + } +} diff --git a/packages/realm_dart/lib/src/native/scheduler_handle.dart b/packages/realm_dart/lib/src/native/scheduler_handle.dart new file mode 100644 index 000000000..4ad210542 --- /dev/null +++ b/packages/realm_dart/lib/src/native/scheduler_handle.dart @@ -0,0 +1,22 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +class SchedulerHandle extends HandleBase { + SchedulerHandle._(Pointer pointer) : super(pointer, 24); + + factory SchedulerHandle(int isolateId, int sendPort) { + final schedulerPtr = realmLib.realm_dart_create_scheduler(isolateId, sendPort); + return SchedulerHandle._(schedulerPtr); + } + + void invoke(int workQueue) { + final queuePointer = Pointer.fromAddress(workQueue); + realmLib.realm_scheduler_perform_work(queuePointer); + } +} diff --git a/packages/realm_dart/lib/src/native/schema_handle.dart b/packages/realm_dart/lib/src/native/schema_handle.dart new file mode 100644 index 000000000..e11512d46 --- /dev/null +++ b/packages/realm_dart/lib/src/native/schema_handle.dart @@ -0,0 +1,90 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; +import 'package:realm_common/realm_common.dart'; + +import '../configuration.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; +import 'to_native.dart'; + +class SchemaHandle extends HandleBase { + SchemaHandle(Pointer pointer) : super(pointer, 24); + + SchemaHandle.unowned(super.pointer) : super.unowned(); + + factory SchemaHandle.from(Iterable schema) { + return using((arena) { + final classCount = schema.length; + + final schemaClasses = arena(classCount); + final schemaProperties = arena>(classCount); + + for (var i = 0; i < classCount; i++) { + final schemaObject = schema.elementAt(i); + final classInfo = (schemaClasses + i).ref; + final propertiesCount = schemaObject.length; + final computedCount = schemaObject.where((p) => p.isComputed).length; + final persistedCount = propertiesCount - computedCount; + + classInfo.name = schemaObject.name.toCharPtr(arena); + classInfo.primary_key = "".toCharPtr(arena); + classInfo.num_properties = persistedCount; + classInfo.num_computed_properties = computedCount; + classInfo.key = RLM_INVALID_CLASS_KEY; + classInfo.flags = schemaObject.baseType.flags; + + final properties = arena(propertiesCount); + + for (var j = 0; j < propertiesCount; j++) { + final schemaProperty = schemaObject[j]; + final propInfo = (properties + j).ref; + propInfo.name = schemaProperty.mapTo.toCharPtr(arena); + propInfo.public_name = (schemaProperty.mapTo != schemaProperty.name ? schemaProperty.name : '').toCharPtr(arena); + propInfo.link_target = (schemaProperty.linkTarget ?? "").toCharPtr(arena); + propInfo.link_origin_property_name = (schemaProperty.linkOriginProperty ?? "").toCharPtr(arena); + propInfo.type = schemaProperty.propertyType.index; + propInfo.collection_type = schemaProperty.collectionType.index; + propInfo.flags = realm_property_flags.RLM_PROPERTY_NORMAL; + + if (schemaProperty.optional) { + propInfo.flags |= realm_property_flags.RLM_PROPERTY_NULLABLE; + } + + switch (schemaProperty.indexType) { + case RealmIndexType.regular: + propInfo.flags |= realm_property_flags.RLM_PROPERTY_INDEXED; + break; + case RealmIndexType.fullText: + propInfo.flags |= realm_property_flags.RLM_PROPERTY_FULLTEXT_INDEXED; + break; + default: + break; + } + + if (schemaProperty.primaryKey) { + classInfo.primary_key = schemaProperty.mapTo.toCharPtr(arena); + propInfo.flags |= realm_property_flags.RLM_PROPERTY_PRIMARY_KEY; + } + } + + schemaProperties[i] = properties; + (schemaProperties + i).value = properties; + } + + return SchemaHandle(realmLib.realm_schema_new(schemaClasses, classCount, schemaProperties)); + }); + } +} + +// From realm.h. Currently not exported from the shared library +// ignore: unused_field, constant_identifier_names +const int RLM_INVALID_CLASS_KEY = 0x7FFFFFFF; +// ignore: unused_field, constant_identifier_names +const int RLM_INVALID_PROPERTY_KEY = -1; +// ignore: unused_field, constant_identifier_names +const int RLM_INVALID_OBJECT_KEY = -1; diff --git a/packages/realm_dart/lib/src/native/session_handle.dart b/packages/realm_dart/lib/src/native/session_handle.dart new file mode 100644 index 000000000..b6e6442d0 --- /dev/null +++ b/packages/realm_dart/lib/src/native/session_handle.dart @@ -0,0 +1,164 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'package:cancellation_token/cancellation_token.dart'; +import 'ffi.dart'; + +import '../realm_dart.dart'; +import '../scheduler.dart'; +import '../session.dart'; +import 'convert_native.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'rooted_handle.dart'; +import 'user_handle.dart'; + +class SessionHandle extends RootedHandleBase { + @override + bool get shouldRoot => true; + + SessionHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 24); + + String get path { + return realmLib.realm_sync_session_get_file_path(pointer).cast().toRealmDartString()!; + } + + ConnectionState get connectionState { + final value = realmLib.realm_sync_session_get_connection_state(pointer); + return ConnectionState.values[value]; + } + + UserHandle get user { + return UserHandle(realmLib.realm_sync_session_get_user(pointer)); + } + + SessionState get state { + final value = realmLib.realm_sync_session_get_state(pointer); + return _convertCoreSessionState(value); + } + + SessionState _convertCoreSessionState(int value) { + switch (value) { + case 0: // RLM_SYNC_SESSION_STATE_ACTIVE + case 1: // RLM_SYNC_SESSION_STATE_DYING + return SessionState.active; + case 2: // RLM_SYNC_SESSION_STATE_INACTIVE + case 3: // RLM_SYNC_SESSION_STATE_WAITING_FOR_ACCESS_TOKEN + case 4: // RLM_SYNC_SESSION_STATE_PAUSED + return SessionState.inactive; + default: + throw Exception("Unexpected SessionState: $value"); + } + } + + void pause() { + realmLib.realm_sync_session_pause(pointer); + } + + void resume() { + realmLib.realm_sync_session_resume(pointer); + } + + void raiseError(int errorCode, bool isFatal) { + using((arena) { + final message = "Simulated session error".toCharPtr(arena); + realmLib.realm_sync_session_handle_error_for_testing(pointer, errorCode, message, isFatal); + }); + } + + Future waitForUpload([CancellationToken? cancellationToken]) { + final completer = CancellableCompleter(cancellationToken); + if (!completer.isCancelled) { + final callback = Pointer.fromFunction)>(_waitCompletionCallback); + final userdata = realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle.pointer); + realmLib.realm_sync_session_wait_for_upload_completion( + pointer, + realmLib.addresses.realm_dart_sync_wait_for_completion_callback, + userdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ); + } + return completer.future; + } + + Future waitForDownload([CancellationToken? cancellationToken]) { + final completer = CancellableCompleter(cancellationToken); + if (!completer.isCancelled) { + final callback = Pointer.fromFunction)>(_waitCompletionCallback); + final userdata = realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle.pointer); + realmLib.realm_sync_session_wait_for_download_completion( + pointer, + realmLib.addresses.realm_dart_sync_wait_for_completion_callback, + userdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ); + } + return completer.future; + } + + static void _waitCompletionCallback(Object userdata, Pointer errorCode) { + final completer = userdata as CancellableCompleter; + if (completer.isCancelled) { + return; + } + if (errorCode != nullptr) { + // Throw RealmException instead of RealmError to be recoverable by the user. + completer.completeError(RealmException(errorCode.toDart().toString())); + } else { + completer.complete(); + } + } + + SyncSessionNotificationTokenHandle subscribeForConnectionStateNotifications(SessionConnectionStateController controller) { + final callback = Pointer.fromFunction(_onConnectionStateChange); + final userdata = realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle.pointer); + return SyncSessionNotificationTokenHandle( + realmLib.realm_sync_session_register_connection_state_change_callback( + pointer, + realmLib.addresses.realm_dart_sync_connection_state_changed_callback, + userdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ), + ); + } + + SyncSessionNotificationTokenHandle subscribeForProgressNotifications( + ProgressDirection direction, + ProgressMode mode, + SessionProgressNotificationsController controller, + ) { + final isStreaming = mode == ProgressMode.reportIndefinitely; + final callback = Pointer.fromFunction(syncProgressCallback); + final userdata = realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle.pointer); + return SyncSessionNotificationTokenHandle( + realmLib.realm_sync_session_register_progress_notifier( + pointer, + realmLib.addresses.realm_dart_sync_progress_callback, + direction.index, + isStreaming, + userdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ), + ); + } +} + +class SyncSessionNotificationTokenHandle extends HandleBase { + SyncSessionNotificationTokenHandle(Pointer pointer) : super(pointer, 32); +} + +void _onConnectionStateChange(Object userdata, int oldState, int newState) { + final controller = userdata as SessionConnectionStateController; + + controller.onConnectionStateChange(ConnectionState.values[oldState], ConnectionState.values[newState]); +} + +void syncProgressCallback(Object userdata, int transferred, int transferable, double estimate) { + final controller = userdata as ProgressNotificationsController; + + controller.onProgress(estimate); +} diff --git a/packages/realm_dart/lib/src/native/set_handle.dart b/packages/realm_dart/lib/src/native/set_handle.dart new file mode 100644 index 000000000..6135ae620 --- /dev/null +++ b/packages/realm_dart/lib/src/native/set_handle.dart @@ -0,0 +1,131 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../realm_dart.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'notification_token_handle.dart'; +import 'query_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; +import 'rooted_handle.dart'; + +class SetHandle extends RootedHandleBase { + SetHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 96); + + ResultsHandle get asResults { + return ResultsHandle(realmLib.realm_set_to_results(pointer), root); + } + + ResultsHandle query(String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + intoRealmQueryArg(args[i], argsPointer + i, arena); + } + final queryHandle = QueryHandle( + realmLib.realm_query_parse_for_set( + pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + root, + ); + return queryHandle.findAll(); + }); + } + + bool insert(Object? value) { + return using((arena) { + final realmValue = value.toNative(arena); + final outIndex = arena(); + final outInserted = arena(); + realmLib.realm_set_insert(pointer, realmValue.ref, outIndex, outInserted).raiseLastErrorIfFalse(); + return outInserted.value; + }); + } + + // TODO: avoid taking the [realm] parameter + Object? elementAt(Realm realm, int index) { + return using((arena) { + final realmValue = arena(); + realmLib.realm_set_get(pointer, index, realmValue).raiseLastErrorIfFalse(); + final result = realmValue.toDartValue( + realm, + () => throw RealmException('Sets cannot contain collections'), + () => throw RealmException('Sets cannot contain collections'), + ); + return result; + }); + } + + bool find(Object? value) { + return using((arena) { + // TODO: how should this behave for collections + final realmValue = value.toNative(arena); + final outIndex = arena(); + final outFound = arena(); + realmLib.realm_set_find(pointer, realmValue.ref, outIndex, outFound).raiseLastErrorIfFalse(); + return outFound.value; + }); + } + + bool remove(Object? value) { + return using((arena) { + // TODO: do we support sets containing mixed collections + final realmValue = value.toNative(arena); + final outErased = arena(); + realmLib.realm_set_erase(pointer, realmValue.ref, outErased).raiseLastErrorIfFalse(); + return outErased.value; + }); + } + + void clear() { + realmLib.realm_set_clear(pointer).raiseLastErrorIfFalse(); + } + + int get size { + return using((arena) { + final outSize = arena(); + realmLib.realm_set_size(pointer, outSize).raiseLastErrorIfFalse(); + return outSize.value; + }); + } + + bool get isValid { + return realmLib.realm_set_is_valid(pointer); + } + + void deleteAll() { + realmLib.realm_set_remove_all(pointer).raiseLastErrorIfFalse(); + } + + SetHandle? resolveIn(RealmHandle frozenRealm) { + return using((arena) { + final resultPtr = arena>(); + realmLib.realm_set_resolve_in(pointer, frozenRealm.pointer, resultPtr).raiseLastErrorIfFalse(); + return resultPtr == nullptr ? null : SetHandle(resultPtr.value, root); + }); + } + + NotificationTokenHandle subscribeForNotifications(NotificationsController controller) { + return NotificationTokenHandle( + realmLib.realm_set_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + nullptr, + Pointer.fromFunction(collectionChangeCallback), + ), + root, + ); + } +} diff --git a/packages/realm_dart/lib/src/native/subscription_handle.dart b/packages/realm_dart/lib/src/native/subscription_handle.dart new file mode 100644 index 000000000..06eb4cf87 --- /dev/null +++ b/packages/realm_dart/lib/src/native/subscription_handle.dart @@ -0,0 +1,28 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import '../realm_class.dart'; +import 'from_native.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +class SubscriptionHandle extends HandleBase { + SubscriptionHandle(Pointer pointer) : super(pointer, 184); + + ObjectId get id => realmLib.realm_sync_subscription_id(pointer).toDart(); + + String? get name => realmLib.realm_sync_subscription_name(pointer).toDart(); + + String get objectClassName => realmLib.realm_sync_subscription_object_class_name(pointer).toDart()!; + + String get queryString => realmLib.realm_sync_subscription_query_string(pointer).toDart()!; + + DateTime get createdAt => realmLib.realm_sync_subscription_created_at(pointer).toDart(); + + DateTime get updatedAt => realmLib.realm_sync_subscription_updated_at(pointer).toDart(); + + bool equalTo(SubscriptionHandle other) => realmLib.realm_equals(pointer.cast(), other.pointer.cast()); +} diff --git a/packages/realm_dart/lib/src/native/subscription_set_handle.dart b/packages/realm_dart/lib/src/native/subscription_set_handle.dart new file mode 100644 index 000000000..a4eadae0a --- /dev/null +++ b/packages/realm_dart/lib/src/native/subscription_set_handle.dart @@ -0,0 +1,81 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:ffi'; + +import 'package:cancellation_token/cancellation_token.dart'; +import 'ffi.dart'; + +import '../realm_dart.dart'; +import '../scheduler.dart'; +import 'convert.dart'; +import 'convert_native.dart'; +import 'error_handling.dart'; +import 'mutable_subscription_set_handle.dart'; +import 'realm_bindings.dart'; +import 'realm_handle.dart'; +import 'realm_library.dart'; +import 'results_handle.dart'; +import 'rooted_handle.dart'; +import 'subscription_handle.dart'; + +class SubscriptionSetHandle extends RootedHandleBase { + @override + bool get shouldRoot => true; + + SubscriptionSetHandle(Pointer pointer, RealmHandle root) : super(root, pointer, 128); + + void refresh() => realmLib.realm_sync_subscription_set_refresh(pointer).raiseLastErrorIfFalse(); + + int get size => realmLib.realm_sync_subscription_set_size(pointer); + + Exception? get error { + final error = realmLib.realm_sync_subscription_set_error_str(pointer); + final message = error.cast().toRealmDartString(treatEmptyAsNull: true); + return message.convert(RealmException.new); + } + + SubscriptionHandle operator [](int index) => SubscriptionHandle(realmLib.realm_sync_subscription_at(pointer, index)); + + SubscriptionHandle? findByName(String name) { + return using((arena) { + final result = realmLib.realm_sync_find_subscription_by_name( + pointer, + name.toCharPtr(arena), + ); + return result.convert(SubscriptionHandle.new); + }); + } + + SubscriptionHandle? findByResults(ResultsHandle results) { + final result = realmLib.realm_sync_find_subscription_by_results( + pointer, + results.pointer, + ); + return result.convert(SubscriptionHandle.new); + } + + int get version => realmLib.realm_sync_subscription_set_version(pointer); + + SubscriptionSetState get state => SubscriptionSetState.values[realmLib.realm_sync_subscription_set_state(pointer)]; + + MutableSubscriptionSetHandle toMutable() => MutableSubscriptionSetHandle(realmLib.realm_sync_make_subscription_set_mutable(pointer), root); + + static void _stateChangeCallback(Object userdata, int state) { + final completer = userdata as CancellableCompleter; + if (!completer.isCancelled) { + completer.complete(SubscriptionSetState.values[state]); + } + } + + Future waitForStateChange(SubscriptionSetState notifyWhen, [CancellationToken? cancellationToken]) { + final completer = CancellableCompleter(cancellationToken); + if (!completer.isCancelled) { + final callback = Pointer.fromFunction(_stateChangeCallback); + final userdata = realmLib.realm_dart_userdata_async_new(completer, callback.cast(), scheduler.handle.pointer); + realmLib.realm_sync_on_subscription_set_state_change_async(pointer, notifyWhen.index, + realmLib.addresses.realm_dart_sync_on_subscription_state_changed_callback, userdata.cast(), realmLib.addresses.realm_dart_userdata_async_free); + } + return completer.future; + } +} diff --git a/packages/realm_dart/lib/src/native/to_native.dart b/packages/realm_dart/lib/src/native/to_native.dart new file mode 100644 index 000000000..5497146fc --- /dev/null +++ b/packages/realm_dart/lib/src/native/to_native.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:realm_dart/src/native/realm_library.dart'; + +import '../realm_dart.dart'; +import '../realm_object.dart'; +import 'decimal128.dart'; +import 'realm_bindings.dart'; + +extension ListEx on List { + Pointer toCharPtr(Allocator allocator) { + return toUint8Ptr(allocator).cast(); + } + + Pointer toUint8Ptr(Allocator allocator) { + final nativeSize = length + 1; + final result = allocator(nativeSize); + final Uint8List native = result.asTypedList(nativeSize); + native.setAll(0, this); // copy + native.last = 0; // zero terminate + return result.cast(); + } +} + +extension StringEx on String { + Pointer toCharPtr(Allocator allocator) { + final units = utf8.encode(this); + return units.toCharPtr(allocator).cast(); + } + + Pointer toRealmString(Allocator allocator) { + final realmString = allocator(); + final units = utf8.encode(this); + realmString.ref.data = units.toCharPtr(allocator).cast(); + realmString.ref.size = units.length; + return realmString; + } +} + +extension NullableObjectEx on Object? { + Pointer toNative(Allocator allocator) { + final self = this; + final realmValue = allocator(); + if (self is RealmValue && self.type.isCollection) { + throw RealmError("Don't use toNative if the value may contain collections. $bugInTheSdkMessage"); + } + _intoRealmValue(self, realmValue.ref, allocator); + return realmValue; + } +} + +extension RealmValueTypeEx on RealmValueType { + String toQueryArgString() { + return switch (this) { + RealmValueType.nullValue => 'null', + RealmValueType.boolean => 'bool', + RealmValueType.string => 'string', + RealmValueType.int => 'int', + RealmValueType.double => 'double', + RealmValueType.object => 'link', + RealmValueType.objectId => 'objectid', + RealmValueType.dateTime => 'date', + RealmValueType.decimal => 'decimal', + RealmValueType.uuid => 'uuid', + RealmValueType.binary => 'binary', + RealmValueType.list => 'array', + RealmValueType.map => 'object', + }; + } +} + +const int _microsecondsPerSecond = 1000 * 1000; +const int _nanosecondsPerMicrosecond = 1000; + +void intoRealmQueryArg(Object? value, Pointer realmQueryArg, Allocator allocator) { + if (value is Iterable) { + realmQueryArg.ref.nb_args = value.length; + realmQueryArg.ref.is_list = true; + realmQueryArg.ref.arg = allocator(value.length); + int i = 0; + for (var item in value) { + _intoRealmValue(item, realmQueryArg.ref.arg[i], allocator); + i++; + } + } else { + realmQueryArg.ref.arg = allocator(); + realmQueryArg.ref.nb_args = 1; + realmQueryArg.ref.is_list = false; + _intoRealmValueHack(value, realmQueryArg.ref.arg.ref, allocator); + } +} + +void _intoRealmValueHack(Object? value, realm_value realmValue, Allocator allocator) { + if (value is GeoShape) { + _intoRealmValue(value.toString(), realmValue, allocator); + } else if (value is RealmValueType) { + _intoRealmValue(value.toQueryArgString(), realmValue, allocator); + } else { + _intoRealmValue(value, realmValue, allocator); + } +} + +void _intoRealmValue(Object? value, realm_value realmValue, Allocator allocator) { + if (value == null) { + realmValue.type = realm_value_type.RLM_TYPE_NULL; + } else if (value is RealmObjectBase) { + // when converting a RealmObjectBase to realm_value.link we assume the object is managed + final link = value.handle.asLink; + realmValue.values.link.target = link.targetKey; + realmValue.values.link.target_table = link.classKey; + realmValue.type = realm_value_type.RLM_TYPE_LINK; + } else if (value is int) { + realmValue.values.integer = value; + realmValue.type = realm_value_type.RLM_TYPE_INT; + } else if (value is bool) { + realmValue.values.boolean = value; + realmValue.type = realm_value_type.RLM_TYPE_BOOL; + } else if (value is String) { + String string = value; + final units = utf8.encode(string); + final result = allocator(units.length); + final Uint8List nativeString = result.asTypedList(units.length); + nativeString.setAll(0, units); + realmValue.values.string.data = result.cast(); + realmValue.values.string.size = units.length; + realmValue.type = realm_value_type.RLM_TYPE_STRING; + } else if (value is double) { + realmValue.values.dnum = value; + realmValue.type = realm_value_type.RLM_TYPE_DOUBLE; + } else if (value is ObjectId) { + final bytes = value.bytes; + for (var i = 0; i < 12; i++) { + realmValue.values.object_id.bytes[i] = bytes[i]; + } + realmValue.type = realm_value_type.RLM_TYPE_OBJECT_ID; + } else if (value is Uuid) { + final bytes = value.bytes.asUint8List(); + for (var i = 0; i < 16; i++) { + realmValue.values.uuid.bytes[i] = bytes[i]; + } + realmValue.type = realm_value_type.RLM_TYPE_UUID; + } else if (value is DateTime) { + final microseconds = value.toUtc().microsecondsSinceEpoch; + final seconds = microseconds ~/ _microsecondsPerSecond; + int nanoseconds = _nanosecondsPerMicrosecond * (microseconds % _microsecondsPerSecond); + if (microseconds < 0 && nanoseconds != 0) { + nanoseconds = nanoseconds - _nanosecondsPerMicrosecond * _microsecondsPerSecond; + } + realmValue.values.timestamp.seconds = seconds; + realmValue.values.timestamp.nanoseconds = nanoseconds; + realmValue.type = realm_value_type.RLM_TYPE_TIMESTAMP; + } else if (value is Decimal128) { + realmValue.values.decimal128 = value.value; + realmValue.type = realm_value_type.RLM_TYPE_DECIMAL128; + } else if (value is Uint8List) { + realmValue.type = realm_value_type.RLM_TYPE_BINARY; + realmValue.values.binary.size = value.length; + realmValue.values.binary.data = allocator(value.length); + realmValue.values.binary.data.asTypedList(value.length).setAll(0, value); + } else if (value is RealmValue) { + if (value is List) { + realmValue.type = realm_value_type.RLM_TYPE_LIST; + } else if (value is Map) { + realmValue.type = realm_value_type.RLM_TYPE_DICTIONARY; + } else { + return _intoRealmValue(value.value, realmValue, allocator); + } + } else { + throw RealmException("Property type ${value.runtimeType} not supported"); + } +} diff --git a/packages/realm_dart/lib/src/native/user_handle.dart b/packages/realm_dart/lib/src/native/user_handle.dart new file mode 100644 index 000000000..1092a0143 --- /dev/null +++ b/packages/realm_dart/lib/src/native/user_handle.dart @@ -0,0 +1,354 @@ +// Copyright 2024 MongoDB, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; + +import 'ffi.dart'; + +import '../credentials.dart'; +import '../realm_dart.dart'; +import '../scheduler.dart'; +import '../user.dart'; +import 'app_handle.dart'; +import 'convert.dart'; +import 'convert_native.dart'; +import 'credentials_handle.dart'; +import 'error_handling.dart'; +import 'handle_base.dart'; +import 'realm_bindings.dart'; +import 'realm_library.dart'; + +class UserHandle extends HandleBase { + UserHandle(Pointer pointer) : super(pointer, 24); + + AppHandle get app { + return realmLib.realm_user_get_app(pointer).convert(AppHandle.new) ?? + (throw RealmException('User does not have an associated app. This is likely due to the user being logged out.')); + } + + UserState get state { + final nativeUserState = realmLib.realm_user_get_state(pointer); + return UserState.values.fromIndex(nativeUserState); + } + + String get id { + final idPtr = realmLib.realm_user_get_identity(pointer).raiseLastErrorIfNull(); + final userId = idPtr.cast().toDartString(); + return userId; + } + + List get identities { + return using((arena) { + return _userGetIdentities(arena); + }); + } + + List _userGetIdentities(Arena arena, {int expectedSize = 2}) { + final actualCount = arena(); + final identitiesPtr = arena(expectedSize); + realmLib.realm_user_get_all_identities(pointer, identitiesPtr, expectedSize, actualCount).raiseLastErrorIfFalse(); + + if (expectedSize < actualCount.value) { + // The supplied array was too small - resize it + arena.free(identitiesPtr); + return _userGetIdentities(arena, expectedSize: actualCount.value); + } + + final result = []; + for (var i = 0; i < actualCount.value; i++) { + final identity = (identitiesPtr + i).ref; + + result.add(UserIdentityInternal.create( + identity.id.cast().toRealmDartString(freeRealmMemory: true)!, AuthProviderTypeInternal.getByValue(identity.provider_type))); + } + + return result; + } + + Future logOut() async { + realmLib.realm_user_log_out(pointer).raiseLastErrorIfFalse(); + } + + String? get deviceId { + final deviceId = realmLib.realm_user_get_device_id(pointer).raiseLastErrorIfNull(); + return deviceId.cast().toRealmDartString(treatEmptyAsNull: true, freeRealmMemory: true); + } + + UserProfile get profileData { + final data = realmLib.realm_user_get_profile_data(pointer).raiseLastErrorIfNull(); + final dynamic profileData = jsonDecode(data.cast().toRealmDartString(freeRealmMemory: true)!); + return UserProfile(profileData as Map); + } + + String get refreshToken { + final token = realmLib.realm_user_get_refresh_token(pointer).raiseLastErrorIfNull(); + return token.cast().toRealmDartString(freeRealmMemory: true)!; + } + + String get accessToken { + final token = realmLib.realm_user_get_access_token(pointer).raiseLastErrorIfNull(); + return token.cast().toRealmDartString(freeRealmMemory: true)!; + } + + String get path { + final syncConfigPtr = realmLib.realm_flx_sync_config_new(pointer).raiseLastErrorIfNull(); + try { + final path = realmLib.realm_app_sync_client_get_default_file_path_for_realm(syncConfigPtr, nullptr); + return path.cast().toRealmDartString(freeRealmMemory: true)!; + } finally { + realmLib.realm_release(syncConfigPtr.cast()); + } + } + + String? get customData { + final customDataPtr = realmLib.realm_user_get_custom_data(pointer); + return customDataPtr.cast().toRealmDartString(freeRealmMemory: true, treatEmptyAsNull: true); + } + + Future linkCredentials(AppHandle app, CredentialsHandle credentials) { + final completer = Completer(); + realmLib + .realm_app_link_user( + app.pointer, + pointer, + credentials.pointer, + realmLib.addresses.realm_dart_user_completion_callback, + createAsyncUserCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + return completer.future; + } + + Future createApiKey(AppHandle app, String name) { + return using((arena) { + final namePtr = name.toCharPtr(arena); + final completer = Completer(); + realmLib + .realm_app_user_apikey_provider_client_create_apikey( + app.pointer, + pointer, + namePtr, + realmLib.addresses.realm_dart_apikey_callback, + _createAsyncApikeyCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + + return completer.future; + }); + } + + Future fetchApiKey(AppHandle app, ObjectId id) { + return using((arena) { + final completer = Completer(); + final nativeId = id.toNative(arena); + realmLib + .realm_app_user_apikey_provider_client_fetch_apikey( + app.pointer, + pointer, + nativeId.ref, + realmLib.addresses.realm_dart_apikey_callback, + _createAsyncApikeyCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + + return completer.future; + }); + } + + Future> fetchAllApiKeys(AppHandle app) { + return using((arena) { + final completer = Completer>(); + realmLib + .realm_app_user_apikey_provider_client_fetch_apikeys( + app.pointer, + pointer, + realmLib.addresses.realm_dart_apikey_list_callback, + _createAsyncApikeyListCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + + return completer.future; + }); + } + + Future deleteApiKey(AppHandle app, ObjectId id) { + return using((arena) { + final completer = Completer(); + final nativeId = id.toNative(arena); + realmLib + .realm_app_user_apikey_provider_client_delete_apikey( + app.pointer, + pointer, + nativeId.ref, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + + return completer.future; + }); + } + + Future disableApiKey(AppHandle app, ObjectId objectId) { + return using((arena) { + final completer = Completer(); + final nativeId = objectId.toNative(arena); + + realmLib + .realm_app_user_apikey_provider_client_disable_apikey( + app.pointer, + pointer, + nativeId.ref, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + + return completer.future; + }); + } + + Future enableApiKey(AppHandle app, ObjectId objectId) { + return using((arena) { + final completer = Completer(); + final nativeId = objectId.toNative(arena); + + realmLib + .realm_app_user_apikey_provider_client_enable_apikey( + app.pointer, + pointer, + nativeId.ref, + realmLib.addresses.realm_dart_void_completion_callback, + createAsyncCallbackUserdata(completer), + realmLib.addresses.realm_dart_userdata_async_free, + ) + .raiseLastErrorIfFalse(); + + return completer.future; + }); + } + + UserNotificationTokenHandle subscribeForNotifications(UserNotificationsController controller) { + final callback = Pointer.fromFunction(_userChangeCallback); + final userdata = realmLib.realm_dart_userdata_async_new(controller, callback.cast(), scheduler.handle.pointer); + final notificationToken = realmLib.realm_sync_user_on_state_change_register_callback( + pointer, + realmLib.addresses.realm_dart_user_change_callback, + userdata.cast(), + realmLib.addresses.realm_dart_userdata_async_free, + ); + return UserNotificationTokenHandle(notificationToken); + } +} + +class UserNotificationTokenHandle extends HandleBase { + UserNotificationTokenHandle(Pointer pointer) : super(pointer, 32); +} + +void _userChangeCallback(Object userdata, int data) { + final controller = userdata as UserNotificationsController; + + controller.onUserChanged(); +} + +Pointer createAsyncUserCallbackUserdata(Completer completer) { + final callback = Pointer.fromFunction< + Void Function( + Pointer, + Pointer, + Pointer, + )>(_appUserCompletionCallback); + + final userdata = realmLib.realm_dart_userdata_async_new( + completer, + callback.cast(), + scheduler.handle.pointer, + ); + + return userdata.cast(); +} + +void _appUserCompletionCallback(Pointer userdata, Pointer user, Pointer error) { + final Completer completer = userdata.toObject(); + + if (error != nullptr) { + completer.completeWithAppError(error); + return; + } + + user = realmLib.realm_clone(user.cast()).cast(); // take an extra reference to the user object + if (user == nullptr) { + completer.completeError(RealmException("Error while cloning user object.")); + return; + } + + completer.complete(UserHandle(user.cast())); +} + +Pointer _createAsyncApikeyCallbackUserdata(Completer completer) { + final callback = Pointer.fromFunction< + Void Function( + Pointer, + Pointer, + Pointer, + )>(_appApiKeyCompletionCallback); + + final userdata = realmLib.realm_dart_userdata_async_new( + completer, + callback.cast(), + scheduler.handle.pointer, + ); + + return userdata.cast(); +} + +void _appApiKeyCompletionCallback(Pointer userdata, Pointer apiKey, Pointer error) { + final Completer completer = userdata.toObject(); + if (error != nullptr) { + completer.completeWithAppError(error); + return; + } + completer.complete(apiKey.ref.toDart()); +} + +Pointer _createAsyncApikeyListCallbackUserdata(Completer> completer) { + final callback = Pointer.fromFunction< + Void Function( + Pointer, + Pointer, + Size count, + Pointer, + )>(_appApiKeyArrayCompletionCallback); + + final userdata = realmLib.realm_dart_userdata_async_new( + completer, + callback.cast(), + scheduler.handle.pointer, + ); + + return userdata.cast(); +} + +void _appApiKeyArrayCompletionCallback(Pointer userdata, Pointer apiKey, int size, Pointer error) { + final Completer> completer = userdata.toObject(); + + if (error != nullptr) { + completer.completeWithAppError(error); + return; + } + + final result = []; + for (var i = 0; i < size; i++) { + result.add(apiKey[i].toDart()); + } + + completer.complete(result); +} diff --git a/packages/realm_dart/lib/src/realm_class.dart b/packages/realm_dart/lib/src/realm_class.dart index 7141edb55..5a50f5b35 100644 --- a/packages/realm_dart/lib/src/realm_class.dart +++ b/packages/realm_dart/lib/src/realm_class.dart @@ -13,7 +13,15 @@ import 'configuration.dart'; import 'list.dart'; import 'logging.dart'; import 'map.dart'; +import 'native/async_open_task_handle.dart'; +import 'native/handle_base.dart'; +import 'native/list_handle.dart'; +import 'native/map_handle.dart'; +import 'native/notification_token_handle.dart'; +import 'native/object_handle.dart'; import 'native/realm_core.dart'; +import 'native/realm_handle.dart'; +import 'native/set_handle.dart'; import 'realm_object.dart'; import 'results.dart'; import 'scheduler.dart'; @@ -30,6 +38,7 @@ export 'package:realm_common/realm_common.dart' GeoCircle, GeoDistance, GeoPoint, + GeoPolygon, GeoRing, GeoShape, Ignored, @@ -55,6 +64,7 @@ export 'app.dart' show AppException, App, MetadataPersistenceMode, AppConfigurat export 'collections.dart' show Move; export "configuration.dart" show + encryptionKeySize, AfterResetCallback, BeforeResetCallback, ClientResetCallback, @@ -83,7 +93,7 @@ export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges, ListExte export 'logging.dart' hide RealmLoggerInternal; export 'map.dart' show RealmMap, RealmMapChanges, RealmMapOfObject; export 'migration.dart' show Migration; -export 'native/realm_core.dart' show Decimal128; +export 'native/decimal128.dart' show Decimal128; export 'realm_object.dart' show AsymmetricObject, @@ -110,7 +120,7 @@ class Realm implements Finalizable { late final RealmMetadata _metadata; late final RealmHandle _handle; final bool _isInMigration; - late final RealmCallbackTokenHandle? _schemaCallbackHandle; + late final CallbackTokenHandle? _schemaCallbackHandle; final List> _schemaChangeListeners = []; /// An object encompassing this `Realm` instance's dynamic API. @@ -126,7 +136,7 @@ class Realm implements Finalizable { /// Gets a value indicating whether this [Realm] is frozen. Frozen Realms are immutable /// and will not update when writes are made to the database. - late final bool isFrozen = realmCore.isFrozen(this); + late final bool isFrozen = handle.isFrozen; /// Opens a `Realm` using a [Configuration] object. Realm(Configuration config) : this._(config); @@ -140,7 +150,7 @@ class Realm implements Finalizable { // schema, so even if we subscribe, we wouldn't be able to see the updates. if (config is FlexibleSyncConfiguration || config.schemaObjects.isEmpty) { // TODO: enable once https://github.com/realm/realm-core/issues/7426 is fixed. - // _schemaCallbackHandle = realmCore.subscribeForSchemaNotifications(this); + //_schemaCallbackHandle = handle!.subscribeForSchemaNotifications(this); _schemaCallbackHandle = null; } else { _schemaCallbackHandle = null; @@ -172,7 +182,7 @@ class Realm implements Finalizable { _ensureDirectory(config); - final asyncOpenHandle = realmCore.createRealmAsyncOpenTask(config); + final asyncOpenHandle = AsyncOpenTaskHandle.from(config); return await CancellableFuture.from(() async { if (cancellationToken != null && cancellationToken.isCancelled) { throw cancellationToken.exception!; @@ -187,17 +197,17 @@ class Realm implements Finalizable { late final RealmHandle realmHandle; try { - realmHandle = await realmCore.openRealmAsync(asyncOpenHandle, cancellationToken); + realmHandle = await asyncOpenHandle.openAsync(cancellationToken); return Realm._(config, realmHandle); } finally { await progressSubscription?.cancel(); } - }, cancellationToken, onCancel: () => realmCore.cancelOpenRealmAsync(asyncOpenHandle)); + }, cancellationToken, onCancel: () => asyncOpenHandle.cancel()); } static RealmHandle _openRealm(Configuration config) { _ensureDirectory(config); - return realmCore.openRealm(config); + return RealmHandle.open(config); } static void _ensureDirectory(Configuration config) { @@ -208,8 +218,8 @@ class Realm implements Finalizable { } void _populateMetadata() { - schema = config.schemaObjects.isNotEmpty ? RealmSchema(config.schemaObjects) : realmCore.readSchema(this); - _metadata = RealmMetadata._(schema.map((c) => realmCore.getObjectMetadata(this, c))); + schema = config.schemaObjects.isNotEmpty ? RealmSchema(config.schemaObjects) : handle.readSchema(); + _metadata = RealmMetadata._(schema.map((c) => handle.getObjectMetadata(c))); } /// Deletes all files associated with a `Realm` located at given [path] @@ -282,16 +292,16 @@ class Realm implements Finalizable { object.manage(this, handle, accessor, false); } - RealmObjectHandle _createObject(RealmObjectBase object, RealmObjectMetadata metadata, bool update) { + ObjectHandle _createObject(RealmObjectBase object, RealmObjectMetadata metadata, bool update) { final key = metadata.classKey; final primaryKey = metadata.primaryKey; if (primaryKey == null) { - return realmCore.createRealmObject(this, key); + return handle.create(key); } if (update) { - return realmCore.getOrCreateRealmObjectWithPrimaryKey(this, key, object.accessor.get(object, primaryKey)); + return _handle.getOrCreateWithPrimaryKey(key, object.accessor.get(object, primaryKey)); } - return realmCore.createRealmObjectWithPrimaryKey(this, key, object.accessor.get(object, primaryKey)); + return _handle.createWithPrimaryKey(key, object.accessor.get(object, primaryKey)); } /// Adds a collection [RealmObject]s to this `Realm`. @@ -316,7 +326,7 @@ class Realm implements Finalizable { _ensureManagedByThis(object, 'delete object from Realm'); - realmCore.deleteRealmObject(object); + object.handle.delete(); } /// Deletes many [RealmObject]s from this `Realm`. @@ -327,15 +337,15 @@ class Realm implements Finalizable { if (items is RealmResults) { _ensureManagedByThis(items, 'delete objects from Realm'); - realmCore.resultsDeleteAll(items); + items.handle.deleteAll(); } else if (items is ManagedRealmList) { _ensureManagedByThis(items, 'delete objects from Realm'); - realmCore.listDeleteAll(items); + items.handle.deleteAll(); } else if (items is ManagedRealmSet) { _ensureManagedByThis(items, 'delete objects from Realm'); - realmCore.realmSetRemoveAll(items); + items.handle.deleteAll(); } else { for (T realmObject in items) { delete(realmObject); @@ -344,7 +354,7 @@ class Realm implements Finalizable { } /// Checks whether the `Realm` is in write transaction. - bool get isInTransaction => realmCore.getIsWritable(this); + bool get isInTransaction => handle.isWritable; /// Synchronously calls the provided callback inside a write transaction. /// @@ -365,14 +375,14 @@ class Realm implements Finalizable { /// Begins a write transaction for this [Realm]. Transaction beginWrite() { - realmCore.beginWrite(this); + handle.beginWrite(); return Transaction._(this); } /// Asynchronously begins a write transaction for this [Realm]. You can supply a /// [CancellationToken] to cancel the operation. Future beginWriteAsync([CancellationToken? cancellationToken]) async { - await realmCore.beginWriteAsync(this, cancellationToken); + await handle.beginWriteAsync(cancellationToken); return Transaction._(this); } @@ -403,18 +413,18 @@ class Realm implements Finalizable { } _schemaCallbackHandle?.release(); - realmCore.closeRealm(this); + handle.close(); handle.release(); } /// Checks whether the `Realm` is closed. - bool get isClosed => _handle.released || realmCore.isRealmClosed(this); + bool get isClosed => _handle.released || handle.isClosed; /// Fast lookup for a [RealmObject] with the specified [primaryKey]. T? find(Object? primaryKey) { final metadata = _metadata.getByType(T); - final handle = realmCore.find(this, metadata.classKey, primaryKey); + final handle = _handle.find(metadata.classKey, primaryKey); if (handle == null) { return null; } @@ -429,7 +439,7 @@ class Realm implements Finalizable { /// The returned [RealmResults] allows iterating all the values without further filtering. RealmResults all() { final metadata = _metadata.getByType(T); - final handle = realmCore.findAll(this, metadata.classKey); + final handle = this.handle.findAll(metadata.classKey); return RealmResultsInternal.create(handle, this, metadata); } @@ -439,8 +449,11 @@ class Realm implements Finalizable { /// and [Predicate Programming Guide.](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789) RealmResults query(String query, [List args = const []]) { final metadata = _metadata.getByType(T); - final handle = realmCore.queryClass(this, metadata.classKey, query, args); - return RealmResultsInternal.create(handle, this, metadata); + return RealmResultsInternal.create( + _handle.queryClass(metadata.classKey, query, args), + this, + metadata, + ); } /// Deletes all [RealmObject]s of type `T` in the `Realm` @@ -462,7 +475,7 @@ class Realm implements Finalizable { return this; } - return Realm._(config, realmCore.freeze(this)); + return Realm._(config, handle.freeze()); } WeakReference? _subscriptions; @@ -476,8 +489,8 @@ class Realm implements Finalizable { var result = _subscriptions?.target; if (result == null || result.handle.released) { - result = SubscriptionSetInternal.create(this, realmCore.getSubscriptions(this)); - realmCore.refreshSubscriptionSet(result); + result = SubscriptionSetInternal.create(this, handle.subscriptions); + result.handle.refresh(); _subscriptions = WeakReference(result); } @@ -496,7 +509,7 @@ class Realm implements Finalizable { var result = _syncSession?.target; if (result == null || result.handle.released) { - result = SessionInternal.create(realmCore.realmGetSession(this)); + result = SessionInternal.create(handle.getSession()); _syncSession = WeakReference(result); } @@ -508,7 +521,7 @@ class Realm implements Finalizable { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! Realm) return false; - return realmCore.realmEquals(this, other); + return handle == other.handle; } /// The logger that will emit log messages from the database and sync operations. @@ -559,7 +572,7 @@ class Realm implements Finalizable { realm.syncSession.pause(); } try { - return realmCore.compact(realm); + return realm.handle.compact(); } finally { realm.close(); } @@ -579,7 +592,7 @@ class Realm implements Finalizable { throw RealmError("Copying a Realm is not allowed within a write transaction or during migration."); } - realmCore.writeCopy(this, config); + handle.writeCopy(config); } /// Update the `Realm` instance and outstanding objects to point to the most recent persisted version. @@ -589,7 +602,7 @@ class Realm implements Finalizable { /// Typically you don't need to call this method since Realm has auto-refresh built-in. /// Note that this may return `true` even if no data has actually changed. bool refresh() { - return realmCore.realmRefresh(this); + return handle.refresh(); } /// Returns a [Future] that will complete when the `Realm` is refreshed to the version which is the @@ -597,7 +610,7 @@ class Realm implements Finalizable { /// /// Note that this may return `true` even if no data has actually changed. Future refreshAsync() async { - return realmCore.realmRefreshAsync(this); + return handle.refreshAsync(); } /// Allows listening for schema changes on this Realm. Only dynamic and synchronized @@ -617,7 +630,7 @@ class Realm implements Finalizable { } void _updateSchema() { - final newSchema = realmCore.readSchema(this); + final newSchema = handle.readSchema(); for (final listener in _schemaChangeListeners) { listener.add(RealmSchemaChanges._(schema, newSchema)); } @@ -626,11 +639,11 @@ class Realm implements Finalizable { final existing = schema.firstWhereOrNull((s) => s.name == schemaObject.name); if (existing == null) { schema.add(schemaObject); - final meta = realmCore.getObjectMetadata(this, schemaObject); + final meta = handle.getObjectMetadata(schemaObject); metadata._add(meta); } else if (schemaObject.length > existing.length) { final existingMeta = metadata.getByName(schemaObject.name); - final propertyMeta = realmCore.getPropertiesMetadata(this, existingMeta.classKey, existingMeta.primaryKey); + final propertyMeta = handle.getPropertiesMetadata(existingMeta.classKey, existingMeta.primaryKey); for (final property in schemaObject) { final existingProp = existing.firstWhereOrNull((e) => e.mapTo == property.mapTo); if (existingProp == null) { @@ -641,6 +654,8 @@ class Realm implements Finalizable { } } } + + void disableAutoRefreshForTesting() => handle.disableAutoRefreshForTesting(); } /// Describes the schema changes that occurred on a Realm @@ -670,7 +685,7 @@ class Transaction { void commit() { final realm = _ensureOpen('commit'); - realmCore.commitWrite(realm); + realm.handle.commitWrite(); _closeTransaction(); } @@ -681,7 +696,7 @@ class Transaction { Future commitAsync([CancellationToken? cancellationToken]) async { final realm = _ensureOpen('commitAsync'); - await realmCore.commitWriteAsync(realm, cancellationToken); + await realm.handle.commitWriteAsync(cancellationToken); _closeTransaction(); } @@ -691,7 +706,7 @@ class Transaction { final realm = _ensureOpen('rollback'); if (!realm.isClosed) { - realmCore.rollbackWrite(realm); + realm.handle.rollbackWrite(); } _closeTransaction(); @@ -733,25 +748,25 @@ extension RealmInternal on Realm { return Realm._(config, handle, isInMigration); } - RealmObjectBase createObject(Type type, RealmObjectHandle handle, RealmObjectMetadata metadata) { + RealmObjectBase createObject(Type type, ObjectHandle handle, RealmObjectMetadata metadata) { final accessor = RealmCoreAccessor(metadata, _isInMigration); return RealmObjectInternal.create(type, this, handle, accessor); } - RealmList createList(RealmListHandle handle, RealmObjectMetadata? metadata) { + RealmList createList(ListHandle handle, RealmObjectMetadata? metadata) { return RealmListInternal.create(handle, this, metadata); } - RealmSet createSet(RealmSetHandle handle, RealmObjectMetadata? metadata) { + RealmSet createSet(SetHandle handle, RealmObjectMetadata? metadata) { return RealmSetInternal.create(handle, this, metadata); } - RealmMap createMap(RealmMapHandle handle, RealmObjectMetadata? metadata) { + RealmMap createMap(MapHandle handle, RealmObjectMetadata? metadata) { return RealmMapInternal.create(handle, this, metadata); } - List getPropertyNames(Type type, List propertyKeys) { - final metadata = _metadata.getByType(type); + List getPropertyNames(RealmObjectBase obj, List propertyKeys) { + final metadata = _metadata.tryGetByType(obj.runtimeType) ?? _metadata.getByName(obj.objectSchema.name); final result = []; for (var key in propertyKeys) { final name = metadata.getPropertyName(key); @@ -764,7 +779,7 @@ extension RealmInternal on Realm { RealmMetadata get metadata => _metadata; - void manageEmbedded(RealmObjectHandle handle, EmbeddedObject object, {bool update = false}) { + void manageEmbedded(ObjectHandle handle, EmbeddedObject object, {bool update = false}) { final metadata = _metadata.getByType(object.runtimeType); final accessor = RealmCoreAccessor(metadata, _isInMigration); @@ -774,8 +789,8 @@ extension RealmInternal on Realm { /// This should only be used for testing RealmResults allEmbedded() { final metadata = _metadata.getByType(T); - final handle = realmCore.findAll(this, metadata.classKey); - return RealmResultsInternal.create(handle, this, metadata); + final resultsHandle = handle.findAll(metadata.classKey); + return RealmResultsInternal.create(resultsHandle, this, metadata); } T? resolveObject(T object) { @@ -787,46 +802,46 @@ extension RealmInternal on Realm { throw RealmStateError("Can't resolve invalidated (deleted) objects"); } - final handle = realmCore.resolveObject(object, this); - if (handle == null) { + final newHandle = object.handle.resolveIn(handle); + if (newHandle == null) { return null; } final metadata = (object.accessor as RealmCoreAccessor).metadata; - return RealmObjectInternal.create(T, this, handle, RealmCoreAccessor(metadata, _isInMigration)) as T; + return RealmObjectInternal.create(T, this, newHandle, RealmCoreAccessor(metadata, _isInMigration)) as T; } RealmList? resolveList(ManagedRealmList list) { - final handle = realmCore.resolveList(list, this); - if (handle == null) { + final newHandle = list.handle.resolveIn(handle); + if (newHandle == null) { return null; } - return createList(handle, list.metadata); + return createList(newHandle, list.metadata); } RealmResults resolveResults(RealmResults results) { - final handle = realmCore.resolveResults(results, this); - return RealmResultsInternal.create(handle, this, results.metadata); + final newHandle = results.handle.resolveIn(handle); + return RealmResultsInternal.create(newHandle, this, results.metadata); } RealmSet? resolveSet(ManagedRealmSet set) { - final handle = realmCore.resolveSet(set, this); - if (handle == null) { + final newHandle = set.handle.resolveIn(handle); + if (newHandle == null) { return null; } - return createSet(handle, set.metadata); + return createSet(newHandle, set.metadata); } RealmMap? resolveMap(ManagedRealmMap map) { - final handle = realmCore.resolveMap(map, this); - if (handle == null) { + final newHandle = map.handle.resolveIn(handle); + if (newHandle == null) { return null; } - return createMap(handle, map.metadata); + return createMap(newHandle, map.metadata); } static MigrationRealm getMigrationRealm(Realm realm) => MigrationRealm._(realm); @@ -850,9 +865,9 @@ extension RealmInternal on Realm { /// @nodoc abstract class NotificationsController implements Finalizable { - RealmNotificationTokenHandle? handle; + NotificationTokenHandle? handle; - RealmNotificationTokenHandle subscribe(); + NotificationTokenHandle subscribe(); void onChanges(HandleBase changesHandle); void onError(RealmError error); @@ -907,6 +922,8 @@ class RealmMetadata { return metadata; } + RealmObjectMetadata? tryGetByType(Type type) => _typeMap[type]; + RealmObjectMetadata getByName(String type) { var metadata = _stringMap[type]; if (metadata == null) { @@ -947,7 +964,7 @@ class DynamicRealm { /// The returned [RealmResults] allows iterating all the values without further filtering. RealmResults all(String className) { final metadata = _realm._metadata.getByName(className); - final handle = realmCore.findAll(_realm, metadata.classKey); + final handle = _realm.handle.findAll(metadata.classKey); return RealmResultsInternal.create(handle, _realm, metadata); } @@ -955,7 +972,7 @@ class DynamicRealm { RealmObject? find(String className, Object primaryKey) { final metadata = _realm._metadata.getByName(className); - final handle = realmCore.find(_realm, metadata.classKey, primaryKey); + final handle = _realm.handle.find(metadata.classKey, primaryKey); if (handle == null) { return null; } @@ -963,6 +980,29 @@ class DynamicRealm { final accessor = RealmCoreAccessor(metadata, _realm._isInMigration); return RealmObjectInternal.create(RealmObject, _realm, handle, accessor) as RealmObject; } + + /// Creates a managed RealmObject with the specified [className] and [primaryKey]. + RealmObject create(String className, {Object? primaryKey}) { + final metadata = _realm._metadata.getByName(className); + + ObjectHandle handle; + if (metadata.primaryKey != null) { + if (primaryKey == null) { + throw RealmError("The class $className has primary key defined, but you didn't pass one"); + } + + handle = _realm._handle.createWithPrimaryKey(metadata.classKey, primaryKey); + } else { + if (primaryKey != null) { + throw RealmError("The class $className doesn't have primary key defined, but you passed $primaryKey"); + } + + handle = _realm._handle.create(metadata.classKey); + } + + final accessor = RealmCoreAccessor(metadata, _realm._isInMigration); + return RealmObjectInternal.create(RealmObject, _realm, handle, accessor) as RealmObject; + } } /// A class used during a migration callback. It exposes a set of dynamic API as @@ -990,8 +1030,8 @@ typedef ProgressCallback = void Function(SyncProgress syncProgress); /// @nodoc class RealmAsyncOpenProgressNotificationsController implements ProgressNotificationsController { - final RealmAsyncOpenTaskHandle _handle; - RealmAsyncOpenTaskProgressNotificationTokenHandle? _tokenHandle; + final AsyncOpenTaskHandle _handle; + AsyncOpenTaskProgressNotificationTokenHandle? _tokenHandle; late final StreamController _streamController; RealmAsyncOpenProgressNotificationsController._(this._handle); @@ -1011,7 +1051,7 @@ class RealmAsyncOpenProgressNotificationsController implements ProgressNotificat throw RealmStateError("Progress subscription already started."); } - _tokenHandle = realmCore.realmAsyncOpenRegisterAsyncOpenProgressNotifier(_handle, this); + _tokenHandle = _handle.registerProgressNotifier(this); } void _stop() { diff --git a/packages/realm_dart/lib/src/realm_object.dart b/packages/realm_dart/lib/src/realm_object.dart index 6b0ed22c0..2e43d28d1 100644 --- a/packages/realm_dart/lib/src/realm_object.dart +++ b/packages/realm_dart/lib/src/realm_object.dart @@ -9,7 +9,10 @@ import 'package:realm_common/realm_common.dart'; import 'configuration.dart'; import 'list.dart'; -import 'native/realm_core.dart'; +import 'native/handle_base.dart'; +import 'native/notification_token_handle.dart'; +import 'native/object_handle.dart'; +import 'native/realm_library.dart'; import 'realm_class.dart'; import 'results.dart'; import 'map.dart'; @@ -156,11 +159,11 @@ class RealmCoreAccessor implements RealmAccessor { if (propertyMeta.propertyType == RealmPropertyType.linkingObjects) { final sourceMeta = object.realm.metadata.getByName(propertyMeta.objectType!); final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; - final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); + final handle = object.handle.getBacklinks(sourceMeta.classKey, sourceProperty.key); return RealmResultsInternal.create(handle, object.realm, sourceMeta); } - final handle = realmCore.getListProperty(object, propertyMeta.key); + final handle = object.handle.getList(propertyMeta.key); final listMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); if (propertyMeta.propertyType == RealmPropertyType.mixed) { @@ -184,7 +187,7 @@ class RealmCoreAccessor implements RealmAccessor { } return object.realm.createList(handle, listMetadata); case RealmCollectionType.set: - final handle = realmCore.getSetProperty(object, propertyMeta.key); + final handle = object.handle.getSet(propertyMeta.key); final setMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); if (setMetadata != null && _isTypeGenericObject()) { switch (setMetadata.schema.baseType) { @@ -201,7 +204,7 @@ class RealmCoreAccessor implements RealmAccessor { return object.realm.createSet(handle, setMetadata); case RealmCollectionType.map: - final handle = realmCore.getMapProperty(object, propertyMeta.key); + final handle = object.handle.getMap(propertyMeta.key); final mapMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); if (propertyMeta.propertyType == RealmPropertyType.mixed) { @@ -225,9 +228,9 @@ class RealmCoreAccessor implements RealmAccessor { } return object.realm.createMap(handle, mapMetadata); default: - var value = realmCore.getProperty(object, propertyMeta.key); + var value = object.handle.getValue(object.realm, propertyMeta.key); - if (value is RealmObjectHandle) { + if (value is ObjectHandle) { final meta = object.realm.metadata; final typeName = propertyMeta.objectType; @@ -235,7 +238,7 @@ class RealmCoreAccessor implements RealmAccessor { late RealmObjectMetadata targetMetadata; if (propertyMeta.propertyType == RealmPropertyType.mixed) { - (type, targetMetadata) = meta.getByClassKey(realmCore.getClassKey(value)); + (type, targetMetadata) = meta.getByClassKey(value.classKey); } else { // If we have an object but the user called the API without providing a generic // arg, we construct a RealmObject since we don't know the type of the object. @@ -246,7 +249,7 @@ class RealmCoreAccessor implements RealmAccessor { value = object.realm.createObject(type, value, targetMetadata); } - if (T == RealmValue) { + if (T == RealmValue || (propertyMeta.propertyType == RealmPropertyType.mixed && _isTypeGenericObject())) { value = RealmValue.from(value); } @@ -262,14 +265,14 @@ class RealmCoreAccessor implements RealmAccessor { final propertyMeta = metadata[name]; try { if (value is RealmValue && value.type.isCollection) { - realmCore.objectSetCollection(object, propertyMeta.key, value); + object.handle.setCollection(object.realm, propertyMeta.key, value); return; } if (value is RealmList) { - final handle = realmCore.getListProperty(object, propertyMeta.key); + final handle = object.handle.getList(propertyMeta.key); if (update) { - realmCore.listClear(handle); + handle.clear(); } for (var i = 0; i < value.length; i++) { @@ -280,9 +283,9 @@ class RealmCoreAccessor implements RealmAccessor { //TODO: set from ManagedRealmList is not supported yet if (value is RealmSet) { - final handle = realmCore.getSetProperty(object, propertyMeta.key); + final handle = object.handle.getSet(propertyMeta.key); if (update) { - realmCore.realmSetClear(handle); + handle.clear(); } // TODO: use realmSetAssign when available in C-API @@ -291,7 +294,7 @@ class RealmCoreAccessor implements RealmAccessor { for (var element in value) { object.realm.addUnmanagedRealmObjectFromValue(element, update); - final result = realmCore.realmSetInsert(handle, element); + final result = handle.insert(element); if (!result) { throw RealmException("Error while adding value $element in RealmSet"); } @@ -300,9 +303,9 @@ class RealmCoreAccessor implements RealmAccessor { } if (value is RealmMap) { - final handle = realmCore.getMapProperty(object, propertyMeta.key); + final handle = object.handle.getMap(propertyMeta.key); if (update) { - realmCore.mapClear(handle); + handle.clear(); } for (var kvp in value.entries) { @@ -316,7 +319,7 @@ class RealmCoreAccessor implements RealmAccessor { throw RealmError("Can't set an embedded object that is already managed"); } - final handle = realmCore.createEmbeddedObject(object, propertyMeta.key); + final handle = object.handle.createEmbedded(propertyMeta.key); object.realm.manageEmbedded(handle, value, update: update); return; } @@ -324,13 +327,13 @@ class RealmCoreAccessor implements RealmAccessor { object.realm.addUnmanagedRealmObjectFromValue(value, update); if (propertyMeta.isPrimaryKey && !isInMigration) { - final currentValue = realmCore.getProperty(object, propertyMeta.key); + final currentValue = object.handle.getValue(object.realm, propertyMeta.key); if (currentValue != value) { throw RealmException("Primary key cannot be changed (original value: '$currentValue', supplied value: '$value')"); } } - realmCore.setProperty(object, propertyMeta.key, value, isDefault); + object.handle.setValue(propertyMeta.key, value, isDefault); } on Exception catch (e) { throw RealmException("Error setting property ${metadata._realmObjectTypeName}.$name Error: $e"); } @@ -363,7 +366,7 @@ extension RealmEntityInternal on RealmEntity { /// [RealmObject] should not be used directly as it is part of the generated class hierarchy. ex: `MyClass extends _MyClass with RealmObject`. /// {@category Realm} mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizable { - RealmObjectHandle? _handle; + ObjectHandle? _handle; RealmAccessor _accessor = RealmValuesAccessor(); static final Map _factories = { // Register default factories for `RealmObject` and `RealmObject?`. Whenever the user @@ -381,7 +384,7 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab } /// @nodoc - static void set(RealmObjectBase object, String name, T? value, {bool update = false}) { + static void set(RealmObjectBase object, String name, T value, {bool update = false}) { object._accessor.set(object, name, value, update: update); } @@ -454,10 +457,10 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab if (other is! RealmObjectBase) return false; if (!isManaged || !other.isManaged) return false; - return realmCore.objectEquals(this, other); + return handle == other.handle; } - late final int _managedHashCode = realmCore.objectGetHashCode(this); + late final int _managedHashCode = handle.hashCode; @override int get hashCode { @@ -474,17 +477,46 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab /// will throw an exception. /// The Object is not valid if its [Realm] is closed or object is deleted. /// Unmanaged objects are always considered valid. - bool get isValid => isManaged ? realmCore.objectIsValid(this) : true; + bool get isValid => isManaged ? handle.isValid : true; - /// Allows listening for property changes on this Realm object + /// Allows listening for property changes on this Realm object. /// /// Returns a [Stream] of [RealmObjectChanges] that can be listened to. /// /// If the object is not managed a [RealmStateError] is thrown. Stream> get changes => throw RealmError("Invalid usage. Use the generated inheritors of RealmObject"); + /// Allows listening for property changes on this Realm object using the specified list of key paths. + /// The key paths indicates which changes in properties should raise a notification. + /// + /// Returns a [Stream] of [RealmObjectChanges] that can be listened to. + /// + /// If the object is not managed a [RealmStateError] is thrown. + /// + /// Example + /// ``` dart + /// @RealmModel() + /// class _Person { + /// late String name; + /// late int age; + /// late List<_Person> friends; + /// } + /// + /// // .... + /// + /// // Only changes to person.age and person.friends will raise a notification + /// person.changesFor(["age", "friends"]).listen( .... ) + /// ``` + Stream> changesFor([List? keyPaths]) => + throw RealmError("Invalid usage. Use the generated inheritors of RealmObject"); + /// @nodoc static Stream> getChanges(T object) { + return getChangesFor(object, null); + } + + /// @nodoc + static Stream> getChangesFor(T object, [List? keyPaths]) { if (!object.isManaged) { throw RealmStateError("Object is not managed"); } @@ -493,7 +525,7 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab throw RealmStateError('Object is frozen and cannot emit changes.'); } - final controller = RealmObjectNotificationsController(object); + final controller = RealmObjectNotificationsController(object, keyPaths); return controller.createStream(); } @@ -567,7 +599,7 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab throw RealmError( "Property $T.$propertyName is a link property that links to ${sourceProperty.objectType} which is different from the type of the current object, which is $runtimeType."); } - final handle = realmCore.getBacklinks(this, sourceMeta.classKey, sourceProperty.key); + final handle = this.handle.getBacklinks(sourceMeta.classKey, sourceProperty.key); return RealmResultsInternal.create(handle, realm, sourceMeta); } @@ -576,10 +608,16 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab } /// @nodoc -mixin RealmObject on RealmObjectBase implements RealmObjectMarker {} +mixin RealmObject on RealmObjectBase implements RealmObjectMarker { + @override + Stream> get changes => throw RealmError("Invalid usage. Use the generated inheritors of RealmObject"); +} /// @nodoc -mixin EmbeddedObject on RealmObjectBase implements EmbeddedObjectMarker {} +mixin EmbeddedObject on RealmObjectBase implements EmbeddedObjectMarker { + @override + Stream> get changes => throw RealmError("Invalid usage. Use the generated inheritors of EmbeddedObject"); +} /// Base for any object that can be persisted in a [Realm], but cannot be retrieved, /// hence cannot be modified. @@ -599,9 +637,9 @@ extension EmbeddedObjectExtension on EmbeddedObject { return null; } - final parent = realmCore.getEmbeddedParent(this); - final (type, metadata) = realm.metadata.getByClassKey(parent.item2); - return realm.createObject(type, parent.item1, metadata); + final (parentHandle, classKey) = handle.parent; + final (type, metadata) = realm.metadata.getByClassKey(classKey); + return realm.createObject(type, parentHandle, metadata); } } @@ -614,7 +652,7 @@ extension RealmObjectInternal on RealmObjectBase { _handle?.keepAlive(); } - void manage(Realm realm, RealmObjectHandle handle, RealmCoreAccessor accessor, bool update) { + void manage(Realm realm, ObjectHandle handle, RealmCoreAccessor accessor, bool update) { if (_handle != null) { //most certainly a bug hence we throw an Error throw ArgumentError("Object is already managed"); @@ -630,7 +668,7 @@ extension RealmObjectInternal on RealmObjectBase { _accessor = accessor; } - static RealmObjectBase create(Type type, Realm realm, RealmObjectHandle handle, RealmCoreAccessor accessor) { + static RealmObjectBase create(Type type, Realm realm, ObjectHandle handle, RealmCoreAccessor accessor) { final object = RealmObjectBase.createObject(type, accessor.metadata); object._handle = handle; object._accessor = accessor; @@ -638,7 +676,7 @@ extension RealmObjectInternal on RealmObjectBase { return object; } - RealmObjectHandle get handle { + ObjectHandle get handle { if (_handle?.released == true) { throw RealmClosedError('Cannot access an object that belongs to a closed Realm'); } @@ -690,18 +728,21 @@ class UserCallbackException extends RealmException { /// Describes the changes in on a single RealmObject since the last time the notification callback was invoked. class RealmObjectChanges implements Finalizable { // ignore: unused_field - final RealmObjectChangesHandle _handle; + final ObjectChangesHandle _handle; /// The realm object being monitored for changes. final T object; /// `True` if the object was deleted. - bool get isDeleted => realmCore.getObjectChangesIsDeleted(_handle); + bool get isDeleted => _handle.isDeleted; /// The property names that have changed. List get properties { - final propertyKeys = realmCore.getObjectChangesProperties(_handle); - return object.realm.getPropertyNames(object.runtimeType, propertyKeys); + final propertyKeys = _handle.properties; + return object.realm + .getPropertyNames(object, propertyKeys) + .map((e) => object.objectSchema.firstWhere((element) => element.mapTo == e || element.name == e).name) + .toList(); } const RealmObjectChanges._(this._handle, this.object); @@ -719,12 +760,23 @@ extension RealmObjectChangesInternal on RealmObjectChange class RealmObjectNotificationsController extends NotificationsController { T realmObject; late final StreamController> streamController; + List? keyPaths; + + RealmObjectNotificationsController(this.realmObject, [List? keyPaths]) { + if (keyPaths != null) { + this.keyPaths = keyPaths; - RealmObjectNotificationsController(this.realmObject); + if (keyPaths.any((element) => element.isEmpty)) { + throw RealmException("It is not allowed to have empty key paths."); + } + // throw early if the key paths are invalid + realmObject.handle.buildAndVerifyKeyPath(keyPaths); + } + } @override - RealmNotificationTokenHandle subscribe() { - return realmCore.subscribeObjectNotifications(realmObject, this); + NotificationTokenHandle subscribe() { + return realmObject.handle.subscribeForNotifications(this, keyPaths); } Stream> createStream() { @@ -734,7 +786,7 @@ class RealmObjectNotificationsController extends Noti @override void onChanges(HandleBase changesHandle) { - if (changesHandle is! RealmObjectChangesHandle) { + if (changesHandle is! ObjectChangesHandle) { throw RealmError("Invalid changes handle. RealmObjectChangesHandle expected"); } @@ -752,12 +804,18 @@ class RealmObjectNotificationsController extends Noti class _ConcreteRealmObject with RealmEntity, RealmObjectBase, RealmObject { @override SchemaObject get objectSchema => RealmObjectBase.getSchema(this)!; // _ConcreteRealmObject should only ever be created for managed objects + + @override + Stream> get changes => RealmObjectBase.getChanges(this); } /// @nodoc class _ConcreteEmbeddedObject with RealmEntity, RealmObjectBase, EmbeddedObject { @override SchemaObject get objectSchema => RealmObjectBase.getSchema(this)!; // _ConcreteEmbeddedObject should only ever be created for managed objects + + @override + Stream> get changes => RealmObjectBase.getChanges(this); } // This is necessary whenever we need to pass T? as the type. @@ -782,6 +840,13 @@ class DynamicRealmObject { return RealmObjectBase.get(_obj, name) as T; } + /// Sets a property by its name. The supplied [value] must be assignable + /// to the property type, otherwise an exception will be thrown. + void set(String name, T value) { + _validatePropertyType(name, RealmCollectionType.none, relaxedNullability: true); + RealmObjectBase.set(_obj, name, value); + } + /// Gets a list by the property name. If a generic type is specified, the property /// type will be validated against the type. Otherwise, a `List` will be /// returned. @@ -806,7 +871,7 @@ class DynamicRealmObject { return RealmObjectBase.get(_obj, name) as RealmMap; } - RealmPropertyMetadata? _validatePropertyType(String name, RealmCollectionType expectedCollectionType) { + RealmPropertyMetadata? _validatePropertyType(String name, RealmCollectionType expectedCollectionType, {bool relaxedNullability = false}) { final accessor = _obj.accessor; if (accessor is RealmCoreAccessor) { final prop = accessor.metadata._propertyKeys[name]; @@ -822,8 +887,15 @@ class DynamicRealmObject { // If the user passed in a type argument, we should validate its nullability; if they invoked // the method without a type arg, we don't if (T != _typeOf() && T != _typeOf() && prop.isNullable != null is T) { - throw RealmException( - "Property '$name' on class '${accessor.metadata.schema.name}' is ${prop.isNullable ? 'nullable' : 'required'} but the generic argument passed to get is $T."); + if (relaxedNullability && prop.isNullable) { + // We're relaxing nullability requirements when setting a property - in that case, we accept + // a non-null generic type argument, even if the property is nullable to allow users to invoke + // .set without a generic argument (i.e. have the compiler infer the generic based on the value + // argument). + } else { + throw RealmException( + "Property '$name' on class '${accessor.metadata.schema.name}' is ${prop.isNullable ? 'nullable' : 'required'} but the generic argument supplied is $T."); + } } final targetType = _getPropertyType(); diff --git a/packages/realm_dart/lib/src/results.dart b/packages/realm_dart/lib/src/results.dart index df92d8ef9..f4c669f8f 100644 --- a/packages/realm_dart/lib/src/results.dart +++ b/packages/realm_dart/lib/src/results.dart @@ -7,7 +7,11 @@ import 'dart:ffi'; import 'package:cancellation_token/cancellation_token.dart'; import 'collections.dart'; -import 'native/realm_core.dart'; +import 'native/collection_changes_handle.dart'; +import 'native/handle_base.dart'; +import 'native/notification_token_handle.dart'; +import 'native/object_handle.dart'; +import 'native/results_handle.dart'; import 'realm_class.dart'; import 'realm_object.dart'; @@ -17,7 +21,7 @@ import 'realm_object.dart'; /// {@category Realm} class RealmResults extends Iterable with RealmEntity implements Finalizable { final RealmObjectMetadata? _metadata; - final RealmResultsHandle _handle; + final ResultsHandle _handle; final int _skipOffset; // to support skip efficiently final _supportsSnapshot = [] is List; @@ -28,7 +32,7 @@ class RealmResults extends Iterable with RealmEntity imple } /// Gets a value indicating whether this collection is still valid to use. - bool get isValid => realmCore.resultsIsValid(this); + bool get isValid => handle.isValid(); /// Returns the element of type `T` at the specified [index]. T operator [](int index) => elementAt(index); @@ -41,13 +45,13 @@ class RealmResults extends Iterable with RealmEntity imple throw RangeError.range(index, 0, length - 1); } - var value = realmCore.resultsGetElementAt(this, _skipOffset + index); + var value = handle.elementAt(realm, _skipOffset + index); - if (value is RealmObjectHandle) { + if (value is ObjectHandle) { late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + (type, targetMetadata) = realm.metadata.getByClassKey(value.classKey); } else { targetMetadata = _metadata!; type = T; @@ -80,7 +84,7 @@ class RealmResults extends Iterable with RealmEntity imple if (start < 0) start = 0; start += _skipOffset; - final index = realmCore.resultsFind(this, element); + final index = handle.find(element); return index < start ? -1 : index; // to align with dart list semantics } @@ -101,7 +105,7 @@ class RealmResults extends Iterable with RealmEntity imple Iterator get iterator { var results = this; if (_supportsSnapshot) { - final handle = realmCore.resultsSnapshot(this); + final handle = this.handle.snapshot(); results = RealmResultsInternal.create(handle, realm, _metadata, _skipOffset); } return _RealmResultsIterator(results); @@ -109,7 +113,7 @@ class RealmResults extends Iterable with RealmEntity imple /// The number of values in this `Results` collection. @override - int get length => realmCore.getResultsCount(this) - _skipOffset; + int get length => handle.count - _skipOffset; @override T get first { @@ -173,7 +177,7 @@ extension RealmResultsOfObject on RealmResults { /// /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://www.mongodb.com/docs/realm/realm-query-language/) RealmResults query(String query, [List args = const []]) { - final handle = realmCore.queryResults(this, query, args); + final handle = this.handle.queryResults(query, args); return RealmResultsInternal.create(handle, realm, _metadata); } } @@ -279,7 +283,7 @@ extension RealmResultsInternal on RealmResults { _handle.keepAlive(); } - RealmResultsHandle get handle { + ResultsHandle get handle { if (_handle.released) { throw RealmClosedError('Cannot access Results that belongs to a closed Realm'); } @@ -289,7 +293,7 @@ extension RealmResultsInternal on RealmResults { RealmObjectMetadata get metadata => _metadata!; - static RealmResults create(RealmResultsHandle handle, Realm realm, RealmObjectMetadata? metadata, [int skip = 0]) => + static RealmResults create(ResultsHandle handle, Realm realm, RealmObjectMetadata? metadata, [int skip = 0]) => RealmResults._(handle, realm, metadata, skip); } @@ -312,8 +316,8 @@ class ResultsNotificationsController extends NotificationsCon ResultsNotificationsController(this.results); @override - RealmNotificationTokenHandle subscribe() { - return realmCore.subscribeResultsNotifications(results, this); + NotificationTokenHandle subscribe() { + return results.handle.subscribeForNotifications(this); } Stream> createStream() { @@ -323,7 +327,7 @@ class ResultsNotificationsController extends NotificationsCon @override void onChanges(HandleBase changesHandle) { - if (changesHandle is! RealmCollectionChangesHandle) { + if (changesHandle is! CollectionChangesHandle) { throw RealmError("Invalid changes handle. RealmCollectionChangesHandle expected"); } diff --git a/packages/realm_dart/lib/src/scheduler.dart b/packages/realm_dart/lib/src/scheduler.dart index d737a48cf..8c60f1ebe 100644 --- a/packages/realm_dart/lib/src/scheduler.dart +++ b/packages/realm_dart/lib/src/scheduler.dart @@ -1,12 +1,13 @@ // Copyright 2022 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 +import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; -import 'package:realm_dart/src/logging.dart'; -import 'native/realm_core.dart'; +import 'package:realm_dart/src/logging.dart'; +import 'native/scheduler_handle.dart'; import 'realm_class.dart'; final _receivePortFinalizer = Finalizer((p) => p.close()); @@ -20,23 +21,34 @@ class Scheduler { Scheduler._() { _receivePortFinalizer.attach(this, _receivePort, detach: this); - - _receivePort.handler = (dynamic message) { - if (message is List) { - // currently the only `message as List` is from the logger. - final category = LogCategory.fromString(message[0] as String); - final level = LogLevel.values[message[1] as int]; - final text = message[2] as String; - Realm.logger.raise((category: category, level: level, message: text)); - } else if (message is int) { - realmCore.invokeScheduler(message); - } else { - Realm.logger.log(LogLevel.error, 'Unexpected Scheduler message type: ${message.runtimeType} - $message'); - } - }; - + // There be dragons here!!! + // + // As of Dart 3.4 (Flutter 3.22) we started seeing uncaught exceptions on + // the receivePort handler (issue #1676), stating that: + // "argument value for 'return_value' is null" in + // RealmLibrary.realm_scheduler_perform_work, but obviously a void method + // don't return anything, so this is really a Dart issue. + // + // However, by ensuring the callback happens in the current zone (as it + // rightfully should), and using bindUnaryCallbackGuarded, we can avoid + // these. + _receivePort.handler = Zone.current.bindUnaryCallbackGuarded(_handle); final sendPort = _receivePort.sendPort; - handle = realmCore.createScheduler(Isolate.current.hashCode, sendPort.nativePort); + handle = SchedulerHandle(Isolate.current.hashCode, sendPort.nativePort); + } + + void _handle(dynamic message) { + if (message is List) { + // currently the only `message as List` is from the logger. + final category = LogCategory.fromString(message[0] as String); + final level = LogLevel.values[message[1] as int]; + final text = message[2] as String; + Realm.logger.raise((category: category, level: level, message: text)); + } else if (message is int) { + handle.invoke(message); + } else { + Realm.logger.log(LogLevel.error, 'Unexpected Scheduler message type: ${message.runtimeType} - $message'); + } } void stop() { diff --git a/packages/realm_dart/lib/src/session.dart b/packages/realm_dart/lib/src/session.dart index 836734ef3..4183a985d 100644 --- a/packages/realm_dart/lib/src/session.dart +++ b/packages/realm_dart/lib/src/session.dart @@ -3,11 +3,11 @@ import 'dart:async'; import 'dart:ffi'; -import '../realm.dart'; -import 'native/realm_core.dart'; -import 'user.dart'; +import '../realm.dart'; import '../src/native/realm_bindings.dart'; +import 'native/session_handle.dart'; +import 'user.dart'; /// An object encapsulating a synchronization session. Sessions represent the /// communication between the client (and a local Realm file on disk), and the @@ -18,38 +18,38 @@ class Session implements Finalizable { final SessionHandle _handle; /// The on-disk path of the file backing the [Realm] this [Session] represents - String get realmPath => realmCore.sessionGetPath(this); + String get realmPath => handle.path; /// The session’s current state. This is different from [connectionState] since a /// session may be active, even if the connection is disconnected (e.g. due to the device /// being offline). - SessionState get state => realmCore.sessionGetState(this); + SessionState get state => handle.state; /// The session’s current connection state. This is the physical state of the connection /// and is different from the session's logical state, which is returned by [state]. - ConnectionState get connectionState => realmCore.sessionGetConnectionState(this); + ConnectionState get connectionState => handle.connectionState; /// The [User] that owns the [Realm] this [Session] is synchronizing. - User get user => UserInternal.create(realmCore.sessionGetUser(this)); + User get user => UserInternal.create(handle.user); Session._(this._handle); /// Pauses any synchronization with the server until the Realm is re-opened again /// after fully closing it or [resume] is called. - void pause() => realmCore.sessionPause(this); + void pause() => handle.pause(); /// Attempts to resume the session and enable synchronization with the server. /// All sessions are active by default and calling this method is only necessary /// if [pause] was called previously. - void resume() => realmCore.sessionResume(this); + void resume() => handle.resume(); /// Waits for the [Session] to finish all pending uploads. /// An optional [cancellationToken] can be used to cancel the wait operation. - Future waitForUpload([CancellationToken? cancellationToken]) => realmCore.sessionWaitForUpload(this, cancellationToken); + Future waitForUpload([CancellationToken? cancellationToken]) => handle.waitForUpload(cancellationToken); /// Waits for the [Session] to finish all pending downloads. /// An optional [cancellationToken] can be used to cancel the wait operation. - Future waitForDownload([CancellationToken? cancellationToken]) => realmCore.sessionWaitForDownload(this, cancellationToken); + Future waitForDownload([CancellationToken? cancellationToken]) => handle.waitForDownload(cancellationToken); /// Gets a [Stream] of [SyncProgress] that can be used to track upload or download progress. Stream getProgressStream(ProgressDirection direction, ProgressMode mode) { @@ -105,9 +105,7 @@ extension SessionInternal on Session { return _handle; } - void raiseError(int errorCode, bool isFatal) { - realmCore.raiseError(this, errorCode, isFatal); - } + void raiseError(int errorCode, bool isFatal) => handle.raiseError(errorCode, isFatal); static SyncProgress createSyncProgress(double progressEstimate) => SyncProgress(progressEstimate: progressEstimate); } @@ -122,7 +120,7 @@ class SessionProgressNotificationsController implements ProgressNotificationsCon final ProgressDirection _direction; final ProgressMode _mode; - RealmSyncSessionConnectionStateNotificationTokenHandle? _tokenHandle; + SyncSessionNotificationTokenHandle? _tokenHandle; late final StreamController _streamController; SessionProgressNotificationsController(this._session, this._direction, this._mode); @@ -145,7 +143,7 @@ class SessionProgressNotificationsController implements ProgressNotificationsCon if (_tokenHandle != null) { throw RealmStateError("Session progress subscription already started."); } - _tokenHandle = realmCore.sessionRegisterProgressNotifier(_session, _direction, _mode, this); + _tokenHandle = _session.handle.subscribeForProgressNotifications(_direction, _mode, this); } void _stop() { @@ -158,7 +156,7 @@ class SessionProgressNotificationsController implements ProgressNotificationsCon class SessionConnectionStateController { final Session _session; late final StreamController _streamController; - RealmSyncSessionConnectionStateNotificationTokenHandle? _token; + SyncSessionNotificationTokenHandle? _token; SessionConnectionStateController(this._session); @@ -175,7 +173,7 @@ class SessionConnectionStateController { if (_token != null) { throw RealmStateError("Session connection state subscription already started"); } - _token = realmCore.sessionRegisterConnectionStateNotifier(_session, this); + _token = _session.handle.subscribeForConnectionStateNotifications(this); } void _stop() { diff --git a/packages/realm_dart/lib/src/set.dart b/packages/realm_dart/lib/src/set.dart index bcfe9d3f3..6ac396f32 100644 --- a/packages/realm_dart/lib/src/set.dart +++ b/packages/realm_dart/lib/src/set.dart @@ -8,7 +8,11 @@ import 'dart:ffi'; import 'package:collection/collection.dart' as collection; -import 'native/realm_core.dart'; +import 'native/collection_changes_handle.dart'; +import 'native/handle_base.dart'; +import 'native/notification_token_handle.dart'; +import 'native/object_handle.dart'; +import 'native/set_handle.dart'; import 'realm_class.dart'; import 'realm_object.dart'; import 'collections.dart'; @@ -121,7 +125,7 @@ class UnmanagedRealmSet extends collection.DelegatingSet w } class ManagedRealmSet with RealmEntity, SetMixin implements RealmSet { - final RealmSetHandle _handle; + final SetHandle _handle; ManagedRealmSet._(this._handle, Realm realm, this._metadata) { setRealm(realm); @@ -131,7 +135,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement late final RealmObjectMetadata? _metadata; @override - bool get isValid => realmCore.realmSetIsValid(this); + bool get isValid => handle.isValid; @override bool add(T value) { @@ -145,7 +149,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement realm.addUnmanagedRealmObjectFromValue(value, false); } - return realmCore.realmSetInsert(_handle, value); + return _handle.insert(value); } @override @@ -155,12 +159,12 @@ class ManagedRealmSet with RealmEntity, SetMixin implement } try { - var value = realmCore.realmSetGetElementAt(this, index); - if (value is RealmObjectHandle) { + var value = handle.elementAt(realm, index); + if (value is ObjectHandle) { late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + (type, targetMetadata) = realm.metadata.getByClassKey(value.classKey); } else { targetMetadata = _metadata!; // will be null for RealmValue, so defer until here type = T; @@ -187,7 +191,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement _ensureManagedByThis(element, "contains"); - return realmCore.realmSetFind(this, element); + return handle.find(element); } @override @@ -203,7 +207,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement _ensureManagedByThis(value, "remove"); - return realmCore.realmSetErase(this, value); + return handle.remove(value); } @override @@ -213,10 +217,10 @@ class ManagedRealmSet with RealmEntity, SetMixin implement Set toSet() => {...this}; @override - void clear() => realmCore.realmSetClear(_handle); + void clear() => _handle.clear(); @override - int get length => realmCore.realmSetSize(this); + int get length => handle.size; @override Stream> get changes { @@ -263,7 +267,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement } @override - RealmResults asResults() => RealmResultsInternal.create(realmCore.resultsFromSet(this), realm, _metadata); + RealmResults asResults() => RealmResultsInternal.create(handle.asResults, realm, _metadata); @override RealmSet freeze() { @@ -282,7 +286,7 @@ extension RealmSetInternal on RealmSet { RealmObjectMetadata? get metadata => asManaged()._metadata; - RealmSetHandle get handle { + SetHandle get handle { final result = asManaged()._handle; if (result.released) { throw RealmClosedError('Cannot access a RealmSet that belongs to a closed Realm'); @@ -291,8 +295,7 @@ extension RealmSetInternal on RealmSet { return result; } - static RealmSet create(RealmSetHandle handle, Realm realm, RealmObjectMetadata? metadata) => - ManagedRealmSet._(handle, realm, metadata); + static RealmSet create(SetHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmSet._(handle, realm, metadata); } class _RealmSetIterator implements Iterator { @@ -337,8 +340,8 @@ class RealmSetNotificationsController extends NotificationsCo RealmSetNotificationsController(this.set); @override - RealmNotificationTokenHandle subscribe() { - return realmCore.subscribeSetNotifications(set, this); + NotificationTokenHandle subscribe() { + return set.handle.subscribeForNotifications(this); } Stream> createStream() { @@ -348,7 +351,7 @@ class RealmSetNotificationsController extends NotificationsCo @override void onChanges(HandleBase changesHandle) { - if (changesHandle is! RealmCollectionChangesHandle) { + if (changesHandle is! CollectionChangesHandle) { throw RealmError("Invalid changes handle. RealmCollectionChangesHandle expected"); } @@ -371,7 +374,7 @@ extension RealmSetOfObject on RealmSet { /// /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { - final handle = realmCore.querySet(asManaged(), query, arguments); + final handle = asManaged().handle.query(query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } } diff --git a/packages/realm_dart/lib/src/subscription.dart b/packages/realm_dart/lib/src/subscription.dart index 48dd9f0ec..9b4a58eba 100644 --- a/packages/realm_dart/lib/src/subscription.dart +++ b/packages/realm_dart/lib/src/subscription.dart @@ -2,50 +2,53 @@ // SPDX-License-Identifier: Apache-2.0 import 'dart:core'; -import 'dart:collection'; import 'dart:ffi'; -import 'native/realm_core.dart'; +import 'native/convert.dart'; +import 'native/mutable_subscription_set_handle.dart'; +import 'native/subscription_handle.dart'; +import 'native/subscription_set_handle.dart'; import 'realm_class.dart'; +import 'results.dart'; /// A class representing a single query subscription. The server will continuously /// evaluate the query that the app subscribed to and will send data /// that matches it as well as remove data that no longer does. /// {@category Sync} -class Subscription implements Finalizable { +final class Subscription implements Finalizable { final SubscriptionHandle _handle; Subscription._(this._handle); - late final ObjectId _id = realmCore.subscriptionId(this); + late final ObjectId _id = _handle.id; /// Name of the [Subscription], if one was provided at creation time. - String? get name => realmCore.subscriptionName(this); + String? get name => _handle.name; /// Class name of objects the [Subscription] refers to. /// /// If your types are remapped using [MapTo], the value /// returned will be the mapped-to value - i.e. the one that Realm uses internally /// rather than the name of the generated Dart class. - String get objectClassName => realmCore.subscriptionObjectClassName(this); + String get objectClassName => _handle.objectClassName; /// Query string that describes the [Subscription]. /// /// Objects matched by the query will be sent to the device by the server. - String get queryString => realmCore.subscriptionQueryString(this); + String get queryString => _handle.queryString; /// Timestamp when this [Subscription] was created. - DateTime get createdAt => realmCore.subscriptionCreatedAt(this); + DateTime get createdAt => _handle.createdAt; /// Timestamp when this [Subscription] was last updated. - DateTime get updatedAt => realmCore.subscriptionUpdatedAt(this); + DateTime get updatedAt => _handle.updatedAt; @override // ignore: hash_and_equals bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! Subscription) return false; - return realmCore.subscriptionEquals(this, other); + return _handle.equalTo(other._handle); } } @@ -59,7 +62,7 @@ extension SubscriptionInternal on Subscription { ObjectId get id => _id; } -class _SubscriptionIterator implements Iterator { +final class _SubscriptionIterator implements Iterator { int _index = -1; final SubscriptionSet _subscriptions; @@ -112,29 +115,25 @@ enum SubscriptionSetState { /// Realm is an expensive operation server-side, even if there's very little data that needs /// downloading. /// {@category Sync} -abstract class SubscriptionSet with IterableMixin implements Finalizable { +sealed class SubscriptionSet with Iterable implements Finalizable { final Realm _realm; - SubscriptionSetHandle _handle; + SubscriptionSetHandle __handle; + SubscriptionSetHandle get _handle => __handle.nullPtrAsNull ?? (throw RealmClosedError('Cannot access a SubscriptionSet that belongs to a closed Realm')); + set _handle(SubscriptionSetHandle value) => __handle = value; - SubscriptionSet._(this._realm, this._handle); + SubscriptionSet._(this._realm, this.__handle); /// Finds an existing [Subscription] in this set by its query /// /// The [query] is represented by the corresponding [RealmResults] object. - Subscription? find(RealmResults query) { - final result = realmCore.findSubscriptionByResults(this, query); - return result == null ? null : Subscription._(result); - } + Subscription? find(RealmResults query) => _handle.findByResults(query.handle).convert(Subscription._); /// Finds an existing [Subscription] in this set by name. - Subscription? findByName(String name) { - final result = realmCore.findSubscriptionByName(this, name); - return result == null ? null : Subscription._(result); - } + Subscription? findByName(String name) => _handle.findByName(name).convert(Subscription._); Future _waitForStateChange(SubscriptionSetState state, [CancellationToken? cancellationToken]) async { - final result = await realmCore.waitForSubscriptionSetStateChange(this, state, cancellationToken); - realmCore.refreshSubscriptionSet(this); + final result = await _handle.waitForStateChange(state, cancellationToken); + _handle.refresh(); return result; } @@ -153,15 +152,15 @@ abstract class SubscriptionSet with IterableMixin implements Final } /// Returns the error if the subscription set is in the [SubscriptionSetState.error] state. - Exception? get error => realmCore.getSubscriptionSetError(this); + Exception? get error => _handle.error; @override - int get length => realmCore.getSubscriptionSetSize(this); + int get length => _handle.size; @override Subscription elementAt(int index) { RangeError.checkValidRange(index, null, length); - return Subscription._(realmCore.subscriptionAt(this, index)); + return Subscription._(_handle[index]); } /// Gets the [Subscription] at the specified index in the set. @@ -180,11 +179,11 @@ abstract class SubscriptionSet with IterableMixin implements Final void update(void Function(MutableSubscriptionSet mutableSubscriptions) action); /// Gets the version of the subscription set. - int get version => realmCore.subscriptionSetGetVersion(this); + int get version => _handle.version; /// Gets the state of the subscription set. SubscriptionSetState get state { - final state = realmCore.subscriptionSetGetState(this); + final state = _handle.state; switch (state) { case SubscriptionSetState._uncommitted: case SubscriptionSetState._bootstrapping: @@ -202,42 +201,36 @@ extension SubscriptionSetInternal on SubscriptionSet { _handle.keepAlive(); } - Realm get realm => _realm; - SubscriptionSetHandle get handle { - if (_handle.released) { - throw RealmClosedError('Cannot access a SubscriptionSet that belongs to a closed Realm'); - } - - return _handle; - } + SubscriptionSetHandle get handle => _handle; static SubscriptionSet create(Realm realm, SubscriptionSetHandle handle) => ImmutableSubscriptionSet._(realm, handle); } -class ImmutableSubscriptionSet extends SubscriptionSet { +final class ImmutableSubscriptionSet extends SubscriptionSet { ImmutableSubscriptionSet._(super.realm, super.handle) : super._(); @override void update(void Function(MutableSubscriptionSet mutableSubscriptions) action) { - final mutableSubscriptions = MutableSubscriptionSet._(realm, realmCore.subscriptionSetMakeMutable(this)); - final oldHandle = _handle; + final old = _handle; + final mutable = _handle.toMutable(); try { - action(mutableSubscriptions); - _handle = realmCore.subscriptionSetCommit(mutableSubscriptions); + action(MutableSubscriptionSet._(_realm, mutable)); + _handle = mutable.commit(); } finally { // Release as early as possible, as we cannot start new update, until this is released! - mutableSubscriptions._handle.release(); - oldHandle.release(); + mutable.release(); + old.release(); } } } /// A mutable view to a [SubscriptionSet]. Obtained by calling [SubscriptionSet.update]. /// {@category Sync} -class MutableSubscriptionSet extends SubscriptionSet { - final MutableSubscriptionSetHandle _handle; +final class MutableSubscriptionSet extends SubscriptionSet { + @override + MutableSubscriptionSetHandle get _handle => super._handle as MutableSubscriptionSetHandle; - MutableSubscriptionSet._(Realm realm, this._handle) : super._(realm, _handle); + MutableSubscriptionSet._(super.realm, MutableSubscriptionSetHandle super.handle) : super._(); @override void update(void Function(MutableSubscriptionSet mutableSubscriptions) action) { @@ -254,28 +247,21 @@ class MutableSubscriptionSet extends SubscriptionSet { /// If [update] is specified to `true`, then any existing query will be replaced. /// Otherwise a [RealmException] is thrown, in case of duplicates. /// {@category Sync} - Subscription add(RealmResults query, {String? name, bool update = false}) { - return Subscription._(realmCore.insertOrAssignSubscription(this, query, name, update)); - } + Subscription add(RealmResults query, {String? name, bool update = false}) => + Subscription._(_handle.insertOrAssignSubscription(query.handle, name, update)); /// Removes the [subscription] from the set, if it exists. - bool remove(Subscription subscription) { - return realmCore.eraseSubscriptionById(this, subscription); - } + bool remove(Subscription subscription) => _handle.erase(subscription._handle); /// Removes the [query] from the set, if it exists. - bool removeByQuery(RealmResults query) { - return realmCore.eraseSubscriptionByResults(this, query); - } + bool removeByQuery(RealmResults query) => _handle.eraseByResults(query.handle); /// Removes the subscription from the set that matches by [name], if it exists. - bool removeByName(String name) { - return realmCore.eraseSubscriptionByName(this, name); - } + bool removeByName(String name) => _handle.eraseByName(name); /// Removes the subscriptions from the set that matches by type, if it exists. bool removeByType() { - final name = realm.schema.singleWhere((e) => e.type == T).name; + final name = _realm.schema.singleWhere((e) => e.type == T).name; var result = false; for (var i = length - 1; i >= 0; i--) { // reverse iteration to avoid index shifting @@ -298,11 +284,7 @@ class MutableSubscriptionSet extends SubscriptionSet { } } } else { - realmCore.clearSubscriptionSet(this); + _handle.clear(); } } } - -extension MutableSubscriptionSetInternal on MutableSubscriptionSet { - MutableSubscriptionSetHandle get handle => _handle; -} diff --git a/packages/realm_dart/lib/src/user.dart b/packages/realm_dart/lib/src/user.dart index 05e1c0bca..2c8e0d6a4 100644 --- a/packages/realm_dart/lib/src/user.dart +++ b/packages/realm_dart/lib/src/user.dart @@ -5,9 +5,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:ffi'; -import 'native/realm_core.dart'; +import 'app.dart'; +import 'credentials.dart'; +import 'native/user_handle.dart'; import 'realm_class.dart'; -import './app.dart'; /// Describes the changes to a [User] instance - for example when the access token is updated or the user state changes. /// Right now, this only conveys information that the user has changed, but in the future it will be enhanced by adding @@ -33,7 +34,7 @@ class User { App get app { // The _app field may be null when we're retrieving a user from the session // rather than from the app. - return _app ??= AppInternal.create(realmCore.userGetApp(_handle)); + return _app ??= AppInternal.create(_handle.app); } late final ApiKeyClient _apiKeys = ApiKeyClient._(this); @@ -41,7 +42,7 @@ class User { /// Gets an [ApiKeyClient] instance that exposes functionality for managing /// user API keys. - /// [API Keys Authentication Docs](https://docs.mongodb.com/realm/authentication/api-key/) + /// [API Keys Authentication Docs](https://www.mongodb.com/docs/atlas/app-services/authentication/api-key/#api-key-authentication) ApiKeyClient get apiKeys { _ensureLoggedIn('access API keys'); @@ -50,7 +51,7 @@ class User { /// Gets a [FunctionsClient] instance that exposes functionality for calling remote Atlas Functions. /// A [FunctionsClient] instance scoped to this [User]. - /// [Atlas Functions Docs](https://docs.mongodb.com/realm/functions/) + /// [Atlas Functions Docs](hhttps://www.mongodb.com/docs/atlas/app-services/functions/#atlas-functions) FunctionsClient get functions { _ensureLoggedIn('access API keys'); @@ -61,49 +62,49 @@ class User { /// The current state of this [User]. UserState get state { - return realmCore.userGetState(this); + return handle.state; } /// Get this [User]'s id on MongoDB Atlas. String get id { - return realmCore.userGetId(this); + return handle.id; } /// Gets a collection of all identities associated with this [User]. List get identities { - return realmCore.userGetIdentities(this); + return handle.identities; } /// Removes the [User]'s local credentials. This will also close any associated Sessions. Future logOut() async { - return await realmCore.userLogOut(this); + return await handle.logOut(); } /// Gets an unique identifier for the current device. String? get deviceId { - return realmCore.userGetDeviceId(this); + return handle.deviceId; } /// Gets the profile information for this [User]. UserProfile get profile { - return realmCore.userGetProfileData(this); + return handle.profileData; } /// Gets the refresh token for this [User]. This is the user's credential for /// accessing [Atlas App Services](https://www.mongodb.com/docs/atlas/app-services/) and should be treated as sensitive data. String get refreshToken { - return realmCore.userGetRefreshToken(this); + return handle.refreshToken; } /// Gets the access token for this [User]. This is the user's credential for /// accessing [Atlas App Services](https://www.mongodb.com/docs/atlas/app-services/) and should be treated as sensitive data. String get accessToken { - return realmCore.userGetAccessToken(this); + return handle.accessToken; } /// The custom user data associated with this [User]. dynamic get customData { - final data = realmCore.userGetCustomData(this); + final data = handle.customData; if (data == null) { return null; } @@ -113,7 +114,7 @@ class User { /// Refreshes the custom data for a this [User]. Future refreshCustomData() async { - await realmCore.userRefreshCustomData(app, this); + await app.handle.refreshCustomData(handle); return customData; } @@ -135,7 +136,7 @@ class User { /// await user.linkCredentials(Credentials.emailPassword("username", "password")); /// ``` Future linkCredentials(Credentials credentials) async { - final userHandle = await realmCore.userLinkCredentials(app, this, credentials); + final userHandle = await handle.linkCredentials(app.handle, credentials.handle); return UserInternal.create(userHandle, app); } @@ -144,7 +145,7 @@ class User { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! User) return false; - return realmCore.userEquals(this, other); + return handle == other.handle; } void _ensureLoggedIn([String clarification = 'perform this action']) { @@ -169,7 +170,7 @@ class UserNotificationsController implements Finalizable { throw RealmStateError("User notifications subscription already started"); } - tokenHandle = realmCore.subscribeUserNotifications(this); + tokenHandle = user.handle.subscribeForNotifications(this); } void stop() { @@ -275,42 +276,42 @@ class ApiKeyClient { Future create(String name) async { _user._ensureLoggedIn('create an API key'); - return realmCore.createApiKey(_user, name); + return _user.handle.createApiKey(_user.app.handle, name); } /// Fetches a specific API key by id. Future fetch(ObjectId id) { _user._ensureLoggedIn('fetch an API key'); - return realmCore.fetchApiKey(_user, id).handle404(); + return _user.handle.fetchApiKey(_user.app.handle, id).handle404(); } /// Fetches all API keys associated with the user. Future> fetchAll() async { _user._ensureLoggedIn('fetch all API keys'); - return realmCore.fetchAllApiKeys(_user); + return _user.handle.fetchAllApiKeys(_user.app.handle); } /// Deletes a specific API key by id. Future delete(ObjectId objectId) { _user._ensureLoggedIn('delete an API key'); - return realmCore.deleteApiKey(_user, objectId).handle404(); + return _user.handle.deleteApiKey(_user.app.handle, objectId).handle404(); } /// Disables an API key by id. Future disable(ObjectId objectId) { _user._ensureLoggedIn('disable an API key'); - return realmCore.disableApiKey(_user, objectId).handle404(id: objectId); + return _user.handle.disableApiKey(_user.app.handle, objectId).handle404(id: objectId); } /// Enables an API key by id. Future enable(ObjectId objectId) { _user._ensureLoggedIn('enable an API key'); - return realmCore.enableApiKey(_user, objectId).handle404(id: objectId); + return _user.handle.enableApiKey(_user.app.handle, objectId).handle404(id: objectId); } } @@ -354,7 +355,7 @@ class FunctionsClient { Future call(String name, [List functionArgs = const []]) async { _user._ensureLoggedIn('call Atlas function'); final args = jsonEncode(functionArgs); - final response = await realmCore.callAppFunction(_user.app, _user, name, args); + final response = await _user.app.handle.callAppFunction(_user.handle, name, args); return jsonDecode(response); } } diff --git a/packages/realm_dart/pubspec.yaml b/packages/realm_dart/pubspec.yaml index 2a1fd7a09..dd5301aae 100644 --- a/packages/realm_dart/pubspec.yaml +++ b/packages/realm_dart/pubspec.yaml @@ -1,13 +1,13 @@ name: realm_dart description: The official Realm SDK for Dart. Realm is a mobile database - an alternative to SQLite and key-value stores. -version: 2.0.0 +version: 2.3.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart issue_tracker: https://github.com/realm/realm-dart/issues environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: args: ^2.3.0 @@ -23,8 +23,8 @@ dependencies: path: ^1.0.0 pubspec_parse: ^1.0.0 pub_semver: ^2.1.0 - realm_common: ^2.0.0 - realm_generator: ^2.0.0 + realm_common: ^2.3.0 + realm_generator: ^2.3.0 tar: ^1.0.1 build_runner: ^2.1.0 http: ^1.0.0 diff --git a/packages/realm_dart/scripts/build-android.sh b/packages/realm_dart/scripts/build-android.sh index e76d37ce7..41bd394c6 100755 --- a/packages/realm_dart/scripts/build-android.sh +++ b/packages/realm_dart/scripts/build-android.sh @@ -21,11 +21,11 @@ fi # Start in the root directory of the project. cd "$(dirname "$0")/.." -ABIS=(x86 x86_64 armeabi-v7a arm64-v8a) +ABIS=(x86_64 armeabi-v7a arm64-v8a) -# only building for x86 if no arguments +# only building for arm64-v8a if no arguments if [ $# -eq 0 ]; then - ABIS=(x86) + ABIS=arm64-v8a) fi for abi in "${ABIS[@]}"; do diff --git a/packages/realm_dart/src/realm_dart.cpp b/packages/realm_dart/src/realm_dart.cpp index a1c5a1ca6..5bab1f20c 100644 --- a/packages/realm_dart/src/realm_dart.cpp +++ b/packages/realm_dart/src/realm_dart.cpp @@ -35,12 +35,6 @@ std::string cpuArch = "arm64"; #pragma message("Building arm64") #endif - -#if REALM_ARCHITECTURE_X86_32 -std::string cpuArch = "x86"; -#pragma message("Building x86") -#endif - #if REALM_ARCHITECTURE_X86_64 std::string cpuArch = "x86_64"; #pragma message("Building x64") @@ -86,7 +80,7 @@ RLM_API void realm_dart_invoke_unlock_callback(realm_userdata_t error, void* unl // Stamped into the library by the build system (see prepare-release.yml) // Keep this method as it is written and do not format it. // We have a github workflow that looks for and replaces this string as it is written here. -RLM_API const char* realm_dart_library_version() { return "2.0.0"; } +RLM_API const char* realm_dart_library_version() { return "2.3.0"; } //for debugging only // RLM_API void realm_dart_gc() { diff --git a/packages/realm_dart/test/app_test.dart b/packages/realm_dart/test/app_test.dart index 56c33579c..9dd41f46e 100644 --- a/packages/realm_dart/test/app_test.dart +++ b/packages/realm_dart/test/app_test.dart @@ -168,7 +168,7 @@ void main() { final user = await app.logIn(Credentials.anonymous()); final user1 = await app.logIn(Credentials.emailPassword(testUsername, testPassword)); - expect(app.users, [user1, user]); + expect(app.users, {user1, user}); }); baasTest('App delete user', (configuration) async { @@ -184,13 +184,19 @@ void main() { await app.deleteUser(user); expect(user.state, UserState.removed); - await expectLater(() => loginWithRetry(app, Credentials.emailPassword(username, strongPassword)), throws("invalid username/password")); + await expectLater( + () => loginWithRetry(app, Credentials.emailPassword(username, strongPassword)), + throwsA(isA() + .having((e) => e.message, 'message', equals('unauthorized')) + .having((e) => e.statusCode, 'statusCode', 401) + .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id='))), + ); }); baasTest('Call Atlas function that does not exist', (configuration) async { final app = App(configuration); final user = await app.logIn(Credentials.anonymous()); - await expectLater(user.functions.call('notExisitingFunction'), throws("function not found: 'notExisitingFunction'")); + await expectLater(user.functions.call('notExisitingFunction'), throws("function not found")); }); baasTest('Call Atlas function with no arguments', (configuration) async { @@ -293,30 +299,23 @@ void main() { expect(app.currentUser, user2); expect( () => app.switchUser(user1), - throws("Switch user failed. User is no longer valid or is logged out"), + throws("User is no longer valid or is logged out"), ); }); baasTest('App get Base URL', (configuration) async { final app = App(configuration); - final credentials = Credentials.anonymous(); - await app.logIn(credentials); - final baseUrl = app.baseUrl; - expect(baseUrl, isNotNull); - expect(baseUrl, configuration.baseUrl); + expect(app.baseUrl, configuration.baseUrl); }); - baasTest('App update Base URL', (configuration) async { - final app = App(configuration); - final credentials = Credentials.anonymous(); - await app.logIn(credentials); - final baseUrl = app.baseUrl; - expect(baseUrl, isNotNull); + baasTest('App update Base URL', (appConfig) async { + final config = await baasHelper!.getAppConfig(customBaseUrl: 'https://services.cloud.mongodb.com'); + final app = App(config); + expect(app.baseUrl, Uri.parse('https://services.cloud.mongodb.com')); // Set it to the same thing to confirm the function works, it's not actually going to update the location - await app.updateBaseUrl(baseUrl!); - final newBaseUrl = app.baseUrl; - expect(newBaseUrl, isNotNull); - expect(newBaseUrl, baseUrl); + await app.updateBaseUrl(Uri.parse(baasHelper!.baseUrl)); + expect(app.baseUrl, appConfig.baseUrl); + expect(app.baseUrl, isNot(Uri.parse('https://services.cloud.mongodb.com'))); }); test('bundleId is salted, hashed and encoded', () { diff --git a/packages/realm_dart/test/baas_helper.dart b/packages/realm_dart/test/baas_helper.dart index 2cea26c29..52a5a368b 100644 --- a/packages/realm_dart/test/baas_helper.dart +++ b/packages/realm_dart/test/baas_helper.dart @@ -151,9 +151,9 @@ class BaasHelper { } } - Future createServerApiKey(App app, String name, {bool enabled = true}) async { + Future createServerApiKey(App app, String name, {bool enabled = true}) { final baasApp = _baasApps.values.firstWhere((ba) => ba.clientAppId == app.id); - return await _baasClient.createApiKey(baasApp.appId, name, enabled); + return _baasClient.createApiKey(baasApp.appId, name, enabled); } static void throwIfSetupFailed() { @@ -177,9 +177,10 @@ class BaasHelper { testing.printOnFailure("Splunk logs: $splunk"); } - Future getAppConfig({AppName appName = AppName.flexible}) => _getAppConfig(appName.name); + Future getAppConfig({AppName appName = AppName.flexible, String? customBaseUrl}) => + _getAppConfig(appName.name, customBaseUrl: customBaseUrl); - Future _getAppConfig(String appName) async { + Future _getAppConfig(String appName, {String? customBaseUrl}) async { final app = _baasApps[appName] ?? _baasApps.values.firstWhere((element) => element.name == BaasClient.defaultAppName, orElse: () => throw RealmError("No BAAS apps")); if (app.error != null) { @@ -189,7 +190,7 @@ class BaasHelper { final temporaryDir = await Directory.systemTemp.createTemp('realm_test_'); return AppConfiguration( app.clientAppId, - baseUrl: Uri.parse(baseUrl), + baseUrl: Uri.parse(customBaseUrl ?? baseUrl), baseFilePath: temporaryDir, maxConnectionTimeout: Duration(minutes: 10), defaultRequestTimeout: Duration(minutes: 7), diff --git a/packages/realm_dart/test/backlinks_test.realm.dart b/packages/realm_dart/test/backlinks_test.realm.dart index b71c1d742..bc35db921 100644 --- a/packages/realm_dart/test/backlinks_test.realm.dart +++ b/packages/realm_dart/test/backlinks_test.realm.dart @@ -71,6 +71,10 @@ class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Source freeze() => RealmObjectBase.freezeObject(this); @@ -184,6 +188,10 @@ class Target extends _Target with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Target freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/configuration_test.dart b/packages/realm_dart/test/configuration_test.dart index aa512c948..5dbdf98eb 100644 --- a/packages/realm_dart/test/configuration_test.dart +++ b/packages/realm_dart/test/configuration_test.dart @@ -3,9 +3,10 @@ import 'dart:io'; import 'dart:math'; -import 'package:test/test.dart' hide test, throws; + import 'package:path/path.dart' as path; import 'package:realm_dart/realm.dart'; + import 'test.dart'; void main() { diff --git a/packages/realm_dart/test/credentials_test.dart b/packages/realm_dart/test/credentials_test.dart index 359703800..b93508d67 100644 --- a/packages/realm_dart/test/credentials_test.dart +++ b/packages/realm_dart/test/credentials_test.dart @@ -148,7 +148,13 @@ void main() { await authProvider.registerUser(username, strongPassword); await authProvider.callResetPasswordFunction(username, newPassword, functionArgs: ['success']); await app.logIn(Credentials.emailPassword(username, newPassword)); - await expectLater(() => app.logIn(Credentials.emailPassword(username, strongPassword)), throws("invalid username/password")); + await expectLater( + app.logIn(Credentials.emailPassword(username, strongPassword)), + throwsA(isA() + .having((e) => e.message, 'message', equals('unauthorized')) + .having((e) => e.statusCode, 'statusCode', 401) + .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id='))), + ); }, appName: AppName.autoConfirm); baasTest('Email/Password - call reset password function with no additional arguments', (configuration) async { @@ -157,12 +163,16 @@ void main() { const String newPassword = "!@#!DQXQWD!223eda"; final authProvider = EmailPasswordAuthProvider(app); await authProvider.registerUser(username, strongPassword); - await expectLater(() async { + await expectLater( // Calling this function with no additional arguments fails for the test // because of the specific implementation of resetFunc in the cloud. // resetFunc returns status 'fail' in case no other status is passed. - return await authProvider.callResetPasswordFunction(username, newPassword); - }, throws('failed to reset password for user "$username"')); + authProvider.callResetPasswordFunction(username, newPassword), + throwsA(isA() + .having((e) => e.message, 'message', contains('failed to reset password for user "$username"')) + .having((e) => e.statusCode, 'statusCode', 400) + .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id='))), + ); }, appName: AppName.autoConfirm); /// JWT Payload data @@ -298,14 +308,26 @@ void main() { var token = "eyJraWQiOiIxIiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI2MmYzOTY4ODhhZjg3MjBiMzczZmYwNmEiLCJlbWFpbCI6Indvbmdfc2lnbml0dXJlX2tleUByZWFsbS5pbyIsImlhdCI6MTY2MDE0MjIxNSwiZXhwIjo0ODEzNzQyMjE1LCJhdWQiOiJtb25nb2RiLmNvbSIsImlzcyI6Imh0dHBzOi8vcmVhbG0uaW8ifQ.Af--ZUCL_KC7lAhrD_d1lq91O7qVwu7GqXifwxKojkLCkbjmAER9K2Xa7BPO8xNstFeX8m9uBo4BCD5B6XmngSmyCj5OZWdiG5LTR_uhA3MnpqcV3Vu40K4Yx8XrjPuCL39xVPnEfPKLGz5TjEcMLa8xMPqo51byX0q3mR2eSS4w1A7c5TiTNuQ23_SCO8aK95SyXwuUmU4mH0iR4sHPtf64WyoAXkx8w5twXExzky1_h473CwtAERdMsBhwz1YzFKP0kxU31pg5SRciF5Ly66sK1fSPTMQPuVdS_wKvAYll8_trWnWS83M3_PWs4UxzOdjSpoK0uqhN-_IC38YOGg"; final credentials = Credentials.jwt(token); - await expectLater(() => app.logIn(credentials), throws("crypto/rsa: verification error")); + await expectLater( + () => app.logIn(credentials), + throwsA(isA() + .having((e) => e.message, 'message', equals('unauthorized')) + .having((e) => e.statusCode, 'statusCode', 401) + .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id='))), + ); }); baasTest('Facebook credentials - invalid or expired token', (configuration) async { final app = App(configuration); final accessToken = 'invalid or expired token'; final credentials = Credentials.facebook(accessToken); - await expectLater(() => app.logIn(credentials), throws("error fetching info from OAuth2 provider")); + await expectLater( + app.logIn(credentials), + throwsA(isA() + .having((e) => e.message, 'message', equals('unauthorized')) + .having((e) => e.statusCode, 'statusCode', 401) + .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id='))), + ); }); baasTest('Function credentials - wrong payload', (configuration) { diff --git a/packages/realm_dart/test/decimal128_test.dart b/packages/realm_dart/test/decimal128_test.dart index ef6d76111..85aec4ca7 100644 --- a/packages/realm_dart/test/decimal128_test.dart +++ b/packages/realm_dart/test/decimal128_test.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:meta/meta.dart'; +import 'package:realm_dart/src/native/decimal128.dart'; import 'package:test/expect.dart' hide throws; import 'package:realm_dart/src/native/realm_core.dart'; diff --git a/packages/realm_dart/test/dynamic_realm_test.dart b/packages/realm_dart/test/dynamic_realm_test.dart index ba20e3713..8d2580c5b 100644 --- a/packages/realm_dart/test/dynamic_realm_test.dart +++ b/packages/realm_dart/test/dynamic_realm_test.dart @@ -4,9 +4,7 @@ // ignore_for_file: avoid_relative_lib_imports import 'package:collection/collection.dart'; -import 'dart:ffi'; import 'dart:typed_data'; -import 'package:test/test.dart' hide test, throws; import 'package:realm_dart/realm.dart'; import 'test.dart'; @@ -115,7 +113,8 @@ void main() { nullableObjectIdProp: objectId, nullableUuidProp: uuid, nullableIntProp: 123, - nullableDecimalProp: Decimal128.fromDouble(4242)); + nullableDecimalProp: Decimal128.fromDouble(4242), + realmValueProp: RealmValue.from('value')); AllTypes _getEmptyAllTypes() => AllTypes('', false, DateTime(0).toUtc(), 0, objectId, uuid, 0, Decimal128.zero, Uint8List(16)); @@ -205,6 +204,9 @@ void main() { expect(actual.dynamic.get('nullableDecimalProp'), expected.nullableDecimalProp); expect(actual.dynamic.get('nullableDecimalProp'), expected.nullableDecimalProp); + expect(actual.dynamic.get('realmValueProp'), expected.realmValueProp); + expect(actual.dynamic.get('realmValueProp'), expected.realmValueProp); + dynamic actualDynamic = actual; expect(actualDynamic.stringProp, expected.stringProp); expect(actualDynamic.nullableStringProp, expected.nullableStringProp); @@ -222,6 +224,97 @@ void main() { expect(actualDynamic.nullableIntProp, expected.nullableIntProp); expect(actualDynamic.decimalProp, expected.decimalProp); expect(actualDynamic.nullableDecimalProp, expected.nullableDecimalProp); + expect(actualDynamic.realmValueProp, expected.realmValueProp); + } + + void _validateDynamicSetters(RealmObject actual, AllTypes expected) { + final oid = ObjectId(); + final uuid = Uuid.v4(); + actual.realm.write(() { + actual.dynamic.set('stringProp', 'updated abc'); + actual.dynamic.set('nullableStringProp', 'updated abc'); + + actual.dynamic.set('boolProp', false); + actual.dynamic.set('nullableBoolProp', false); + + actual.dynamic.set('dateProp', DateTime.utc(1999)); + actual.dynamic.set('nullableDateProp', DateTime.utc(1999)); + + actual.dynamic.set('doubleProp', -999.111); + actual.dynamic.set('nullableDoubleProp', 111.999); + + actual.dynamic.set('objectIdProp', oid); + actual.dynamic.set('nullableObjectIdProp', oid); + + actual.dynamic.set('uuidProp', uuid); + actual.dynamic.set('nullableUuidProp', uuid); + + actual.dynamic.set('intProp', 42); + actual.dynamic.set('nullableIntProp', -42); + + actual.dynamic.set('decimalProp', Decimal128.fromDouble(500)); + actual.dynamic.set('nullableDecimalProp', Decimal128.infinity); + + actual.dynamic.set('realmValueProp', RealmValue.from([true, 5])); + }); + + expect(actual.dynamic.get('stringProp'), 'updated abc'); + expect(actual.dynamic.get('nullableStringProp'), 'updated abc'); + expect(actual.dynamic.get('boolProp'), false); + expect(actual.dynamic.get('nullableBoolProp'), false); + expect(actual.dynamic.get('dateProp'), DateTime.utc(1999)); + expect(actual.dynamic.get('nullableDateProp'), DateTime.utc(1999)); + expect(actual.dynamic.get('doubleProp'), -999.111); + expect(actual.dynamic.get('nullableDoubleProp'), 111.999); + expect(actual.dynamic.get('objectIdProp'), oid); + expect(actual.dynamic.get('nullableObjectIdProp'), oid); + expect(actual.dynamic.get('uuidProp'), uuid); + expect(actual.dynamic.get('nullableUuidProp'), uuid); + expect(actual.dynamic.get('intProp'), 42); + expect(actual.dynamic.get('nullableIntProp'), -42); + expect(actual.dynamic.get('decimalProp'), Decimal128.fromDouble(500)); + expect(actual.dynamic.get('nullableDecimalProp'), Decimal128.infinity); + expect(actual.dynamic.get('realmValueProp').asList().map((e) => e.value), [true, 5]); + + dynamic actualDynamic = actual; + + actual.realm.write(() { + actualDynamic.stringProp = expected.stringProp; + actualDynamic.nullableStringProp = expected.nullableStringProp; + actualDynamic.boolProp = expected.boolProp; + actualDynamic.nullableBoolProp = expected.nullableBoolProp; + actualDynamic.dateProp = expected.dateProp; + actualDynamic.nullableDateProp = expected.nullableDateProp; + actualDynamic.doubleProp = expected.doubleProp; + actualDynamic.nullableDoubleProp = expected.nullableDoubleProp; + actualDynamic.objectIdProp = expected.objectIdProp; + actualDynamic.nullableObjectIdProp = expected.nullableObjectIdProp; + actualDynamic.uuidProp = expected.uuidProp; + actualDynamic.nullableUuidProp = expected.nullableUuidProp; + actualDynamic.intProp = expected.intProp; + actualDynamic.nullableIntProp = expected.nullableIntProp; + actualDynamic.decimalProp = expected.decimalProp; + actualDynamic.nullableDecimalProp = expected.nullableDecimalProp; + actualDynamic.realmValueProp = expected.realmValueProp; + }); + + expect(actualDynamic.stringProp, expected.stringProp); + expect(actualDynamic.nullableStringProp, expected.nullableStringProp); + expect(actualDynamic.boolProp, expected.boolProp); + expect(actualDynamic.nullableBoolProp, expected.nullableBoolProp); + expect(actualDynamic.dateProp, expected.dateProp); + expect(actualDynamic.nullableDateProp, expected.nullableDateProp); + expect(actualDynamic.doubleProp, expected.doubleProp); + expect(actualDynamic.nullableDoubleProp, expected.nullableDoubleProp); + expect(actualDynamic.objectIdProp, expected.objectIdProp); + expect(actualDynamic.nullableObjectIdProp, expected.nullableObjectIdProp); + expect(actualDynamic.uuidProp, expected.uuidProp); + expect(actualDynamic.nullableUuidProp, expected.nullableUuidProp); + expect(actualDynamic.intProp, expected.intProp); + expect(actualDynamic.nullableIntProp, expected.nullableIntProp); + expect(actualDynamic.decimalProp, expected.decimalProp); + expect(actualDynamic.nullableDecimalProp, expected.nullableDecimalProp); + expect(actualDynamic.realmValueProp, expected.realmValueProp); } void _validateDynamicCollections(RealmObject actual, AllCollections expected) { @@ -576,8 +669,8 @@ void main() { }); }); - group('RealmObject.dynamic.get when isDynamic=$isDynamic', () { - test('gets all property types', () { + group('RealmObject.dynamic.get/set when isDynamic=$isDynamic', () { + test('works for all property types', () { final config = Configuration.local([AllTypes.schema]); final staticRealm = getRealm(config); @@ -597,9 +690,10 @@ void main() { _validateDynamic(obj1, _getPopulatedAllTypes()); _validateDynamic(obj2, _getEmptyAllTypes()); + _validateDynamicSetters(obj1, _getPopulatedAllTypes()); }); - test('gets normal links', () { + test('works for normal links', () { final config = Configuration.local([LinksClass.schema]); final staticRealm = getRealm(config); @@ -633,6 +727,22 @@ void main() { expect(dynamicObj2.link.id, uuid1); assertSchemaMatches(dynamicObj2.link.objectSchema as SchemaObject, LinksClass.schema); + + dynamicRealm.write(() { + obj1.dynamic.set('link', obj1); + obj2.dynamic.set('link', null); + }); + + expect(obj1.dynamic.get('link'), obj1); + expect(obj2.dynamic.get('link'), isNull); + + dynamicRealm.write(() { + dynamicObj1.link = null; + dynamicObj2.link = obj1; + }); + + expect(dynamicObj1.link, isNull); + expect(dynamicObj2.link, obj1); }); test('fails with non-existent property', () { @@ -647,6 +757,11 @@ void main() { dynamic dynamicObj = obj; expect(() => obj.dynamic.get('i-dont-exist'), throws("Property 'i-dont-exist' does not exist on class 'AllTypes'")); expect(() => dynamicObj.idontexist, throws("Property idontexist does not exist on class AllTypes")); + + dynamicRealm.write(() { + expect(() => obj.dynamic.set('i-dont-exist', true), throws("Property 'i-dont-exist' does not exist on class 'AllTypes'")); + expect(() => dynamicObj.idontexist = 5, throws("Property idontexist does not exist on class AllTypes")); + }); }); test('fails with wrong type', () { @@ -670,10 +785,34 @@ void main() { "Property 'nullableStringProp' on class 'AllTypes' is not the correct type. Expected 'RealmPropertyType.int', got 'RealmPropertyType.string'.")); expect(() => obj.dynamic.get('nullableIntProp'), - throws("Property 'nullableIntProp' on class 'AllTypes' is nullable but the generic argument passed to get is int.")); + throws("Property 'nullableIntProp' on class 'AllTypes' is nullable but the generic argument supplied is int.")); expect(() => obj.dynamic.get('intProp'), - throws("Property 'intProp' on class 'AllTypes' is required but the generic argument passed to get is int?.")); + throws("Property 'intProp' on class 'AllTypes' is required but the generic argument supplied is int?.")); + + dynamic dynamicObj = obj; + dynamicRealm.write(() { + expect( + () => obj.dynamic.set('stringProp', 5), + throws( + "Property 'stringProp' on class 'AllTypes' is not the correct type. Expected 'RealmPropertyType.int', got 'RealmPropertyType.string'.")); + + expect( + () => obj.dynamic.set('nullableStringProp', 5), + throws( + "Property 'nullableStringProp' on class 'AllTypes' is not the correct type. Expected 'RealmPropertyType.int', got 'RealmPropertyType.string'.")); + + expect(() => obj.dynamic.set('intProp', null), + throws("Property 'intProp' on class 'AllTypes' is required but the generic argument supplied is int?.")); + + expect(() => dynamicObj.stringProp = true, throws("Type mismatch for property 'AllTypes.stringProp'")); + expect(() => dynamicObj.nullableStringProp = 5, throws("Type mismatch for property 'AllTypes.nullableStringProp'")); + expect(() => dynamicObj.intProp = null, throws("Invalid null value for non-nullable property 'AllTypes.intProp'")); + + // Passing a non-null value to nullable property should be fine + expect(() => obj.dynamic.set('nullableIntProp', 999), returnsNormally); + expect(() => dynamicObj.nullableIntProp = 1000, returnsNormally); + }); }); test('fails on collection properties', () { @@ -699,6 +838,22 @@ void main() { () => obj.dynamic.get('stringList'), throws( "Property 'stringList' on class 'AllCollections' is 'RealmCollectionType.list' but the method used to access it expected 'RealmCollectionType.none'.")); + + dynamic dynamicObj = obj; + dynamicRealm.write(() { + expect( + () => obj.dynamic.set('stringList', 5), + throws( + "Property 'stringList' on class 'AllCollections' is 'RealmCollectionType.list' but the method used to access it expected 'RealmCollectionType.none'.")); + + expect( + () => obj.dynamic.set('stringList', null), + throws( + "Property 'stringList' on class 'AllCollections' is 'RealmCollectionType.list' but the method used to access it expected 'RealmCollectionType.none'.")); + + expect(() => dynamicObj.stringList = 5, throws("Type mismatch for property 'AllCollections.stringList'")); + expect(() => dynamicObj.stringList = null, throws("Type mismatch for property 'AllCollections.stringList'")); + }); }); }); @@ -834,6 +989,178 @@ void main() { }); } }); + + group('.changes when isDynamic=$isDynamic', () { + test('Returns stream for objects', () async { + final config = Configuration.local( + [ObjectWithEmbedded.schema, AllTypesEmbedded.schema, RecursiveEmbedded1.schema, RecursiveEmbedded2.schema, RecursiveEmbedded3.schema]); + + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(ObjectWithEmbedded('abc', recursiveObject: RecursiveEmbedded1('child 1'))); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final toplevelChanges = >[]; + final embeddedChanges = >[]; + final topLevel = dynamicRealm.dynamic.all(ObjectWithEmbedded.schema.name).single; + final embedded = topLevel.dynamic.get('recursiveObject')!; + + final topLevelSubscription = topLevel.changes.listen((event) { + toplevelChanges.add(event); + }); + + final embeddedSubscription = embedded.changes.listen((event) { + embeddedChanges.add(event); + }); + + final newUuid = Uuid.v4(); + dynamicRealm.write(() { + topLevel.dynamic.set('differentiator', newUuid); + embedded.dynamic.set('value', 'updated child 1'); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + expect(topLevel.dynamic.get('differentiator'), newUuid); + expect(embedded.dynamic.get('value'), 'updated child 1'); + + expect(toplevelChanges, hasLength(2)); + expect(toplevelChanges[0].properties, isEmpty); // First notification is delivered with empty properties + expect(toplevelChanges[1].object, topLevel); + expect(toplevelChanges[1].properties, hasLength(1)); + expect(toplevelChanges[1].properties[0], 'differentiator'); + + expect(embeddedChanges, hasLength(2)); + expect(embeddedChanges[0].properties, isEmpty); // First notification is delivered with empty properties + expect(embeddedChanges[1].object, embedded); + expect(embeddedChanges[1].properties, hasLength(1)); + expect(embeddedChanges[1].properties[0], 'value'); + + topLevelSubscription.cancel(); + embeddedSubscription.cancel(); + }); + + test('Returns stream for collection of primitives', () async { + final config = Configuration.local([AllCollections.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(_getPopulatedAllCollections()); + }); + + final dynamicRealm = _getDynamicRealm(staticRealm); + final obj = dynamicRealm.dynamic.all(AllCollections.schema.name).single; + + final listChanges = >[]; + final setChanges = >[]; + final mapChanges = >[]; + + final list = obj.dynamic.getList('stringList'); + final listSub = list.changes.listen((event) { + listChanges.add(event); + }); + + final set = obj.dynamic.getSet('doubleSet'); + final setSub = set.changes.listen((event) { + setChanges.add(event); + }); + + final map = obj.dynamic.getMap('nullableUuidMap'); + final mapSub = map.changes.listen((event) { + mapChanges.add(event); + }); + + dynamicRealm.write(() { + list[0] = 'new string'; + set.clear(); + map['new map value'] = null; + }); + + await Future.delayed(Duration(milliseconds: 20)); + + expect(listChanges, hasLength(2)); + expect(listChanges[1].inserted, isEmpty); + expect(listChanges[1].deleted, isEmpty); + expect(listChanges[1].modified, [0]); + + expect(setChanges, hasLength(2)); + expect(setChanges[1].deleted, [0, 1]); + expect(setChanges[1].inserted, isEmpty); + expect(setChanges[1].modified, isEmpty); + + expect(mapChanges, hasLength(2)); + expect(mapChanges[1].inserted, ['new map value']); + expect(mapChanges[1].deleted, isEmpty); + expect(mapChanges[1].modified, isEmpty); + + listSub.cancel(); + setSub.cancel(); + mapSub.cancel(); + }); + + test('Returns stream for collection of objects', () async { + final config = Configuration.local([LinksClass.schema]); + final staticRealm = getRealm(config); + + final uuid1 = Uuid.v4(); + final uuid2 = Uuid.v4(); + + staticRealm.write(() { + final obj1 = staticRealm.add(LinksClass(uuid1)); + staticRealm.add(LinksClass(uuid2, list: [obj1, obj1], linksSet: {obj1}, map: {'a': obj1, 'b': obj1})); + }); + + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.find(LinksClass.schema.name, uuid2)!; + + final listChanges = >[]; + final setChanges = >[]; + final mapChanges = >[]; + + final list = obj.dynamic.getList('list'); + final listSub = list.changes.listen((event) { + listChanges.add(event); + }); + + final set = obj.dynamic.getSet('linksSet'); + final setSub = set.changes.listen((event) { + setChanges.add(event); + }); + + final map = obj.dynamic.getMap('map'); + final mapSub = map.changes.listen((event) { + mapChanges.add(event); + }); + + dynamicRealm.write(() { + list[0] = dynamicRealm.dynamic.create(LinksClass.schema.name, primaryKey: Uuid.v4()); + set.clear(); + map['new map value'] = null; + }); + + await Future.delayed(Duration(milliseconds: 20)); + + expect(listChanges, hasLength(2)); + expect(listChanges[1].inserted, isEmpty); + expect(listChanges[1].deleted, isEmpty); + expect(listChanges[1].modified, [0]); + + expect(setChanges, hasLength(2)); + expect(setChanges[1].deleted, [0]); + expect(setChanges[1].inserted, isEmpty); + expect(setChanges[1].modified, isEmpty); + + expect(mapChanges, hasLength(2)); + expect(mapChanges[1].inserted, ['new map value']); + expect(mapChanges[1].deleted, isEmpty); + expect(mapChanges[1].modified, isEmpty); + + listSub.cancel(); + setSub.cancel(); + mapSub.cancel(); + }); + }); } test('RealmObject.dynamic.get when static can get all property types', () { diff --git a/packages/realm_dart/test/dynamic_realm_test.realm.dart b/packages/realm_dart/test/dynamic_realm_test.realm.dart index c65d84fc2..1829305bd 100644 --- a/packages/realm_dart/test/dynamic_realm_test.realm.dart +++ b/packages/realm_dart/test/dynamic_realm_test.realm.dart @@ -34,6 +34,10 @@ class Taskv2 extends _Taskv2 with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Taskv2 freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/embedded_test.dart b/packages/realm_dart/test/embedded_test.dart index f49968f18..9936d7d67 100644 --- a/packages/realm_dart/test/embedded_test.dart +++ b/packages/realm_dart/test/embedded_test.dart @@ -1,6 +1,7 @@ // Copyright 2022 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 +import 'package:realm_dart/src/native/decimal128.dart'; import 'package:test/test.dart' hide test, throws; import 'package:realm_dart/realm.dart'; @@ -840,7 +841,7 @@ void main() { ['text1', null, 2.2, 3] // Searching by different type of values and null ]); expect(results.length, 3); - }); + }, skip: true); } extension on RealmObjectBase { diff --git a/packages/realm_dart/test/geospatial_test.realm.dart b/packages/realm_dart/test/geospatial_test.realm.dart index 7757ff370..70e002d74 100644 --- a/packages/realm_dart/test/geospatial_test.realm.dart +++ b/packages/realm_dart/test/geospatial_test.realm.dart @@ -41,6 +41,10 @@ class Location extends _Location Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Location freeze() => RealmObjectBase.freezeObject(this); @@ -108,6 +112,10 @@ class Restaurant extends _Restaurant Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Restaurant freeze() => RealmObjectBase.freezeObject(this); @@ -169,6 +177,11 @@ class LocationList extends _LocationList Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override LocationList freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/indexed_test.realm.dart b/packages/realm_dart/test/indexed_test.realm.dart index 783221c33..b1ee9d3f4 100644 --- a/packages/realm_dart/test/indexed_test.realm.dart +++ b/packages/realm_dart/test/indexed_test.realm.dart @@ -64,6 +64,11 @@ class WithIndexes extends _WithIndexes Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override WithIndexes freeze() => RealmObjectBase.freezeObject(this); @@ -181,6 +186,10 @@ class NoIndexes extends _NoIndexes Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NoIndexes freeze() => RealmObjectBase.freezeObject(this); @@ -270,6 +279,11 @@ class ObjectWithFTSIndex extends _ObjectWithFTSIndex Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override ObjectWithFTSIndex freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/migration_test.realm.dart b/packages/realm_dart/test/migration_test.realm.dart index 9e087e83e..8673710e7 100644 --- a/packages/realm_dart/test/migration_test.realm.dart +++ b/packages/realm_dart/test/migration_test.realm.dart @@ -26,6 +26,11 @@ class PersonIntName extends _PersonIntName Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override PersonIntName freeze() => RealmObjectBase.freezeObject(this); @@ -87,6 +92,10 @@ class StudentV1 extends _StudentV1 Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override StudentV1 freeze() => RealmObjectBase.freezeObject(this); @@ -151,6 +160,11 @@ class MyObjectWithTypo extends _MyObjectWithTypo Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override MyObjectWithTypo freeze() => RealmObjectBase.freezeObject(this); @@ -216,6 +230,11 @@ class MyObjectWithoutTypo extends _MyObjectWithoutTypo Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override MyObjectWithoutTypo freeze() => RealmObjectBase.freezeObject(this); @@ -275,6 +294,11 @@ class MyObjectWithoutValue extends _MyObjectWithoutValue Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override MyObjectWithoutValue freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/realm_map_test.realm.dart b/packages/realm_dart/test/realm_map_test.realm.dart index 81ed9dc79..866fe01ca 100644 --- a/packages/realm_dart/test/realm_map_test.realm.dart +++ b/packages/realm_dart/test/realm_map_test.realm.dart @@ -32,6 +32,10 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Car freeze() => RealmObjectBase.freezeObject(this); @@ -89,6 +93,11 @@ class EmbeddedValue extends _EmbeddedValue Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override EmbeddedValue freeze() => RealmObjectBase.freezeObject(this); @@ -359,6 +368,11 @@ class TestRealmMaps extends _TestRealmMaps Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override TestRealmMaps freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/realm_object_test.dart b/packages/realm_dart/test/realm_object_test.dart index be46bbf34..9f07050a9 100644 --- a/packages/realm_dart/test/realm_object_test.dart +++ b/packages/realm_dart/test/realm_object_test.dart @@ -7,6 +7,7 @@ import 'dart:typed_data'; import 'package:test/test.dart' hide test, throws; import 'package:realm_dart/realm.dart'; +import 'backlinks_test.dart'; import 'test.dart'; part 'realm_object_test.realm.dart'; @@ -74,9 +75,655 @@ class _BoolValue { late bool value; } +@RealmModel() +class _TestNotificationObject { + late String? stringProperty; + + late int? intProperty; + + @MapTo("_remappedIntProperty") + late int? remappedIntProperty; + + late _TestNotificationObject? link; + + late _TestNotificationEmbeddedObject? embedded; + + late List<_TestNotificationObject> listLinks; + + late Set<_TestNotificationObject> setLinks; + + late Map mapLinks; + + @Backlink(#link) + late Iterable<_TestNotificationObject> backlink; +} + +@RealmModel(ObjectType.embeddedObject) +class _TestNotificationEmbeddedObject { + late String? stringProperty; + + late int? intProperty; +} + void main() { setupTests(); + group("RealmObject keypath filtering", () { + Future verifyNotifications(T obj, List> changeList, List? changedProperties, + {bool isDeleted = false}) async { + await Future.delayed(Duration(milliseconds: 20)); + + if (changedProperties == null) { + expect(changeList, hasLength(0)); + return; + } + + expect(changeList, hasLength(1)); + var changes = changeList[0]; + + if (isDeleted) { + expect(changes.isDeleted, isTrue); + return; + } + + expect(changes.isDeleted, isFalse); + expect(changes.object, obj); + expect(changes.properties, unorderedEquals(changedProperties)); + changeList.clear(); + } + + test('basic single keypath test', () async { + var config = Configuration.local([Dog.schema, Person.schema]); + var realm = getRealm(config); + + final dog = Dog("Mario"); + + realm.write(() { + realm.add(dog); + }); + + final externalChanges = >[]; + final subscription = dog.changesFor(["age"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + dog.age = 2; + dog.owner = Person("owner"); + }); + + await verifyNotifications(dog, externalChanges, ["age"]); + subscription.cancel(); + }); + + test('basic multiple keypaths test', () async { + var config = Configuration.local([Dog.schema, Person.schema]); + var realm = getRealm(config); + + final dog = Dog("Mario"); + + realm.write(() { + realm.add(dog); + }); + + final externalChanges = >[]; + final subscription = dog.changesFor(["age", "owner"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + dog.age = 2; + dog.owner = Person("owner"); + }); + + await verifyNotifications(dog, externalChanges, ["age", "owner"]); + subscription.cancel(); + }); + + test('empty keypath', () async { + var config = Configuration.local([Dog.schema, Person.schema]); + var realm = getRealm(config); + + final dog = Dog("Mario"); + + realm.write(() { + realm.add(dog); + }); + + expect(() { + dog.changesFor([""]); + }, throws("It is not allowed to have empty key paths.")); + + expect(() { + dog.changesFor(["age", ""]); + }, throws("It is not allowed to have empty key paths.")); + }); + + test('unknown keypath', () async { + var config = Configuration.local([Dog.schema, Person.schema]); + var realm = getRealm(config); + + final dog = Dog("Mario"); + + realm.write(() { + realm.add(dog); + }); + + expect(() { + dog.changesFor(["unknown"]); + }, throws("Property 'unknown' in KeyPath 'unknown' is not a valid property in Dog.")); + + expect(() { + dog.changesFor(["age", "unknown"]); + }, throws("Property 'unknown' in KeyPath 'unknown' is not a valid property in Dog.")); + }); + + test('embedded', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["embedded"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + // Property change + realm.write(() { + tno.embedded = TestNotificationEmbeddedObject(); + }); + + await verifyNotifications(tno, externalChanges, ["embedded"]); + + // Nested property change -- should not raise + realm.write(() { + tno.embedded?.stringProperty = "NewVal"; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('nested property on embedded', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["embedded.stringProperty"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + // Property change + realm.write(() { + tno.embedded = TestNotificationEmbeddedObject(); + }); + + await verifyNotifications(tno, externalChanges, ["embedded"]); + + // Nested property change in keypath -- should raise + realm.write(() { + tno.embedded?.stringProperty = "NewVal"; + }); + + await verifyNotifications(tno, externalChanges, ["embedded"]); + + // Nested property change not on keypath -- should not raise + realm.write(() { + tno.embedded?.intProperty = 23; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('link', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["link"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + // Property change + realm.write(() { + tno.link = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["link"]); + + // Nested property change -- should not raise + realm.write(() { + tno.link?.stringProperty = "NewVal"; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('nested property on link', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["link.stringProperty"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + // Top level property change + realm.write(() { + tno.link = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["link"]); + + // Nested property on keypath -- should raise + realm.write(() { + tno.link?.stringProperty = "NewVal"; + }); + + await verifyNotifications(tno, externalChanges, ["link"]); + + // Nested property not on keypath -- should not raise + realm.write(() { + tno.link?.intProperty = 23; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('mappedProperty', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["remappedIntProperty"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + tno.remappedIntProperty = 23; + tno.stringProperty = "newVal"; + }); + + // This fails because it's raising a notification for "_remappedIntProperty" + await verifyNotifications(tno, externalChanges, ["remappedIntProperty"]); + + subscription.cancel(); + }); + + test('collection top level', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["listLinks", "setLinks", "mapLinks"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + // Collection changes -- should raise + realm.write(() { + tno.listLinks.add(TestNotificationObject()); + tno.setLinks.add(TestNotificationObject()); + tno.mapLinks["test"] = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks"]); + + // Nested properties -- should not raise + realm.write(() { + tno.listLinks[0].stringProperty = "newVal"; + tno.setLinks.elementAt(0).stringProperty = "newVal"; + tno.mapLinks["test"]?.stringProperty = "newVal"; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('collection nested', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["listLinks.stringProperty", "setLinks.stringProperty", "mapLinks.stringProperty"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + // Collection changes -- should raise + realm.write(() { + tno.listLinks.add(TestNotificationObject()); + tno.setLinks.add(TestNotificationObject()); + tno.mapLinks["test"] = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks"]); + + // Nested properties -- should raise + realm.write(() { + tno.listLinks[0].stringProperty = "newVal"; + tno.setLinks.elementAt(0).stringProperty = "newVal"; + tno.mapLinks["test"]?.stringProperty = "newVal"; + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks"]); + + subscription.cancel(); + }); + + test('backlink', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["backlink"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + realm.add(TestNotificationObject(link: tno)); + }); + + expect(tno.backlink.length, 1); + + await verifyNotifications(tno, externalChanges, ["backlink"]); + + subscription.cancel(); + }, skip: "Needs to be re-enabled when https://github.com/realm/realm-dart/issues/1631 is investigated"); + + test('null gives default subscriptions', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(null).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + tno.stringProperty = "test"; + tno.intProperty = 23; + tno.link = TestNotificationObject(); + tno.listLinks.add(TestNotificationObject()); + tno.setLinks.add(TestNotificationObject()); + tno.mapLinks["test"] = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks", "stringProperty", "intProperty", "link"]); + + realm.write(() { + tno.link?.stringProperty = "test"; + tno.listLinks[0].stringProperty = "newVal"; + tno.setLinks.elementAt(0).stringProperty = "newVal"; + tno.mapLinks["test"]?.stringProperty = "newVal"; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('empty list gives default subscriptions', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor([]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + tno.stringProperty = "test"; + tno.intProperty = 23; + tno.link = TestNotificationObject(); + tno.listLinks.add(TestNotificationObject()); + tno.setLinks.add(TestNotificationObject()); + tno.mapLinks["test"] = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks", "stringProperty", "intProperty", "link"]); + + realm.write(() { + tno.link?.stringProperty = "test"; + tno.listLinks[0].stringProperty = "newVal"; + tno.setLinks.elementAt(0).stringProperty = "newVal"; + tno.mapLinks["test"]?.stringProperty = "newVal"; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('wildcard', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["*"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + tno.stringProperty = "test"; + tno.intProperty = 23; + tno.link = TestNotificationObject(); + tno.listLinks.add(TestNotificationObject()); + tno.setLinks.add(TestNotificationObject()); + tno.mapLinks["test"] = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks", "stringProperty", "intProperty", "link"]); + + // Deeper than the top level wildcard - should not raise + realm.write(() { + tno.listLinks[0].stringProperty = "newVal"; + tno.setLinks.elementAt(0).stringProperty = "newVal"; + tno.mapLinks["test"]?.stringProperty = "newVal"; + }); + + await verifyNotifications(tno, externalChanges, null); + + subscription.cancel(); + }); + + test('nested wildcard', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["*.*"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + tno.stringProperty = "test"; + tno.intProperty = 23; + tno.link = TestNotificationObject(); + tno.listLinks.add(TestNotificationObject()); + tno.setLinks.add(TestNotificationObject()); + tno.mapLinks["test"] = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks", "stringProperty", "intProperty", "link"]); + + realm.write(() { + tno.link?.stringProperty = "test"; + tno.listLinks[0].stringProperty = "newVal"; + tno.setLinks.elementAt(0).stringProperty = "newVal"; + tno.mapLinks["test"]?.stringProperty = "newVal"; + + tno.link?.link = TestNotificationObject(); + tno.link?.link?.link = TestNotificationObject(); + + tno.listLinks[0].link = TestNotificationObject(); + tno.setLinks.elementAt(0).link = TestNotificationObject(); + tno.mapLinks["test"]?.link = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks", "link"]); + + realm.write(() { + tno.link?.link?.link?.stringProperty = "test"; + }); + + await verifyNotifications(tno, externalChanges, ["link"]); + + subscription.cancel(); + }); + + test('test nested wildcard', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["*.*"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + realm.write(() { + tno.link = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["link"]); + + realm.write(() { + tno.link?.link = TestNotificationObject(); + tno.link?.link?.link = TestNotificationObject(); + tno.link?.link?.link?.link = TestNotificationObject(); + tno.link?.link?.link?.link?.link = TestNotificationObject(); + tno.link?.link?.link?.link?.link?.link = TestNotificationObject(); + }); + + await verifyNotifications(tno, externalChanges, ["link"]); + + realm.write(() { + tno.link?.link?.link?.link?.link?.link?.stringProperty = "test"; + }); + + // Why is it still raising a notification for link? + await verifyNotifications(tno, externalChanges, ["link"]); + + subscription.cancel(); + }); + + test('subscribing multiple times merges keypaths', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); + var realm = getRealm(config); + + final tno = TestNotificationObject(); + + realm.write(() { + realm.add(tno); + }); + + final externalChanges = >[]; + final subscription = tno.changesFor(["intProperty"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges.add(changes); + }); + + final externalChanges2 = >[]; + final subscription2 = tno.changesFor(["stringProperty"]).listen((changes) { + if (changes.properties.isNotEmpty) externalChanges2.add(changes); + }); + + realm.write(() { + tno.intProperty = 23; + tno.stringProperty = "test"; + tno.link = TestNotificationObject(); + }); + + // Both of these fails because both "intProperty and "stringProperty" are reported as changed + await verifyNotifications(tno, externalChanges, ["intProperty", "stringProperty"]); + + subscription.cancel(); + subscription2.cancel(); + }); + }); + test('RealmObject get property', () { var config = Configuration.local([Car.schema]); var realm = getRealm(config); @@ -263,7 +910,7 @@ void main() { callNum++; expect(changes.isDeleted, true); expect(changes.object, dog); - expect(changes.properties, []); + expect(changes.properties.isEmpty, true); } }); diff --git a/packages/realm_dart/test/realm_object_test.realm.dart b/packages/realm_dart/test/realm_object_test.realm.dart index a04188cfd..20c88bcdd 100644 --- a/packages/realm_dart/test/realm_object_test.realm.dart +++ b/packages/realm_dart/test/realm_object_test.realm.dart @@ -26,6 +26,11 @@ class ObjectIdPrimaryKey extends _ObjectIdPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override ObjectIdPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -81,6 +86,11 @@ class NullableObjectIdPrimaryKey extends _NullableObjectIdPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableObjectIdPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -138,6 +148,11 @@ class IntPrimaryKey extends _IntPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override IntPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -192,6 +207,11 @@ class NullableIntPrimaryKey extends _NullableIntPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableIntPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -248,6 +268,11 @@ class StringPrimaryKey extends _StringPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override StringPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -303,6 +328,11 @@ class NullableStringPrimaryKey extends _NullableStringPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableStringPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -359,6 +389,11 @@ class UuidPrimaryKey extends _UuidPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override UuidPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -413,6 +448,11 @@ class NullableUuidPrimaryKey extends _NullableUuidPrimaryKey Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableUuidPrimaryKey freeze() => RealmObjectBase.freezeObject(this); @@ -472,6 +512,11 @@ class RemappedFromAnotherFile extends _RemappedFromAnotherFile Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override RemappedFromAnotherFile freeze() => RealmObjectBase.freezeObject(this); @@ -537,6 +582,10 @@ class BoolValue extends _BoolValue Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override BoolValue freeze() => RealmObjectBase.freezeObject(this); @@ -574,3 +623,268 @@ class BoolValue extends _BoolValue @override SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } + +class TestNotificationObject extends _TestNotificationObject + with RealmEntity, RealmObjectBase, RealmObject { + TestNotificationObject({ + String? stringProperty, + int? intProperty, + int? remappedIntProperty, + TestNotificationObject? link, + TestNotificationEmbeddedObject? embedded, + Iterable listLinks = const [], + Set setLinks = const {}, + Map mapLinks = const {}, + }) { + RealmObjectBase.set(this, 'stringProperty', stringProperty); + RealmObjectBase.set(this, 'intProperty', intProperty); + RealmObjectBase.set(this, '_remappedIntProperty', remappedIntProperty); + RealmObjectBase.set(this, 'link', link); + RealmObjectBase.set(this, 'embedded', embedded); + RealmObjectBase.set>( + this, 'listLinks', RealmList(listLinks)); + RealmObjectBase.set>( + this, 'setLinks', RealmSet(setLinks)); + RealmObjectBase.set>( + this, 'mapLinks', RealmMap(mapLinks)); + } + + TestNotificationObject._(); + + @override + String? get stringProperty => + RealmObjectBase.get(this, 'stringProperty') as String?; + @override + set stringProperty(String? value) => + RealmObjectBase.set(this, 'stringProperty', value); + + @override + int? get intProperty => RealmObjectBase.get(this, 'intProperty') as int?; + @override + set intProperty(int? value) => + RealmObjectBase.set(this, 'intProperty', value); + + @override + int? get remappedIntProperty => + RealmObjectBase.get(this, '_remappedIntProperty') as int?; + @override + set remappedIntProperty(int? value) => + RealmObjectBase.set(this, '_remappedIntProperty', value); + + @override + TestNotificationObject? get link => + RealmObjectBase.get(this, 'link') + as TestNotificationObject?; + @override + set link(covariant TestNotificationObject? value) => + RealmObjectBase.set(this, 'link', value); + + @override + TestNotificationEmbeddedObject? get embedded => + RealmObjectBase.get(this, 'embedded') + as TestNotificationEmbeddedObject?; + @override + set embedded(covariant TestNotificationEmbeddedObject? value) => + RealmObjectBase.set(this, 'embedded', value); + + @override + RealmList get listLinks => + RealmObjectBase.get(this, 'listLinks') + as RealmList; + @override + set listLinks(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get setLinks => + RealmObjectBase.get(this, 'setLinks') + as RealmSet; + @override + set setLinks(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get mapLinks => + RealmObjectBase.get(this, 'mapLinks') + as RealmMap; + @override + set mapLinks(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmResults get backlink { + if (!isManaged) { + throw RealmError('Using backlinks is only possible for managed objects.'); + } + return RealmObjectBase.get(this, 'backlink') + as RealmResults; + } + + @override + set backlink(covariant RealmResults value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + + @override + TestNotificationObject freeze() => + RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + 'stringProperty': stringProperty.toEJson(), + 'intProperty': intProperty.toEJson(), + '_remappedIntProperty': remappedIntProperty.toEJson(), + 'link': link.toEJson(), + 'embedded': embedded.toEJson(), + 'listLinks': listLinks.toEJson(), + 'setLinks': setLinks.toEJson(), + 'mapLinks': mapLinks.toEJson(), + }; + } + + static EJsonValue _toEJson(TestNotificationObject value) => value.toEJson(); + static TestNotificationObject _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'stringProperty': EJsonValue stringProperty, + 'intProperty': EJsonValue intProperty, + '_remappedIntProperty': EJsonValue remappedIntProperty, + 'link': EJsonValue link, + 'embedded': EJsonValue embedded, + 'listLinks': EJsonValue listLinks, + 'setLinks': EJsonValue setLinks, + 'mapLinks': EJsonValue mapLinks, + } => + TestNotificationObject( + stringProperty: fromEJson(stringProperty), + intProperty: fromEJson(intProperty), + remappedIntProperty: fromEJson(remappedIntProperty), + link: fromEJson(link), + embedded: fromEJson(embedded), + listLinks: fromEJson(listLinks), + setLinks: fromEJson(setLinks), + mapLinks: fromEJson(mapLinks), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(TestNotificationObject._); + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, TestNotificationObject, + 'TestNotificationObject', [ + SchemaProperty('stringProperty', RealmPropertyType.string, + optional: true), + SchemaProperty('intProperty', RealmPropertyType.int, optional: true), + SchemaProperty('remappedIntProperty', RealmPropertyType.int, + mapTo: '_remappedIntProperty', optional: true), + SchemaProperty('link', RealmPropertyType.object, + optional: true, linkTarget: 'TestNotificationObject'), + SchemaProperty('embedded', RealmPropertyType.object, + optional: true, linkTarget: 'TestNotificationEmbeddedObject'), + SchemaProperty('listLinks', RealmPropertyType.object, + linkTarget: 'TestNotificationObject', + collectionType: RealmCollectionType.list), + SchemaProperty('setLinks', RealmPropertyType.object, + linkTarget: 'TestNotificationObject', + collectionType: RealmCollectionType.set), + SchemaProperty('mapLinks', RealmPropertyType.object, + optional: true, + linkTarget: 'TestNotificationObject', + collectionType: RealmCollectionType.map), + SchemaProperty('backlink', RealmPropertyType.linkingObjects, + linkOriginProperty: 'link', + collectionType: RealmCollectionType.list, + linkTarget: 'TestNotificationObject'), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} + +class TestNotificationEmbeddedObject extends _TestNotificationEmbeddedObject + with RealmEntity, RealmObjectBase, EmbeddedObject { + TestNotificationEmbeddedObject({ + String? stringProperty, + int? intProperty, + }) { + RealmObjectBase.set(this, 'stringProperty', stringProperty); + RealmObjectBase.set(this, 'intProperty', intProperty); + } + + TestNotificationEmbeddedObject._(); + + @override + String? get stringProperty => + RealmObjectBase.get(this, 'stringProperty') as String?; + @override + set stringProperty(String? value) => + RealmObjectBase.set(this, 'stringProperty', value); + + @override + int? get intProperty => RealmObjectBase.get(this, 'intProperty') as int?; + @override + set intProperty(int? value) => + RealmObjectBase.set(this, 'intProperty', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor( + this, keyPaths); + + @override + TestNotificationEmbeddedObject freeze() => + RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + 'stringProperty': stringProperty.toEJson(), + 'intProperty': intProperty.toEJson(), + }; + } + + static EJsonValue _toEJson(TestNotificationEmbeddedObject value) => + value.toEJson(); + static TestNotificationEmbeddedObject _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'stringProperty': EJsonValue stringProperty, + 'intProperty': EJsonValue intProperty, + } => + TestNotificationEmbeddedObject( + stringProperty: fromEJson(stringProperty), + intProperty: fromEJson(intProperty), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(TestNotificationEmbeddedObject._); + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.embeddedObject, + TestNotificationEmbeddedObject, 'TestNotificationEmbeddedObject', [ + SchemaProperty('stringProperty', RealmPropertyType.string, + optional: true), + SchemaProperty('intProperty', RealmPropertyType.int, optional: true), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} diff --git a/packages/realm_dart/test/realm_set_test.realm.dart b/packages/realm_dart/test/realm_set_test.realm.dart index d8c5df19c..0bf0ccd68 100644 --- a/packages/realm_dart/test/realm_set_test.realm.dart +++ b/packages/realm_dart/test/realm_set_test.realm.dart @@ -32,6 +32,10 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Car freeze() => RealmObjectBase.freezeObject(this); @@ -272,6 +276,11 @@ class TestRealmSets extends _TestRealmSets Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override TestRealmSets freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/realm_test.dart b/packages/realm_dart/test/realm_test.dart index abbd55891..2397040c8 100644 --- a/packages/realm_dart/test/realm_test.dart +++ b/packages/realm_dart/test/realm_test.dart @@ -9,7 +9,6 @@ import 'dart:isolate'; import 'package:path/path.dart' as p; import 'package:realm_dart/realm.dart'; import 'package:realm_dart/src/native/realm_core.dart'; -import 'package:test/test.dart' hide test, throws; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; @@ -393,6 +392,9 @@ void main() { //Ensure the team exists in realm var teams = realm.all(); expect(teams.length, 1); + expect(teams[0].players, newPlayers); + final allPersons = realm.all(); + expect(allPersons.length, 3); //Delete team players realm.write(() => realm.deleteMany(teams[0].players)); @@ -401,7 +403,6 @@ void main() { expect(teams[0].players.length, 0); //Reload all persons from realm and ensure they are deleted - final allPersons = realm.all(); expect(allPersons.length, 0); }); @@ -1819,7 +1820,7 @@ void main() { final results = realm.query(r"name == $0", [personName]); expect(realm.refresh(), false); - realmCore.realmDisableAutoRefreshForTesting(realm); + realm.disableAutoRefreshForTesting(); ReceivePort receivePort = ReceivePort(); Isolate.spawn((SendPort sendPort) async { diff --git a/packages/realm_dart/test/realm_value_test.dart b/packages/realm_dart/test/realm_value_test.dart index 78efa383d..eb679f203 100644 --- a/packages/realm_dart/test/realm_value_test.dart +++ b/packages/realm_dart/test/realm_value_test.dart @@ -1,11 +1,15 @@ // Copyright 2022 MongoDB, Inc. // SPDX-License-Identifier: Apache-2.0 +import 'dart:math'; import 'dart:typed_data'; -import 'package:test/test.dart' hide test, throws; +import 'package:collection/collection.dart'; +import 'package:realm_common/realm_common.dart' show memEquals; import 'package:realm_dart/realm.dart'; +import 'package:test/test.dart' hide test, throws; +import 'session_test.dart' show validateSessionStates; import 'test.dart'; part 'realm_value_test.realm.dart'; @@ -15,30 +19,41 @@ class _TuckedIn { int x = 42; } -@RealmModel() -class _AnythingGoes { - @Indexed() - late RealmValue oneAny; - late List manyAny; - late Map dictOfAny; - late Set setOfAny; -} - -@RealmModel() -class _Stuff { - int i = 42; -} - void main() { setupTests(); Realm getMixedRealm() { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final config = Configuration.local([ObjectWithRealmValue.schema, ObjectWithInt.schema, TuckedIn.schema]); return getRealm(config); } + Future logInAndGetSyncedRealm(AppConfiguration appConfig, ObjectId differentiator) async { + final realm = await getIntegrationRealm(appConfig: appConfig, differentiator: differentiator); + realm.subscriptions.update((mutableSubscriptions) { + mutableSubscriptions.add(realm.query(r'differentiator = $0', [differentiator])); + mutableSubscriptions.add(realm.query(r'differentiator = $0', [differentiator])); + }); + await realm.subscriptions.waitForSynchronization(); + + return realm; + } + + Future<(Realm, Realm)> logInAndGetSyncedRealms(AppConfiguration appConfig, ObjectId differentiator) async { + final realm1 = await logInAndGetSyncedRealm(appConfig, differentiator); + final realm2 = await logInAndGetSyncedRealm(appConfig, differentiator); + expect(realm1.all().isEmpty, true); + expect(realm2.all().isEmpty, true); + + return (realm1, realm2); + } + + Future waitForSynchronization({required Realm uploadRealm, required Realm downloadRealm}) async { + await uploadRealm.syncSession.waitForUpload(); + await downloadRealm.syncSession.waitForDownload(); + } + group('RealmValue', () { - final primitiveValues = [ + final primitiveValues = [ null, true, 'text', @@ -48,30 +63,48 @@ void main() { ObjectId.fromHexString('64c13ab08edf48a008793cac'), Uuid.fromString('7a459a5e-5eb6-45f6-9b72-8f794e324105'), Decimal128.fromDouble(128.128), - Uint8List.fromList([1, 2, 0]) + Uint8List.fromList(List.generate(21, (index) => index)), // 21 % 8 != 0 so not a multiple of 8 bytes ]; for (final x in primitiveValues) { test('Roundtrip ${x.runtimeType} $x', () { final realm = getMixedRealm(); - final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); + final something = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(x)))); expect(something.oneAny.value.runtimeType, x.runtimeType); expect(something.oneAny.value, x); expect(something.oneAny, RealmValue.from(x)); }); + baasTest('Roundtrip ${x.runtimeType} $x', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(x)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + expect(object2.oneAny.value.runtimeType, x.runtimeType); + expect(object2.oneAny.value, x); + expect(object2.oneAny, RealmValue.from(x)); + }); + final queryArg = RealmValue.from(x); test('Query @type == ${queryArg.type} $x', () { final realm = getMixedRealm(); realm.write(() { // Add all values, we're going to query for just one of them. for (final v in primitiveValues) { - realm.add(AnythingGoes(oneAny: RealmValue.from(v))); + realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(v))); } - realm.add(AnythingGoes(oneAny: RealmValue.from(Stuff()))); + realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(ObjectWithInt(ObjectId())))); }); - final matches = realm.query(r'oneAny.@type == $0', [queryArg.type]); + final matches = realm.query(r'oneAny.@type == $0', [queryArg.type]); expect(matches.length, 1); expect(matches.single.oneAny.value, x); expect(matches.single.oneAny.type, queryArg.type); @@ -80,42 +113,72 @@ void main() { } test('Roundtrip object', () { - final stuff = Stuff(i: 123); final realm = getMixedRealm(); - final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(stuff)))); - expect(something.oneAny.value.runtimeType, Stuff); - expect(something.oneAny.as().i, 123); + final child = ObjectWithInt(ObjectId(), i: 123); + final parent = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(child)))); + expect(parent.oneAny.value.runtimeType, ObjectWithInt); + expect(parent.oneAny.as().i, 123); + }); + + baasTest('Roundtrip object', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final child1 = ObjectWithInt(ObjectId(), differentiator: differentiator, i: 123); + final parent1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(child1)); + realm1.write(() => realm1.add(parent1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + expect(realm2.all().single.i, 123); + final parent2 = realm2.all().single; + expect(parent2.id, parent1.id); + + expect(parent2.oneAny.value.runtimeType, ObjectWithInt); + final child2 = parent2.oneAny.as(); + expect(child2.i, 123); + + // Update child object in second realm. + const newValue = 456; + realm2.write(() => child2.i = newValue); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check updated object in first realm. + expect(parent1.oneAny.as().i, newValue); }); test('Query @type == object', () { final realm = getMixedRealm(); realm.write(() { for (final v in primitiveValues) { - realm.add(AnythingGoes(oneAny: RealmValue.from(v))); + realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(v))); } - realm.add(AnythingGoes(oneAny: RealmValue.from(Stuff(i: 123)))); + realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(ObjectWithInt(ObjectId(), i: 123)))); }); - final matches = realm.query(r'oneAny.@type == $0', [RealmValueType.object]); + final matches = realm.query(r'oneAny.@type == $0', [RealmValueType.object]); expect(matches.length, 1); - expect(matches.single.oneAny.as().i, 123); + expect(matches.single.oneAny.as().i, 123); expect(matches.single.oneAny.type, RealmValueType.object); }); test('Illegal value', () { final realm = getMixedRealm(); - expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(realm)))), throwsArgumentError); + expect(() => realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(realm)))), throwsArgumentError); }); test('Embedded object not allowed in RealmValue', () { final realm = getMixedRealm(); - expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(TuckedIn())))), throwsArgumentError); + expect(() => realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(TuckedIn())))), throwsArgumentError); }); for (final x in primitiveValues) { test('Switch $x', () { - final something = AnythingGoes(oneAny: RealmValue.from(x)); + final something = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(x)); final value = something.oneAny.value; switch (something.oneAny.type) { @@ -135,7 +198,7 @@ void main() { expect(value, isA()); break; case RealmValueType.object: - expect(value is AnythingGoes || value is Stuff, true); + expect(value is ObjectWithRealmValue || value is ObjectWithInt, true); break; case RealmValueType.dateTime: expect(value, isA()); @@ -161,7 +224,7 @@ void main() { for (final x in primitiveValues) { test('If-is $x', () { - final something = AnythingGoes(oneAny: RealmValue.from(x)); + final something = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(x)); final value = something.oneAny.value; final type = something.oneAny.type; if (value == null) { @@ -184,9 +247,9 @@ void main() { expect(type, RealmValueType.decimal); } else if (value is Uint8List) { expect(type, RealmValueType.binary); - } else if (value is AnythingGoes) { + } else if (value is ObjectWithRealmValue) { expect(type, RealmValueType.object); - } else if (value is Stuff) { + } else if (value is ObjectWithInt) { expect(type, RealmValueType.object); } else { fail('$value not handled correctly in if-is'); @@ -196,30 +259,31 @@ void main() { test('Unknown schema for RealmValue.value after bad migration', () { { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema], schemaVersion: 0); + final config = Configuration.local([ObjectWithRealmValue.schema, ObjectWithInt.schema], schemaVersion: 0); Realm.deleteRealm(config.path); final realm = Realm(config); - final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.realmObject(Stuff())))); + final object = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.realmObject(ObjectWithInt(ObjectId()))); + final something = realm.write(() => realm.add(object)); expect(something.oneAny, isA()); - expect(something.oneAny.value, isA()); - expect(something.oneAny.as().i, 42); + expect(something.oneAny.value, isA()); + expect(something.oneAny.as().i, 42); realm.close(); } - // From here on Stuff is unknown + // From here on ObjectWithInt is unknown final config = Configuration.local( - [AnythingGoes.schema], + [ObjectWithRealmValue.schema], schemaVersion: 1, migrationCallback: (migration, oldSchemaVersion) { - // forget to handle RealmValue pointing to Stuff + // forget to handle RealmValue pointing to ObjectWithInt }, ); final realm = getRealm(config); - final something = realm.all()[0]; - // something.oneAny points to a Stuff, but that is not known, so returns null. + final something = realm.all()[0]; + // something.oneAny points to a ObjectWithInt, but that is not known, so returns null. // A better option would be to return a DynamicRealmObject, but c-api does // not currently allow this. expect(something.oneAny, const RealmValue.nullValue()); // at least we don't crash :-) @@ -227,76 +291,114 @@ void main() { }); group('List', () { - final now = DateTime.now().toUtc(); - final values = [ - null, - true, - 'text', - 42, - 3.14, - AnythingGoes(), - Stuff(), - now, - ObjectId.fromTimestamp(now), - Uuid.v4(), - Decimal128.fromInt(128), - ]; + final differentiator = ObjectId(); + List getValues() { + final now = DateTime.now().toUtc(); + return [ + null, + true, + 'text', + 42, + 3.14, + ObjectWithRealmValue(ObjectId(), differentiator: differentiator), + ObjectWithInt(ObjectId(), differentiator: differentiator), + now, + ObjectId.fromTimestamp(now), + Uuid.v4(), + Decimal128.fromInt(128), + ]; + } test('Roundtrip', () { + final values = getValues(); final realm = getMixedRealm(); - final something = realm.write(() => realm.add(AnythingGoes(manyAny: values.map(RealmValue.from)))); + final something = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), manyAny: values.map(RealmValue.from)))); expect(something.manyAny.map((e) => e.value), values); expect(something.manyAny, values.map(RealmValue.from)); }); - }); - test('Query with list of realm values in arguments', () { - final now = DateTime.now().toUtc(); - final values = [ - null, - true, - 'text', - 42, - 3.14, - AnythingGoes(), - Stuff(), - now, - ObjectId.fromTimestamp(now), - Uuid.v4(), - Decimal128.fromInt(128), - ]; - final realm = getMixedRealm(); - final realmValues = values.map(RealmValue.from); - realm.write(() => realm.add(AnythingGoes(manyAny: realmValues, oneAny: realmValues.last))); + baasTest('Roundtrip', (appConfig) async { + final values = getValues(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); - var results = realm.query("manyAny IN \$0", [values]); - expect(results.first.manyAny, realmValues); + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, manyAny: values.map(RealmValue.from)); + realm1.write(() => realm1.add(object1)); - results = realm.query("oneAny IN \$0", [values]); - expect(results.first.oneAny, realmValues.last); - }); + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + expect(realm2.all().single.i, 42); + expect(realm2.all().length, 2); + final object2 = realm2.query(r'_id == $0', [object1.id]).single; + expect(object2.manyAny.length, values.length); + expect(object2.manyAny[0].value, values[0]); + + // Add new item in second realm. + const newValue = 'new value'; + realm2.write(() => object2.manyAny.add(RealmValue.from(newValue))); - test('Set with numeric values', () { - final realm = getMixedRealm(); - final values = [RealmValue.int(0), RealmValue.double(0.0), RealmValue.bool(false), RealmValue.decimal128(Decimal128.zero), RealmValue.nullValue()]; - final obj = realm.write(() => realm.add(AnythingGoes())..setOfAny.addAll(values)); + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); - expect(obj.setOfAny, unorderedMatches([RealmValue.int(0), RealmValue.bool(false), RealmValue.nullValue()])); + // Check new item in first realm. + expect(object1.manyAny.length, values.length + 1); + expect(object1.manyAny.last.value, newValue); + }); + + test('Query with list of realm values in arguments', () { + final values = getValues(); + final realm = getMixedRealm(); + final realmValues = values.map(RealmValue.from); + realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), manyAny: realmValues, oneAny: realmValues.last))); + + var results = realm.query("manyAny IN \$0", [values]); + expect(results.first.manyAny, realmValues); + + results = realm.query("oneAny IN \$0", [values]); + expect(results.first.oneAny, realmValues.last); + }); }); - test('Set removes duplicates', () { - final realm = getMixedRealm(); - final values = [ - RealmValue.int(1), - RealmValue.nullValue(), - RealmValue.double(2.0), - RealmValue.string('abc'), - RealmValue.nullValue(), - RealmValue.string('abc') - ]; - final obj = realm.write(() => realm.add(AnythingGoes())..setOfAny.addAll(values)); + group('Set', () { + final numericValues = [RealmValue.int(0), RealmValue.double(0.0), RealmValue.bool(false), RealmValue.decimal128(Decimal128.zero), RealmValue.nullValue()]; + + test('With numeric values', () { + final realm = getMixedRealm(); + final obj = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId()))..setOfAny.addAll(numericValues)); + + expect(obj.setOfAny, unorderedMatches([RealmValue.int(0), RealmValue.bool(false), RealmValue.nullValue()])); + }); + + baasTest('With numeric values', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator); + realm1.write(() => realm1.add(object1)..setOfAny.addAll(numericValues)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + expect(object2.setOfAny, unorderedMatches([RealmValue.int(0), RealmValue.bool(false), RealmValue.nullValue()])); + }); - expect(obj.setOfAny, unorderedMatches([RealmValue.int(1), RealmValue.double(2.0), RealmValue.nullValue(), RealmValue.string('abc')])); + test('Removes duplicates', () { + final realm = getMixedRealm(); + final values = [ + RealmValue.int(1), + RealmValue.nullValue(), + RealmValue.double(2.0), + RealmValue.string('abc'), + RealmValue.nullValue(), + RealmValue.string('abc') + ]; + final obj = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId()))..setOfAny.addAll(values)); + + expect(obj.setOfAny, unorderedMatches([RealmValue.int(1), RealmValue.double(2.0), RealmValue.nullValue(), RealmValue.string('abc')])); + }); }); group('Collections in RealmValue', () { @@ -332,7 +434,7 @@ void main() { final list = RealmValue.list([RealmValue.from(5)]); final map = RealmValue.map({'a': RealmValue.from('abc')}); - final obj = AnythingGoes(); + final obj = ObjectWithRealmValue(ObjectId()); expect(() => obj.setOfAny.add(list), throws()); expect(() => obj.setOfAny.add(map), throws()); @@ -348,7 +450,8 @@ void main() { final realm = getMixedRealm(); final list = RealmValue.from([5]); - final obj = AnythingGoes(oneAny: list, manyAny: [list], dictOfAny: {'value': list}); + final obj = ObjectWithRealmValue(ObjectId(), oneAny: list, manyAny: [list], dictOfAny: {'value': list}); + expect(obj.oneAny.value, isA>()); expect(obj.oneAny.asList().length, 1); expect(obj.oneAny.asList().single.value, 5); @@ -365,7 +468,7 @@ void main() { realm.add(obj); }); - final foundObj = realm.all().single; + final foundObj = realm.all().single; expect(foundObj.oneAny.value, isA>()); expect(foundObj.oneAny.asList().length, 1); expect(foundObj.oneAny.asList()[0].value, 5); @@ -389,11 +492,52 @@ void main() { expect(obj.dictOfAny['value']!.asList()[1].value, 'abc'); }); + baasTest('List get and set', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final list = RealmValue.from([5]); + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: list, manyAny: [list], dictOfAny: {'value': list}); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + expect(object2.oneAny.value, isA>()); + expect(object2.oneAny.asList().length, 1); + expect(object2.oneAny.asList().single.value, 5); + + expect(object2.manyAny[0].value, isA>()); + expect(object2.manyAny[0].asList().length, 1); + expect(object2.manyAny[0].asList().single.value, 5); + + expect(object2.dictOfAny['value']!.value, isA>()); + expect(object2.dictOfAny['value']!.asList().length, 1); + expect(object2.dictOfAny['value']!.asList().single.value, 5); + + // Add new items in second realm. + realm2.write(() { + object2.oneAny.asList().add(RealmValue.from('abc')); + object2.manyAny[0].asList().add(RealmValue.from('abc')); + object2.dictOfAny['value']!.asList().add(RealmValue.from('abc')); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check new items in first realm. + expect(object1.oneAny.asList()[1].value, 'abc'); + expect(object1.manyAny[0].asList()[1].value, 'abc'); + expect(object1.dictOfAny['value']!.asList()[1].value, 'abc'); + }); + test('Map get and set', () { final realm = getMixedRealm(); final map = RealmValue.from({'foo': 5}); - final obj = AnythingGoes(oneAny: map, manyAny: [map], dictOfAny: {'value': map}); + final obj = ObjectWithRealmValue(ObjectId(), oneAny: map, manyAny: [map], dictOfAny: {'value': map}); expect(obj.oneAny.value, isA>()); expect(obj.oneAny.asMap().length, 1); expect(obj.oneAny.asMap()['foo']!.value, 5); @@ -410,7 +554,7 @@ void main() { realm.add(obj); }); - final foundObj = realm.all().single; + final foundObj = realm.all().single; expect(foundObj.oneAny.value, isA>()); expect(foundObj.oneAny.asMap().length, 1); expect(foundObj.oneAny.asMap()['foo']!.value, 5); @@ -434,15 +578,56 @@ void main() { expect(obj.dictOfAny['value']!.asMap()['bar']!.value, 'abc'); }); + baasTest('Map get and set', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final map = RealmValue.from({'foo': 5}); + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: map, manyAny: [map], dictOfAny: {'value': map}); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + expect(object1.oneAny.value, isA>()); + expect(object1.oneAny.asMap().length, 1); + expect(object1.oneAny.asMap()['foo']!.value, 5); + + expect(object1.manyAny[0].value, isA>()); + expect(object1.manyAny[0].asMap().length, 1); + expect(object1.manyAny[0].asMap()['foo']!.value, 5); + + expect(object1.dictOfAny['value']!.value, isA>()); + expect(object1.dictOfAny['value']!.asMap().length, 1); + expect(object1.dictOfAny['value']!.asMap()['foo']!.value, 5); + + // Add new items in second realm. + realm2.write(() { + object2.oneAny.asMap()['bar'] = RealmValue.from('abc'); + object2.manyAny[0].asMap()['bar'] = RealmValue.from('abc'); + object2.dictOfAny['value']!.asMap()['bar'] = RealmValue.from('abc'); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check new items in first realm. + expect(object1.oneAny.asMap()['bar']!.value, 'abc'); + expect(object1.manyAny[0].asMap()['bar']!.value, 'abc'); + expect(object1.dictOfAny['value']!.asMap()['bar']!.value, 'abc'); + }); + for (var isManaged in [true, false]) { final managedString = isManaged ? 'managed' : 'unmanaged'; RealmValue persistIfNecessary(RealmValue rv, Realm realm) { if (isManaged) { realm.write(() { - realm.add(AnythingGoes(oneAny: rv)); + realm.add(ObjectWithRealmValue(ObjectId(), oneAny: rv)); }); - return realm.all().first.oneAny; + return realm.all().first.oneAny; } return rv; @@ -456,9 +641,8 @@ void main() { } } - test('List when $managedString works with all types', () { - final realm = getMixedRealm(); - final originalList = [ + List getListAllTypes({ObjectId? differentiator}) { + return [ null, 1, true, @@ -469,10 +653,15 @@ void main() { ObjectId.fromHexString('5f63e882536de46d71877979'), Uuid.fromString('3809d6d9-7618-4b3d-8044-2aa35fd02f31'), Uint8List.fromList([1, 2, 0]), - Stuff(i: 123), + ObjectWithInt(ObjectId(), differentiator: differentiator, i: 123), [5, 'abc'], {'int': -10, 'string': 'abc'} ]; + } + + test('List when $managedString works with all types', () { + final realm = getMixedRealm(); + final originalList = getListAllTypes(); final foundValue = persistIfNecessary(RealmValue.from(originalList), realm); expect(foundValue.value, isA>()); expect(foundValue.type, RealmValueType.list); @@ -487,9 +676,9 @@ void main() { } final storedObj = foundList[primitiveCount]; - expect(storedObj.value, isA()); - expect(storedObj.as().isManaged, isManaged); - expect(storedObj.as().i, 123); + expect(storedObj.value, isA()); + expect(storedObj.as().isManaged, isManaged); + expect(storedObj.as().i, 123); final storedList = foundList[primitiveCount + 1]; expectMatches(storedList, [5, 'abc']); @@ -499,9 +688,74 @@ void main() { expect(storedDict.asMap()['non-existent'], null); }); + // This test only needs to run once, but it's placed + // here to be collocated with the above test. + if (isManaged) { + baasTest('List works with all types', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final originalList = getListAllTypes(differentiator: differentiator); + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(originalList)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + + final foundValue = object2.oneAny; + expect(foundValue.value, isA>()); + expect(foundValue.type, RealmValueType.list); + + final foundList = foundValue.asList(); + expect(foundList.length, originalList.length); + + // Last 3 elements are objects/collections, so they are treated specially. + final primitiveCount = originalList.length - 3; + for (var i = 0; i < primitiveCount; i++) { + expect(foundList[i].value, originalList[i]); + } + + final storedObjIndex = primitiveCount; + final storedObj = foundList[storedObjIndex]; + expect(storedObj.value, isA()); + expect(storedObj.as().isManaged, true); + expect(storedObj.as().i, 123); + + final storedListIndex = primitiveCount + 1; + final storedList = foundList[storedListIndex]; + expectMatches(storedList, [5, 'abc']); + + final storedDictIndex = primitiveCount + 2; + final storedDict = foundList[storedDictIndex]; + expectMatches(storedDict, {'int': -10, 'string': 'abc'}); + expect(storedDict.asMap()['non-existent'], null); + + // Update and add items in second realm. + realm2.write(() { + storedObj.as().i = 456; + storedList.asList()[0] = RealmValue.from('updated'); + storedList.asList().add(RealmValue.from('new-value')); + storedDict.asMap()['string'] = RealmValue.from('updated'); + storedDict.asMap()['new-value'] = RealmValue.from('new-value'); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check updated items in first realm. + final list = object1.oneAny.asList(); + expect(list[storedObjIndex].as().i, 456); + expectMatches(list[storedListIndex], ['updated', 'abc', 'new-value']); + expectMatches(list[storedDictIndex], {'int': -10, 'string': 'updated', 'new-value': 'new-value'}); + }); + } + test('List when $managedString can be reassigned', () { final realm = getMixedRealm(); - final obj = AnythingGoes(oneAny: RealmValue.from([true, 5.3])); + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([true, 5.3])); if (isManaged) { realm.write(() => realm.add(obj)); } @@ -510,6 +764,7 @@ void main() { expectMatches(obj.oneAny, [true, 5.3]); writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(['foo'])); + expectMatches(obj.oneAny, ['foo']); writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(999)); expectMatches(obj.oneAny, 999); @@ -518,9 +773,46 @@ void main() { expectMatches(obj.oneAny, {'int': -100}); }); - test('Map when $managedString works with all types', () { - final realm = getMixedRealm(); - final originalMap = { + if (isManaged) { + baasTest('List can be reassigned', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from([true, 5.3])); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.oneAny.type, RealmValueType.list); + expectMatches(object2.oneAny, [true, 5.3]); + + // Reassign value in second realm. + realm2.write(() => object2.oneAny = RealmValue.from(['foo'])); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check and reassign new value in first realm. + expectMatches(object1.oneAny, ['foo']); + realm1.write(() => object1.oneAny = RealmValue.from(999)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check and reassign new value in second realm. + expectMatches(object2.oneAny, 999); + realm2.write(() => object2.oneAny = RealmValue.from({'int': -100})); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check new value in first realm. + expectMatches(object2.oneAny, {'int': -100}); + }); + } + + Map getDictAllTypes({ObjectId? differentiator}) { + return { 'primitive_null': null, 'primitive_int': 1, 'primitive_bool': true, @@ -531,10 +823,15 @@ void main() { 'primitive_objectId': ObjectId.fromHexString('5f63e882536de46d71877979'), 'primitive_uuid': Uuid.fromString('3809d6d9-7618-4b3d-8044-2aa35fd02f31'), 'primitive_binary': Uint8List.fromList([1, 2, 0]), - 'object': Stuff(i: 123), + 'object': ObjectWithInt(ObjectId(), differentiator: differentiator, i: 123), 'list': [5, 'abc'], 'map': {'int': -10, 'string': 'abc'} }; + } + + test('Map when $managedString works with all types', () { + final realm = getMixedRealm(); + final originalMap = getDictAllTypes(); final foundValue = persistIfNecessary(RealmValue.from(originalMap), realm); expect(foundValue.value, isA>()); expect(foundValue.type, RealmValueType.map); @@ -542,14 +839,15 @@ void main() { final foundMap = foundValue.asMap(); expect(foundMap.length, foundMap.length); - for (var key in originalMap.keys.where((k) => k.startsWith('primitive_'))) { + final primitiveKeys = originalMap.keys.where((k) => k.startsWith('primitive_')); + for (var key in primitiveKeys) { expect(foundMap[key]!.value, originalMap[key]); } final storedObj = foundMap['object']!; - expect(storedObj.value, isA()); - expect(storedObj.as().isManaged, isManaged); - expect(storedObj.as().i, 123); + expect(storedObj.value, isA()); + expect(storedObj.as().isManaged, isManaged); + expect(storedObj.as().i, 123); final storedList = foundMap['list']!; expectMatches(storedList, [5, 'abc']); @@ -558,9 +856,69 @@ void main() { expectMatches(storedDict, {'int': -10, 'string': 'abc'}); }); + // This test only needs to run once, but it's placed + // here to be collocated with the above test. + if (isManaged) { + baasTest('Map works with all types', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final originalMap = getDictAllTypes(differentiator: differentiator); + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(originalMap)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + + final foundValue = object2.oneAny; + expect(foundValue.value, isA>()); + expect(foundValue.type, RealmValueType.map); + + final foundMap = foundValue.asMap(); + expect(foundMap.length, foundMap.length); + + final primitiveKeys = originalMap.keys.where((k) => k.startsWith('primitive_')); + for (var key in primitiveKeys) { + expect(foundMap[key]!.value, originalMap[key]); + } + + final storedObj = foundMap['object']!; + expect(storedObj.value, isA()); + expect(storedObj.as().isManaged, isManaged); + expect(storedObj.as().i, 123); + + final storedList = foundMap['list']!; + expectMatches(storedList, [5, 'abc']); + + final storedDict = foundMap['map']!; + expectMatches(storedDict, {'int': -10, 'string': 'abc'}); + + // Update and add items in second realm. + realm2.write(() { + storedObj.as().i = 456; + storedList.asList()[0] = RealmValue.from('updated'); + storedList.asList().add(RealmValue.from('new-value')); + storedDict.asMap()['string'] = RealmValue.from('updated'); + storedDict.asMap()['new-value'] = RealmValue.from('new-value'); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check updated items in first realm. + final map = object1.oneAny.asMap(); + expect(map['object']?.as().i, 456); + expectMatches(map['list']!, ['updated', 'abc', 'new-value']); + expectMatches(map['map']!, {'int': -10, 'string': 'updated', 'new-value': 'new-value'}); + }); + } + test('Map when $managedString can be reassigned', () { final realm = getMixedRealm(); - final obj = AnythingGoes(oneAny: RealmValue.from({'bool': true, 'double': 5.3})); + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from({'bool': true, 'double': 5.3})); if (isManaged) { realm.write(() => realm.add(obj)); } @@ -578,14 +936,52 @@ void main() { expectMatches(obj.oneAny, [1.23456789]); }); + if (isManaged) { + baasTest('Map can be reassigned', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from({'bool': true, 'double': 5.3})); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.oneAny.type, RealmValueType.map); + expectMatches(object2.oneAny, {'bool': true, 'double': 5.3}); + + // Reassign value in second realm. + realm2.write(() => object2.oneAny = RealmValue.from({'newKey': 'new value'})); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check and reassign new value in first realm. + expectMatches(object1.oneAny, {'newKey': 'new value'}); + realm1.write(() => object1.oneAny = RealmValue.from(999)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check and reassign new value in second realm. + expectMatches(object2.oneAny, 999); + realm2.write(() => object2.oneAny = RealmValue.from([1.23456789])); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check new value in first realm. + expectMatches(object1.oneAny, [1.23456789]); + }); + } + test('Map inside list when $managedString can be reassigned', () { final realm = getMixedRealm(); - final obj = AnythingGoes( + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([ - true, - {'foo': 'bar'}, - 5.3 - ])); + true, + {'foo': 'bar'}, + 5.3 + ])); if (isManaged) { realm.write(() => realm.add(obj)); } @@ -611,26 +1007,98 @@ void main() { 5.3, {'new': 5} ]); - - // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 - // writeIfNecessary(realm, () { - // list[1] = list[1]; - // }); - // expectMatches(obj.oneAny, [ - // true, - // {'new': 5}, - // 5.3, - // {'new': 5} - // ]); }); + if (isManaged) { + baasTest('Map inside list can be reassigned', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), + differentiator: differentiator, + oneAny: RealmValue.from([ + true, + {'foo': 'bar'}, + 5.3 + ])); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expect(object2.oneAny.type, RealmValueType.list); + expectMatches(object2.oneAny, [ + true, + {'foo': 'bar'}, + 5.3 + ]); + + final syncedList = object2.oneAny.asList(); + expect(syncedList[1].type, RealmValueType.map); + + // Reassign map in second realm. + realm2.write(() => syncedList[1] = RealmValue.from({'new': 5})); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check and reassign new value in first realm. + expectMatches(object1.oneAny, [ + true, + {'new': 5}, + 5.3 + ]); + final list = object1.oneAny.asList(); + realm1.write(() => list[1] = RealmValue.from([1.23456789])); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check and reassign new value in second realm. + expectMatches(object2.oneAny, [ + true, + [1.23456789], + 5.3 + ]); + realm2.write(() => syncedList[1] = RealmValue.from(999)); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check new value in first realm. + expectMatches(object1.oneAny, [true, 999, 5.3]); + }); + } + + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 + test('Map inside list when $managedString can self-assign', () { + final realm = getMixedRealm(); + final originalList = [ + true, + {'foo': 'bar'}, + 5.3 + ]; + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(originalList)); + if (isManaged) { + realm.write(() => realm.add(obj)); + } + + final list = obj.oneAny.asList(); + + expect(list[1].type, RealmValueType.map); + + writeIfNecessary(realm, () { + list[1] = list[1]; + }); + expectMatches(obj.oneAny, originalList); + }, skip: true); + test('Map inside map when $managedString can be reassigned', () { final realm = getMixedRealm(); - final obj = AnythingGoes( + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from({ - 'a': 5, - 'b': {'foo': 'bar'} - })); + 'a': 5, + 'b': {'foo': 'bar'} + })); if (isManaged) { realm.write(() => realm.add(obj)); @@ -655,26 +1123,38 @@ void main() { 'b': {'new': 5}, 'c': {'new': 5}, }); - - // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 - // writeIfNecessary(realm, () { - // map['a'] = map['a']; - // }); - // expectMatches(obj.oneAny, { - // 'a': 5, - // 'b': {'new': 5}, - // 'c': {'new': 5}, - // }); }); + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 + test('Map inside map when $managedString can self-assign', () { + final realm = getMixedRealm(); + final originalMap = { + 'a': 5, + 'b': {'foo': 'bar'} + }; + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(originalMap)); + if (isManaged) { + realm.write(() => realm.add(obj)); + } + + final map = obj.oneAny.asMap(); + + expect(map['b']!.type, RealmValueType.map); + + writeIfNecessary(realm, () { + map['b'] = map['b']!; + }); + expectMatches(obj.oneAny, originalMap); + }, skip: true); + test('List inside list when $managedString can be reassigned', () { final realm = getMixedRealm(); - final obj = AnythingGoes( + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([ - true, - ['foo'], - 5.3 - ])); + true, + ['foo'], + 5.3 + ])); if (isManaged) { realm.write(() => realm.add(obj)); } @@ -700,26 +1180,38 @@ void main() { 5.3, [5, true] ]); - - // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 - // writeIfNecessary(realm, () { - // list[1] = list[1]; - // }); - // expectMatches(obj.oneAny, [ - // true, - // [5, true], - // 5.3, - // [5, true] - // ]); }); + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 + test('List inside list when $managedString can self-assign', () { + final realm = getMixedRealm(); + final originalList = [ + true, + ['foo'], + 5.3 + ]; + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(originalList)); + if (isManaged) { + realm.write(() => realm.add(obj)); + } + + final list = obj.oneAny.asList(); + + expect(list[1].type, RealmValueType.list); + + writeIfNecessary(realm, () { + list[1] = list[1]; + }); + expectMatches(obj.oneAny, originalList); + }, skip: true); + test('List inside map when $managedString can be reassigned', () { final realm = getMixedRealm(); - final obj = AnythingGoes( + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from({ - 'a': 5, - 'b': ['foo'] - })); + 'a': 5, + 'b': ['foo'] + })); if (isManaged) { realm.write(() => realm.add(obj)); @@ -744,39 +1236,33 @@ void main() { 'b': [999, true], 'c': [999, true] }); - - // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 - // writeIfNecessary(realm, () { - // map['a'] = map['a']; - // }); - // expectMatches(obj.oneAny, { - // 'a': 5, - // 'b': [999, true], - // 'c': [999, true] - // }); }); - test('RealmValue when $managedString can store complex struct', () { + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 + test('List inside map when $managedString can self-assign', () { final realm = getMixedRealm(); - final rv = persistIfNecessary( - RealmValue.from([ - {'0_bool': true, '0_double': 5.3}, - { - '1_int': 5, - '1_map': { - '2_decimal': Decimal128.fromDouble(0.1), - '2_list': [ - 'bla bla', - { - '3_dict': {'4_string': 'abc'} - } - ] - } - } - ]), - realm); + final originalMap = { + 'a': 5, + 'b': ['foo'] + }; + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(originalMap)); + + if (isManaged) { + realm.write(() => realm.add(obj)); + } + + final map = obj.oneAny.asMap(); + + expect(map['b']!.type, RealmValueType.list); + + writeIfNecessary(realm, () { + map['b'] = map['b']!; + }); + expectMatches(obj.oneAny, originalMap); + }, skip: true); - expectMatches(rv, [ + group('Complex structs', () { + final originalList = [ {'0_bool': true, '0_double': 5.3}, { '1_int': 5, @@ -790,37 +1276,221 @@ void main() { ] } } - ]); + ]; - writeIfNecessary(realm, () { - rv.asList().removeAt(0); + test('RealmValue when $managedString can store complex struct', () { + final realm = getMixedRealm(); + final rv = persistIfNecessary(RealmValue.from(originalList), realm); + + expectMatches(rv, originalList); + + writeIfNecessary(realm, () { + rv.asList().removeAt(0); + }); + + expectMatches(rv, [ + { + '1_int': 5, + '1_map': { + '2_decimal': Decimal128.fromDouble(0.1), + '2_list': [ + 'bla bla', + { + '3_dict': {'4_string': 'abc'} + } + ] + } + } + ]); + + writeIfNecessary(realm, () { + rv.asList()[0].asMap()['1_double'] = RealmValue.double(5.5); + rv.asList()[0].asMap().remove('1_map'); + rv.asList().add(RealmValue.bool(true)); + }); + + expectMatches(rv, [ + {'1_int': 5, '1_double': 5.5}, + true + ]); }); - expectMatches(rv, [ - { - '1_int': 5, + test('RealmValue list when $managedString can remove nested collections', () { + final realm = getMixedRealm(); + + final listWithMap = [ + { + '1_map': {'2_string': 'map value'}, + '1_list': ['list value'] + } + ]; + final rv = persistIfNecessary(RealmValue.from(listWithMap), realm); + + expectMatches(rv, listWithMap); + + writeIfNecessary(realm, () { + rv.asList()[0].asMap().remove('1_list'); + rv.asList()[0].asMap().remove('1_map'); + }); + + expect(rv.asList()[0].asMap().isEmpty, true); + }); + + test('RealmValue map when $managedString can remove nested collections', () { + final realm = getMixedRealm(); + + final mapWithMap = { '1_map': { - '2_decimal': Decimal128.fromDouble(0.1), - '2_list': [ - 'bla bla', - { - '3_dict': {'4_string': 'abc'} - } - ] + '2_map': {'3_string': 'map value'}, + '2_list': ['list value'] } - } - ]); + }; + final rv = persistIfNecessary(RealmValue.from(mapWithMap), realm); - writeIfNecessary(realm, () { - rv.asList()[0].asMap()['1_double'] = RealmValue.double(5.5); - rv.asList()[0].asMap().remove('1_map'); - rv.asList().add(RealmValue.bool(true)); + expectMatches(rv, mapWithMap); + + writeIfNecessary(realm, () { + rv.asMap()['1_map']!.asMap().remove('2_map'); + rv.asMap()['1_map']!.asMap().remove('2_list'); + }); + + expect(rv.asMap()['1_map']!.asMap().isEmpty, true); }); - expectMatches(rv, [ - {'1_int': 5, '1_double': 5.5}, - true - ]); + if (isManaged) { + baasTest('RealmValue can store complex struct', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(originalList)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expectMatches(object2.oneAny, originalList); + + // Remove list item in second realm. + realm2.write(() => object2.oneAny.asList().removeAt(0)); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check list in first realm. + expectMatches(object1.oneAny, [ + { + '1_int': 5, + '1_map': { + '2_decimal': Decimal128.fromDouble(0.1), + '2_list': [ + 'bla bla', + { + '3_dict': {'4_string': 'abc'} + } + ] + } + } + ]); + + // Make updates in first realm. + realm1.write(() { + final list = object1.oneAny.asList(); + list[0].asMap()['1_double'] = RealmValue.double(5.5); + list.add(RealmValue.bool(true)); + }); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check updated list in second realm. + expectMatches(object2.oneAny, [ + { + '1_int': 5, + '1_map': { + '2_decimal': Decimal128.fromDouble(0.1), + '2_list': [ + 'bla bla', + { + '3_dict': {'4_string': 'abc'} + } + ] + }, + '1_double': 5.5 + }, + true + ]); + }); + + // Skipped until this is fixed: https://github.com/realm/realm-core/issues/7573 + baasTest('RealmValue list can remove nested collections', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final listWithMap = [ + { + '1_map': {'2_string': 'map value'}, + '1_list': ['list value'] + } + ]; + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(listWithMap)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expectMatches(object2.oneAny, listWithMap); + + // Remove nested map and list in second realm. + realm2.write(() { + // TODO: Committing these removals will cause termination: + // libc++abi: terminating due to uncaught exception of type realm::StaleAccessor: This collection is no more + object2.oneAny.asList()[0].asMap().remove('1_map'); + object2.oneAny.asList()[0].asMap().remove('1_list'); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check object in first realm. + expect(object1.oneAny.asList()[0].asMap().isEmpty, true); + }, skip: true); + + // Skipped until this is fixed: https://github.com/realm/realm-core/issues/7573 + baasTest('RealmValue map can remove nested collections', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final mapWithMap = { + '1_map': { + '2_map': {'3_string': 'map value'}, + '2_list': ['list value'] + } + }; + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(mapWithMap)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check object values in second realm. + final object2 = realm2.all().single; + expectMatches(object2.oneAny, mapWithMap); + + // Remove nested map and list in second realm. + realm2.write(() { + // TODO: Committing these removals will cause termination: + // libc++abi: terminating due to uncaught exception of type realm::StaleAccessor: This collection is no more + object2.oneAny.asMap()['1_map']!.asMap().remove('2_map'); + object2.oneAny.asMap()['1_map']!.asMap().remove('2_list'); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Check object in first realm. + expect(object1.oneAny.asMap()['1_map']!.asMap().isEmpty, true); + }, skip: true); + } }); } @@ -828,7 +1498,7 @@ void main() { final realm = getMixedRealm(); final originalList = [1]; final managedValue = realm.write(() { - return realm.add(AnythingGoes(oneAny: RealmValue.from(originalList))).oneAny; + return realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(originalList))).oneAny; }); final unmanagedValue = RealmValue.from(originalList); @@ -841,8 +1511,10 @@ void main() { expect(managedValue == unmanagedValue, false); expect(unmanagedValue == managedValue, false); - expect(managedValue == managedValue, false); - expect(unmanagedValue == unmanagedValue, false); + // reference equality implies equality + // https://github.com/realm/realm-dart/issues/1632 + expect(managedValue == managedValue, true); + expect(unmanagedValue == unmanagedValue, true); // ignore: unrelated_type_equality_checks expect(managedValue == originalList, false); @@ -853,9 +1525,9 @@ void main() { test('List.indexOf for list', () { final realm = getMixedRealm(); - final originalList = [1]; + final originalList = [RealmValue.int(1)]; final managedList = realm.write(() { - return realm.add(AnythingGoes(manyAny: [RealmValue.from(originalList)])).manyAny; + return realm.add(ObjectWithRealmValue(ObjectId(), manyAny: [RealmValue.from(originalList)])).manyAny; }); final unmanagedList = [RealmValue.from(originalList)]; @@ -872,17 +1544,19 @@ void main() { expect(managedList.asResults().contains(RealmValue.from(originalList)), false); expect(managedList.asResults().contains(managedList.first), false); - expect(unmanagedList.indexOf(RealmValue.from(originalList)), -1); - expect(unmanagedList.indexOf(unmanagedList.first), -1); - expect(unmanagedList.contains(RealmValue.from(originalList)), false); - expect(unmanagedList.contains(unmanagedList.first), false); + // reference equality implies equality + // https://github.com/realm/realm-dart/issues/1632 + expect(unmanagedList.indexOf(RealmValue.from(originalList)), isNot(-1)); + expect(unmanagedList.indexOf(unmanagedList.first), isNot(-1)); + expect(unmanagedList.contains(RealmValue.from(originalList)), true); + expect(unmanagedList.contains(unmanagedList.first), true); }); test('List.indexOf for map', () { final realm = getMixedRealm(); - final originalMap = {'foo': 1}; + final originalMap = {'foo': RealmValue.int(1)}; final managedList = realm.write(() { - return realm.add(AnythingGoes(manyAny: [RealmValue.from(originalMap)])).manyAny; + return realm.add(ObjectWithRealmValue(ObjectId(), manyAny: [RealmValue.from(originalMap)])).manyAny; }); final unmanagedList = [RealmValue.from(originalMap)]; @@ -899,17 +1573,18 @@ void main() { expect(managedList.asResults().contains(RealmValue.from(originalMap)), false); expect(managedList.asResults().contains(managedList.first), false); - expect(unmanagedList.indexOf(RealmValue.from(originalMap)), -1); - expect(unmanagedList.indexOf(unmanagedList.first), -1); - expect(unmanagedList.contains(RealmValue.from(originalMap)), false); - expect(unmanagedList.contains(unmanagedList.first), false); + // reference equality implies equality + expect(unmanagedList.indexOf(RealmValue.from(originalMap)), isNot(-1)); + expect(unmanagedList.indexOf(unmanagedList.first), isNot(-1)); + expect(unmanagedList.contains(RealmValue.from(originalMap)), true); + expect(unmanagedList.contains(unmanagedList.first), true); }); test('Map inside RealmValue equality', () { final realm = getMixedRealm(); - final originalMap = {'foo': 'bar'}; + final originalMap = {'foo': RealmValue.string('bar')}; final managedValue = realm.write(() { - return realm.add(AnythingGoes(oneAny: RealmValue.from(originalMap))).oneAny; + return realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from(originalMap))).oneAny; }); final unmanagedValue = RealmValue.from(originalMap); @@ -922,21 +1597,20 @@ void main() { expect(managedValue == unmanagedValue, false); expect(unmanagedValue == managedValue, false); - expect(managedValue == managedValue, false); - expect(unmanagedValue == unmanagedValue, false); - // ignore: unrelated_type_equality_checks - expect(managedValue == originalMap, false); + expect(managedValue == originalMap, false); // ignore: unrelated_type_equality_checks - // ignore: unrelated_type_equality_checks - expect(unmanagedValue == originalMap, false); + // reference equality implies equality + expect(managedValue == managedValue, true); + expect(unmanagedValue == unmanagedValue, true); + expect(unmanagedValue == originalMap, true); // ignore: unrelated_type_equality_checks }); test('Map.contains for list', () { final realm = getMixedRealm(); - final originalList = [1]; + final originalList = [RealmValue.int(1)]; final managedMap = realm.write(() { - return realm.add(AnythingGoes(dictOfAny: {'foo': RealmValue.from(originalList)})).dictOfAny; + return realm.add(ObjectWithRealmValue(ObjectId(), dictOfAny: {'foo': RealmValue.from(originalList)})).dictOfAny; }); final unmanagedMap = {'foo': RealmValue.from(originalList)}; @@ -949,18 +1623,20 @@ void main() { expect(managedMap.values.contains(RealmValue.from(originalList)), false); expect(managedMap.values.contains(managedMap.values.first), false); - expect(unmanagedMap.containsValue(RealmValue.from(originalList)), false); - expect(unmanagedMap.containsValue(unmanagedMap.values.first), false); - - expect(unmanagedMap.values.contains(RealmValue.from(originalList)), false); expect(unmanagedMap.values.contains(managedMap.values.first), false); + + // reference equality implies equality + expect(unmanagedMap.containsValue(RealmValue.from(originalList)), true); + expect(unmanagedMap.containsValue(unmanagedMap.values.first), true); + + expect(unmanagedMap.values.contains(RealmValue.from(originalList)), true); }); test('Map.contains for map', () { final realm = getMixedRealm(); final originalMap = {'bar': 1}; final managedMap = realm.write(() { - return realm.add(AnythingGoes(dictOfAny: {'foo': RealmValue.from(originalMap)})).dictOfAny; + return realm.add(ObjectWithRealmValue(ObjectId(), dictOfAny: {'foo': RealmValue.from(originalMap)})).dictOfAny; }); final unmanagedMap = {'foo': RealmValue.from(originalMap)}; @@ -974,17 +1650,19 @@ void main() { expect(managedMap.values.contains(managedMap.values.first), false); expect(unmanagedMap.containsValue(RealmValue.from(originalMap)), false); - expect(unmanagedMap.containsValue(unmanagedMap.values.first), false); - expect(unmanagedMap.values.contains(RealmValue.from(originalMap)), false); expect(unmanagedMap.values.contains(managedMap.values.first), false); + + // reference equality implies equality + // https://github.com/realm/realm-dart/issues/1632 + expect(unmanagedMap.containsValue(unmanagedMap.values.first), true); }); test('Map.indexOf for map', () { final realm = getMixedRealm(); final originalMap = {'foo': 1}; final managedList = realm.write(() { - return realm.add(AnythingGoes(manyAny: [RealmValue.from(originalMap)])).manyAny; + return realm.add(ObjectWithRealmValue(ObjectId(), manyAny: [RealmValue.from(originalMap)])).manyAny; }); final unmanagedList = [RealmValue.from(originalMap)]; @@ -1002,9 +1680,11 @@ void main() { expect(managedList.asResults().contains(managedList.first), false); expect(unmanagedList.indexOf(RealmValue.from(originalMap)), -1); - expect(unmanagedList.indexOf(unmanagedList.first), -1); expect(unmanagedList.contains(RealmValue.from(originalMap)), false); - expect(unmanagedList.contains(unmanagedList.first), false); + // reference equality implies equality + // https://github.com/realm/realm-dart/issues/1632 + expect(unmanagedList.indexOf(unmanagedList.first), isNot(-1)); + expect(unmanagedList.contains(unmanagedList.first), true); }); test('List in RealmValue when unmanaged is equal to original list', () { @@ -1017,7 +1697,7 @@ void main() { final list = [RealmValue.bool(true), RealmValue.string('abc')]; final rv = RealmValue.list(list); final realm = getMixedRealm(); - final obj = realm.write(() => realm.add(AnythingGoes(oneAny: rv))); + final obj = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), oneAny: rv))); expect(identical(obj.oneAny.asList(), list), false); }); @@ -1031,26 +1711,268 @@ void main() { final map = {'bool': RealmValue.bool(true), 'str': RealmValue.string('abc')}; final rv = RealmValue.map(map); final realm = getMixedRealm(); - final obj = realm.write(() => realm.add(AnythingGoes(oneAny: rv))); + final obj = realm.write(() => realm.add(ObjectWithRealmValue(ObjectId(), oneAny: rv))); expect(identical(obj.oneAny.asMap(), map), false); }); + baasTest('List has conflicting writes resolved', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final originalList = ['original 0', 'original 1', 'original 2']; + final rvList = RealmValue.from(originalList); + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: rvList, manyAny: [rvList], dictOfAny: {'key': rvList}); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check synced object in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + expect(object2.oneAny.value, isA>()); + expectMatches(object2.oneAny, originalList); + + expect(object2.manyAny[0].value, isA>()); + expectMatches(object2.manyAny[0], originalList); + + expect(object2.dictOfAny['key']!.value, isA>()); + expectMatches(object2.dictOfAny['key']!, originalList); + + // Pause sessions and do conflicting writes. + realm1.syncSession.pause(); + realm2.syncSession.pause(); + await validateSessionStates("State after pause", realm1.syncSession, + expectedSessionState: SessionState.inactive, expectedConnectionState: ConnectionState.disconnected); + await validateSessionStates("State after pause", realm2.syncSession, + expectedSessionState: SessionState.inactive, expectedConnectionState: ConnectionState.disconnected); + + // Realm1 changes: update type at index0, update at index 1, remove at index 2. + realm1.write(() { + object1.oneAny.asList()[0] = RealmValue.from(['updated str->lst (index 0) - realm1']); + object1.oneAny.asList()[1] = RealmValue.from('updated str->str (index 1) - realm1'); + object1.oneAny.asList().removeAt(2); + + object1.manyAny[0].asList()[0] = RealmValue.from(['updated str->lst (index 0) - realm1']); + object1.manyAny[0].asList()[1] = RealmValue.from('updated str->str (index 1) - realm1'); + object1.manyAny[0].asList().removeAt(2); + + object1.dictOfAny['key']!.asList()[0] = RealmValue.from(['updated str->lst (index 0) - realm1']); + object1.dictOfAny['key']!.asList()[1] = RealmValue.from('updated str->str (index 1) - realm1'); + object1.dictOfAny['key']!.asList().removeAt(2); + }); + + // Realm2 changes: update type at index0, update at index 1, update at index 2, add at end (index 3). + realm2.write(() { + object2.oneAny.asList()[0] = RealmValue.from({'key': 'updated str->map (index 0) - realm2'}); + object2.oneAny.asList()[1] = RealmValue.from('updated str->str (index 1) - realm2'); + object2.oneAny.asList()[2] = RealmValue.from('updated str->str (index 2) - realm2'); + object2.oneAny.asList().add(RealmValue.from('added str (index 3) - realm2')); + + object2.manyAny[0].asList()[0] = RealmValue.from({'key': 'updated str->map (index 0) - realm2'}); + object2.manyAny[0].asList()[1] = RealmValue.from('updated str->str (index 1) - realm2'); + object2.manyAny[0].asList()[2] = RealmValue.from('updated str->str (index 2) - realm2'); + object2.manyAny[0].asList().add(RealmValue.from('added str (index 3) - realm2')); + + object2.dictOfAny['key']!.asList()[0] = RealmValue.from({'key': 'updated str->map (index 0) - realm2'}); + object2.dictOfAny['key']!.asList()[1] = RealmValue.from('updated str->str (index 1) - realm2'); + object2.dictOfAny['key']!.asList()[2] = RealmValue.from('updated str->str (index 2) - realm2'); + object2.dictOfAny['key']!.asList().add(RealmValue.from('added str (index 3) - realm2')); + }); + + // Resume sessions and check resolution. + realm1.syncSession.resume(); + realm2.syncSession.resume(); + await validateSessionStates("State after resume", realm1.syncSession, + expectedSessionState: SessionState.active, expectedConnectionState: ConnectionState.connected); + await validateSessionStates("State after resume", realm2.syncSession, + expectedSessionState: SessionState.active, expectedConnectionState: ConnectionState.connected); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Update of type at index 0 in realm2 should have won. + expect(object1.oneAny.asList()[0].value, object2.oneAny.asList()[0].value); + expect(object1.oneAny.asList()[0].value, isA>()); + expectMatches(object1.oneAny.asList()[0], {'key': 'updated str->map (index 0) - realm2'}); + + expect(object1.manyAny[0].asList()[0].value, object2.manyAny[0].asList()[0].value); + expect(object1.manyAny[0].asList()[0].value, isA>()); + expectMatches(object1.manyAny[0].asList()[0], {'key': 'updated str->map (index 0) - realm2'}); + + expect(object1.dictOfAny['key']!.asList()[0].value, object2.dictOfAny['key']!.asList()[0].value); + expect(object1.dictOfAny['key']!.asList()[0].value, isA>()); + expectMatches(object1.dictOfAny['key']!.asList()[0], {'key': 'updated str->map (index 0) - realm2'}); + + // Update at index 1 in realm2 should have won. + expect(object1.oneAny.asList()[1].value, object2.oneAny.asList()[1].value); + expect(object1.oneAny.asList()[1].value, 'updated str->str (index 1) - realm2'); + + expect(object1.manyAny[0].asList()[1].value, object2.manyAny[0].asList()[1].value); + expect(object1.manyAny[0].asList()[1].value, 'updated str->str (index 1) - realm2'); + + expect(object1.dictOfAny['key']!.asList()[1].value, object2.dictOfAny['key']!.asList()[1].value); + expect(object1.dictOfAny['key']!.asList()[1].value, 'updated str->str (index 1) - realm2'); + + // Removal at index 2 in realm1 should have won over update in realm2. + expect(object1.oneAny.asList().contains(RealmValue.from('updated str->str (index 2) - realm2')), false); + expect(object2.oneAny.asList().contains(RealmValue.from('updated str->str (index 2) - realm2')), false); + + expect(object1.manyAny[0].asList().contains(RealmValue.from('updated str->str (index 2) - realm2')), false); + expect(object2.manyAny[0].asList().contains(RealmValue.from('updated str->str (index 2) - realm2')), false); + + expect(object1.dictOfAny['key']!.asList().contains(RealmValue.from('updated str->str (index 2) - realm2')), false); + expect(object2.dictOfAny['key']!.asList().contains(RealmValue.from('updated str->str (index 2) - realm2')), false); + + // Adding at index 3 in realm2 should have resolved to adding at index 2. + expect(object1.oneAny.asList()[2].value, object2.oneAny.asList()[2].value); + expect(object1.oneAny.asList()[2].value, 'added str (index 3) - realm2'); + + expect(object1.manyAny[0].asList()[2].value, object2.manyAny[0].asList()[2].value); + expect(object1.manyAny[0].asList()[2].value, 'added str (index 3) - realm2'); + + expect(object1.dictOfAny['key']!.asList()[2].value, object2.dictOfAny['key']!.asList()[2].value); + expect(object1.dictOfAny['key']!.asList()[2].value, 'added str (index 3) - realm2'); + }); + + baasTest('Map has conflicting writes resolved', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final originalMap = {'key0': 'original 0', 'key1': 'original 1', 'key2': 'original 2'}; + final rvMap = RealmValue.from(originalMap); + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: rvMap, manyAny: [rvMap], dictOfAny: {'key': rvMap}); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Check synced object in second realm. + final object2 = realm2.all().single; + expect(object2.id, object1.id); + expect(object2.oneAny.value, isA>()); + expectMatches(object2.oneAny, originalMap); + + expect(object2.manyAny[0].value, isA>()); + expectMatches(object2.manyAny[0], originalMap); + + expect(object2.dictOfAny['key']!.value, isA>()); + expectMatches(object2.dictOfAny['key']!, originalMap); + + // Pause sessions and do conflicting writes. + realm1.syncSession.pause(); + realm2.syncSession.pause(); + await validateSessionStates("State after pause", realm1.syncSession, + expectedSessionState: SessionState.inactive, expectedConnectionState: ConnectionState.disconnected); + await validateSessionStates("State after pause", realm2.syncSession, + expectedSessionState: SessionState.inactive, expectedConnectionState: ConnectionState.disconnected); + + // Realm1 changes: update type at key0, update at key1, remove at key2. + realm1.write(() { + object1.oneAny.asMap()['key0'] = RealmValue.from(['updated str->lst (key0) - realm1']); + object1.oneAny.asMap()['key1'] = RealmValue.from('updated str->str (key1) - realm1'); + object1.oneAny.asMap().remove('key2'); + + object1.manyAny[0].asMap()['key0'] = RealmValue.from(['updated str->lst (key0) - realm1']); + object1.manyAny[0].asMap()['key1'] = RealmValue.from('updated str->str (key1) - realm1'); + object1.manyAny[0].asMap().remove('key2'); + + object1.dictOfAny['key']!.asMap()['key0'] = RealmValue.from(['updated str->lst (key0) - realm1']); + object1.dictOfAny['key']!.asMap()['key1'] = RealmValue.from('updated str->str (key1) - realm1'); + object1.dictOfAny['key']!.asMap().remove('key2'); + }); + + // Realm2 changes: update type at key0, update at key1, update at key2, add key3. + realm2.write(() { + object2.oneAny.asMap()['key0'] = RealmValue.from({'key': 'updated str->map (key0) - realm2'}); + object2.oneAny.asMap()['key1'] = RealmValue.from('updated str->str (key1) - realm2'); + object2.oneAny.asMap()['key2'] = RealmValue.from('updated str->str (key2) - realm2'); + object2.oneAny.asMap()['key3'] = RealmValue.from('added str (key3) - realm2'); + + object2.manyAny[0].asMap()['key0'] = RealmValue.from({'key': 'updated str->map (key0) - realm2'}); + object2.manyAny[0].asMap()['key1'] = RealmValue.from('updated str->str (key1) - realm2'); + object2.manyAny[0].asMap()['key2'] = RealmValue.from('updated str->str (key2) - realm2'); + object2.manyAny[0].asMap()['key3'] = RealmValue.from('added str (key3) - realm2'); + + object2.dictOfAny['key']!.asMap()['key0'] = RealmValue.from({'key': 'updated str->map (key0) - realm2'}); + object2.dictOfAny['key']!.asMap()['key1'] = RealmValue.from('updated str->str (key1) - realm2'); + object2.dictOfAny['key']!.asMap()['key2'] = RealmValue.from('updated str->str (key2) - realm2'); + object2.dictOfAny['key']!.asMap()['key3'] = RealmValue.from('added str (key3) - realm2'); + }); + + // Resume sessions and check resolution. + realm1.syncSession.resume(); + realm2.syncSession.resume(); + await validateSessionStates("State after resume", realm1.syncSession, + expectedSessionState: SessionState.active, expectedConnectionState: ConnectionState.connected); + await validateSessionStates("State after resume", realm2.syncSession, + expectedSessionState: SessionState.active, expectedConnectionState: ConnectionState.connected); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + + // Update of type at key0 in realm2 should have won. + expect(object1.oneAny.asMap()['key0']!.value, object2.oneAny.asMap()['key0']!.value); + expect(object1.oneAny.asMap()['key0']!.value, isA>()); + expectMatches(object1.oneAny.asMap()['key0']!, {'key': 'updated str->map (key0) - realm2'}); + + expect(object1.manyAny[0].asMap()['key0']!.value, object2.manyAny[0].asMap()['key0']!.value); + expect(object1.manyAny[0].asMap()['key0']!.value, isA>()); + expectMatches(object1.manyAny[0].asMap()['key0']!, {'key': 'updated str->map (key0) - realm2'}); + + expect(object1.dictOfAny['key']!.asMap()['key0']!.value, object2.dictOfAny['key']!.asMap()['key0']!.value); + expect(object1.dictOfAny['key']!.asMap()['key0']!.value, isA>()); + expectMatches(object1.dictOfAny['key']!.asMap()['key0']!, {'key': 'updated str->map (key0) - realm2'}); + + // Update at key1 in realm2 should have won. + expect(object1.oneAny.asMap()['key1']!.value, object2.oneAny.asMap()['key1']!.value); + expect(object1.oneAny.asMap()['key1']!.value, 'updated str->str (key1) - realm2'); + + expect(object1.manyAny[0].asMap()['key1']!.value, object2.manyAny[0].asMap()['key1']!.value); + expect(object1.manyAny[0].asMap()['key1']!.value, 'updated str->str (key1) - realm2'); + + expect(object1.dictOfAny['key']!.asMap()['key1']!.value, object2.dictOfAny['key']!.asMap()['key1']!.value); + expect(object1.dictOfAny['key']!.asMap()['key1']!.value, 'updated str->str (key1) - realm2'); + + // Update at key2 in realm2 (last writer) should have won over "removal" in realm1 + // due to OT acting on two updates (the removal being a `KeyErased` update). + expect(object1.oneAny.asMap()['key2']!.value, object2.oneAny.asMap()['key2']!.value); + expect(object1.oneAny.asMap()['key2']!.value, 'updated str->str (key2) - realm2'); + + expect(object1.manyAny[0].asMap()['key2']!.value, object2.manyAny[0].asMap()['key2']!.value); + expect(object1.manyAny[0].asMap()['key2']!.value, 'updated str->str (key2) - realm2'); + + expect(object1.dictOfAny['key']!.asMap()['key2']!.value, object2.dictOfAny['key']!.asMap()['key2']!.value); + expect(object1.dictOfAny['key']!.asMap()['key2']!.value, 'updated str->str (key2) - realm2'); + + // Adding key3 in realm2 should have resolved to the same thing. + expect(object1.oneAny.asMap()['key3']!.value, object2.oneAny.asMap()['key3']!.value); + expect(object1.oneAny.asMap()['key3']!.value, 'added str (key3) - realm2'); + + expect(object1.manyAny[0].asMap()['key3']!.value, object2.manyAny[0].asMap()['key3']!.value); + expect(object1.manyAny[0].asMap()['key3']!.value, 'added str (key3) - realm2'); + + expect(object1.dictOfAny['key']!.asMap()['key3']!.value, object2.dictOfAny['key']!.asMap()['key3']!.value); + expect(object1.dictOfAny['key']!.asMap()['key3']!.value, 'added str (key3) - realm2'); + }); + test('Notifications', () async { final realm = getMixedRealm(); - final obj = AnythingGoes( + final obj = ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([ - 5, - { - 'foo': 'bar', - 'list': [10] - } - ])); + 5, + { + 'string': 'bar', + 'list': [10] + } + ])); realm.write(() { realm.add(obj); }); - final List> parentChanges = []; + // Add listeners. + final List> parentChanges = []; final subscription = obj.changes.listen((event) { parentChanges.add(event); }); @@ -1071,12 +1993,14 @@ void main() { listChanges.clear(); mapChanges.clear(); + // Add item to list. realm.write(() { obj.oneAny.asList().add(RealmValue.bool(true)); }); await Future.delayed(Duration(milliseconds: 20)); + // Expect listeners to be fired. expect(parentChanges, hasLength(1)); expect(parentChanges[0].properties, ['oneAny']); @@ -1089,6 +2013,7 @@ void main() { expect(mapChanges, hasLength(0)); + // Update and add entry in nested dictionary. realm.write(() { obj.oneAny.asList()[1].asMap()['list'] = RealmValue.from([10]); obj.oneAny.asList()[1].asMap()['new-value'] = RealmValue.from({'foo': 'bar'}); @@ -1096,6 +2021,7 @@ void main() { await Future.delayed(Duration(milliseconds: 20)); + // Expect listeners to be fired. expect(parentChanges, hasLength(2)); expect(parentChanges[1].properties, ['oneAny']); @@ -1113,29 +2039,59 @@ void main() { expect(mapChanges[0].isCleared, false); expect(mapChanges[0].isCollectionDeleted, false); + // Remove entry in nested dictionary. realm.write(() { - obj.oneAny.asList().removeAt(1); + obj.oneAny.asList()[1].asMap().remove('string'); }); await Future.delayed(Duration(milliseconds: 20)); + // Expect listeners to be fired. expect(parentChanges, hasLength(3)); expect(parentChanges[2].properties, ['oneAny']); expect(listChanges, hasLength(3)); expect(listChanges[2].inserted, isEmpty); - expect(listChanges[2].deleted, [1]); - expect(listChanges[2].modified, isEmpty); + expect(listChanges[2].deleted, isEmpty); + expect(listChanges[2].modified, [1]); expect(listChanges[2].isCleared, false); expect(listChanges[2].isCollectionDeleted, false); expect(mapChanges, hasLength(2)); - expect(mapChanges[1].isCollectionDeleted, true); + expect(mapChanges[1].modified, isEmpty); + expect(mapChanges[1].inserted, isEmpty); + expect(mapChanges[1].deleted, ['string']); + expect(mapChanges[1].isCleared, false); + expect(mapChanges[1].isCollectionDeleted, false); + expect(mapChanges[1].isCollectionDeleted, false); + + // Remove dictionary from list. + realm.write(() { + obj.oneAny.asList().removeAt(1); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + // Expect listeners to be fired. + expect(parentChanges, hasLength(4)); + expect(parentChanges[3].properties, ['oneAny']); + + expect(listChanges, hasLength(4)); + expect(listChanges[3].inserted, isEmpty); + expect(listChanges[3].deleted, [1]); + expect(listChanges[3].modified, isEmpty); + expect(listChanges[3].isCleared, false); + expect(listChanges[3].isCollectionDeleted, false); + expect(mapChanges, hasLength(3)); + expect(mapChanges[2].isCollectionDeleted, true); + + // Cancel subscriptions. subscription.cancel(); listSubscription.cancel(); mapSubscription.cancel(); + // Overwrite list with primitive. realm.write(() { obj.oneAny = RealmValue.bool(false); }); @@ -1143,63 +2099,221 @@ void main() { await Future.delayed(Duration(milliseconds: 20)); // Subscriptions have been canceled - shouldn't get more notifications + expect(parentChanges, hasLength(4)); + expect(listChanges, hasLength(4)); + expect(mapChanges, hasLength(3)); + }); + + baasTest('Notifications', (appConfig) async { + final differentiator = ObjectId(); + final (realm1, realm2) = await logInAndGetSyncedRealms(appConfig, differentiator); + + // Add object in first realm. + final list = [ + 5, + { + 'string': 'bar', + 'list': [10] + } + ]; + final object1 = ObjectWithRealmValue(ObjectId(), differentiator: differentiator, oneAny: RealmValue.from(list)); + realm1.write(() => realm1.add(object1)); + + await waitForSynchronization(uploadRealm: realm1, downloadRealm: realm2); + + // Add listeners in first realm. + final List> parentChanges = []; + final subscription = object1.changes.listen((event) { + parentChanges.add(event); + }); + + final List> listChanges = []; + final listSubscription = object1.oneAny.asList().changes.listen((event) { + listChanges.add(event); + }); + + final List> mapChanges = []; + final mapSubscription = object1.oneAny.asList()[1].asMap().changes.listen((event) { + mapChanges.add(event); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + parentChanges.clear(); + listChanges.clear(); + mapChanges.clear(); + + // Get object in second realm. + final object2 = realm2.query(r'_id == $0', [object1.id]).single; + + // Add item to list in second realm. + realm2.write(() { + object2.oneAny.asList().add(RealmValue.bool(true)); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + await Future.delayed(Duration(milliseconds: 20)); + + // Expect listeners to be fired in first realm. + expect(parentChanges, hasLength(1)); + expect(parentChanges[0].properties, ['oneAny']); + + expect(listChanges, hasLength(1)); + expect(listChanges[0].inserted, [2]); + expect(listChanges[0].deleted, isEmpty); + expect(listChanges[0].modified, isEmpty); + expect(listChanges[0].isCleared, false); + expect(listChanges[0].isCollectionDeleted, false); + + expect(mapChanges, hasLength(0)); + + // Update and add entry in nested dictionary in second realm. + realm2.write(() { + object2.oneAny.asList()[1].asMap()['list'] = RealmValue.from([10]); + object2.oneAny.asList()[1].asMap()['new-value'] = RealmValue.from({'foo': 'bar'}); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + await Future.delayed(Duration(milliseconds: 20)); + + // Expect listeners to be fired in first realm. + expect(parentChanges, hasLength(2)); + expect(parentChanges[1].properties, ['oneAny']); + + expect(listChanges, hasLength(2)); + expect(listChanges[1].inserted, isEmpty); + expect(listChanges[1].deleted, isEmpty); + expect(listChanges[1].modified, [1]); + expect(listChanges[1].isCleared, false); + expect(listChanges[1].isCollectionDeleted, false); + + expect(mapChanges, hasLength(1)); + expect(mapChanges[0].modified, ['list']); + expect(mapChanges[0].inserted, ['new-value']); + expect(mapChanges[0].deleted, isEmpty); + expect(mapChanges[0].isCleared, false); + expect(mapChanges[0].isCollectionDeleted, false); + + // Remove entry in nested dictionary in second realm. + realm2.write(() { + object2.oneAny.asList()[1].asMap().remove('string'); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + await Future.delayed(Duration(milliseconds: 20)); + + // Expect listeners to be fired in first realm. expect(parentChanges, hasLength(3)); + expect(parentChanges[2].properties, ['oneAny']); + expect(listChanges, hasLength(3)); + expect(listChanges[2].inserted, isEmpty); + expect(listChanges[2].deleted, isEmpty); + expect(listChanges[2].modified, [1]); + expect(listChanges[2].isCleared, false); + expect(listChanges[2].isCollectionDeleted, false); + expect(mapChanges, hasLength(2)); + expect(mapChanges[1].modified, isEmpty); + expect(mapChanges[1].inserted, isEmpty); + expect(mapChanges[1].deleted, ['string']); + expect(mapChanges[1].isCleared, false); + expect(mapChanges[1].isCollectionDeleted, false); + expect(mapChanges[1].isCollectionDeleted, false); + + // Remove dictionary from list in second realm. + realm2.write(() { + object2.oneAny.asList().removeAt(1); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + await Future.delayed(Duration(milliseconds: 20)); + + // Expect listeners to be fired in first realm. + expect(parentChanges, hasLength(4)); + expect(parentChanges[3].properties, ['oneAny']); + + expect(listChanges, hasLength(4)); + expect(listChanges[3].inserted, isEmpty); + expect(listChanges[3].deleted, [1]); + expect(listChanges[3].modified, isEmpty); + expect(listChanges[3].isCleared, false); + expect(listChanges[3].isCollectionDeleted, false); + + expect(mapChanges, hasLength(3)); + expect(mapChanges[2].isCollectionDeleted, true); + + // Cancel subscriptions. + subscription.cancel(); + listSubscription.cancel(); + mapSubscription.cancel(); + + // Overwrite list with primitive in second realm. + realm2.write(() { + object2.oneAny = RealmValue.bool(false); + }); + + await waitForSynchronization(uploadRealm: realm2, downloadRealm: realm1); + await Future.delayed(Duration(milliseconds: 20)); + + // Subscriptions have been canceled - shouldn't get more notifications. + expect(parentChanges, hasLength(4)); + expect(listChanges, hasLength(4)); + expect(mapChanges, hasLength(3)); }); test('Queries', () { final realm = getMixedRealm(); - late AnythingGoes first; - late AnythingGoes second; - late AnythingGoes third; + late ObjectWithRealmValue first; + late ObjectWithRealmValue second; + late ObjectWithRealmValue third; realm.write(() { - first = realm.add(AnythingGoes( + first = realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([ - 1, - 'a', - {'foo': 'bar'} - ]))); + 1, + 'a', + {'foo': 'bar'} + ]))); - second = realm.add(AnythingGoes( + second = realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([ - 2, - {'foo': 'baz'} - ]))); + 2, + {'foo': 'baz'} + ]))); - third = realm.add(AnythingGoes( + third = realm.add(ObjectWithRealmValue(ObjectId(), oneAny: RealmValue.from([ - 3, - 'c', - { - 'foo': {'child': 5}, - 'bar': 10 - }, - 3.4 - ]))); + 3, + 'c', + { + 'foo': {'child': 5}, + 'bar': 10 + }, + 3.4 + ]))); }); - final listElementQuery = realm.query('oneAny[0] < 3'); + final listElementQuery = realm.query('oneAny[0] < 3'); expect(listElementQuery, unorderedMatches([first, second])); - final listLengthQuery = realm.query('oneAny.@size > 3'); + final listLengthQuery = realm.query('oneAny.@size > 3'); expect(listLengthQuery, unorderedMatches([third])); - final listStarQuery = realm.query('oneAny[*] == 3.4'); + final listStarQuery = realm.query('oneAny[*] == 3.4'); expect(listStarQuery, unorderedMatches([third])); - final typeQuery = realm.query("oneAny[2].@type == 'dictionary'"); + final typeQuery = realm.query("oneAny[2].@type == 'dictionary'"); expect(typeQuery, unorderedMatches([first, third])); - final dictionaryInListQuery = realm.query("oneAny[*].foo BEGINSWITH 'ba'"); + final dictionaryInListQuery = realm.query("oneAny[*].foo BEGINSWITH 'ba'"); expect(dictionaryInListQuery, unorderedMatches([first, second])); - final dictionaryKeysQuery = realm.query("oneAny[*].foo.@keys == 'child'"); + final dictionaryKeysQuery = realm.query("oneAny[*].foo.@keys == 'child'"); expect(dictionaryKeysQuery, unorderedMatches([third])); - final noMatchesQuery = realm.query("oneAny[*].bar == 9"); + final noMatchesQuery = realm.query("oneAny[*].bar == 9"); expect(noMatchesQuery, isEmpty); }); }); @@ -1300,4 +2414,48 @@ void main() { expect(result.value, isNull); }); }); + + test('memEquals', () { + const samples = 100; + const max = 100000; + const half = max ~/ 2; + final listEquals = ListEquality().equals; + + final memEqualsClock = Stopwatch(); + final listEqualsClock = Stopwatch(); + for (int i = 0; i < samples; i++) { + final rnd = Random(i); + final length = rnd.nextInt(half) + half; // 50-100 KB. + final list = List.generate(length, (index) => index)..shuffle(rnd); + final bin1 = Uint8List.fromList(list); + final bin2 = Uint8List.fromList(list); + + // Check correctness. + expect(identical(bin1, bin2), isFalse); // Different instances. + expect(listEquals(bin1, bin2), isTrue); // Sanity check. + expect(memEquals(bin1, bin2), isTrue); // Check correctness. + expect(RealmValue.from(bin1), RealmValue.from(bin2)); + + // Measure performance. + listEqualsClock.start(); + final resultListEquals = listEquals(bin1, bin2); + listEqualsClock.stop(); + + memEqualsClock.start(); + final resultMemEquals = memEquals(bin1, bin2); + memEqualsClock.stop(); + + expect(resultListEquals, resultMemEquals); // Sanity check. + + bin2[rnd.nextInt(bin2.length)]--; // Change one byte. + + expect(listEquals(bin1, bin2), isFalse); // Sanity check. + expect(memEquals(bin1, bin2), isFalse); // Check correctness. + expect(RealmValue.from(bin1), isNot(RealmValue.from(bin2))); + } + + // Not quite 8 times speedup, but close enough. Setting the threshold to 4 + // for test stability sake. + expect(listEqualsClock.elapsedTicks ~/ 4, greaterThan(memEqualsClock.elapsedTicks)); + }); } diff --git a/packages/realm_dart/test/realm_value_test.realm.dart b/packages/realm_dart/test/realm_value_test.realm.dart index f14cdabd4..e3523d6e2 100644 --- a/packages/realm_dart/test/realm_value_test.realm.dart +++ b/packages/realm_dart/test/realm_value_test.realm.dart @@ -33,6 +33,10 @@ class TuckedIn extends _TuckedIn Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override TuckedIn freeze() => RealmObjectBase.freezeObject(this); @@ -66,163 +70,3 @@ class TuckedIn extends _TuckedIn @override SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } - -class AnythingGoes extends _AnythingGoes - with RealmEntity, RealmObjectBase, RealmObject { - AnythingGoes({ - RealmValue oneAny = const RealmValue.nullValue(), - Iterable manyAny = const [], - Map dictOfAny = const {}, - Set setOfAny = const {}, - }) { - RealmObjectBase.set(this, 'oneAny', oneAny); - RealmObjectBase.set>( - this, 'manyAny', RealmList(manyAny)); - RealmObjectBase.set>( - this, 'dictOfAny', RealmMap(dictOfAny)); - RealmObjectBase.set>( - this, 'setOfAny', RealmSet(setOfAny)); - } - - AnythingGoes._(); - - @override - RealmValue get oneAny => - RealmObjectBase.get(this, 'oneAny') as RealmValue; - @override - set oneAny(RealmValue value) => RealmObjectBase.set(this, 'oneAny', value); - - @override - RealmList get manyAny => - RealmObjectBase.get(this, 'manyAny') as RealmList; - @override - set manyAny(covariant RealmList value) => - throw RealmUnsupportedSetError(); - - @override - RealmMap get dictOfAny => - RealmObjectBase.get(this, 'dictOfAny') - as RealmMap; - @override - set dictOfAny(covariant RealmMap value) => - throw RealmUnsupportedSetError(); - - @override - RealmSet get setOfAny => - RealmObjectBase.get(this, 'setOfAny') as RealmSet; - @override - set setOfAny(covariant RealmSet value) => - throw RealmUnsupportedSetError(); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - AnythingGoes freeze() => RealmObjectBase.freezeObject(this); - - EJsonValue toEJson() { - return { - 'oneAny': oneAny.toEJson(), - 'manyAny': manyAny.toEJson(), - 'dictOfAny': dictOfAny.toEJson(), - 'setOfAny': setOfAny.toEJson(), - }; - } - - static EJsonValue _toEJson(AnythingGoes value) => value.toEJson(); - static AnythingGoes _fromEJson(EJsonValue ejson) { - return switch (ejson) { - { - 'oneAny': EJsonValue oneAny, - 'manyAny': EJsonValue manyAny, - 'dictOfAny': EJsonValue dictOfAny, - 'setOfAny': EJsonValue setOfAny, - } => - AnythingGoes( - oneAny: fromEJson(oneAny), - manyAny: fromEJson(manyAny), - dictOfAny: fromEJson(dictOfAny), - setOfAny: fromEJson(setOfAny), - ), - _ => raiseInvalidEJson(ejson), - }; - } - - static final schema = () { - RealmObjectBase.registerFactory(AnythingGoes._); - register(_toEJson, _fromEJson); - return SchemaObject(ObjectType.realmObject, AnythingGoes, 'AnythingGoes', [ - SchemaProperty('oneAny', RealmPropertyType.mixed, - optional: true, indexType: RealmIndexType.regular), - SchemaProperty('manyAny', RealmPropertyType.mixed, - optional: true, collectionType: RealmCollectionType.list), - SchemaProperty('dictOfAny', RealmPropertyType.mixed, - optional: true, collectionType: RealmCollectionType.map), - SchemaProperty('setOfAny', RealmPropertyType.mixed, - optional: true, collectionType: RealmCollectionType.set), - ]); - }(); - - @override - SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; -} - -class Stuff extends _Stuff with RealmEntity, RealmObjectBase, RealmObject { - static var _defaultsSet = false; - - Stuff({ - int i = 42, - }) { - if (!_defaultsSet) { - _defaultsSet = RealmObjectBase.setDefaults({ - 'i': 42, - }); - } - RealmObjectBase.set(this, 'i', i); - } - - Stuff._(); - - @override - int get i => RealmObjectBase.get(this, 'i') as int; - @override - set i(int value) => RealmObjectBase.set(this, 'i', value); - - @override - Stream> get changes => - RealmObjectBase.getChanges(this); - - @override - Stuff freeze() => RealmObjectBase.freezeObject(this); - - EJsonValue toEJson() { - return { - 'i': i.toEJson(), - }; - } - - static EJsonValue _toEJson(Stuff value) => value.toEJson(); - static Stuff _fromEJson(EJsonValue ejson) { - return switch (ejson) { - { - 'i': EJsonValue i, - } => - Stuff( - i: fromEJson(i), - ), - _ => raiseInvalidEJson(ejson), - }; - } - - static final schema = () { - RealmObjectBase.registerFactory(Stuff._); - register(_toEJson, _fromEJson); - return SchemaObject(ObjectType.realmObject, Stuff, 'Stuff', [ - SchemaProperty('i', RealmPropertyType.int), - ]); - }(); - - @override - SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; -} diff --git a/packages/realm_dart/test/session_test.dart b/packages/realm_dart/test/session_test.dart index cd5691361..3a567a5f5 100644 --- a/packages/realm_dart/test/session_test.dart +++ b/packages/realm_dart/test/session_test.dart @@ -6,6 +6,19 @@ import 'package:test/test.dart' hide test, throws; import 'package:realm_dart/realm.dart'; import 'test.dart'; +Future validateSessionStates(String validationName, Session session, + {SessionState? expectedSessionState, ConnectionState? expectedConnectionState}) async { + if (expectedSessionState != null) { + await waitForCondition(() => session.state.name == expectedSessionState.name, + message: 'Expected ${session.state} to equal $expectedSessionState. Validation: $validationName', timeout: const Duration(seconds: 15)); + } + + if (expectedConnectionState != null) { + await waitForCondition(() => session.connectionState.name == expectedConnectionState.name, + message: 'Expected ${session.connectionState} to equal $expectedConnectionState. Validation: $validationName', timeout: const Duration(seconds: 15)); + } +} + void main() { setupTests(); @@ -44,19 +57,6 @@ void main() { expect(realm.syncSession, isNotNull); }); - Future validateSessionStates(String validationName, Session session, - {SessionState? expectedSessionState, ConnectionState? expectedConnectionState}) async { - if (expectedSessionState != null) { - await waitForCondition(() => session.state.name == expectedSessionState.name, - message: 'Expected ${session.state} to equal $expectedSessionState. Validation: $validationName', timeout: const Duration(seconds: 15)); - } - - if (expectedConnectionState != null) { - await waitForCondition(() => session.connectionState.name == expectedConnectionState.name, - message: 'Expected ${session.connectionState} to equal $expectedConnectionState. Validation: $validationName', timeout: const Duration(seconds: 15)); - } - } - baasTest('SyncSession.pause/resume', (configuration) async { final realm = await getIntegrationRealm(); diff --git a/packages/realm_dart/test/sync_migration_test.dart b/packages/realm_dart/test/sync_migration_test.dart index 51e502a68..f8d6ed488 100644 --- a/packages/realm_dart/test/sync_migration_test.dart +++ b/packages/realm_dart/test/sync_migration_test.dart @@ -159,7 +159,7 @@ void main() { expect(objv2.stringValue, isNull); expect(objv2.uuidValue, isNull); expect(objv2.binaryValue, isNull); - }, appName: AppName.staticSchema); + }, appName: AppName.staticSchema, skip: true); baasTest('Can remove field', (appConfig) async { final differentiator = ObjectId(); @@ -187,8 +187,15 @@ void main() { }, appName: AppName.staticSchema); baasTest('Fails with a future schema version', (appConfig) { - expectLater(() => openRealm(appConfig, NullablesV1.schema, ObjectId(), schemaVersion: 3), - throwsA(isA().having((e) => e.message, 'message', contains('schema version in BIND 3 is greater than latest schema version 2')))); + expectLater( + () => openRealm(appConfig, NullablesV1.schema, ObjectId(), schemaVersion: 3), + throwsA(isA().having( + (e) => e.message, + 'message', + // Locally this seems to throw: 'Failed to open realm: Error details missing'. + // While on CI it throws: + // contains('Failed to open realm: Client provided invalid schema version: client presented schema version "3" is greater than latest schema version "2"') + contains('Failed to open realm')))); }, appName: AppName.staticSchema); baasTest('Realm can be migrated through consequtive versions (0->1->2)', (appConfig) async { @@ -230,7 +237,7 @@ void main() { expect(objv2.stringValue, isNull); expect(objv2.uuidValue, isNull); expect(objv2.binaryValue, isNull); - }, appName: AppName.staticSchema); + }, appName: AppName.staticSchema, skip:true); baasTest('Realm can be migrated skipping versions (0->2)', (appConfig) async { final differentiator = ObjectId(); @@ -255,5 +262,5 @@ void main() { expect(objv2.stringValue, isNull); expect(objv2.uuidValue, isNull); expect(objv2.binaryValue, isNull); - }, appName: AppName.staticSchema); + }, appName: AppName.staticSchema, skip: true); } diff --git a/packages/realm_dart/test/sync_migration_test.realm.dart b/packages/realm_dart/test/sync_migration_test.realm.dart index 88929128e..fddf772f5 100644 --- a/packages/realm_dart/test/sync_migration_test.realm.dart +++ b/packages/realm_dart/test/sync_migration_test.realm.dart @@ -110,6 +110,11 @@ class NullablesV0 extends _NullablesV0 Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullablesV0 freeze() => RealmObjectBase.freezeObject(this); @@ -299,6 +304,11 @@ class NullablesV1 extends _NullablesV1 Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullablesV1 freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_dart/test/test.dart b/packages/realm_dart/test/test.dart index c2e68dc42..fdf2044cf 100644 --- a/packages/realm_dart/test/test.dart +++ b/packages/realm_dart/test/test.dart @@ -7,18 +7,20 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'package:logging/logging.dart'; + import 'package:meta/meta.dart'; import 'package:path/path.dart' as _path; -import 'package:realm_dart/src/logging.dart'; -import 'package:test/test.dart' hide test; -import 'package:test/test.dart' as testing; import 'package:realm_dart/realm.dart'; -import 'package:realm_dart/src/native/realm_core.dart'; import 'package:realm_dart/src/configuration.dart'; +import 'package:realm_dart/src/logging.dart'; +import 'package:realm_dart/src/native/realm_core.dart'; +import 'package:realm_dart/src/realm_object.dart'; +import 'package:test/test.dart'; import 'baas_helper.dart'; +export 'package:test/test.dart'; + export 'baas_helper.dart' show AppName; part 'test.realm.dart'; @@ -134,6 +136,8 @@ class _AllTypes { late int? nullableIntProp; late Decimal128? nullableDecimalProp; late Uint8List? nullableBinaryProp; + + late RealmValue realmValueProp; } @RealmModel() @@ -376,38 +380,43 @@ class _Symmetric { late ObjectId id; } +@RealmModel() +class _ObjectWithRealmValue { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; + late ObjectId? differentiator; + + @Indexed() + late RealmValue oneAny; + late List manyAny; + late Map dictOfAny; + late Set setOfAny; +} + +@RealmModel() +class _ObjectWithInt { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; + late ObjectId? differentiator; + + int i = 42; +} + String? testName; final _openRealms = Queue(); String testUsername = "realm-test@realm.io"; String testPassword = "123456"; -final int encryptionKeySize = 64; const int maxInt = 9223372036854775807; const int minInt = -9223372036854775808; const int jsMaxInt = 9007199254740991; const int jsMinInt = -9007199254740991; -//Overrides test method so we can filter tests -void test(String name, dynamic Function() testFunction, {dynamic skip, Map? onPlatform}) { - if (testName != null && !name.contains(testName!)) { - return; - } - - var timeout = 60; - assert(() { - if (Platform.environment['CI'] == null) { - timeout = Duration(minutes: 5).inSeconds; - } - - return true; - }()); - - testing.test(name, testFunction, skip: skip, onPlatform: onPlatform, timeout: Timeout(Duration(seconds: timeout))); -} - void xtest(String? name, dynamic Function() testFunction, {dynamic skip, Map? onPlatform}) { - testing.test(name, testFunction, skip: "Test is disabled"); + test(name, testFunction, skip: "Test is disabled"); } BaasHelper? baasHelper; @@ -418,7 +427,7 @@ void setupTests() { Realm.logger.setLogLevel(LogLevel.detail); Realm.logger.onRecord.listen((record) { - testing.printOnFailure('${DateTime.now().toUtc()} ${record.category} ${record.level.name}: ${record.message}'); + printOnFailure('${DateTime.now().toUtc()} ${record.category} ${record.level.name}: ${record.message}'); }); // Enable this to print platform info, including current PID @@ -577,7 +586,7 @@ Future baasTest( baasHelper!.printSplunkLogLink(appName, baasHelper?.baseUrl); final config = await baasHelper!.getAppConfig(appName: appName); await testFunction(config); - }, skip: skip); + }, skip: skip, tags: 'baas'); } dynamic shouldSkip(dynamic skip) { @@ -593,6 +602,7 @@ dynamic shouldSkip(dynamic skip) { String getAutoverifiedEmail() => 'realm_tests_do_autoverify_${generateRandomEmail()}'; +/// Registers, logs in, and returns the new user. Future getIntegrationUser({App? app, AppConfiguration? appConfig}) async { app ??= App(appConfig ?? await baasHelper!.getAppConfig()); final email = getAutoverifiedEmail(); @@ -610,6 +620,10 @@ FlexibleSyncConfiguration getIntegrationConfig(User user) { return Configuration.flexibleSync(user, getSyncSchema())..sessionStopPolicy = SessionStopPolicy.immediately; } +/// Returns a synced realm after logging in a user. +/// +/// A subscription for querying all [NullableTypes] objects containing +/// the `differentiator` will be added if a `differentiator` is provided. Future getIntegrationRealm({App? app, ObjectId? differentiator, AppConfiguration? appConfig, bool waitForSync = true}) async { app ??= App(appConfig ?? await baasHelper!.getAppConfig()); final user = await getIntegrationUser(app: app, appConfig: appConfig); @@ -668,7 +682,7 @@ Future waitForConditionWithResult(FutureOr Function() getter, FutureOr< } extension RealmObjectTest on RealmObjectBase { - String toJson() => realmCore.objectToString(this); + String toJson() => handle.objectToString(); } extension on int { @@ -747,6 +761,8 @@ List getSyncSchema() { Asymmetric.schema, Embedded.schema, Symmetric.schema, + ObjectWithRealmValue.schema, + ObjectWithInt.schema, ]; } diff --git a/packages/realm_dart/test/test.realm.dart b/packages/realm_dart/test/test.realm.dart index 468443eb0..a4f300f2a 100644 --- a/packages/realm_dart/test/test.realm.dart +++ b/packages/realm_dart/test/test.realm.dart @@ -25,6 +25,10 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Car freeze() => RealmObjectBase.freezeObject(this); @@ -77,6 +81,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); @@ -144,6 +152,10 @@ class Dog extends _Dog with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Dog freeze() => RealmObjectBase.freezeObject(this); @@ -224,6 +236,10 @@ class Team extends _Team with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Team freeze() => RealmObjectBase.freezeObject(this); @@ -309,6 +325,10 @@ class Student extends _Student with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Student freeze() => RealmObjectBase.freezeObject(this); @@ -410,6 +430,10 @@ class School extends _School with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override School freeze() => RealmObjectBase.freezeObject(this); @@ -495,6 +519,11 @@ class RemappedClass extends $RemappedClass Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override RemappedClass freeze() => RealmObjectBase.freezeObject(this); @@ -556,6 +585,10 @@ class Task extends _Task with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Task freeze() => RealmObjectBase.freezeObject(this); @@ -618,6 +651,10 @@ class Product extends _Product with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Product freeze() => RealmObjectBase.freezeObject(this); @@ -686,6 +723,10 @@ class Schedule extends _Schedule Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Schedule freeze() => RealmObjectBase.freezeObject(this); @@ -755,6 +796,10 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Foo freeze() => RealmObjectBase.freezeObject(this); @@ -815,6 +860,7 @@ class AllTypes extends _AllTypes int? nullableIntProp, Decimal128? nullableDecimalProp, Uint8List? nullableBinaryProp, + RealmValue realmValueProp = const RealmValue.nullValue(), }) { RealmObjectBase.set(this, 'stringProp', stringProp); RealmObjectBase.set(this, 'boolProp', boolProp); @@ -834,6 +880,7 @@ class AllTypes extends _AllTypes RealmObjectBase.set(this, 'nullableIntProp', nullableIntProp); RealmObjectBase.set(this, 'nullableDecimalProp', nullableDecimalProp); RealmObjectBase.set(this, 'nullableBinaryProp', nullableBinaryProp); + RealmObjectBase.set(this, 'realmValueProp', realmValueProp); } AllTypes._(); @@ -958,10 +1005,21 @@ class AllTypes extends _AllTypes set nullableBinaryProp(Uint8List? value) => RealmObjectBase.set(this, 'nullableBinaryProp', value); + @override + RealmValue get realmValueProp => + RealmObjectBase.get(this, 'realmValueProp') as RealmValue; + @override + set realmValueProp(RealmValue value) => + RealmObjectBase.set(this, 'realmValueProp', value); + @override Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override AllTypes freeze() => RealmObjectBase.freezeObject(this); @@ -985,6 +1043,7 @@ class AllTypes extends _AllTypes 'nullableIntProp': nullableIntProp.toEJson(), 'nullableDecimalProp': nullableDecimalProp.toEJson(), 'nullableBinaryProp': nullableBinaryProp.toEJson(), + 'realmValueProp': realmValueProp.toEJson(), }; } @@ -1010,6 +1069,7 @@ class AllTypes extends _AllTypes 'nullableIntProp': EJsonValue nullableIntProp, 'nullableDecimalProp': EJsonValue nullableDecimalProp, 'nullableBinaryProp': EJsonValue nullableBinaryProp, + 'realmValueProp': EJsonValue realmValueProp, } => AllTypes( fromEJson(stringProp), @@ -1030,6 +1090,7 @@ class AllTypes extends _AllTypes nullableIntProp: fromEJson(nullableIntProp), nullableDecimalProp: fromEJson(nullableDecimalProp), nullableBinaryProp: fromEJson(nullableBinaryProp), + realmValueProp: fromEJson(realmValueProp), ), _ => raiseInvalidEJson(ejson), }; @@ -1065,6 +1126,7 @@ class AllTypes extends _AllTypes optional: true), SchemaProperty('nullableBinaryProp', RealmPropertyType.binary, optional: true), + SchemaProperty('realmValueProp', RealmPropertyType.mixed, optional: true), ]); }(); @@ -1130,6 +1192,10 @@ class LinksClass extends _LinksClass Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override LinksClass freeze() => RealmObjectBase.freezeObject(this); @@ -1693,6 +1759,11 @@ class AllCollections extends _AllCollections Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override AllCollections freeze() => RealmObjectBase.freezeObject(this); @@ -2057,6 +2128,11 @@ class NullableTypes extends _NullableTypes Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableTypes freeze() => RealmObjectBase.freezeObject(this); @@ -2185,6 +2261,10 @@ class Event extends _Event with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Event freeze() => RealmObjectBase.freezeObject(this); @@ -2282,6 +2362,10 @@ class Party extends _Party with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Party freeze() => RealmObjectBase.freezeObject(this); @@ -2382,6 +2466,10 @@ class Friend extends _Friend with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Friend freeze() => RealmObjectBase.freezeObject(this); @@ -2459,6 +2547,10 @@ class When extends _When with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override When freeze() => RealmObjectBase.freezeObject(this); @@ -2532,6 +2624,10 @@ class Player extends _Player with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Player freeze() => RealmObjectBase.freezeObject(this); @@ -2597,6 +2693,10 @@ class Game extends _Game with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Game freeze() => RealmObjectBase.freezeObject(this); @@ -2859,6 +2959,11 @@ class AllTypesEmbedded extends _AllTypesEmbedded Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override AllTypesEmbedded freeze() => RealmObjectBase.freezeObject(this); @@ -3072,6 +3177,11 @@ class ObjectWithEmbedded extends _ObjectWithEmbedded Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override ObjectWithEmbedded freeze() => RealmObjectBase.freezeObject(this); @@ -3185,6 +3295,11 @@ class RecursiveEmbedded1 extends _RecursiveEmbedded1 Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override RecursiveEmbedded1 freeze() => RealmObjectBase.freezeObject(this); @@ -3287,6 +3402,11 @@ class RecursiveEmbedded2 extends _RecursiveEmbedded2 Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override RecursiveEmbedded2 freeze() => RealmObjectBase.freezeObject(this); @@ -3358,6 +3478,11 @@ class RecursiveEmbedded3 extends _RecursiveEmbedded3 Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override RecursiveEmbedded3 freeze() => RealmObjectBase.freezeObject(this); @@ -3423,6 +3548,11 @@ class ObjectWithDecimal extends _ObjectWithDecimal Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override ObjectWithDecimal freeze() => RealmObjectBase.freezeObject(this); @@ -3503,6 +3633,10 @@ class Asymmetric extends _Asymmetric Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Asymmetric freeze() => RealmObjectBase.freezeObject(this); @@ -3584,6 +3718,10 @@ class Embedded extends _Embedded Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Embedded freeze() => RealmObjectBase.freezeObject(this); @@ -3646,6 +3784,10 @@ class Symmetric extends _Symmetric Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Symmetric freeze() => RealmObjectBase.freezeObject(this); @@ -3680,3 +3822,229 @@ class Symmetric extends _Symmetric @override SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } + +class ObjectWithRealmValue extends _ObjectWithRealmValue + with RealmEntity, RealmObjectBase, RealmObject { + ObjectWithRealmValue( + ObjectId id, { + ObjectId? differentiator, + RealmValue oneAny = const RealmValue.nullValue(), + Iterable manyAny = const [], + Map dictOfAny = const {}, + Set setOfAny = const {}, + }) { + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'differentiator', differentiator); + RealmObjectBase.set(this, 'oneAny', oneAny); + RealmObjectBase.set>( + this, 'manyAny', RealmList(manyAny)); + RealmObjectBase.set>( + this, 'dictOfAny', RealmMap(dictOfAny)); + RealmObjectBase.set>( + this, 'setOfAny', RealmSet(setOfAny)); + } + + ObjectWithRealmValue._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + ObjectId? get differentiator => + RealmObjectBase.get(this, 'differentiator') as ObjectId?; + @override + set differentiator(ObjectId? value) => + RealmObjectBase.set(this, 'differentiator', value); + + @override + RealmValue get oneAny => + RealmObjectBase.get(this, 'oneAny') as RealmValue; + @override + set oneAny(RealmValue value) => RealmObjectBase.set(this, 'oneAny', value); + + @override + RealmList get manyAny => + RealmObjectBase.get(this, 'manyAny') as RealmList; + @override + set manyAny(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get dictOfAny => + RealmObjectBase.get(this, 'dictOfAny') + as RealmMap; + @override + set dictOfAny(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get setOfAny => + RealmObjectBase.get(this, 'setOfAny') as RealmSet; + @override + set setOfAny(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + + @override + ObjectWithRealmValue freeze() => + RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + '_id': id.toEJson(), + 'differentiator': differentiator.toEJson(), + 'oneAny': oneAny.toEJson(), + 'manyAny': manyAny.toEJson(), + 'dictOfAny': dictOfAny.toEJson(), + 'setOfAny': setOfAny.toEJson(), + }; + } + + static EJsonValue _toEJson(ObjectWithRealmValue value) => value.toEJson(); + static ObjectWithRealmValue _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_id': EJsonValue id, + 'differentiator': EJsonValue differentiator, + 'oneAny': EJsonValue oneAny, + 'manyAny': EJsonValue manyAny, + 'dictOfAny': EJsonValue dictOfAny, + 'setOfAny': EJsonValue setOfAny, + } => + ObjectWithRealmValue( + fromEJson(id), + differentiator: fromEJson(differentiator), + oneAny: fromEJson(oneAny), + manyAny: fromEJson(manyAny), + dictOfAny: fromEJson(dictOfAny), + setOfAny: fromEJson(setOfAny), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(ObjectWithRealmValue._); + register(_toEJson, _fromEJson); + return SchemaObject( + ObjectType.realmObject, ObjectWithRealmValue, 'ObjectWithRealmValue', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + SchemaProperty('differentiator', RealmPropertyType.objectid, + optional: true), + SchemaProperty('oneAny', RealmPropertyType.mixed, + optional: true, indexType: RealmIndexType.regular), + SchemaProperty('manyAny', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.list), + SchemaProperty('dictOfAny', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('setOfAny', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.set), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} + +class ObjectWithInt extends _ObjectWithInt + with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + ObjectWithInt( + ObjectId id, { + ObjectId? differentiator, + int i = 42, + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'i': 42, + }); + } + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'differentiator', differentiator); + RealmObjectBase.set(this, 'i', i); + } + + ObjectWithInt._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + ObjectId? get differentiator => + RealmObjectBase.get(this, 'differentiator') as ObjectId?; + @override + set differentiator(ObjectId? value) => + RealmObjectBase.set(this, 'differentiator', value); + + @override + int get i => RealmObjectBase.get(this, 'i') as int; + @override + set i(int value) => RealmObjectBase.set(this, 'i', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + + @override + ObjectWithInt freeze() => RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + '_id': id.toEJson(), + 'differentiator': differentiator.toEJson(), + 'i': i.toEJson(), + }; + } + + static EJsonValue _toEJson(ObjectWithInt value) => value.toEJson(); + static ObjectWithInt _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_id': EJsonValue id, + 'differentiator': EJsonValue differentiator, + 'i': EJsonValue i, + } => + ObjectWithInt( + fromEJson(id), + differentiator: fromEJson(differentiator), + i: fromEJson(i), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(ObjectWithInt._); + register(_toEJson, _fromEJson); + return SchemaObject( + ObjectType.realmObject, ObjectWithInt, 'ObjectWithInt', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + SchemaProperty('differentiator', RealmPropertyType.objectid, + optional: true), + SchemaProperty('i', RealmPropertyType.int), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} diff --git a/packages/realm_dart/test/user_test.dart b/packages/realm_dart/test/user_test.dart index 47b6570ed..df21c1087 100644 --- a/packages/realm_dart/test/user_test.dart +++ b/packages/realm_dart/test/user_test.dart @@ -382,7 +382,7 @@ void main() { await expectLater( () => app.logIn(credentials), throwsA(isA() - .having((e) => e.message, 'message', contains('invalid API key')) + .having((e) => e.message, 'message', equals('unauthorized')) .having((e) => e.statusCode, 'statusCode', 401) .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id=')))); @@ -405,7 +405,7 @@ void main() { await expectLater( () => app.logIn(credentials), throwsA(isA() - .having((e) => e.message, 'message', contains('invalid API key')) + .having((e) => e.message, 'message', equals('unauthorized')) .having((e) => e.statusCode, 'statusCode', 401) .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id=')))); }); @@ -452,7 +452,7 @@ void main() { await expectLater( () async => await app.logIn(credentials), throwsA(isA() - .having((e) => e.message, 'message', 'invalid API key') + .having((e) => e.message, 'message', 'unauthorized') .having((e) => e.statusCode, 'statusCode', 401) .having((e) => e.linkToServerLogs, 'linkToServerLogs', contains('logs?co_id=')))); }); diff --git a/packages/realm_generator/lib/src/class_element_ex.dart b/packages/realm_generator/lib/src/class_element_ex.dart index 00c8fd0e4..4f35d4b55 100644 --- a/packages/realm_generator/lib/src/class_element_ex.dart +++ b/packages/realm_generator/lib/src/class_element_ex.dart @@ -202,7 +202,19 @@ extension ClassElementEx on ClassElement { } } - return RealmModelInfo(name, modelName, realmName, mappedFields, objectType); + // Get the generator configuration + final index = realmModelInfo?.value.getField('generatorConfig')?.getField('ctorStyle')?.getField('index')?.toIntValue(); + final ctorStyle = index != null ? CtorStyle.values[index] : CtorStyle.onlyOptionalNamed; + final config = GeneratorConfig(ctorStyle: ctorStyle); + + return RealmModelInfo( + name, + modelName, + realmName, + mappedFields, + objectType, + config, + ); } on InvalidGenerationSourceError catch (_) { rethrow; } catch (e, s) { diff --git a/packages/realm_generator/lib/src/dart_type_ex.dart b/packages/realm_generator/lib/src/dart_type_ex.dart index 2cdf2a179..5a0b5de6e 100644 --- a/packages/realm_generator/lib/src/dart_type_ex.dart +++ b/packages/realm_generator/lib/src/dart_type_ex.dart @@ -24,7 +24,7 @@ extension DartTypeEx on DartType { if (element == null) return null; final realmModelAnnotation = realmModelChecker.firstAnnotationOfExact(element!); if (realmModelAnnotation == null) return null; // not a RealmModel - final index = realmModelAnnotation.getField('type')!.getField('index')!.toIntValue()!; + final index = realmModelAnnotation.getField('baseType')!.getField('index')!.toIntValue()!; return ObjectType.values[index]; } diff --git a/packages/realm_generator/lib/src/field_element_ex.dart b/packages/realm_generator/lib/src/field_element_ex.dart index 85305b644..4115c5a6a 100644 --- a/packages/realm_generator/lib/src/field_element_ex.dart +++ b/packages/realm_generator/lib/src/field_element_ex.dart @@ -69,8 +69,8 @@ extension FieldElementEx on FieldElement { return null; } - if (ignoredInfo != null || isPrivate) { - // skip ignored and private fields + if (ignoredInfo != null) { + // skip ignored fields return null; } diff --git a/packages/realm_generator/lib/src/realm_model_info.dart b/packages/realm_generator/lib/src/realm_model_info.dart index 801bd2740..6e9e3963a 100644 --- a/packages/realm_generator/lib/src/realm_model_info.dart +++ b/packages/realm_generator/lib/src/realm_model_info.dart @@ -11,14 +11,26 @@ extension on Iterable { Iterable except(bool Function(T) test) => where((e) => !test(e)); } +extension on String { + String nonPrivate() => startsWith('_') ? substring(1) : this; +} + class RealmModelInfo { final String name; final String modelName; final String realmName; final List fields; final ObjectType baseType; + final GeneratorConfig config; - const RealmModelInfo(this.name, this.modelName, this.realmName, this.fields, this.baseType); + const RealmModelInfo( + this.name, + this.modelName, + this.realmName, + this.fields, + this.baseType, + this.config, + ); Iterable toCode() sync* { yield 'class $name extends $modelName with RealmEntity, RealmObjectBase, ${baseType.className} {'; @@ -32,22 +44,29 @@ class RealmModelInfo { yield ''; } - bool requiredCondition(RealmFieldInfo f) => f.isRequired || f.isPrimaryKey; - final required = allSettable.where(requiredCondition); - final notRequired = allSettable.except(requiredCondition); + bool required(RealmFieldInfo f) => f.isRequired || f.isPrimaryKey; + bool usePositional(RealmFieldInfo f) => config.ctorStyle != CtorStyle.allNamed && required(f); + String paramName(RealmFieldInfo f) => usePositional(f) ? f.name : f.name.nonPrivate(); + final positional = allSettable.where(usePositional); + final named = allSettable.except(usePositional); // Constructor yield '$name('; { - yield* required.map((f) => '${f.mappedTypeName} ${f.name},'); - if (notRequired.isNotEmpty) { + yield* positional.map((f) => '${f.mappedTypeName} ${paramName(f)},'); + if (named.isNotEmpty) { yield '{'; - yield* notRequired.map((f) { - if (f.isRealmCollection) { - final collectionPrefix = f.type.isDartCoreList ? 'Iterable<' : f.type.isDartCoreSet ? 'Set<' : 'Map ${f.name}${f.initializer},'; - } - return '${f.mappedTypeName} ${f.name}${f.initializer},'; + yield* named.map((f) { + final requiredPrefix = required(f) ? 'required ' : ''; + final param = paramName(f); + final collectionPrefix = switch (f) { + _ when f.isDartCoreList => 'Iterable<', + _ when f.isDartCoreSet => 'Set<', + _ when f.isDartCoreMap => 'Map '', + }; + final typePrefix = f.isRealmCollection ? '$collectionPrefix${f.type.basicMappedName}>' : f.mappedTypeName; + return '$requiredPrefix$typePrefix $param${f.initializer},'; }); yield '}'; } @@ -63,13 +82,14 @@ class RealmModelInfo { } yield* allSettable.map((f) { + final param = paramName(f); if (f.type.isUint8List && f.hasDefaultValue) { - return "RealmObjectBase.set(this, '${f.realmName}', ${f.name} ?? ${f.fieldElement.initializerExpression});"; + return "RealmObjectBase.set(this, '${f.realmName}', $param ?? ${f.fieldElement.initializerExpression});"; } if (f.isRealmCollection) { - return "RealmObjectBase.set<${f.mappedTypeName}>(this, '${f.realmName}', ${f.mappedTypeName}(${f.name}));"; + return "RealmObjectBase.set<${f.mappedTypeName}>(this, '${f.realmName}', ${f.mappedTypeName}($param));"; } - return "RealmObjectBase.set(this, '${f.realmName}', ${f.name});"; + return "RealmObjectBase.set(this, '${f.realmName}', $param);"; }); } yield '}'; @@ -88,6 +108,10 @@ class RealmModelInfo { yield 'Stream> get changes => RealmObjectBase.getChanges<$name>(this);'; yield ''; + yield '@override'; + yield 'Stream> changesFor([List? keyPaths]) => RealmObjectBase.getChangesFor<$name>(this, keyPaths);'; + yield ''; + // Freeze yield '@override'; yield '$name freeze() => RealmObjectBase.freezeObject<$name>(this);'; @@ -121,8 +145,8 @@ class RealmModelInfo { } yield '} => $name('; { - yield* required.map((f) => 'fromEJson(${f.name}),'); - yield* notRequired.map((f) => '${f.name}: fromEJson(${f.name}),'); + yield* positional.map((f) => 'fromEJson(${f.name}),'); + yield* named.map((f) => '${paramName(f)}: fromEJson(${f.name}),'); } yield '),'; yield '_ => raiseInvalidEJson(ejson),'; diff --git a/packages/realm_generator/pubspec.yaml b/packages/realm_generator/pubspec.yaml index 72eb7acc0..55c312122 100644 --- a/packages/realm_generator/pubspec.yaml +++ b/packages/realm_generator/pubspec.yaml @@ -3,23 +3,24 @@ description: >- Generates RealmObject classes from Realm data model classes. This package is part of the official Realm Flutter and Realm Dart SDKs. -version: 2.0.0 +version: 2.3.0 homepage: https://www.realm.io repository: https://github.com/realm/realm-dart issue_tracker: https://github.com/realm/realm-dart/issues environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: analyzer: ^6.0.0 build_resolvers: ^2.0.9 build: ^2.0.0 dart_style: ^2.2.0 - realm_common: ^2.0.0 + realm_common: ^2.3.0 source_gen: ^1.1.0 source_span: ^1.8.0 + collection: ^1.18.0 dev_dependencies: build_runner: ^2.1.0 diff --git a/packages/realm_generator/test/good_test_data/all_named_ctor.dart b/packages/realm_generator/test/good_test_data/all_named_ctor.dart new file mode 100644 index 000000000..0ca97194d --- /dev/null +++ b/packages/realm_generator/test/good_test_data/all_named_ctor.dart @@ -0,0 +1,12 @@ +import 'package:realm_common/realm_common.dart'; + +part 'all_named_ctor.realm.dart'; + +const config = GeneratorConfig(ctorStyle: CtorStyle.allNamed); +const realmModel = RealmModel.using(baseType: ObjectType.realmObject, generatorConfig: config); + +@realmModel +class _Person { + late String name; + int age = 42; +} diff --git a/packages/realm_generator/test/good_test_data/all_named_ctor.expected b/packages/realm_generator/test/good_test_data/all_named_ctor.expected new file mode 100644 index 000000000..295a2722f --- /dev/null +++ b/packages/realm_generator/test/good_test_data/all_named_ctor.expected @@ -0,0 +1,82 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'all_named_ctor.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + Person({ + required String name, + int age = 42, + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'age': 42, + }); + } + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'age', age); + } + + Person._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + int get age => RealmObjectBase.get(this, 'age') as int; + @override + set age(int value) => RealmObjectBase.set(this, 'age', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + + @override + Person freeze() => RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + 'name': name.toEJson(), + 'age': age.toEJson(), + }; + } + + static EJsonValue _toEJson(Person value) => value.toEJson(); + static Person _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'name': EJsonValue name, + 'age': EJsonValue age, + } => + Person( + name: fromEJson(name), + age: fromEJson(age), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(Person._); + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Person, 'Person', [ + SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('age', RealmPropertyType.int), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} diff --git a/packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart b/packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart new file mode 100644 index 000000000..a5a96725a --- /dev/null +++ b/packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart @@ -0,0 +1,2 @@ + +part of 'all_named_ctor.dart'; diff --git a/packages/realm_generator/test/good_test_data/all_types.expected b/packages/realm_generator/test/good_test_data/all_types.expected index d975ea269..c32e4a538 100644 --- a/packages/realm_generator/test/good_test_data/all_types.expected +++ b/packages/realm_generator/test/good_test_data/all_types.expected @@ -39,6 +39,10 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Foo freeze() => RealmObjectBase.freezeObject(this); @@ -235,6 +239,10 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Bar freeze() => RealmObjectBase.freezeObject(this); @@ -398,6 +406,11 @@ class PrimitiveTypes extends _PrimitiveTypes Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override PrimitiveTypes freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/another_mapto.expected_multi b/packages/realm_generator/test/good_test_data/another_mapto.expected_multi index 077783060..62b443a45 100644 --- a/packages/realm_generator/test/good_test_data/another_mapto.expected_multi +++ b/packages/realm_generator/test/good_test_data/another_mapto.expected_multi @@ -38,6 +38,10 @@ class MappedToo extends _MappedToo Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override MappedToo freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/asymmetric_object.expected b/packages/realm_generator/test/good_test_data/asymmetric_object.expected index fc75ec583..14f3de6fd 100644 --- a/packages/realm_generator/test/good_test_data/asymmetric_object.expected +++ b/packages/realm_generator/test/good_test_data/asymmetric_object.expected @@ -54,6 +54,10 @@ class Asymmetric extends _Asymmetric Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Asymmetric freeze() => RealmObjectBase.freezeObject(this); @@ -88,8 +92,7 @@ class Asymmetric extends _Asymmetric static final schema = () { RealmObjectBase.registerFactory(Asymmetric._); register(_toEJson, _fromEJson); - return SchemaObject( - ObjectType.realmObject, Asymmetric, 'Asymmetric', [ + return SchemaObject(ObjectType.realmObject, Asymmetric, 'Asymmetric', [ SchemaProperty('id', RealmPropertyType.objectid, mapTo: '_id', primaryKey: true), SchemaProperty('children', RealmPropertyType.object, @@ -131,6 +134,10 @@ class Embedded extends _Embedded Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Embedded freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/binary_type.expected b/packages/realm_generator/test/good_test_data/binary_type.expected index f48add498..7fc9f2a0d 100644 --- a/packages/realm_generator/test/good_test_data/binary_type.expected +++ b/packages/realm_generator/test/good_test_data/binary_type.expected @@ -36,6 +36,10 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Foo freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/const_initializer.dart b/packages/realm_generator/test/good_test_data/const_initializer.dart index 9fcf7b601..fba9d4ca0 100644 --- a/packages/realm_generator/test/good_test_data/const_initializer.dart +++ b/packages/realm_generator/test/good_test_data/const_initializer.dart @@ -13,7 +13,7 @@ class _ConstInitializer { int minusMinusOne = -(-1); int add = 1 + 1; int identifier = myConst; - + double infinity = double.infinity; double nan = double.nan; double negativeInfinity = double.negativeInfinity; diff --git a/packages/realm_generator/test/good_test_data/const_initializer.expected b/packages/realm_generator/test/good_test_data/const_initializer.expected index a1f399694..2f040fea8 100644 --- a/packages/realm_generator/test/good_test_data/const_initializer.expected +++ b/packages/realm_generator/test/good_test_data/const_initializer.expected @@ -186,6 +186,11 @@ class ConstInitializer extends _ConstInitializer Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override ConstInitializer freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/embedded_annotations.expected b/packages/realm_generator/test/good_test_data/embedded_annotations.expected index 2d7e133cc..bf7c4cc85 100644 --- a/packages/realm_generator/test/good_test_data/embedded_annotations.expected +++ b/packages/realm_generator/test/good_test_data/embedded_annotations.expected @@ -37,6 +37,10 @@ class Parent extends _Parent with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Parent freeze() => RealmObjectBase.freezeObject(this); @@ -115,6 +119,10 @@ class Child1 extends _Child1 with RealmEntity, RealmObjectBase, EmbeddedObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Child1 freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/embedded_objects.expected b/packages/realm_generator/test/good_test_data/embedded_objects.expected index 48ffc30f1..291024fdb 100644 --- a/packages/realm_generator/test/good_test_data/embedded_objects.expected +++ b/packages/realm_generator/test/good_test_data/embedded_objects.expected @@ -36,6 +36,10 @@ class Parent extends _Parent with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Parent freeze() => RealmObjectBase.freezeObject(this); @@ -121,6 +125,10 @@ class Child1 extends _Child1 with RealmEntity, RealmObjectBase, EmbeddedObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Child1 freeze() => RealmObjectBase.freezeObject(this); @@ -300,6 +308,10 @@ class Child2 extends _Child2 with RealmEntity, RealmObjectBase, EmbeddedObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Child2 freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/indexable_types.expected b/packages/realm_generator/test/good_test_data/indexable_types.expected index 20facb812..657a38fbd 100644 --- a/packages/realm_generator/test/good_test_data/indexable_types.expected +++ b/packages/realm_generator/test/good_test_data/indexable_types.expected @@ -155,6 +155,10 @@ class Indexable extends _Indexable Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Indexable freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/list_initialization.expected b/packages/realm_generator/test/good_test_data/list_initialization.expected index 45f49bc96..d0463f516 100644 --- a/packages/realm_generator/test/good_test_data/list_initialization.expected +++ b/packages/realm_generator/test/good_test_data/list_initialization.expected @@ -116,6 +116,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/map.expected b/packages/realm_generator/test/good_test_data/map.expected index 7c6f5e6e6..62c71fda1 100644 --- a/packages/realm_generator/test/good_test_data/map.expected +++ b/packages/realm_generator/test/good_test_data/map.expected @@ -123,6 +123,10 @@ class LotsOfMaps extends _LotsOfMaps Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override LotsOfMaps freeze() => RealmObjectBase.freezeObject(this); @@ -178,8 +182,7 @@ class LotsOfMaps extends _LotsOfMaps static final schema = () { RealmObjectBase.registerFactory(LotsOfMaps._); register(_toEJson, _fromEJson); - return SchemaObject( - ObjectType.realmObject, LotsOfMaps, 'LotsOfMaps', [ + return SchemaObject(ObjectType.realmObject, LotsOfMaps, 'LotsOfMaps', [ SchemaProperty('persons', RealmPropertyType.object, optional: true, linkTarget: 'Person', @@ -229,6 +232,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/mapto.expected b/packages/realm_generator/test/good_test_data/mapto.expected index eee0a9ff8..8f6fcac83 100644 --- a/packages/realm_generator/test/good_test_data/mapto.expected +++ b/packages/realm_generator/test/good_test_data/mapto.expected @@ -55,6 +55,10 @@ class Original extends $Original Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Original freeze() => RealmObjectBase.freezeObject(this); @@ -86,8 +90,7 @@ class Original extends $Original static final schema = () { RealmObjectBase.registerFactory(Original._); register(_toEJson, _fromEJson); - return SchemaObject( - ObjectType.realmObject, Original, 'another type', [ + return SchemaObject(ObjectType.realmObject, Original, 'another type', [ SchemaProperty('primitiveProperty', RealmPropertyType.int, mapTo: 'remapped primitive'), SchemaProperty('objectProperty', RealmPropertyType.object, diff --git a/packages/realm_generator/test/good_test_data/optional_argument.expected b/packages/realm_generator/test/good_test_data/optional_argument.expected index 0b9dea736..5ca9325c2 100644 --- a/packages/realm_generator/test/good_test_data/optional_argument.expected +++ b/packages/realm_generator/test/good_test_data/optional_argument.expected @@ -26,6 +26,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/pinhole.expected b/packages/realm_generator/test/good_test_data/pinhole.expected index 7626ec3c6..494bbceb5 100644 --- a/packages/realm_generator/test/good_test_data/pinhole.expected +++ b/packages/realm_generator/test/good_test_data/pinhole.expected @@ -32,6 +32,10 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Foo freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/primary_key.expected b/packages/realm_generator/test/good_test_data/primary_key.expected index e7e4d5e58..d60f5e3a1 100644 --- a/packages/realm_generator/test/good_test_data/primary_key.expected +++ b/packages/realm_generator/test/good_test_data/primary_key.expected @@ -25,6 +25,10 @@ class IntPK extends _IntPK with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override IntPK freeze() => RealmObjectBase.freezeObject(this); @@ -78,6 +82,11 @@ class NullableIntPK extends _NullableIntPK Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableIntPK freeze() => RealmObjectBase.freezeObject(this); @@ -133,6 +142,10 @@ class StringPK extends _StringPK Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override StringPK freeze() => RealmObjectBase.freezeObject(this); @@ -186,6 +199,11 @@ class NullableStringPK extends _NullableStringPK Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableStringPK freeze() => RealmObjectBase.freezeObject(this); @@ -242,6 +260,10 @@ class ObjectIdPK extends _ObjectIdPK Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override ObjectIdPK freeze() => RealmObjectBase.freezeObject(this); @@ -295,6 +317,11 @@ class NullableObjectIdPK extends _NullableObjectIdPK Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableObjectIdPK freeze() => RealmObjectBase.freezeObject(this); @@ -350,6 +377,10 @@ class UuidPK extends _UuidPK with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override UuidPK freeze() => RealmObjectBase.freezeObject(this); @@ -403,6 +434,11 @@ class NullableUuidPK extends _NullableUuidPK Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override NullableUuidPK freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/private_fields.dart b/packages/realm_generator/test/good_test_data/private_fields.dart new file mode 100644 index 000000000..17e80800e --- /dev/null +++ b/packages/realm_generator/test/good_test_data/private_fields.dart @@ -0,0 +1,11 @@ +// ignore_for_file: unused_element, prefer_final_fields, unused_field + +import 'package:realm_common/realm_common.dart'; + +part 'private_fields.realm.dart'; + +@RealmModel() +class _WithPrivateFields { + late String _plain; + int _withDefault = 0; +} diff --git a/packages/realm_generator/test/good_test_data/private_fields.expected b/packages/realm_generator/test/good_test_data/private_fields.expected new file mode 100644 index 000000000..5d727d62d --- /dev/null +++ b/packages/realm_generator/test/good_test_data/private_fields.expected @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'private_fields.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class WithPrivateFields extends _WithPrivateFields + with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + WithPrivateFields( + String _plain, { + int withDefault = 0, + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + '_withDefault': 0, + }); + } + RealmObjectBase.set(this, '_plain', _plain); + RealmObjectBase.set(this, '_withDefault', withDefault); + } + + WithPrivateFields._(); + + @override + String get _plain => RealmObjectBase.get(this, '_plain') as String; + @override + set _plain(String value) => RealmObjectBase.set(this, '_plain', value); + + @override + int get _withDefault => RealmObjectBase.get(this, '_withDefault') as int; + @override + set _withDefault(int value) => + RealmObjectBase.set(this, '_withDefault', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + + @override + WithPrivateFields freeze() => + RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + '_plain': _plain.toEJson(), + '_withDefault': _withDefault.toEJson(), + }; + } + + static EJsonValue _toEJson(WithPrivateFields value) => value.toEJson(); + static WithPrivateFields _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_plain': EJsonValue _plain, + '_withDefault': EJsonValue _withDefault, + } => + WithPrivateFields( + fromEJson(_plain), + withDefault: fromEJson(_withDefault), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(WithPrivateFields._); + register(_toEJson, _fromEJson); + return SchemaObject( + ObjectType.realmObject, WithPrivateFields, 'WithPrivateFields', [ + SchemaProperty('_plain', RealmPropertyType.string), + SchemaProperty('_withDefault', RealmPropertyType.int), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} diff --git a/packages/realm_generator/test/good_test_data/private_fields.realm.dart b/packages/realm_generator/test/good_test_data/private_fields.realm.dart new file mode 100644 index 000000000..502678a0f --- /dev/null +++ b/packages/realm_generator/test/good_test_data/private_fields.realm.dart @@ -0,0 +1,5 @@ +// MOCK FILE! This file exists to ensure the parent file is valid Dart. +// The parent will be used as input to the realm_generator in a test, and the +// output compared to the .expected file. + +part of 'private_fields.dart'; diff --git a/packages/realm_generator/test/good_test_data/realm_set.expected b/packages/realm_generator/test/good_test_data/realm_set.expected index 42addd289..c5e4afc6f 100644 --- a/packages/realm_generator/test/good_test_data/realm_set.expected +++ b/packages/realm_generator/test/good_test_data/realm_set.expected @@ -25,6 +25,10 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Car freeze() => RealmObjectBase.freezeObject(this); @@ -241,6 +245,10 @@ class RealmSets extends _RealmSets Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override RealmSets freeze() => RealmObjectBase.freezeObject(this); @@ -353,4 +361,4 @@ class RealmSets extends _RealmSets @override SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; -} \ No newline at end of file +} diff --git a/packages/realm_generator/test/good_test_data/required_argument.expected b/packages/realm_generator/test/good_test_data/required_argument.expected index bcc016240..fb1478894 100644 --- a/packages/realm_generator/test/good_test_data/required_argument.expected +++ b/packages/realm_generator/test/good_test_data/required_argument.expected @@ -25,6 +25,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/required_argument_with_default_value.expected b/packages/realm_generator/test/good_test_data/required_argument_with_default_value.expected index b7c0fd3cc..b0ad1b3a1 100644 --- a/packages/realm_generator/test/good_test_data/required_argument_with_default_value.expected +++ b/packages/realm_generator/test/good_test_data/required_argument_with_default_value.expected @@ -32,6 +32,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this); diff --git a/packages/realm_generator/test/good_test_data/user_defined_getter.expected b/packages/realm_generator/test/good_test_data/user_defined_getter.expected index 8eb457bd0..470f09635 100644 --- a/packages/realm_generator/test/good_test_data/user_defined_getter.expected +++ b/packages/realm_generator/test/good_test_data/user_defined_getter.expected @@ -25,6 +25,10 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Person freeze() => RealmObjectBase.freezeObject(this);