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

Add support for RealmObject.getBacklinks #1483

Merged
merged 3 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

The map keys may not contain `.` or start with `$`. (Issue [#685](https://github.com/realm/realm-dart/issues/685))
* Added a new exception - `MigrationRequiredException` that will be thrown when a local Realm is opened with a schema that differs from the schema on disk and no migration callback is supplied. Additionally, a `helpLink` property has been added to `RealmException` and its subclasses to provide a link to the documentation for the error. (Issue [#1448](https://github.com/realm/realm-dart/issues/1448))
* Added `RealmObject.getBacklinks<SourceType>('sourceProperty')` which is a method allowing you to look up all objects of type `SourceType` which link to the current object via their `sourceProperty` property. (Issue [#1480](https://github.com/realm/realm-dart/issues/1480))

### Fixed
* Fixed warnings being emitted by the realm generator requesting that `xyz.g.dart` be included with `part 'xyz.g.dart';` for `xyz.dart` files that import `realm` but don't have realm models defined. Those should not need generated parts and including the part file would have resulted in an empty file with `// ignore_for_file: type=lint` being generated. (PR [#1443](https://github.com/realm/realm-dart/pull/1443))
Expand Down
57 changes: 55 additions & 2 deletions lib/src/realm_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,24 @@ class RealmObjectMetadata {

RealmObjectMetadata(this.schema, this.classKey, this._propertyKeys);

RealmPropertyMetadata operator [](String propertyName) =>
_propertyKeys[propertyName] ?? (throw RealmException("Property $propertyName does not exist on class $_realmObjectTypeName"));
RealmPropertyMetadata operator [](String propertyName) {
var meta = _propertyKeys[propertyName];
if (meta == null) {
// We couldn't find a proeprty by the name supplied by the user - this may be because the _propertyKeys
// map is keyed on the property names as they exist in the database while the user supplied the public
// name (i.e. the name of the property in the model). Try and look up the property by the public name and
// then try to re-fetch the property meta using the database name.
final publicName = schema.properties.firstWhereOrNull((e) => e.name == propertyName)?.mapTo;
if (publicName != null && publicName != propertyName) {
meta = _propertyKeys[publicName];
}
}

return meta ?? (throw RealmException("Property $propertyName does not exist on class $_realmObjectTypeName"));
}
// _propertyKeys[propertyName] ??
// schema.properties.firstWhereOrNull((p) => p.name == propertyName) ??
// (throw RealmException("Property $propertyName does not exist on class $_realmObjectTypeName"));

String? getPropertyName(int propertyKey) {
for (final entry in _propertyKeys.entries) {
Expand Down Expand Up @@ -501,6 +517,43 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab

/// Creates a frozen snapshot of this [RealmObject].
RealmObjectBase freeze() => freezeObject(this);

/// Returns all the objects of type [T] that link to this object via [propertyName].
/// Example:
/// ```dart
/// @RealmModel()
/// class School {
/// late String name;
/// }
///
/// @RealmModel()
/// class Student {
/// School? school;
/// }
///
/// // Find all students in a school
/// final school = realm.all<School>().first;
/// final allStudents = school.getBacklinks<Student>('school');
/// ```
RealmResults<T> getBacklinks<T>(String propertyName) {
if (!isManaged) {
throw RealmStateError("Can't look up backlinks of unmanaged objects.");
}

final sourceMeta = realm.metadata.getByType(T);
final sourceProperty = sourceMeta[propertyName];

if (sourceProperty.objectType == null) {
throw RealmError("Property $T.$propertyName is not a link property - it is a property of type ${sourceProperty.propertyType}");
}

if (sourceProperty.objectType != realm.metadata.getByType(runtimeType).schema.name) {
throw RealmError(
"Property $T.$propertyName is a link property that links to ${sourceProperty.objectType} which is different from the type of the current object, which is $runtimeType.");
}
final handle = realmCore.getBacklinks(this, sourceMeta.classKey, sourceProperty.key);
return RealmResultsInternal.create<T>(handle, realm, sourceMeta);
}
}

/// @nodoc
Expand Down
137 changes: 136 additions & 1 deletion test/backlinks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
//
////////////////////////////////////////////////////////////////////////////////

import 'package:test/expect.dart' hide throws;
import 'package:test/test.dart' hide test, throws;

import '../lib/realm.dart';
import 'test.dart';
Expand All @@ -29,6 +29,12 @@ class _Source {
@MapTo('et mål') // to throw a curve ball..
_Target? oneTarget;
late List<_Target> manyTargets;

// These are the same as the properties above, but don't have defined backlinks
// on Target
@MapTo('dynamisk mål')
_Target? dynamicTarget;
late List<_Target> dynamicManyTargets;
}

@RealmModel()
Expand All @@ -40,6 +46,8 @@ class _Target {

@Backlink(#manyTargets)
late Iterable<_Source> manyToMany;

_Source? source;
}

Future<void> main([List<String>? args]) async {
Expand Down Expand Up @@ -168,4 +176,131 @@ Future<void> main([List<String>? args]) async {
expect(s.manyTargets.map((t) => t.name), targets.map((t) => t.name));
}
});

group('getBacklinks() tests', () {
(Target theOne, List<Target> targets, Iterable<String> expectedSources) populateData() {
final config = Configuration.local([Target.schema, Source.schema]);
final realm = getRealm(config);

final theOne = Target(name: 'the one');
final targets = List.generate(100, (i) => Target(name: 'T$i'));
final sources = List.generate(100, (i) {
return i % 2 == 0
? Source(name: 'TargetLessSource$i')
: Source(name: 'S$i', manyTargets: targets, oneTarget: theOne, dynamicManyTargets: targets, dynamicTarget: theOne);
});

final expectedSources = sources.where((e) => e.name.startsWith('S')).map((e) => e.name);

realm.write(() {
realm.addAll(sources);
realm.addAll(targets);
realm.add(theOne);
});

return (theOne, targets, expectedSources);
}

test('pointing to a valid property', () {
final (theOne, targets, expectedSources) = populateData();

// getBacklinks should work with both the public and the @MapTo property names
expect(theOne.getBacklinks<Source>('oneTarget').map((s) => s.name), expectedSources);
expect(theOne.getBacklinks<Source>('et mål').map((s) => s.name), expectedSources);

expect(theOne.getBacklinks<Source>('dynamicTarget').map((s) => s.name), expectedSources);
expect(theOne.getBacklinks<Source>('dynamisk mål').map((s) => s.name), expectedSources);

for (final t in targets) {
expect(t.getBacklinks<Source>('manyTargets').map((s) => s.name), expectedSources);
expect(t.getBacklinks<Source>('dynamicManyTargets').map((s) => s.name), expectedSources);
}
});

test('notifications', () {
final config = Configuration.local([Target.schema, Source.schema]);
final realm = getRealm(config);

final target = realm.write(() => realm.add(Target()));

expectLater(
target.getBacklinks<Source>('oneTarget').changes,
emitsInOrder(<Matcher>[
isA<RealmResultsChanges<Source>>().having((ch) => ch.inserted, 'inserted', <int>[]),
isA<RealmResultsChanges<Source>>().having((ch) => ch.inserted, 'inserted', [0]),
isA<RealmResultsChanges<Source>>().having((ch) => ch.inserted, 'inserted', [1]),
isA<RealmResultsChanges<Source>>() //
.having((ch) => ch.inserted, 'inserted', [0, 2]) // is this surprising?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That backlinks does not have a "natural" order, you mean?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I copied the test, so didn't take a look at the comment. I can remove it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. My own comment I see 😊

.having((ch) => ch.deleted, 'deleted', [0]) //
.having((ch) => ch.modified, 'modified', [1]),
]));

final first = realm.write(() => realm.add(Source(oneTarget: target)));

final second = realm.write(() => realm.add(Source(oneTarget: target)));

realm.write(() {
realm.add(Source(oneTarget: target));
realm.add(Source(oneTarget: target));
second.name = "changed second";
realm.delete(first);
});
});

test('pointing to a non-existent property throws', () {
final (theOne, _, _) = populateData();

expect(() => theOne.getBacklinks<Source>('foo'),
throwsA(isA<RealmException>().having((p0) => p0.message, 'message', 'Property foo does not exist on class Source')));
});

test('on an unmanaged object throws', () {
final theOne = Target(name: 'the one');
expect(() => theOne.getBacklinks<Source>('oneTarget'),
throwsA(isA<RealmStateError>().having((p0) => p0.message, 'message', "Can't look up backlinks of unmanaged objects.")));
});

test('on a deleted object throws', () {
final (theOne, _, _) = populateData();
theOne.realm.write(() => theOne.realm.delete(theOne));

expect(theOne.isValid, false);
expect(theOne.isManaged, true);

expect(
() => theOne.getBacklinks<Source>('oneTarget'),
throwsA(
isA<RealmException>().having((p0) => p0.message, 'message', contains("Accessing object of type Target which has been invalidated or deleted."))));
});

test('with a dynamic type argument throws', () {
final (theOne, _, _) = populateData();
expect(() => theOne.getBacklinks('oneTarget'),
throwsA(isA<RealmError>().having((p0) => p0.message, 'message', contains("Object type dynamic not configured in the current Realm's schema."))));
});

test('with an invalid type argument throws', () {
final (theOne, _, _) = populateData();
expect(() => theOne.getBacklinks('oneTarget'),
throwsA(isA<RealmError>().having((p0) => p0.message, 'message', contains("Object type dynamic not configured in the current Realm's schema."))));
});

test('pointing to a non-link property throws', () {
final (theOne, _, _) = populateData();

expect(
() => theOne.getBacklinks<Source>('name'),
throwsA(isA<RealmError>()
.having((p0) => p0.message, 'message', 'Property Source.name is not a link property - it is a property of type RealmPropertyType.string')));
});

test('pointing to a link property of incorrect type throws', () {
final (theOne, _, _) = populateData();

expect(
() => theOne.getBacklinks<Target>('source'),
throwsA(isA<RealmError>().having((p0) => p0.message, 'message',
'Property Target.source is a link property that links to Source which is different from the type of the current object, which is Target.')));
});
});
}
34 changes: 34 additions & 0 deletions test/backlinks_test.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading