Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RDART-969: Keypath filtering on collections #1714

Merged
merged 18 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>? 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<Person>().changesFor(["age", "friends"]).listen( .... )
```

### Fixed
* `Realm.writeAsync` did not handle async callbacks (`Future<T> Function()`) correctly. (Issue [#1667](https://github.com/realm/realm-dart/issues/1667))
Expand Down
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/list_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>? keyPaths, int? classKey);
}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/map_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ abstract interface class MapHandle extends HandleBase {
ResultsHandle query(String query, List<Object?> args);
bool remove(String key);
MapHandle? resolveIn(RealmHandle frozenRealm);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);
}
26 changes: 15 additions & 11 deletions packages/realm_dart/lib/src/handles/native/list_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,20 @@ class ListHandle extends CollectionHandleBase<realm_list> 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<String>? 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,
);
});
}
}
26 changes: 15 additions & 11 deletions packages/realm_dart/lib/src/handles/native/map_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,21 @@ class MapHandle extends CollectionHandleBase<realm_dictionary> 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<String>? 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,
);
});
}
}

Expand Down
24 changes: 2 additions & 22 deletions packages/realm_dart/lib/src/handles/native/object_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ class ObjectHandle extends RootedHandleBase<realm_object> implements intf.Object
}

@override
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List<String>? keyPaths]) {
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey) {
return using((arena) {
final kpNative = buildAndVerifyKeyPath(keyPaths);
final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey);
papafe marked this conversation as resolved.
Show resolved Hide resolved
return NotificationTokenHandle(
realmLib.realm_object_add_notification_callback(
pointer,
Expand All @@ -152,26 +152,6 @@ class ObjectHandle extends RootedHandleBase<realm_object> implements intf.Object
});
}

Pointer<realm_key_path_array> buildAndVerifyKeyPath(List<String>? keyPaths) {
return using((arena) {
if (keyPaths == null) {
return nullptr;
}

final length = keyPaths.length;
final keypathsNative = arena<Pointer<Char>>(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<String>? keyPaths) => buildAndVerifyKeyPath(keyPaths);

@override
// equals handled by HandleBase<T>
// ignore: hash_and_equals
Expand Down
27 changes: 25 additions & 2 deletions packages/realm_dart/lib/src/handles/native/realm_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ class RealmHandle extends HandleBase<shared_realm> implements intf.RealmHandle {
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}

final configHandle = ConfigHandle.from(config);

return RealmHandle(realmLib
.realm_open(configHandle.pointer) //
.raiseLastErrorIfNull());
Expand Down Expand Up @@ -478,6 +478,29 @@ class RealmHandle extends HandleBase<shared_realm> implements intf.RealmHandle {
}
return result;
}

Pointer<realm_key_path_array> buildAndVerifyKeyPath(List<String>? 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<Pointer<Char>>(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<String>? keyPaths, int? classKey) => buildAndVerifyKeyPath(keyPaths, classKey);
}

class CallbackTokenHandle extends RootedHandleBase<realm_callback_token> implements intf.CallbackTokenHandle {
Expand Down
25 changes: 14 additions & 11 deletions packages/realm_dart/lib/src/handles/native/results_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,19 @@ class ResultsHandle extends RootedHandleBase<realm_results> 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<String>? 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,
);
});
}
}
25 changes: 14 additions & 11 deletions packages/realm_dart/lib/src/handles/native/set_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,19 @@ class SetHandle extends RootedHandleBase<realm_set> 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<String>? 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,
);
});
}
}
4 changes: 1 addition & 3 deletions packages/realm_dart/lib/src/handles/object_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ abstract interface class ObjectHandle extends HandleBase {

ObjectHandle? resolveIn(RealmHandle frozenRealm);

NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List<String>? keyPaths]);

void verifyKeyPath(List<String>? keyPaths);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);

@override
// equals handled by HandleBase<T>
Expand Down
2 changes: 2 additions & 0 deletions packages/realm_dart/lib/src/handles/realm_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ abstract interface class RealmHandle extends HandleBase {
Map<String, RealmPropertyMetadata> getPropertiesMetadata(int classKey, String? primaryKeyName);

RealmObjectMetadata getObjectMetadata(SchemaObject schema);

void verifyKeyPath(List<String> keyPaths, int? classKey);
}

abstract class CallbackTokenHandle extends HandleBase {}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/results_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>? keyPaths, int? classKey);
}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/set_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ abstract interface class SetHandle extends HandleBase {

SetHandle? resolveIn(RealmHandle frozenRealm);

NotificationTokenHandle subscribeForNotifications(NotificationsController controller);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);
}
34 changes: 29 additions & 5 deletions packages/realm_dart/lib/src/list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,18 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
RealmResults<T> asResults() => RealmResultsInternal.create<T>(handle.asResults(), realm, metadata);

@override
Stream<RealmListChanges<T>> get changes {
Stream<RealmListChanges<T>> get changes => _changesFor(null);

Stream<RealmListChanges<T>> _changesFor([List<String>? keyPaths]) {
if (isFrozen) {
throw RealmStateError('List is frozen and cannot emit changes');
}
final controller = ListNotificationsController<T>(asManaged());

if (keyPaths != null && _metadata == null) {
throw RealmStateError('Key paths can be used only with collections of Realm objects');
}

final controller = ListNotificationsController<T>(asManaged(), keyPaths);
return controller.createStream();
}
}
Expand Down Expand Up @@ -238,7 +245,7 @@ class UnmanagedRealmList<T extends Object?> extends collection.DelegatingList<T>
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<T extends RealmObjectBase> on RealmList<T> {
/// Filters the list and returns a new [RealmResults] according to the provided [query] (with optional [arguments]).
Expand All @@ -250,6 +257,17 @@ extension RealmListOfObject<T extends RealmObjectBase> on RealmList<T> {
final handle = asManaged().handle.query(query, arguments);
return RealmResultsInternal.create<T>(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<RealmListChanges<T>> changesFor([List<String>? keyPaths]) {
if (!isManaged) {
throw RealmStateError("Unmanaged lists don't support changes");
}

return (this as ManagedRealmList<T>)._changesFor(keyPaths);
}
}

/// @nodoc
Expand Down Expand Up @@ -323,12 +341,18 @@ class RealmListChanges<T extends Object?> extends RealmCollectionChanges {
class ListNotificationsController<T extends Object?> extends NotificationsController {
final ManagedRealmList<T> list;
late final StreamController<RealmListChanges<T>> streamController;
List<String>? keyPaths;

ListNotificationsController(this.list);
ListNotificationsController(this.list, [List<String>? 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<RealmListChanges<T>> createStream() {
Expand Down
Loading
Loading