From c465b7285e7979bff6829acfbd55b25ffbbf3dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 9 Sep 2022 14:56:34 +0200 Subject: [PATCH 1/8] Allow results of primitives. Moves results of objects specific functionality to extension method to avoid misuse. --- .vscode/settings.json | 1 + lib/src/list.dart | 21 +++++---- lib/src/native/realm_core.dart | 15 +++++- lib/src/realm_class.dart | 8 ++-- lib/src/results.dart | 83 ++++++++++++++++++++++------------ test/results_test.dart | 12 +++++ test/subscription_test.dart | 14 +++--- 7 files changed, 106 insertions(+), 48 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a4900236..7fda08abe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "dart.lineLength": 160, "cSpell.words": [ "apikeys", + "BEGINSWITH", "bson", "deallocated", "deleter", diff --git a/lib/src/list.dart b/lib/src/list.dart index 2ecb53176..e3244d864 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -42,6 +42,8 @@ abstract class RealmList with RealmEntity implements List, /// and it's parent object hasn't been deleted. bool get isValid; + RealmResults get asResults; + factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); factory RealmList(Iterable items) => UnmanagedRealmList(items); @@ -165,6 +167,9 @@ class ManagedRealmList with RealmEntity, ListMixin impleme final frozenRealm = realm.freeze(); return frozenRealm.resolveList(this)!; } + + @override + RealmResults get asResults => RealmResultsInternal.createFromList(this, realm, _metadata); } class UnmanagedRealmList extends collection.DelegatingList with RealmEntity implements RealmList { @@ -181,6 +186,9 @@ class UnmanagedRealmList extends collection.DelegatingList @override RealmList freeze() => throw RealmStateError("Unmanaged lists can't be frozen"); + + @override + RealmResults get asResults => throw RealmStateError("Unmanaged lists can't be converted to results"); } // The query operations on lists, as well as the ability to subscribe for notifications, @@ -194,8 +202,7 @@ extension RealmListOfObject on RealmList { /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) /// 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 arguments = const []]) { - final managedList = asManaged(); - final handle = realmCore.queryList(managedList, query, arguments); + final handle = realmCore.queryList(asManaged, query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } @@ -204,9 +211,7 @@ extension RealmListOfObject on RealmList { if (isFrozen) { throw RealmStateError('List is frozen and cannot emit changes'); } - - final managedList = asManaged(); - final controller = ListNotificationsController(managedList); + final controller = ListNotificationsController(asManaged); return controller.createStream(); } } @@ -222,10 +227,10 @@ extension RealmListInternal on RealmList { } } - ManagedRealmList asManaged() => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); + ManagedRealmList get asManaged => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); RealmListHandle get handle { - final result = asManaged()._handle; + final result = asManaged._handle; if (result.released) { throw RealmClosedError('Cannot access a list that belongs to a closed Realm'); } @@ -233,7 +238,7 @@ extension RealmListInternal on RealmList { return result; } - RealmObjectMetadata? get metadata => asManaged()._metadata; + RealmObjectMetadata? get metadata => asManaged._metadata; static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index c043e7420..bd768597e 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -875,7 +875,20 @@ class _RealmCore { }); } - RealmObjectHandle getObjectAt(RealmResults results, int index) { + RealmResultsHandle resultsFromList(RealmList list) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_list_to_results(list.handle._pointer)); + return RealmResultsHandle._(pointer, list.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); + }); + } + + 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); } diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 8e605bd7f..870eca2c2 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -84,10 +84,10 @@ export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges; export 'realm_object.dart' show RealmEntity, RealmException, UserCallbackException, RealmObject, RealmObjectBase, EmbeddedObject, RealmObjectChanges, DynamicRealmObject; export 'realm_property.dart'; -export 'results.dart' show RealmResults, RealmResultsChanges; +export 'results.dart' show RealmResults, RealmResultsChanges, RealmResultsOfObject; +export 'session.dart' show Session, SessionState, ConnectionState, ProgressDirection, ProgressMode, SyncProgress, ConnectionStateChange; export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet; export 'user.dart' show User, UserState, UserIdentity, ApiKeyClient, ApiKey, FunctionsClient; -export 'session.dart' show Session, SessionState, ConnectionState, ProgressDirection, ProgressMode, SyncProgress, ConnectionStateChange; export 'migration.dart' show Migration; export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, CancelledException; @@ -628,12 +628,14 @@ extension RealmInternal on Realm { return createList(handle, list.metadata); } - RealmResults resolveResults(RealmResults results) { + RealmResults resolveResults(RealmResults results) { final handle = realmCore.resolveResults(results, this); return RealmResultsInternal.create(handle, this, results.metadata); } static MigrationRealm getMigrationRealm(Realm realm) => MigrationRealm._(realm); + + bool get isInMigration => _isInMigration; } /// @nodoc diff --git a/lib/src/results.dart b/lib/src/results.dart index 76565eceb..e88cc5d42 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -15,7 +15,6 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// - import 'dart:async'; import 'dart:collection' as collection; import 'dart:ffi'; @@ -29,7 +28,7 @@ import 'realm_object.dart'; /// added to or deleted from the Realm that match the underlying query. /// /// {@category Realm} -class RealmResults extends collection.IterableBase with RealmEntity implements Finalizable { +class RealmResults extends collection.IterableBase with RealmEntity implements Finalizable { final RealmObjectMetadata? _metadata; final RealmResultsHandle _handle; @@ -41,17 +40,14 @@ class RealmResults extends collection.IterableBase /// Returns the element of type `T` at the specified [index]. T operator [](int index) { - final handle = realmCore.getObjectAt(this, index); - return realm.createObject(T, handle, _metadata!) as T; - } - - /// Returns a new [RealmResults] filtered according to the provided query. - /// - /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) - /// 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 handle = realmCore.queryResults(this, query, args); - return RealmResultsInternal.create(handle, realm, _metadata); + final meta = _metadata; + if (meta != null) { + final handle = realmCore.resultsGetObjectAt(this, index); + final accessor = RealmCoreAccessor(metadata, realm.isInMigration); + return RealmObjectInternal.create(T, realm, handle, accessor) as T; + } else { + return realmCore.resultsGetElementAt(this, index) as T; + } } /// `true` if the `Results` collection is empty. @@ -73,16 +69,6 @@ class RealmResults extends collection.IterableBase @override int get length => realmCore.getResultsCount(this); - /// Allows listening for changes when the contents of this collection changes. - Stream> get changes { - if (isFrozen) { - throw RealmStateError('Results are frozen and cannot emit changes'); - } - - final controller = ResultsNotificationsController(this); - return controller.createStream(); - } - /// Creates a frozen snapshot of this query. RealmResults freeze() { if (isFrozen) { @@ -94,6 +80,35 @@ class RealmResults extends collection.IterableBase } } +// The query operations on results, as well as the ability to subscribe for notifications, +// only work for results of objects (core restriction), so we add these as an extension methods +// to allow the compiler to prevent misuse. +extension RealmResultsOfObject on RealmResults { + RealmResults snapshot() { + final handle = realmCore.resultsSnapshot(this); + return RealmResults._(handle, realm, _metadata); + } + + /// Returns a new [RealmResults] filtered according to the provided query. + /// + /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) + /// 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 handle = realmCore.queryResults(this, query, args); + return RealmResultsInternal.create(handle, realm, _metadata); + } + + /// Allows listening for changes when the contents of this collection changes. + Stream> get changes { + if (isFrozen) { + throw RealmStateError('Results are frozen and cannot emit changes'); + } + + final controller = ResultsNotificationsController(this); + return controller.createStream(); + } +} + /// @nodoc //RealmResults package internal members extension RealmResultsInternal on RealmResults { @@ -110,11 +125,21 @@ extension RealmResultsInternal on RealmResults { return _handle; } - RealmObjectMetadata? get metadata => _metadata; - - static RealmResults create(RealmResultsHandle handle, Realm realm, RealmObjectMetadata? metadata) { - return RealmResults._(handle, realm, metadata); - } + RealmObjectMetadata get metadata => _metadata!; + + static RealmResults create( + RealmResultsHandle handle, + Realm realm, + RealmObjectMetadata? metadata, + ) => + RealmResults._(handle, realm, metadata); + + static RealmResults createFromList( + RealmList list, + Realm realm, + RealmObjectMetadata? metadata, + ) => + RealmResults._(realmCore.resultsFromList(list), realm, metadata); } /// Describes the changes in a Realm results collection since the last time the notification callback was invoked. @@ -158,7 +183,7 @@ class ResultsNotificationsController extends Notifica } } -class _RealmResultsIterator implements Iterator { +class _RealmResultsIterator implements Iterator { final RealmResults _results; int _index; T? _current; diff --git a/test/results_test.dart b/test/results_test.dart index e099fc922..85e3f7871 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -549,4 +549,16 @@ Future main([List? args]) async { expect(realm.query("name CONTAINS 'a'").query("name CONTAINS 'l'"), isNot([alice, carol])); expect(realm.query("name CONTAINS 'a'").query("name CONTAINS 'l'"), [carol]); }); + + test('Results of primitives', () { + var config = Configuration.local([Player.schema, Game.schema]); + var realm = getRealm(config); + + final scores = [-1, null, 0, 1]; + final alice = Player('Alice', scoresByRound: scores); + realm.write(() => realm.add(alice)); + + expect(alice.scoresByRound, scores); + expect(alice.scoresByRound.asResults, scores); + }); } diff --git a/test/subscription_test.dart b/test/subscription_test.dart index 1a341ded9..b0cf31bb2 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -295,18 +295,18 @@ Future main([List? args]) async { ObjectId newOid() => ObjectId.fromBytes(randomBytes(12)); - final oids = {}; + final objectIds = {}; const max = 1000; subscriptions.update((mutableSubscriptions) { - oids.addAll([ + objectIds.addAll([ for (int i = 0; i < max; ++i) mutableSubscriptions.add(realm.query(r'_id == $0', [newOid()])).id ]); }); - expect(oids.length, max); // no collisions + expect(objectIds.length, max); // no collisions expect(subscriptions.length, max); for (final sub in subscriptions) { - expect(sub.id, isIn(oids)); + expect(sub.id, isIn(objectIds)); } }); @@ -519,7 +519,7 @@ Future main([List? args]) async { expect(() => realm.write(() => realm.add(Task(ObjectId()))), throws("no flexible sync subscription has been created")); }); - testSubscriptions('Subscription on unqueryable field sould throw', (realm) async { + testSubscriptions('Subscription on non-queryable field should throw', (realm) async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.add(realm.all()); }); @@ -533,7 +533,7 @@ Future main([List? args]) async { isCompleted: false, durationInMinutes: 10, ), - Event(ObjectId(), name: "Some other eveent", isCompleted: true, durationInMinutes: 60), + Event(ObjectId(), name: "Some other event", isCompleted: true, durationInMinutes: 60), ]); }); @@ -571,7 +571,7 @@ Future main([List? args]) async { realm.addAll([ Event(ObjectId(), name: "NPMG Event", isCompleted: true, durationInMinutes: 30), Event(ObjectId(), name: "NPMG Meeting", isCompleted: false, durationInMinutes: 10), - Event(ObjectId(), name: "Some other eveent", isCompleted: true, durationInMinutes: 60), + Event(ObjectId(), name: "Some other event", isCompleted: true, durationInMinutes: 60), ]); }); From 6cfd2e30d21100cd4f2c9ded818a0001ceec028e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 11 Oct 2022 10:28:24 +0200 Subject: [PATCH 2/8] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd1db9df..3ba0956bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Enhancements * Added `MutableSubscriptionSet.removeByType` for removing subscriptions by their realm object type. (Issue [#317](https://github.com/realm/realm-dart/issues/317)) +* Support results of primitives, ie. `RealmResult`. + Result of primitives do not support notifications and that is captured in the type system. ([#162](https://github.com/realm/realm-dart/issues/162)) ### Fixed * Fixed a wrong mapping for `AuthProviderType` returned by `User.provider` for google, facebook and apple credentials. From 9759852b8b8501d345190ab47fa7bc5deb67fbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 25 Oct 2022 09:20:02 +0200 Subject: [PATCH 3/8] Address PR feedback --- CHANGELOG.md | 2 +- lib/src/list.dart | 16 ++++++++-------- lib/src/results.dart | 10 +--------- test/results_test.dart | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba0956bf..6ed7a8451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Enhancements * Added `MutableSubscriptionSet.removeByType` for removing subscriptions by their realm object type. (Issue [#317](https://github.com/realm/realm-dart/issues/317)) * Support results of primitives, ie. `RealmResult`. - Result of primitives do not support notifications and that is captured in the type system. ([#162](https://github.com/realm/realm-dart/issues/162)) + Result of primitives do not support notifications. ([#162](https://github.com/realm/realm-dart/issues/162)) ### Fixed * Fixed a wrong mapping for `AuthProviderType` returned by `User.provider` for google, facebook and apple credentials. diff --git a/lib/src/list.dart b/lib/src/list.dart index e3244d864..269d78a24 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -42,7 +42,7 @@ abstract class RealmList with RealmEntity implements List, /// and it's parent object hasn't been deleted. bool get isValid; - RealmResults get asResults; + RealmResults asResults(); factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); factory RealmList(Iterable items) => UnmanagedRealmList(items); @@ -169,7 +169,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } @override - RealmResults get asResults => RealmResultsInternal.createFromList(this, realm, _metadata); + RealmResults asResults() => RealmResultsInternal.create(realmCore.resultsFromList(this), realm, metadata); } class UnmanagedRealmList extends collection.DelegatingList with RealmEntity implements RealmList { @@ -188,7 +188,7 @@ class UnmanagedRealmList extends collection.DelegatingList RealmList freeze() => throw RealmStateError("Unmanaged lists can't be frozen"); @override - RealmResults get asResults => throw RealmStateError("Unmanaged lists can't be converted to results"); + RealmResults asResults() => throw RealmStateError("Unmanaged lists can't be converted to results"); } // The query operations on lists, as well as the ability to subscribe for notifications, @@ -202,7 +202,7 @@ extension RealmListOfObject on RealmList { /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) /// 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 arguments = const []]) { - final handle = realmCore.queryList(asManaged, query, arguments); + final handle = realmCore.queryList(asManaged(), query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } @@ -211,7 +211,7 @@ extension RealmListOfObject on RealmList { if (isFrozen) { throw RealmStateError('List is frozen and cannot emit changes'); } - final controller = ListNotificationsController(asManaged); + final controller = ListNotificationsController(asManaged()); return controller.createStream(); } } @@ -227,10 +227,10 @@ extension RealmListInternal on RealmList { } } - ManagedRealmList get asManaged => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); + ManagedRealmList asManaged() => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); RealmListHandle get handle { - final result = asManaged._handle; + final result = asManaged()._handle; if (result.released) { throw RealmClosedError('Cannot access a list that belongs to a closed Realm'); } @@ -238,7 +238,7 @@ extension RealmListInternal on RealmList { return result; } - RealmObjectMetadata? get metadata => asManaged._metadata; + RealmObjectMetadata? get metadata => asManaged()._metadata; static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); diff --git a/lib/src/results.dart b/lib/src/results.dart index e88cc5d42..ab58bf54e 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -91,8 +91,7 @@ extension RealmResultsOfObject on RealmResults { /// Returns a new [RealmResults] filtered according to the provided query. /// - /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) - /// and [Predicate Programming Guide.](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789) + /// 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); return RealmResultsInternal.create(handle, realm, _metadata); @@ -133,13 +132,6 @@ extension RealmResultsInternal on RealmResults { RealmObjectMetadata? metadata, ) => RealmResults._(handle, realm, metadata); - - static RealmResults createFromList( - RealmList list, - Realm realm, - RealmObjectMetadata? metadata, - ) => - RealmResults._(realmCore.resultsFromList(list), realm, metadata); } /// Describes the changes in a Realm results collection since the last time the notification callback was invoked. diff --git a/test/results_test.dart b/test/results_test.dart index 85e3f7871..68fb35cff 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -559,6 +559,6 @@ Future main([List? args]) async { realm.write(() => realm.add(alice)); expect(alice.scoresByRound, scores); - expect(alice.scoresByRound.asResults, scores); + expect(alice.scoresByRound.asResults(), scores); }); } From 340ac3a91d3eb8f675ab0f31fbbc4daa1965f7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 25 Oct 2022 14:33:23 +0200 Subject: [PATCH 4/8] Support notifications on List of primitives --- lib/src/list.dart | 29 +++++---- test/list_test.dart | 155 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 11 deletions(-) diff --git a/lib/src/list.dart b/lib/src/list.dart index 269d78a24..4f9d4a217 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -42,6 +42,7 @@ abstract class RealmList with RealmEntity implements List, /// and it's parent object hasn't been deleted. bool get isValid; + /// Converts this [List] to a [RealmResults]. RealmResults asResults(); factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); @@ -49,6 +50,9 @@ abstract class RealmList with RealmEntity implements List, /// Creates a frozen snapshot of this `RealmList`. RealmList freeze(); + + /// Allows listening for changes when the contents of this collection changes. + Stream> get changes; } class ManagedRealmList with RealmEntity, ListMixin implements RealmList { @@ -170,6 +174,15 @@ class ManagedRealmList with RealmEntity, ListMixin impleme @override RealmResults asResults() => RealmResultsInternal.create(realmCore.resultsFromList(this), realm, metadata); + + @override + Stream> get changes { + if (isFrozen) { + throw RealmStateError('List is frozen and cannot emit changes'); + } + final controller = ListNotificationsController(asManaged()); + return controller.createStream(); + } } class UnmanagedRealmList extends collection.DelegatingList with RealmEntity implements RealmList { @@ -189,6 +202,9 @@ class UnmanagedRealmList extends collection.DelegatingList @override RealmResults asResults() => throw RealmStateError("Unmanaged lists can't be converted to results"); + + @override + Stream> get changes => throw RealmStateError("Unmanaged lists don't support changes"); } // The query operations on lists, as well as the ability to subscribe for notifications, @@ -205,15 +221,6 @@ extension RealmListOfObject on RealmList { final handle = realmCore.queryList(asManaged(), query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } - - /// Allows listening for changes when the contents of this collection changes. - Stream> get changes { - if (isFrozen) { - throw RealmStateError('List is frozen and cannot emit changes'); - } - final controller = ListNotificationsController(asManaged()); - return controller.createStream(); - } } /// @nodoc @@ -280,7 +287,7 @@ extension RealmListInternal on RealmList { } /// Describes the changes in a Realm results collection since the last time the notification callback was invoked. -class RealmListChanges extends RealmCollectionChanges { +class RealmListChanges extends RealmCollectionChanges { /// The collection being monitored for changes. final RealmList list; @@ -288,7 +295,7 @@ class RealmListChanges extends RealmCollectionChanges { } /// @nodoc -class ListNotificationsController extends NotificationsController { +class ListNotificationsController extends NotificationsController { final ManagedRealmList list; late final StreamController> streamController; diff --git a/test/list_test.dart b/test/list_test.dart index 1688bce91..2497e7b7b 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -20,6 +20,8 @@ import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; import 'test.dart'; @@ -310,6 +312,159 @@ Future main([List? args]) async { await Future.delayed(Duration(milliseconds: 20)); }); + void testListNotificationsHelper( + String opName, + RealmList Function(AllCollections c) getList, + List Function(RealmListChanges ch) getIndexes, + void Function(RealmList list, int index) op, + List> listOfIndexes, { + Iterable Function()? factory, + }) { + test('RealmList<$T>.$opName notifications', () { + final config = Configuration.local([AllCollections.schema]); + final realm = getRealm(config); + + final allCollections = realm.write(() => realm.add(AllCollections())); + + final list = getList(allCollections); + if (factory != null) { + realm.write(() => list.addAll(factory())); + } + + expectLater( + list.changes.map((e) => getIndexes(e)), + emitsInOrder([ + [], + ...listOfIndexes.map( + (l) => l.sorted((a, b) => a - b), + ) + ].map((indexes) => equals(indexes))), + ); + + for (final indexes in listOfIndexes) { + realm.write(() { + for (final i in indexes) { + op(list, i); + } + }); + } + }); + } + + // Here you can add more insert patterns + final inserts = [ + [0], + [1], + [1, 2, 3], + [0, 4], + [1, 2], + [8, 10], + [0], + [11], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ]; + + @isTest + void testListInsertNotifications( + RealmList Function(AllCollections c) getList, + void Function(RealmList list, int index) op, + ) { + testListNotificationsHelper('insert', getList, (ch) => ch.inserted, op, inserts); + } + + testListInsertNotifications((c) => c.nullableBools, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.bools, (c, i) => c.insert(i, i % 2 == 0)); + testListInsertNotifications((c) => c.nullableDates, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.dates, (c, i) => c.insert(i, DateTime(i))); + testListInsertNotifications((c) => c.nullableDoubles, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.doubles, (c, i) => c.insert(i, i.toDouble())); + testListInsertNotifications((c) => c.nullableInts, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.ints, (c, i) => c.insert(i, i)); + testListInsertNotifications((c) => c.nullableObjectIds, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.objectIds, (c, i) => c.insert(i, ObjectId())); + testListInsertNotifications((c) => c.nullableStrings, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.strings, (c, i) => c.insert(i, '$i')); + testListInsertNotifications((c) => c.nullableUuids, (c, i) => c.insert(i, null)); + testListInsertNotifications((c) => c.uuids, (c, i) => c.insert(i, Uuid.v4())); + + final deletes = [ + [0], + [1], + [3, 2, 1], + [4, 0], + [2, 1], + [10, 8], + [0], + [11], + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + ]; + + @isTest + void testListDeleteNotifications( + RealmList Function(AllCollections c) getList, + T Function(int i) indexToValue, + ) { + testListNotificationsHelper('deleted', getList, (ch) => ch.deleted, (c, i) => c.removeAt(i), deletes, + factory: () => List.generate( + 100, + (i) => indexToValue(i), + )); + } + + testListDeleteNotifications((c) => c.nullableBools, (i) => null); + testListDeleteNotifications((c) => c.bools, (i) => i % 2 == 0); + testListDeleteNotifications((c) => c.nullableDates, (i) => null); + testListDeleteNotifications((c) => c.dates, (i) => DateTime(i)); + testListDeleteNotifications((c) => c.nullableDoubles, (i) => null); + testListDeleteNotifications((c) => c.doubles, (i) => i.toDouble()); + testListDeleteNotifications((c) => c.nullableInts, (i) => null); + testListDeleteNotifications((c) => c.ints, (i) => i); + testListDeleteNotifications((c) => c.nullableObjectIds, (i) => null); + testListDeleteNotifications((c) => c.objectIds, (i) => ObjectId()); + testListDeleteNotifications((c) => c.nullableStrings, (i) => null); + testListDeleteNotifications((c) => c.strings, (i) => '$i'); + testListDeleteNotifications((c) => c.nullableUuids, (i) => null); + testListDeleteNotifications((c) => c.uuids, (i) => Uuid.v4()); + + final modifications = [ + [0], + [1], + [2, 1, 3], + [4, 0], + [1, 2], + [10, 8], + [0], + [11], + [10, 7, 8, 9, 3, 2, 6, 5, 1, 0, 4] + ]; + + @isTest + void testListModificationNotifications( + RealmList Function(AllCollections c) getList, + T Function(int i) indexToValue, + ) { + testListNotificationsHelper('modified', getList, (ch) => ch.modified, (c, i) => c[i] = indexToValue(i), modifications, + factory: () => List.generate( + 100, + (i) => indexToValue(i), + )); + } + + testListModificationNotifications((c) => c.nullableBools, (i) => null); + testListModificationNotifications((c) => c.bools, (i) => i % 2 == 0); + testListModificationNotifications((c) => c.nullableDates, (i) => null); + testListModificationNotifications((c) => c.dates, (i) => DateTime(i)); + testListModificationNotifications((c) => c.nullableDoubles, (i) => null); + testListModificationNotifications((c) => c.doubles, (i) => i.toDouble()); + testListModificationNotifications((c) => c.nullableInts, (i) => null); + testListModificationNotifications((c) => c.ints, (i) => i); + testListModificationNotifications((c) => c.nullableObjectIds, (i) => null); + testListModificationNotifications((c) => c.objectIds, (i) => ObjectId()); + testListModificationNotifications((c) => c.nullableStrings, (i) => null); + testListDeleteNotifications((c) => c.strings, (i) => '$i'); + testListDeleteNotifications((c) => c.nullableUuids, (i) => null); + testListDeleteNotifications((c) => c.uuids, (i) => Uuid.v4()); + test('List query', () { final config = Configuration.local([Team.schema, Person.schema]); final realm = getRealm(config); From c494af585d47c9afbf053475f799f41cc5c0f506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 25 Oct 2022 14:46:33 +0200 Subject: [PATCH 5/8] Support notifications on results of primitives --- lib/src/results.dart | 34 ++++++++++++++-------------------- test/results_test.dart | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/lib/src/results.dart b/lib/src/results.dart index ab58bf54e..d5ce6cb60 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -78,24 +78,6 @@ class RealmResults extends collection.IterableBase with Re final frozenRealm = realm.freeze(); return frozenRealm.resolveResults(this); } -} - -// The query operations on results, as well as the ability to subscribe for notifications, -// only work for results of objects (core restriction), so we add these as an extension methods -// to allow the compiler to prevent misuse. -extension RealmResultsOfObject on RealmResults { - RealmResults snapshot() { - final handle = realmCore.resultsSnapshot(this); - return RealmResults._(handle, realm, _metadata); - } - - /// Returns a new [RealmResults] filtered according to the provided query. - /// - /// 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); - return RealmResultsInternal.create(handle, realm, _metadata); - } /// Allows listening for changes when the contents of this collection changes. Stream> get changes { @@ -108,6 +90,18 @@ extension RealmResultsOfObject on RealmResults { } } +// The query operations 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. + /// + /// 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); + return RealmResultsInternal.create(handle, realm, _metadata); + } +} + /// @nodoc //RealmResults package internal members extension RealmResultsInternal on RealmResults { @@ -135,7 +129,7 @@ extension RealmResultsInternal on RealmResults { } /// Describes the changes in a Realm results collection since the last time the notification callback was invoked. -class RealmResultsChanges extends RealmCollectionChanges { +class RealmResultsChanges extends RealmCollectionChanges { /// The results collection being monitored for changes. final RealmResults results; @@ -143,7 +137,7 @@ class RealmResultsChanges extends RealmCollectionChan } /// @nodoc -class ResultsNotificationsController extends NotificationsController { +class ResultsNotificationsController extends NotificationsController { final RealmResults results; late final StreamController> streamController; diff --git a/test/results_test.dart b/test/results_test.dart index 68fb35cff..23464099c 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -561,4 +561,35 @@ Future main([List? args]) async { expect(alice.scoresByRound, scores); expect(alice.scoresByRound.asResults(), scores); }); + + test('Results of primitives notifications', () { + var config = Configuration.local([Player.schema, Game.schema]); + var realm = getRealm(config); + + final scores = [-1, null, 0, 1]; + final alice = Player('Alice', scoresByRound: scores); + realm.write(() => realm.add(alice)); + + final results = alice.scoresByRound.asResults(); + expectLater( + results.changes, + emitsInOrder([ + isA().having((ch) => ch.inserted, 'inserted', []), + isA().having((ch) => ch.inserted, 'inserted', [4]), + isA() // + .having((ch) => ch.inserted, 'inserted', [0]) // + .having((ch) => ch.deleted, 'deleted', [1]), + isA().having((ch) => ch.deleted, 'deleted', [2, 4]), + ])); + + realm.write(() => alice.scoresByRound.add(2)); + realm.write(() { + alice.scoresByRound.insert(0, 3); + alice.scoresByRound.remove(null); + }); + realm.write(() { + alice.scoresByRound.removeAt(2); + alice.scoresByRound.removeAt(3); // index 4 in old list + }); + }); } From b1c20a297fff2cfa86e8750da46a637af1a13e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 25 Oct 2022 14:47:54 +0200 Subject: [PATCH 6/8] Update CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed7a8451..377e724e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ ### Enhancements * Added `MutableSubscriptionSet.removeByType` for removing subscriptions by their realm object type. (Issue [#317](https://github.com/realm/realm-dart/issues/317)) -* Support results of primitives, ie. `RealmResult`. - Result of primitives do not support notifications. ([#162](https://github.com/realm/realm-dart/issues/162)) +* Support results of primitives, ie. `RealmResult`. (Issue [#162](https://github.com/realm/realm-dart/issues/162)) +* Support notifications on all managed realm lists, including list of primitives, ie. `RealmList.changes` is supported. ([#893](https://github.com/realm/realm-dart/pull/893)) ### Fixed * Fixed a wrong mapping for `AuthProviderType` returned by `User.provider` for google, facebook and apple credentials. From 39b6532a966cd6f4e99dd6e9c97a2132a2604d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 25 Oct 2022 15:22:20 +0200 Subject: [PATCH 7/8] Test List.asResults().query --- test/results_test.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/results_test.dart b/test/results_test.dart index 23464099c..3176d4f7c 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -592,4 +592,31 @@ Future main([List? args]) async { alice.scoresByRound.removeAt(3); // index 4 in old list }); }); + + test('List.asResults().query', () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + final alice = Person('Alice'); + final bob = Person('Bob'); + final carol = Person('Carol'); + final dan = Person('Dan'); + + final team = realm.write(() { + return realm.add(Team('Class of 92', players: [alice, bob, carol, dan])); + }); + + final playersAsResults = team.players.asResults(); + + expect(playersAsResults, [alice, bob, carol, dan]); + expect(playersAsResults.query('FALSEPREDICATE').query('TRUEPREDICATE'), isEmpty); + expect(playersAsResults.query('FALSEPREDICATE').query('TRUEPREDICATE'), isNot(realm.all())); + expect(playersAsResults.query("name CONTAINS 'a'"), [carol, dan]); // Alice is capital 'a' + expect(playersAsResults.query("name CONTAINS 'l'"), [alice, carol]); + expect(playersAsResults.query("name CONTAINS 'a'").query("name CONTAINS 'l'"), isNot([alice, carol])); + expect(playersAsResults.query("name CONTAINS 'a'").query("name CONTAINS 'l'"), [carol]); + + expect(() => realm.write(() => realm.deleteMany(playersAsResults.query("name CONTAINS 'a'"))), returnsNormally); + expect(team.players, [alice, bob]); // Alice is capital 'a' + }); } From f28f1b23b4d66395f5a022535e01bb39f5821c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 25 Oct 2022 15:38:48 +0200 Subject: [PATCH 8/8] Test on type instead of _metadata --- lib/src/results.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/results.dart b/lib/src/results.dart index d5ce6cb60..28ca0b7f7 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -40,8 +40,7 @@ class RealmResults extends collection.IterableBase with Re /// Returns the element of type `T` at the specified [index]. T operator [](int index) { - final meta = _metadata; - if (meta != null) { + if (this is RealmResults) { final handle = realmCore.resultsGetObjectAt(this, index); final accessor = RealmCoreAccessor(metadata, realm.isInMigration); return RealmObjectInternal.create(T, realm, handle, accessor) as T;