From 16f34e5e7601cd3ac35844a00c1922f773ba0dcb Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 14 Jun 2024 15:31:04 +0200 Subject: [PATCH] RDART-969: Keypath filtering on collections (#1714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added basic tests for collections keypath filtering * Various fixes * Basic tests * Added basic tests * Added fix for realm objects collections * Added changelog and added ticket * Clean up * Small correction * Apply suggestions from code review Co-authored-by: Kasper Overgård Nielsen * Fixed changelog * Avoiding calling native for classkey * Simplified code * Various fixes * Small fix * Added changesFor to extension methods * Corrected error --------- Co-authored-by: Kasper Overgård Nielsen --- CHANGELOG.md | 16 +- .../lib/src/handles/list_handle.dart | 2 +- .../lib/src/handles/map_handle.dart | 2 +- .../lib/src/handles/native/list_handle.dart | 26 +- .../lib/src/handles/native/map_handle.dart | 26 +- .../lib/src/handles/native/object_handle.dart | 24 +- .../lib/src/handles/native/realm_handle.dart | 27 +- .../src/handles/native/results_handle.dart | 25 +- .../lib/src/handles/native/set_handle.dart | 25 +- .../lib/src/handles/object_handle.dart | 4 +- .../lib/src/handles/realm_handle.dart | 2 + .../lib/src/handles/results_handle.dart | 2 +- .../lib/src/handles/set_handle.dart | 2 +- packages/realm_dart/lib/src/list.dart | 34 +- packages/realm_dart/lib/src/map.dart | 36 +- packages/realm_dart/lib/src/realm_object.dart | 13 +- packages/realm_dart/lib/src/results.dart | 23 +- packages/realm_dart/lib/src/set.dart | 40 +- packages/realm_dart/test/list_test.dart | 79 +++ packages/realm_dart/test/realm_map_test.dart | 78 +++ .../realm_dart/test/realm_map_test.realm.dart | 11 + .../realm_dart/test/realm_object_test.dart | 15 +- packages/realm_dart/test/realm_set_test.dart | 80 +++ .../realm_dart/test/realm_set_test.realm.dart | 11 + packages/realm_dart/test/results_test.dart | 540 +++++++++++++++++- .../realm_dart/test/results_test.realm.dart | 442 ++++++++++++++ 26 files changed, 1470 insertions(+), 115 deletions(-) create mode 100644 packages/realm_dart/test/results_test.realm.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d2a41f5..807b0d617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,9 +50,23 @@ ### Enhancements * Report the originating error that caused a client reset to occur. (Core 14.9.0) -* Allow the realm package, and code generated by realm_generator to be included when building +* Allow the realm package, and code generated by realm_generator to be included when building for web without breaking compilation. (Issue [#1374](https://github.com/realm/realm-dart/issues/1374), PR [#1713](https://github.com/realm/realm-dart/pull/1713)). This does **not** imply that realm works on web! +* Added support for specifying key paths when listening to notifications on a collection with the `changesFor([List? keyPaths])` method. Available on `RealmResults`, `RealmList`, `RealmSet`, and `RealmMap`. The key paths indicates what properties should raise a notification, if changed either directly or transitively. + ```dart + @RealmModel() + class _Person { + late String name; + late int age; + late List<_Person> friends; + } + + // .... + + // Only changes to "age" or "friends" of any of the elements of the collection, together with changes to the collection itself, will raise a notification + realm.all().changesFor(["age", "friends"]).listen( .... ) + ``` ### Fixed * `Realm.writeAsync` did not handle async callbacks (`Future Function()`) correctly. (Issue [#1667](https://github.com/realm/realm-dart/issues/1667)) diff --git a/packages/realm_dart/lib/src/handles/list_handle.dart b/packages/realm_dart/lib/src/handles/list_handle.dart index 4606fe831..3627e191f 100644 --- a/packages/realm_dart/lib/src/handles/list_handle.dart +++ b/packages/realm_dart/lib/src/handles/list_handle.dart @@ -28,5 +28,5 @@ abstract interface class ListHandle extends HandleBase { void removeAt(int index); ListHandle? resolveIn(RealmHandle frozenRealm); ObjectHandle setEmbeddedAt(int index); - NotificationTokenHandle subscribeForNotifications(NotificationsController controller); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey); } diff --git a/packages/realm_dart/lib/src/handles/map_handle.dart b/packages/realm_dart/lib/src/handles/map_handle.dart index 2ffa43557..af18f6ae3 100644 --- a/packages/realm_dart/lib/src/handles/map_handle.dart +++ b/packages/realm_dart/lib/src/handles/map_handle.dart @@ -27,5 +27,5 @@ abstract interface class MapHandle extends HandleBase { ResultsHandle query(String query, List args); bool remove(String key); MapHandle? resolveIn(RealmHandle frozenRealm); - NotificationTokenHandle subscribeForNotifications(NotificationsController controller); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey); } diff --git a/packages/realm_dart/lib/src/handles/native/list_handle.dart b/packages/realm_dart/lib/src/handles/native/list_handle.dart index 222710e17..7aae83d42 100644 --- a/packages/realm_dart/lib/src/handles/native/list_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/list_handle.dart @@ -147,16 +147,20 @@ class ListHandle extends CollectionHandleBase implements intf.ListHa } @override - 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, - ); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey) { + return using((Arena arena) { + final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey); + + return NotificationTokenHandle( + realmLib.realm_list_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + kpNative, + Pointer.fromFunction(collectionChangeCallback), + ), + root, + ); + }); } } diff --git a/packages/realm_dart/lib/src/handles/native/map_handle.dart b/packages/realm_dart/lib/src/handles/native/map_handle.dart index 0a3877dd2..044d4d3c5 100644 --- a/packages/realm_dart/lib/src/handles/native/map_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/map_handle.dart @@ -178,17 +178,21 @@ class MapHandle extends CollectionHandleBase implements intf.M } @override - 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, - ); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey) { + return using((Arena arena) { + final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey); + + return NotificationTokenHandle( + realmLib.realm_dictionary_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + kpNative, + Pointer.fromFunction(_mapChangeCallback), + ), + root, + ); + }); } } diff --git a/packages/realm_dart/lib/src/handles/native/object_handle.dart b/packages/realm_dart/lib/src/handles/native/object_handle.dart index ecc6e0eed..16f34bbab 100644 --- a/packages/realm_dart/lib/src/handles/native/object_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/object_handle.dart @@ -136,9 +136,9 @@ class ObjectHandle extends RootedHandleBase implements intf.Object } @override - NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List? keyPaths]) { + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey) { return using((arena) { - final kpNative = buildAndVerifyKeyPath(keyPaths); + final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey); return NotificationTokenHandle( realmLib.realm_object_add_notification_callback( pointer, @@ -152,26 +152,6 @@ class ObjectHandle extends RootedHandleBase implements intf.Object }); } - 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(); - }); - } - - void verifyKeyPath(List? keyPaths) => buildAndVerifyKeyPath(keyPaths); - @override // equals handled by HandleBase // ignore: hash_and_equals diff --git a/packages/realm_dart/lib/src/handles/native/realm_handle.dart b/packages/realm_dart/lib/src/handles/native/realm_handle.dart index 9a5831672..492882ab6 100644 --- a/packages/realm_dart/lib/src/handles/native/realm_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/realm_handle.dart @@ -42,9 +42,9 @@ class RealmHandle extends HandleBase implements intf.RealmHandle { if (!dir.existsSync()) { dir.createSync(recursive: true); } - + final configHandle = ConfigHandle.from(config); - + return RealmHandle(realmLib .realm_open(configHandle.pointer) // .raiseLastErrorIfNull()); @@ -478,6 +478,29 @@ class RealmHandle extends HandleBase implements intf.RealmHandle { } return result; } + + Pointer buildAndVerifyKeyPath(List? keyPaths, int? classKey) { + if (keyPaths == null || classKey == null) { + return nullptr; + } + + if (keyPaths.any((element) => element.isEmpty || element.trim().isEmpty)) { + throw RealmException("None of the key paths provided can be empty or consisting only of white spaces"); + } + + return using((arena) { + final length = keyPaths.length; + final keypathsNative = arena>(length); + for (int i = 0; i < length; i++) { + keypathsNative[i] = keyPaths[i].toCharPtr(arena); + } + + return realmLib.realm_create_key_path_array(pointer, classKey, length, keypathsNative).raiseLastErrorIfNull(); + }); + } + + @override + void verifyKeyPath(List? keyPaths, int? classKey) => buildAndVerifyKeyPath(keyPaths, classKey); } class CallbackTokenHandle extends RootedHandleBase implements intf.CallbackTokenHandle { diff --git a/packages/realm_dart/lib/src/handles/native/results_handle.dart b/packages/realm_dart/lib/src/handles/native/results_handle.dart index 43c32a06f..f7cf893a8 100644 --- a/packages/realm_dart/lib/src/handles/native/results_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/results_handle.dart @@ -114,16 +114,19 @@ class ResultsHandle extends RootedHandleBase implements intf.Resu } @override - 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, - ); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey) { + return using((Arena arena) { + final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey); + return NotificationTokenHandle( + realmLib.realm_results_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + kpNative, + Pointer.fromFunction(collectionChangeCallback), + ), + root, + ); + }); } } diff --git a/packages/realm_dart/lib/src/handles/native/set_handle.dart b/packages/realm_dart/lib/src/handles/native/set_handle.dart index 0a2de4a01..2027d9deb 100644 --- a/packages/realm_dart/lib/src/handles/native/set_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/set_handle.dart @@ -130,16 +130,19 @@ class SetHandle extends RootedHandleBase implements intf.SetHandle { } @override - 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, - ); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey) { + return using((Arena arena) { + final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey); + return NotificationTokenHandle( + realmLib.realm_set_add_notification_callback( + pointer, + controller.toPersistentHandle(), + realmLib.addresses.realm_dart_delete_persistent_handle, + kpNative, + Pointer.fromFunction(collectionChangeCallback), + ), + root, + ); + }); } } diff --git a/packages/realm_dart/lib/src/handles/object_handle.dart b/packages/realm_dart/lib/src/handles/object_handle.dart index a3ceaa60a..eef65d7f3 100644 --- a/packages/realm_dart/lib/src/handles/object_handle.dart +++ b/packages/realm_dart/lib/src/handles/object_handle.dart @@ -38,9 +38,7 @@ abstract interface class ObjectHandle extends HandleBase { ObjectHandle? resolveIn(RealmHandle frozenRealm); - NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List? keyPaths]); - - void verifyKeyPath(List? keyPaths); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey); @override // equals handled by HandleBase diff --git a/packages/realm_dart/lib/src/handles/realm_handle.dart b/packages/realm_dart/lib/src/handles/realm_handle.dart index a70d0817f..9aabcd81b 100644 --- a/packages/realm_dart/lib/src/handles/realm_handle.dart +++ b/packages/realm_dart/lib/src/handles/realm_handle.dart @@ -64,6 +64,8 @@ abstract interface class RealmHandle extends HandleBase { Map getPropertiesMetadata(int classKey, String? primaryKeyName); RealmObjectMetadata getObjectMetadata(SchemaObject schema); + + void verifyKeyPath(List keyPaths, int? classKey); } abstract class CallbackTokenHandle extends HandleBase {} diff --git a/packages/realm_dart/lib/src/handles/results_handle.dart b/packages/realm_dart/lib/src/handles/results_handle.dart index 69e05cc42..b5266906b 100644 --- a/packages/realm_dart/lib/src/handles/results_handle.dart +++ b/packages/realm_dart/lib/src/handles/results_handle.dart @@ -24,5 +24,5 @@ abstract interface class ResultsHandle extends HandleBase { ResultsHandle resolveIn(RealmHandle realmHandle); Object? elementAt(Realm realm, int index); - NotificationTokenHandle subscribeForNotifications(NotificationsController controller); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey); } diff --git a/packages/realm_dart/lib/src/handles/set_handle.dart b/packages/realm_dart/lib/src/handles/set_handle.dart index bbad63523..151148498 100644 --- a/packages/realm_dart/lib/src/handles/set_handle.dart +++ b/packages/realm_dart/lib/src/handles/set_handle.dart @@ -30,5 +30,5 @@ abstract interface class SetHandle extends HandleBase { SetHandle? resolveIn(RealmHandle frozenRealm); - NotificationTokenHandle subscribeForNotifications(NotificationsController controller); + NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List? keyPaths, int? classKey); } diff --git a/packages/realm_dart/lib/src/list.dart b/packages/realm_dart/lib/src/list.dart index 4f6ae75f2..d706b0194 100644 --- a/packages/realm_dart/lib/src/list.dart +++ b/packages/realm_dart/lib/src/list.dart @@ -195,11 +195,18 @@ class ManagedRealmList with RealmEntity, ListMixin impleme RealmResults asResults() => RealmResultsInternal.create(handle.asResults(), realm, metadata); @override - Stream> get changes { + Stream> get changes => _changesFor(null); + + Stream> _changesFor([List? keyPaths]) { if (isFrozen) { throw RealmStateError('List is frozen and cannot emit changes'); } - final controller = ListNotificationsController(asManaged()); + + if (keyPaths != null && _metadata == null) { + throw RealmStateError('Key paths can be used only with collections of Realm objects'); + } + + final controller = ListNotificationsController(asManaged(), keyPaths); return controller.createStream(); } } @@ -238,7 +245,7 @@ class UnmanagedRealmList extends collection.DelegatingList int get hashCode => _base.hashCode; } -// The query operations on lists, only work for list of objects (core restriction), +// Query operations and keypath filtering on lists only work for list of objects (core restriction), // so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmListOfObject on RealmList { /// Filters the list and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). @@ -250,6 +257,17 @@ extension RealmListOfObject on RealmList { final handle = asManaged().handle.query(query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } + + /// Allows listening for changes when the contents of this collection changes on one of the provided [keyPaths]. + /// If [keyPaths] is null, default notifications will be raised (same as [RealmList.change]). + /// If [keyPaths] is an empty list, only notifications related to the collection itself will be raised (such as adding or removing elements). + Stream> changesFor([List? keyPaths]) { + if (!isManaged) { + throw RealmStateError("Unmanaged lists don't support changes"); + } + + return (this as ManagedRealmList)._changesFor(keyPaths); + } } /// @nodoc @@ -323,12 +341,18 @@ class RealmListChanges extends RealmCollectionChanges { class ListNotificationsController extends NotificationsController { final ManagedRealmList list; late final StreamController> streamController; + List? keyPaths; - ListNotificationsController(this.list); + ListNotificationsController(this.list, [List? keyPaths]) { + if (keyPaths != null) { + this.keyPaths = keyPaths; + list.realm.handle.verifyKeyPath(keyPaths, list._metadata?.classKey); + } + } @override NotificationTokenHandle subscribe() { - return list.handle.subscribeForNotifications(this); + return list.handle.subscribeForNotifications(this, keyPaths, list._metadata?.classKey); } Stream> createStream() { diff --git a/packages/realm_dart/lib/src/map.dart b/packages/realm_dart/lib/src/map.dart index ba7e70df2..d7dedfbcf 100644 --- a/packages/realm_dart/lib/src/map.dart +++ b/packages/realm_dart/lib/src/map.dart @@ -144,11 +144,18 @@ class ManagedRealmMap with RealmEntity, MapMixin i } @override - Stream> get changes { + Stream> get changes => _changesFor(null); + + Stream> _changesFor([List? keyPaths]) { if (isFrozen) { - throw RealmStateError('Map is frozen and cannot emit changes'); + throw RealmStateError('List is frozen and cannot emit changes'); + } + + if (keyPaths != null && _metadata == null) { + throw RealmStateError('Key paths can be used only with collections of Realm objects'); } - final controller = MapNotificationsController(asManaged()); + + final controller = MapNotificationsController(asManaged(), keyPaths); return controller.createStream(); } @@ -210,7 +217,7 @@ class RealmMapChanges { bool get isCollectionDeleted => _changes.isDeleted; } -// The query operations on maps only work for maps of objects (core restriction), +// Query operations and keypath filtering on maps only work for maps of objects (core restriction), // so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmMapOfObject on RealmMap { /// Filters the map values and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). @@ -222,6 +229,17 @@ extension RealmMapOfObject on RealmMap { final handle = asManaged().handle.query(query, arguments); return RealmResultsInternal.create(handle, realm, metadata); } + + /// Allows listening for changes when the contents of this collection changes on one of the provided [keyPaths]. + /// If [keyPaths] is null, default notifications will be raised (same as [RealmMap.change]). + /// If [keyPaths] is an empty list, only notifications related to the collection itself will be raised (such as adding or removing elements). + Stream> changesFor([List? keyPaths]) { + if (!isManaged) { + throw RealmStateError("Unmanaged maps don't support changes"); + } + + return (this as ManagedRealmMap)._changesFor(keyPaths); + } } /// @nodoc @@ -273,12 +291,18 @@ extension RealmMapInternal on RealmMap { class MapNotificationsController extends NotificationsController { final ManagedRealmMap map; late final StreamController> streamController; + List? keyPaths; - MapNotificationsController(this.map); + MapNotificationsController(this.map, [List? keyPaths]) { + if (keyPaths != null) { + this.keyPaths = keyPaths; + map.realm.handle.verifyKeyPath(keyPaths, map._metadata?.classKey); + } + } @override NotificationTokenHandle subscribe() { - return map.handle.subscribeForNotifications(this); + return map.handle.subscribeForNotifications(this, keyPaths, map._metadata?.classKey); } Stream> createStream() { diff --git a/packages/realm_dart/lib/src/realm_object.dart b/packages/realm_dart/lib/src/realm_object.dart index 9e8fa98ce..fc17fe799 100644 --- a/packages/realm_dart/lib/src/realm_object.dart +++ b/packages/realm_dart/lib/src/realm_object.dart @@ -525,7 +525,7 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker { throw RealmStateError('Object is frozen and cannot emit changes.'); } - final controller = RealmObjectNotificationsController(object, keyPaths); + final controller = RealmObjectNotificationsController(object, keyPaths, (object.accessor as RealmCoreAccessor).metadata.classKey); return controller.createStream(); } @@ -750,22 +750,21 @@ class RealmObjectNotificationsController extends Noti T realmObject; late final StreamController> streamController; List? keyPaths; + int? classKey; - RealmObjectNotificationsController(this.realmObject, [List? keyPaths]) { + RealmObjectNotificationsController(this.realmObject, List? keyPaths, int? classKey) { if (keyPaths != null) { this.keyPaths = keyPaths; + this.classKey = classKey; - 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.verifyKeyPath(keyPaths); + realmObject.realm.handle.verifyKeyPath(keyPaths, classKey); } } @override NotificationTokenHandle subscribe() { - return realmObject.handle.subscribeForNotifications(this, keyPaths); + return realmObject.handle.subscribeForNotifications(this, keyPaths, classKey); } Stream> createStream() { diff --git a/packages/realm_dart/lib/src/results.dart b/packages/realm_dart/lib/src/results.dart index ea054067e..b878ebbcd 100644 --- a/packages/realm_dart/lib/src/results.dart +++ b/packages/realm_dart/lib/src/results.dart @@ -159,17 +159,19 @@ class RealmResults extends Iterable with RealmEntity { } /// Allows listening for changes when the contents of this collection changes. - Stream> get changes { + Stream> get changes => _changesFor(null); + + Stream> _changesFor([List? keyPaths]) { if (isFrozen) { throw RealmStateError('Results are frozen and cannot emit changes'); } - final controller = ResultsNotificationsController(this); + final controller = ResultsNotificationsController(this, keyPaths); return controller.createStream(); } } -// The query operations on results only work for results of objects (core restriction), +// Query operations and keypath filtering on results only work for results of objects (core restriction), // so we add it as an extension methods to allow the compiler to prevent misuse. extension RealmResultsOfObject on RealmResults { /// Returns a new [RealmResults] filtered according to the provided query. @@ -179,6 +181,11 @@ extension RealmResultsOfObject on RealmResults { final handle = this.handle.queryResults(query, args); return RealmResultsInternal.create(handle, realm, _metadata); } + + /// Allows listening for changes when the contents of this collection changes on one of the provided [keyPaths]. + /// If [keyPaths] is null, default notifications will be raised (same as [RealmResults.change]). + /// If [keyPaths] is an empty list, only notifications related to the collection itself will be raised (such as adding or removing elements). + Stream> changesFor([List? keyPaths]) => _changesFor(keyPaths); } class _SubscribedRealmResult extends RealmResults { @@ -306,12 +313,18 @@ class RealmResultsChanges extends RealmCollectionChanges { class ResultsNotificationsController extends NotificationsController { final RealmResults results; late final StreamController> streamController; + List? keyPaths; - ResultsNotificationsController(this.results); + ResultsNotificationsController(this.results, [List? keyPaths]) { + if (keyPaths != null) { + this.keyPaths = keyPaths; + results.realm.handle.verifyKeyPath(keyPaths, results._metadata?.classKey); + } + } @override NotificationTokenHandle subscribe() { - return results.handle.subscribeForNotifications(this); + return results.handle.subscribeForNotifications(this, keyPaths, results._metadata?.classKey); } Stream> createStream() { diff --git a/packages/realm_dart/lib/src/set.dart b/packages/realm_dart/lib/src/set.dart index 613350ee3..d53d271fb 100644 --- a/packages/realm_dart/lib/src/set.dart +++ b/packages/realm_dart/lib/src/set.dart @@ -104,17 +104,17 @@ class UnmanagedRealmSet extends collection.DelegatingSet w @override // ignore: unused_element - RealmObjectMetadata? get _metadata => throw RealmError("Unmanaged RealmSets don't have metadata associated with them."); + RealmObjectMetadata? get _metadata => throw RealmError("Unmanaged sets don't have metadata associated with them."); @override // ignore: unused_element - set _metadata(RealmObjectMetadata? _) => throw RealmError("Unmanaged RealmSets don't have metadata associated with them."); + set _metadata(RealmObjectMetadata? _) => throw RealmError("Unmanaged sets don't have metadata associated with them."); @override bool get isValid => true; @override - Stream> get changes => throw RealmStateError("Unmanaged RealmSets don't support changes"); + Stream> get changes => throw RealmStateError("Unmanaged sets don't support changes"); @override RealmResults asResults() => throw RealmStateError("Unmanaged sets can't be converted to results"); @@ -222,11 +222,18 @@ class ManagedRealmSet with RealmEntity, SetMixin implement int get length => handle.size; @override - Stream> get changes { + Stream> get changes => _changesFor(null); + + Stream> _changesFor([List? keyPaths]) { if (isFrozen) { throw RealmStateError('Set is frozen and cannot emit changes'); } - final controller = RealmSetNotificationsController(asManaged()); + + if (keyPaths != null && _metadata == null) { + throw RealmStateError('Key paths can be used only with collections of Realm objects'); + } + + final controller = RealmSetNotificationsController(asManaged(), keyPaths); return controller.createStream(); } @@ -335,12 +342,18 @@ class RealmSetChanges extends RealmCollectionChanges { class RealmSetNotificationsController extends NotificationsController { final ManagedRealmSet set; late final StreamController> streamController; + List? keyPaths; - RealmSetNotificationsController(this.set); + RealmSetNotificationsController(this.set, [List? keyPaths]) { + if (keyPaths != null) { + this.keyPaths = keyPaths; + set.realm.handle.verifyKeyPath(keyPaths, set._metadata?.classKey); + } + } @override NotificationTokenHandle subscribe() { - return set.handle.subscribeForNotifications(this); + return set.handle.subscribeForNotifications(this, keyPaths, set._metadata?.classKey); } Stream> createStream() { @@ -364,7 +377,7 @@ class RealmSetNotificationsController extends NotificationsCo } } -// The query operations on sets only work for sets of objects (core restriction), +// Query operations and keypath filtering on sets only work for sets of objects (core restriction), // so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmSetOfObject on RealmSet { /// Filters the set and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). @@ -376,6 +389,17 @@ extension RealmSetOfObject on RealmSet { final handle = asManaged().handle.query(query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } + + /// Allows listening for changes when the contents of this collection changes on one of the provided [keyPaths]. + /// If [keyPaths] is null, default notifications will be raised (same as [RealmSet.change]). + /// If [keyPaths] is an empty list, only notifications related to the collection itself will be raised (such as adding or removing elements). + Stream> changesFor([List? keyPaths]) { + if (!isManaged) { + throw RealmStateError("Unmanaged sets don't support changes"); + } + + return (this as ManagedRealmSet)._changesFor(keyPaths); + } } extension on RealmSet { diff --git a/packages/realm_dart/test/list_test.dart b/packages/realm_dart/test/list_test.dart index e6634e2c2..44d64afdb 100644 --- a/packages/realm_dart/test/list_test.dart +++ b/packages/realm_dart/test/list_test.dart @@ -515,6 +515,85 @@ void main() { expect(() => freezeList(team.players), throws("Unmanaged lists can't be frozen")); }); + test('UnmanagedList.changes throws', () { + final team = Team('team'); + + expect(() => team.players.changes, throws("Unmanaged lists don't support changes")); + expect(() => team.players.changesFor(["test"]), throws("Unmanaged lists don't support changes")); + }); + + test('RealmList.changesFor works with keypaths', () async { + var config = Configuration.local([School.schema, Student.schema]); + var realm = getRealm(config); + + final students = realm.write(() { + return realm.add(School("Liceo Pizzi")); + }).students; + + final externalChanges = >[]; + final subscription = students.changesFor(["yearOfBirth"]).listen((changes) { + externalChanges.add(changes); + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + final firstNotification = externalChanges[0]; + expect(firstNotification.inserted.isEmpty, true); + expect(firstNotification.deleted.isEmpty, true); + expect(firstNotification.modified.isEmpty, true); + expect(firstNotification.isCleared, false); + externalChanges.clear(); + + final student = Student(222); + realm.write(() { + students.add(student); + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + var notification = externalChanges[0]; + expect(notification.inserted, [0]); + expect(notification.deleted.isEmpty, true); + expect(notification.modified.isEmpty, true); + expect(notification.isCleared, false); + externalChanges.clear(); + + // We expect notifications because "yearOfBirth" is in the keypaths + realm.write(() { + student.yearOfBirth = 1999; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + notification = externalChanges[0]; + expect(notification.inserted.isEmpty, true); + expect(notification.deleted.isEmpty, true); + expect(notification.modified, [0]); + expect(notification.isCleared, false); + externalChanges.clear(); + + // We don't expect notifications because "name" is not in the keypaths + realm.write(() { + student.name = "Luis"; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 0); + + subscription.cancel(); + + // No more notifications after cancelling subscription + realm.write(() { + student.yearOfBirth = 1299; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 0); + }); + test('List.freeze when frozen returns same object', () { final config = Configuration.local([Team.schema, Person.schema]); final realm = getRealm(config); diff --git a/packages/realm_dart/test/realm_map_test.dart b/packages/realm_dart/test/realm_map_test.dart index 9056e370a..b3be05a69 100644 --- a/packages/realm_dart/test/realm_map_test.dart +++ b/packages/realm_dart/test/realm_map_test.dart @@ -17,6 +17,7 @@ class _Car { @PrimaryKey() late String make; late String? color; + late int? year; } @RealmModel(ObjectType.embeddedObject) @@ -950,6 +951,83 @@ void main() { }); }); + group('keypath filtering', () { + test('on unmanaged dictionary throws', () { + final map = TestRealmMaps(0).objectsMap; + + expect(() => map.changes, throws("Unmanaged maps don't support changes")); + expect(() => map.changesFor(["test"]), throws("Unmanaged maps don't support changes")); + }); + + test('works as expected', () async { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + final externalChanges = >[]; + final subscription = map.changesFor(["year"]).listen((changes) { + externalChanges.add(changes); + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + final firstNotification = externalChanges[0]; + expect(firstNotification.inserted.isEmpty, true); + expect(firstNotification.deleted.isEmpty, true); + expect(firstNotification.modified.isEmpty, true); + expect(firstNotification.isCleared, false); + externalChanges.clear(); + + realm.write(() { + map["test1"] = Car("BMW"); + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + var notification = externalChanges[0]; + expect(notification.inserted, ["test1"]); + expect(notification.deleted.isEmpty, true); + expect(notification.modified.isEmpty, true); + expect(notification.isCleared, false); + externalChanges.clear(); + + // We expect notifications because "year" is in the keypaths + realm.write(() { + map["test1"]?.year = 1999; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + notification = externalChanges[0]; + expect(notification.inserted.isEmpty, true); + expect(notification.deleted.isEmpty, true); + expect(notification.modified, ["test1"]); + expect(notification.isCleared, false); + externalChanges.clear(); + + // We don't expect notifications because "color" is not in the keypaths + realm.write(() { + map["test1"]?.color = "blue"; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 0); + + subscription.cancel(); + + // No more notifications after cancelling subscription + realm.write(() { + map["test1"]?.year = 22222; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 0); + }); + }); + runTests(boolTestValues, (e) => e.boolMap); runTests(nullableBoolTestValues, (e) => e.nullableBoolMap); diff --git a/packages/realm_dart/test/realm_map_test.realm.dart b/packages/realm_dart/test/realm_map_test.realm.dart index 866fe01ca..928119434 100644 --- a/packages/realm_dart/test/realm_map_test.realm.dart +++ b/packages/realm_dart/test/realm_map_test.realm.dart @@ -11,9 +11,11 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Car( String make, { String? color, + int? year, }) { RealmObjectBase.set(this, 'make', make); RealmObjectBase.set(this, 'color', color); + RealmObjectBase.set(this, 'year', year); } Car._(); @@ -28,6 +30,11 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { @override set color(String? value) => RealmObjectBase.set(this, 'color', value); + @override + int? get year => RealmObjectBase.get(this, 'year') as int?; + @override + set year(int? value) => RealmObjectBase.set(this, 'year', value); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -43,6 +50,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { return { 'make': make.toEJson(), 'color': color.toEJson(), + 'year': year.toEJson(), }; } @@ -52,10 +60,12 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { { 'make': EJsonValue make, 'color': EJsonValue color, + 'year': EJsonValue year, } => Car( fromEJson(make), color: fromEJson(color), + year: fromEJson(year), ), _ => raiseInvalidEJson(ejson), }; @@ -67,6 +77,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { return SchemaObject(ObjectType.realmObject, Car, 'Car', [ SchemaProperty('make', RealmPropertyType.string, primaryKey: true), SchemaProperty('color', RealmPropertyType.string, optional: true), + SchemaProperty('year', RealmPropertyType.int, optional: true), ]); }(); diff --git a/packages/realm_dart/test/realm_object_test.dart b/packages/realm_dart/test/realm_object_test.dart index 9f07050a9..60dfd0566 100644 --- a/packages/realm_dart/test/realm_object_test.dart +++ b/packages/realm_dart/test/realm_object_test.dart @@ -180,7 +180,7 @@ void main() { subscription.cancel(); }); - test('empty keypath', () async { + test('empty or whitespace keypath', () async { var config = Configuration.local([Dog.schema, Person.schema]); var realm = getRealm(config); @@ -192,11 +192,11 @@ void main() { expect(() { dog.changesFor([""]); - }, throws("It is not allowed to have empty key paths.")); + }, throws("None of the key paths provided can be empty or consisting only of white spaces")); expect(() { - dog.changesFor(["age", ""]); - }, throws("It is not allowed to have empty key paths.")); + dog.changesFor(["age", " "]); + }, throws("None of the key paths provided can be empty or consisting only of white spaces")); }); test('unknown keypath', () async { @@ -522,7 +522,8 @@ void main() { subscription.cancel(); }); - test('empty list gives default subscriptions', () async { + //TODO Remove skip when https://github.com/realm/realm-core/issues/7805 is solved + test('empty list gives no subscriptions', () async { var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); var realm = getRealm(config); @@ -546,7 +547,7 @@ void main() { tno.mapLinks["test"] = TestNotificationObject(); }); - await verifyNotifications(tno, externalChanges, ["listLinks", "setLinks", "mapLinks", "stringProperty", "intProperty", "link"]); + await verifyNotifications(tno, externalChanges, null); realm.write(() { tno.link?.stringProperty = "test"; @@ -558,7 +559,7 @@ void main() { await verifyNotifications(tno, externalChanges, null); subscription.cancel(); - }); + }, skip: true); test('wildcard', () async { var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema]); diff --git a/packages/realm_dart/test/realm_set_test.dart b/packages/realm_dart/test/realm_set_test.dart index ffb420814..f226cf425 100644 --- a/packages/realm_dart/test/realm_set_test.dart +++ b/packages/realm_dart/test/realm_set_test.dart @@ -56,6 +56,7 @@ class _Car { @PrimaryKey() late String make; late String? color; + late int? year; } @RealmModel() @@ -761,6 +762,13 @@ void main() { expect(() => set.boolSet.freeze(), throws("Unmanaged sets can't be frozen")); }); + test('UnmanagedSet.changes throws', () { + final set = TestRealmSets(1); + + expect(() => set.objectsSet.changes, throws("Unmanaged sets don't support changes")); + expect(() => set.objectsSet.changesFor(["test"]), throws("Unmanaged sets don't support changes")); + }); + test('RealmSet.changes - await for with yield ', () async { var config = Configuration.local([TestRealmSets.schema, Car.schema]); var realm = getRealm(config); @@ -794,6 +802,78 @@ void main() { } }); + test('RealmSet.changesFor works with keypaths', () async { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final cars = realm.write(() { + return realm.add(TestRealmSets(1)); + }).objectsSet; + + final externalChanges = >[]; + final subscription = cars.changesFor(["year"]).listen((changes) { + externalChanges.add(changes); + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + final firstNotification = externalChanges[0]; + expect(firstNotification.inserted.isEmpty, true); + expect(firstNotification.deleted.isEmpty, true); + expect(firstNotification.modified.isEmpty, true); + expect(firstNotification.isCleared, false); + externalChanges.clear(); + + final bmw = Car("BMW"); + realm.write(() { + cars.add(bmw); + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + var notification = externalChanges[0]; + expect(notification.inserted, [0]); + expect(notification.deleted.isEmpty, true); + expect(notification.modified.isEmpty, true); + expect(notification.isCleared, false); + externalChanges.clear(); + + // We expect notifications because "year" is in the keypaths + realm.write(() { + bmw.year = 1999; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 1); + + notification = externalChanges[0]; + expect(notification.inserted.isEmpty, true); + expect(notification.deleted.isEmpty, true); + expect(notification.modified, [0]); + expect(notification.isCleared, false); + externalChanges.clear(); + + // We don't expect notifications because "color" is not in the keypaths + realm.write(() { + bmw.color = "blue"; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 0); + + subscription.cancel(); + + // No more notifications after cancelling subscription + realm.write(() { + bmw.year = 222; + }); + + await Future.delayed(const Duration(milliseconds: 20)); + expect(externalChanges.length, 0); + }); + test('Query on RealmSet with IN-operator', () { var config = Configuration.local([TestRealmSets.schema, Car.schema]); var realm = getRealm(config); diff --git a/packages/realm_dart/test/realm_set_test.realm.dart b/packages/realm_dart/test/realm_set_test.realm.dart index 0bf0ccd68..238be7250 100644 --- a/packages/realm_dart/test/realm_set_test.realm.dart +++ b/packages/realm_dart/test/realm_set_test.realm.dart @@ -11,9 +11,11 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Car( String make, { String? color, + int? year, }) { RealmObjectBase.set(this, 'make', make); RealmObjectBase.set(this, 'color', color); + RealmObjectBase.set(this, 'year', year); } Car._(); @@ -28,6 +30,11 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { @override set color(String? value) => RealmObjectBase.set(this, 'color', value); + @override + int? get year => RealmObjectBase.get(this, 'year') as int?; + @override + set year(int? value) => RealmObjectBase.set(this, 'year', value); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -43,6 +50,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { return { 'make': make.toEJson(), 'color': color.toEJson(), + 'year': year.toEJson(), }; } @@ -52,10 +60,12 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { { 'make': EJsonValue make, 'color': EJsonValue color, + 'year': EJsonValue year, } => Car( fromEJson(make), color: fromEJson(color), + year: fromEJson(year), ), _ => raiseInvalidEJson(ejson), }; @@ -67,6 +77,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { return SchemaObject(ObjectType.realmObject, Car, 'Car', [ SchemaProperty('make', RealmPropertyType.string, primaryKey: true), SchemaProperty('color', RealmPropertyType.string, optional: true), + SchemaProperty('year', RealmPropertyType.int, optional: true), ]); }(); diff --git a/packages/realm_dart/test/results_test.dart b/packages/realm_dart/test/results_test.dart index 8dbea271f..add587f33 100644 --- a/packages/realm_dart/test/results_test.dart +++ b/packages/realm_dart/test/results_test.dart @@ -4,15 +4,553 @@ // ignore_for_file: unused_local_variable import 'dart:typed_data'; - import 'package:test/test.dart' hide test, throws; import 'package:realm_dart/realm.dart'; import 'test.dart'; +part 'results_test.realm.dart'; + +@RealmModel() +class _TestNotificationObject { + late String? stringProperty; + + late int? intProperty; + + @MapTo("_remappedIntProperty") + late int? remappedIntProperty; + + late _TestNotificationObject? link; + + late List<_TestNotificationObject> list; + + late Set<_TestNotificationObject> set; + + late Map map; + + late _TestNotificationDifferentType? linkDifferentType; + + late List<_TestNotificationDifferentType> listDifferentType; + + late Set<_TestNotificationDifferentType> setDifferentType; + + late Map mapDifferentType; + + late _TestNotificationEmbeddedObject? embedded; + + @Backlink(#link) + late Iterable<_TestNotificationObject> backlink; +} + +@RealmModel(ObjectType.embeddedObject) +class _TestNotificationEmbeddedObject { + late String? stringProperty; + + late int? intProperty; +} + +@RealmModel() +class _TestNotificationDifferentType { + late String? stringProperty; + + late int? intProperty; + + late _TestNotificationDifferentType? link; +} + void main() { setupTests(); + group('Results notifications with keypaths', () { + Future verifyNotifications(List> changeList, + {List? expectedInserted, + List? expectedModified, + List? expectedDeleted, + List? expectedMoved, + bool expectedIsCleared = false, + bool expectedNotifications = true}) async { + await Future.delayed(const Duration(milliseconds: 20)); + + if (!expectedNotifications) { + expect(changeList.length, 0); + return; + } + + expect(changeList.length, 1); + final changes = changeList[0]; + + expect(changes.inserted, expectedInserted ?? []); + expect(changes.modified, expectedModified ?? []); + expect(changes.deleted, expectedDeleted ?? []); + expect(changes.moved, expectedMoved ?? []); + expect(changes.isCleared, expectedIsCleared); + + changeList.clear(); + } + + bool isFirstNotification(RealmResultsChanges changes) { + return changes.inserted.isEmpty && + changes.modified.isEmpty && + changes.deleted.isEmpty && + changes.newModified.isEmpty && + changes.moved.isEmpty && + !changes.isCleared; + } + + test('throws on invalid keypath', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + expect(() { + realm.all().changesFor(["stringProperty", "inv"]).listen((changes) {}); + }, throws("Property 'inv' in KeyPath 'inv' is not a valid property in TestNotificationObject")); + + expect(() { + realm.all().changesFor(["stringProperty", "link.inv2"]).listen((changes) {}); + }, throws("Property 'inv2' in KeyPath 'link.inv2' is not a valid property in TestNotificationObject")); + }); + + test('throws on empty or whitespace keypath', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + expect(() { + realm.all().changesFor(["stringProperty", ""]).listen((changes) {}); + }, throws("None of the key paths provided can be empty or consisting only of white spaces")); + + expect(() { + realm.all().changesFor(["stringProperty", " "]).listen((changes) {}); + }, throws("None of the key paths provided can be empty or consisting only of white spaces")); + }); + + test('null keypaths behaves like default', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(null).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.stringProperty = "testString"; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.embedded = TestNotificationEmbeddedObject(); + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.stringProperty = "test"; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + subscription.cancel(); + }); + + //TODO Remove skip when of https://github.com/realm/realm-core/issues/7805 is solved + test('empty keypath raises only shallow notifications', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor([]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.stringProperty = "testString"; + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }, skip: true); + + test('multiple keypaths', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["stringProperty", "intProperty"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.stringProperty = "testString"; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.remappedIntProperty = 25; + tno.embedded = TestNotificationEmbeddedObject(); + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('scalar top level property', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["stringProperty"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.stringProperty = "testString"; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.intProperty = 23; + tno.remappedIntProperty = 25; + tno.embedded = TestNotificationEmbeddedObject(); + tno.linkDifferentType = TestNotificationDifferentType(); + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('nested property on link', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["linkDifferentType.intProperty"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.stringProperty = "test"; + + tno.intProperty = 23; + tno.embedded = TestNotificationEmbeddedObject(); + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('nested property on collection', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["listDifferentType.intProperty"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.listDifferentType[0].intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.listDifferentType[0].stringProperty = "23"; + + tno.intProperty = 23; + tno.embedded = TestNotificationEmbeddedObject(); + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('collection top level property', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["listDifferentType"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.listDifferentType[0].stringProperty = "34"; + tno.listDifferentType[0].intProperty = 23; + tno.intProperty = 23; + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('wildcard top level', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["*"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.mapDifferentType["test"] = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + // No notifications deeper than one level + realm.write(() { + tno.listDifferentType[0].stringProperty = "34"; + tno.mapDifferentType["test"]?.intProperty = 23; + tno.linkDifferentType?.intProperty = 21; + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('wildcard nested', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["*.*"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.mapDifferentType["test"] = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.listDifferentType[0].stringProperty = "34"; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.intProperty = 21; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.link = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + // No notifications deeper than two levels + realm.write(() { + tno.linkDifferentType?.link?.intProperty = 24; + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('wildcard nested on top level property', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["linkDifferentType.*"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.link = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + // No notifications deeper than one level on linkDifferentType + // No notifications for other keypaths + realm.write(() { + tno.linkDifferentType?.link?.intProperty = 23; + + tno.listDifferentType.add(TestNotificationDifferentType()); + tno.intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + + test('nested property on wildcard', () async { + var config = Configuration.local([TestNotificationObject.schema, TestNotificationEmbeddedObject.schema, TestNotificationDifferentType.schema]); + var realm = getRealm(config); + + final externalChanges = >[]; + final subscription = realm.all().changesFor(["*.intProperty"]).listen((changes) { + if (!isFirstNotification(changes)) externalChanges.add(changes); + }); + + final tno = TestNotificationObject(); + realm.write(() { + realm.add(tno); + }); + await verifyNotifications(externalChanges, expectedInserted: [0]); + + realm.write(() { + tno.linkDifferentType = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.linkDifferentType?.intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.listDifferentType.add(TestNotificationDifferentType()); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.listDifferentType[0].intProperty = 23; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.mapDifferentType["test"] = TestNotificationDifferentType(); + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + realm.write(() { + tno.mapDifferentType["test"]?.intProperty = 22; + }); + await verifyNotifications(externalChanges, expectedModified: [0]); + + // No notifications not on keypath + realm.write(() { + tno.linkDifferentType?.link?.stringProperty = "23"; + tno.listDifferentType[0].stringProperty = "22"; + tno.mapDifferentType["test"]?.stringProperty = "22"; + }); + await verifyNotifications(externalChanges, expectedNotifications: false); + + subscription.cancel(); + }); + }); + test('Results all should not return null', () { var config = Configuration.local([Car.schema]); var realm = getRealm(config); diff --git a/packages/realm_dart/test/results_test.realm.dart b/packages/realm_dart/test/results_test.realm.dart new file mode 100644 index 000000000..7362763db --- /dev/null +++ b/packages/realm_dart/test/results_test.realm.dart @@ -0,0 +1,442 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'results_test.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class TestNotificationObject extends _TestNotificationObject + with RealmEntity, RealmObjectBase, RealmObject { + TestNotificationObject({ + String? stringProperty, + int? intProperty, + int? remappedIntProperty, + TestNotificationObject? link, + Iterable list = const [], + Set set = const {}, + Map map = const {}, + TestNotificationDifferentType? linkDifferentType, + Iterable listDifferentType = const [], + Set setDifferentType = const {}, + Map mapDifferentType = const {}, + TestNotificationEmbeddedObject? embedded, + }) { + RealmObjectBase.set(this, 'stringProperty', stringProperty); + RealmObjectBase.set(this, 'intProperty', intProperty); + RealmObjectBase.set(this, '_remappedIntProperty', remappedIntProperty); + RealmObjectBase.set(this, 'link', link); + RealmObjectBase.set>( + this, 'list', RealmList(list)); + RealmObjectBase.set>( + this, 'set', RealmSet(set)); + RealmObjectBase.set>( + this, 'map', RealmMap(map)); + RealmObjectBase.set(this, 'linkDifferentType', linkDifferentType); + RealmObjectBase.set>( + this, + 'listDifferentType', + RealmList(listDifferentType)); + RealmObjectBase.set>( + this, + 'setDifferentType', + RealmSet(setDifferentType)); + RealmObjectBase.set>( + this, + 'mapDifferentType', + RealmMap(mapDifferentType)); + RealmObjectBase.set(this, 'embedded', embedded); + } + + 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 + RealmList get list => + RealmObjectBase.get(this, 'list') + as RealmList; + @override + set list(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get set => + RealmObjectBase.get(this, 'set') + as RealmSet; + @override + set set(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get map => + RealmObjectBase.get(this, 'map') + as RealmMap; + @override + set map(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + TestNotificationDifferentType? get linkDifferentType => + RealmObjectBase.get( + this, 'linkDifferentType') as TestNotificationDifferentType?; + @override + set linkDifferentType(covariant TestNotificationDifferentType? value) => + RealmObjectBase.set(this, 'linkDifferentType', value); + + @override + RealmList get listDifferentType => + RealmObjectBase.get( + this, 'listDifferentType') + as RealmList; + @override + set listDifferentType( + covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get setDifferentType => + RealmObjectBase.get( + this, 'setDifferentType') as RealmSet; + @override + set setDifferentType( + covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get mapDifferentType => + RealmObjectBase.get( + this, 'mapDifferentType') as RealmMap; + @override + set mapDifferentType( + covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + TestNotificationEmbeddedObject? get embedded => + RealmObjectBase.get(this, 'embedded') + as TestNotificationEmbeddedObject?; + @override + set embedded(covariant TestNotificationEmbeddedObject? value) => + RealmObjectBase.set(this, 'embedded', value); + + @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(), + 'list': list.toEJson(), + 'set': set.toEJson(), + 'map': map.toEJson(), + 'linkDifferentType': linkDifferentType.toEJson(), + 'listDifferentType': listDifferentType.toEJson(), + 'setDifferentType': setDifferentType.toEJson(), + 'mapDifferentType': mapDifferentType.toEJson(), + 'embedded': embedded.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, + 'list': EJsonValue list, + 'set': EJsonValue set, + 'map': EJsonValue map, + 'linkDifferentType': EJsonValue linkDifferentType, + 'listDifferentType': EJsonValue listDifferentType, + 'setDifferentType': EJsonValue setDifferentType, + 'mapDifferentType': EJsonValue mapDifferentType, + 'embedded': EJsonValue embedded, + } => + TestNotificationObject( + stringProperty: fromEJson(stringProperty), + intProperty: fromEJson(intProperty), + remappedIntProperty: fromEJson(remappedIntProperty), + link: fromEJson(link), + list: fromEJson(list), + set: fromEJson(set), + map: fromEJson(map), + linkDifferentType: fromEJson(linkDifferentType), + listDifferentType: fromEJson(listDifferentType), + setDifferentType: fromEJson(setDifferentType), + mapDifferentType: fromEJson(mapDifferentType), + embedded: fromEJson(embedded), + ), + _ => 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('list', RealmPropertyType.object, + linkTarget: 'TestNotificationObject', + collectionType: RealmCollectionType.list), + SchemaProperty('set', RealmPropertyType.object, + linkTarget: 'TestNotificationObject', + collectionType: RealmCollectionType.set), + SchemaProperty('map', RealmPropertyType.object, + optional: true, + linkTarget: 'TestNotificationObject', + collectionType: RealmCollectionType.map), + SchemaProperty('linkDifferentType', RealmPropertyType.object, + optional: true, linkTarget: 'TestNotificationDifferentType'), + SchemaProperty('listDifferentType', RealmPropertyType.object, + linkTarget: 'TestNotificationDifferentType', + collectionType: RealmCollectionType.list), + SchemaProperty('setDifferentType', RealmPropertyType.object, + linkTarget: 'TestNotificationDifferentType', + collectionType: RealmCollectionType.set), + SchemaProperty('mapDifferentType', RealmPropertyType.object, + optional: true, + linkTarget: 'TestNotificationDifferentType', + collectionType: RealmCollectionType.map), + SchemaProperty('embedded', RealmPropertyType.object, + optional: true, linkTarget: 'TestNotificationEmbeddedObject'), + 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; +} + +class TestNotificationDifferentType extends _TestNotificationDifferentType + with RealmEntity, RealmObjectBase, RealmObject { + TestNotificationDifferentType({ + String? stringProperty, + int? intProperty, + TestNotificationDifferentType? link, + }) { + RealmObjectBase.set(this, 'stringProperty', stringProperty); + RealmObjectBase.set(this, 'intProperty', intProperty); + RealmObjectBase.set(this, 'link', link); + } + + TestNotificationDifferentType._(); + + @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 + TestNotificationDifferentType? get link => + RealmObjectBase.get(this, 'link') + as TestNotificationDifferentType?; + @override + set link(covariant TestNotificationDifferentType? value) => + RealmObjectBase.set(this, 'link', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor( + this, keyPaths); + + @override + TestNotificationDifferentType freeze() => + RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + 'stringProperty': stringProperty.toEJson(), + 'intProperty': intProperty.toEJson(), + 'link': link.toEJson(), + }; + } + + static EJsonValue _toEJson(TestNotificationDifferentType value) => + value.toEJson(); + static TestNotificationDifferentType _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'stringProperty': EJsonValue stringProperty, + 'intProperty': EJsonValue intProperty, + 'link': EJsonValue link, + } => + TestNotificationDifferentType( + stringProperty: fromEJson(stringProperty), + intProperty: fromEJson(intProperty), + link: fromEJson(link), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(TestNotificationDifferentType._); + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, TestNotificationDifferentType, + 'TestNotificationDifferentType', [ + SchemaProperty('stringProperty', RealmPropertyType.string, + optional: true), + SchemaProperty('intProperty', RealmPropertyType.int, optional: true), + SchemaProperty('link', RealmPropertyType.object, + optional: true, linkTarget: 'TestNotificationDifferentType'), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +}