diff --git a/CHANGELOG.md b/CHANGELOG.md index 20507a2ca..4bb92d3fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed * Added more validations when using `User.apiKeys` to return more meaningful errors when the user cannot perform API key actions - e.g. when the user has been logged in with API key credentials or when the user has been logged out. (Issue [#950](https://github.com/realm/realm-dart/issues/950)) * Fixed `dart run realm_dart generate` and `flutter pub run realm generate` commands to exit with the correct error code on failure. +* Added more descriptive error messages when passing objects managed by another Realm as arguments to `Realm.add/delete/deleteMany`. (PR [#942](https://github.com/realm/realm-dart/pull/942)) ### Compatibility * Realm Studio: 12.0.0 or later. diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 53e39f738..534c9dff4 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -111,14 +111,13 @@ class Realm implements Finalizable { /// Gets a value indicating whether this [Realm] is frozen. Frozen Realms are immutable /// and will not update when writes are made to the database. - late final bool isFrozen; + late final bool isFrozen = realmCore.isFrozen(this); /// Opens a `Realm` using a [Configuration] object. Realm(Configuration config) : this._(config); Realm._(this.config, [RealmHandle? handle, this._isInMigration = false]) : _handle = handle ?? _openRealm(config) { _populateMetadata(); - isFrozen = realmCore.isFrozen(this); } static RealmHandle _openRealm(Configuration config) { @@ -177,6 +176,8 @@ class Realm implements Finalizable { /// Throws [RealmException] if there is no write transaction created with [write]. T add(T object, {bool update = false}) { if (object.isManaged) { + _ensureManagedByThis(object, 'add object to Realm'); + return object; } @@ -216,19 +217,31 @@ class Realm implements Finalizable { } /// Deletes a [RealmObject] from this `Realm`. - void delete(T object) => realmCore.deleteRealmObject(object); + void delete(T object) { + if (!object.isManaged) { + throw RealmError('Cannot delete an unmanaged object'); + } + + _ensureManagedByThis(object, 'delete object from Realm'); + + realmCore.deleteRealmObject(object); + } /// Deletes many [RealmObject]s from this `Realm`. /// /// Throws [RealmException] if there is no active write transaction. void deleteMany(Iterable items) { if (items is RealmResults) { + _ensureManagedByThis(items, 'delete objects from Realm'); + realmCore.resultsDeleteAll(items); } else if (items is RealmList) { + _ensureManagedByThis(items, 'delete objects from Realm'); + realmCore.listDeleteAll(items); } else { for (T realmObject in items) { - realmCore.deleteRealmObject(realmObject); + delete(realmObject); } } } @@ -417,6 +430,16 @@ class Realm implements Finalizable { /// This is a workaround of a Dart VM bug and will be removed in a future version of the SDK. static void shutdown() => scheduler.stop(); + void _ensureManagedByThis(RealmEntity entity, String operation) { + if (entity.realm != this) { + if (entity.isFrozen) { + throw RealmError('Cannot $operation because the object is managed by a frozen Realm'); + } + + throw RealmError('Cannot $operation because the object is managed by another Realm instance'); + } + } + void _ensureWritable() { if (isFrozen) { throw RealmError('Starting a write transaction on a frozen Realm is not allowed.'); diff --git a/test/realm_test.dart b/test/realm_test.dart index 960d71c8c..773e05648 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -816,6 +816,145 @@ Future main([List? args]) async { expect(stored.location.name, 'Europe/Copenhagen'); }); + test('Realm.add with frozen object argument throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final frozenPeter = freezeObject(realm.write(() { + return realm.add(Person('Peter')); + })); + + realm.write(() { + expect(() => realm.add(frozenPeter), throws('Cannot add object to Realm because the object is managed by a frozen Realm')); + }); + }); + + test('Realm.delete frozen object throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final frozenPeter = freezeObject(realm.write(() { + return realm.add(Person('Peter')); + })); + + realm.write(() { + expect(() => realm.delete(frozenPeter), throws('Cannot delete object from Realm because the object is managed by a frozen Realm')); + }); + }); + + test('Realm.delete unmanaged object throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + realm.write(() { + expect(() => realm.delete(Person('Peter')), throws('Cannot delete an unmanaged object')); + }); + }); + + test('Realm.deleteMany frozen results throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + realm.write(() { + realm.add(Person('Peter')); + }); + + final frozenPeople = freezeResults(realm.all()); + + realm.write(() { + expect(() => realm.deleteMany(frozenPeople), throws('Cannot delete objects from Realm because the object is managed by a frozen Realm')); + }); + }); + + test('Realm.deleteMany frozen list throws', () { + final realm = getRealm(Configuration.local([Person.schema, Team.schema])); + final team = realm.write(() { + return realm.add(Team('Team 1', players: [Person('Peter')])); + }); + + final frozenPlayers = freezeList(team.players); + + realm.write(() { + expect(() => realm.deleteMany(frozenPlayers), throws('Cannot delete objects from Realm because the object is managed by a frozen Realm')); + }); + }); + + test('Realm.deleteMany regular list with frozen elements throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final peter = realm.write(() { + return realm.add(Person('Peter')); + }); + + final frozenPeter = freezeObject(peter); + + realm.write(() { + expect( + () => realm.deleteMany([peter, frozenPeter]), throws('Cannot delete object from Realm because the object is managed by a frozen Realm')); + }); + }); + + test('Realm.add with object from another Realm throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final otherRealm = getRealm(Configuration.local([Person.schema])); + + final peter = realm.write(() { + return realm.add(Person('Peter')); + }); + + otherRealm.write(() { + expect(() => otherRealm.add(peter), throws('Cannot add object to Realm because the object is managed by another Realm instance')); + }); + }); + + test('Realm.delete object from another Realm throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final otherRealm = getRealm(Configuration.local([Person.schema])); + + final peter = realm.write(() { + return realm.add(Person('Peter')); + }); + + otherRealm.write(() { + expect(() => otherRealm.delete(peter), throws('Cannot delete object from Realm because the object is managed by another Realm instance')); + }); + }); + + test('Realm.deleteMany results from another Realm throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final otherRealm = getRealm(Configuration.local([Person.schema])); + + realm.write(() { + realm.add(Person('Peter')); + }); + + final people = realm.all(); + + otherRealm.write(() { + expect( + () => otherRealm.deleteMany(people), throws('Cannot delete objects from Realm because the object is managed by another Realm instance')); + }); + }); + + test('Realm.deleteMany list from another Realm throws', () { + final realm = getRealm(Configuration.local([Person.schema, Team.schema])); + final otherRealm = getRealm(Configuration.local([Person.schema])); + + final team = realm.write(() { + return realm.add(Team('Team 1', players: [Person('Peter')])); + }); + + otherRealm.write(() { + expect(() => otherRealm.deleteMany(team.players), + throws('Cannot delete objects from Realm because the object is managed by another Realm instance')); + }); + }); + + test('Realm.deleteMany regular list with elements from another Realm throws', () { + final realm = getRealm(Configuration.local([Person.schema])); + final otherRealm = getRealm(Configuration.local([Person.schema])); + + final peter = realm.write(() { + return realm.add(Person('Peter')); + }); + + otherRealm.write(() { + expect( + () => otherRealm.deleteMany([peter]), throws('Cannot delete object from Realm because the object is managed by another Realm instance')); + }); + }); + test('Realm - encryption works', () { var config = Configuration.local([Friend.schema], path: p.join(Configuration.defaultStoragePath, "${generateRandomString(8)}.realm")); var realm = getRealm(config);