diff --git a/lib/src/cli/common/archive.dart b/lib/src/cli/common/archive.dart index 856101cf8..0fb412bd4 100644 --- a/lib/src/cli/common/archive.dart +++ b/lib/src/cli/common/archive.dart @@ -24,7 +24,7 @@ class Archive { // Create an archive of files Future archive(Directory sourceDir, File outputFile) async { if (!await sourceDir.exists()) { - throw Exception("Source directory $sourceDir does not exists"); + throw Exception("Source directory $sourceDir does not exist"); } await findEntries(sourceDir).transform(tarWriter).transform(gzip.encoder).pipe(outputFile.openWrite()); diff --git a/lib/src/list.dart b/lib/src/list.dart index 8576ade14..0df712f6e 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -30,7 +30,7 @@ import 'results.dart'; /// added to or deleted from the collection or from the Realm. /// /// {@category Realm} -abstract class RealmList with RealmEntity implements List, Finalizable { +abstract class RealmList with RealmEntity implements List, Finalizable { late final RealmObjectMetadata? _metadata; /// Gets a value indicating whether this collection is still valid to use. @@ -44,7 +44,7 @@ abstract class RealmList with RealmEntity implements List, factory RealmList(Iterable items) => UnmanagedRealmList(items); } -class ManagedRealmList extends collection.ListBase with RealmEntity implements RealmList { +class ManagedRealmList extends collection.ListBase with RealmEntity implements RealmList { final RealmListHandle _handle; @override @@ -97,7 +97,7 @@ class ManagedRealmList extends collection.ListBase with Rea bool get isValid => realmCore.listIsValid(this); } -class UnmanagedRealmList extends collection.ListBase with RealmEntity implements RealmList { +class UnmanagedRealmList extends collection.ListBase with RealmEntity implements RealmList { final _unmanaged = []; // use T? for length= UnmanagedRealmList([Iterable? items]) { @@ -156,7 +156,7 @@ extension RealmListOfObject on RealmList { } /// @nodoc -extension RealmListInternal on RealmList { +extension RealmListInternal on RealmList { @pragma('vm:never-inline') void keepAlive() { final self = this; @@ -170,7 +170,7 @@ extension RealmListInternal on RealmList { RealmListHandle get handle => asManaged()._handle; - static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); + static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); static void setValue(RealmListHandle handle, Realm realm, int index, Object? value, {bool update = false}) { if (index < 0) { diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 5fb1cce95..0ca9df8e0 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -572,7 +572,9 @@ class _RealmCore { final property = propertiesPtr.elementAt(i); final propertyName = property.ref.name.cast().toRealmDartString()!; final objectType = property.ref.link_target.cast().toRealmDartString(treatEmptyAsNull: true); - final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, RealmCollectionType.values.elementAt(property.ref.collection_type)); + final isNullable = property.ref.flags & realm_property_flags.RLM_PROPERTY_NULLABLE != 0; + final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, RealmPropertyType.values.elementAt(property.ref.type), isNullable, + RealmCollectionType.values.elementAt(property.ref.collection_type)); result[propertyName] = propertyMeta; } return result; @@ -2053,7 +2055,7 @@ extension on Pointer { case realm_value_type.RLM_TYPE_INT: return ref.values.integer; case realm_value_type.RLM_TYPE_BOOL: - return ref.values.boolean != 0; + return ref.values.boolean; case realm_value_type.RLM_TYPE_STRING: return ref.values.string.data.cast().toRealmDartString(length: ref.values.string.size)!; case realm_value_type.RLM_TYPE_FLOAT: diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 3964ac138..9ae59d7d0 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -76,7 +76,7 @@ export "configuration.dart" export 'credentials.dart' show Credentials, AuthProviderType, EmailPasswordAuthProvider; export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges; -export 'realm_object.dart' show RealmEntity, RealmException, RealmObject, RealmObjectChanges; +export 'realm_object.dart' show RealmEntity, RealmException, RealmObject, RealmObjectChanges, DynamicRealmObject; export 'realm_property.dart'; export 'results.dart' show RealmResults, RealmResultsChanges; export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet; @@ -389,8 +389,8 @@ extension RealmInternal on Realm { return RealmObjectInternal.create(type, this, handle, accessor); } - RealmList createList(RealmListHandle handle, RealmObjectMetadata? metadata) { - return RealmListInternal.create(handle, this, metadata); + RealmList createList(RealmListHandle handle, RealmObjectMetadata? metadata) { + return RealmListInternal.create(handle, this, metadata); } List getPropertyNames(Type type, List propertyKeys) { diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index bdd804877..95c7616ef 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -23,8 +23,10 @@ import 'list.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; +typedef DartDynamic = dynamic; + abstract class RealmAccessor { - Object? get(RealmObject object, String name); + Object? get(RealmObject object, String name); void set(RealmObject object, String name, Object? value, {bool isDefault = false, bool update = false}); static final Map> _defaultValues = >{}; @@ -60,7 +62,7 @@ class RealmValuesAccessor implements RealmAccessor { final Map _values = {}; @override - Object? get(RealmObject object, String name) { + Object? get(RealmObject object, String name) { if (!_values.containsKey(name)) { return RealmAccessor.getDefaultValue(object.runtimeType, name); } @@ -104,7 +106,7 @@ class RealmObjectMetadata { RealmObjectMetadata(this.name, this.type, this.primaryKey, this.classKey, this._propertyKeys); RealmPropertyMetadata operator [](String propertyName) => - _propertyKeys[propertyName] ?? (throw RealmException("Property $propertyName does not exists on class $_realmObjectTypeName")); + _propertyKeys[propertyName] ?? (throw RealmException("Property $propertyName does not exist on class $_realmObjectTypeName")); String? getPropertyName(int propertyKey) { for (final entry in _propertyKeys.entries) { @@ -119,8 +121,10 @@ class RealmObjectMetadata { class RealmPropertyMetadata { final int key; final RealmCollectionType collectionType; + final RealmPropertyType propertyType; + final bool isNullable; final String? objectType; - const RealmPropertyMetadata(this.key, this.objectType, [this.collectionType = RealmCollectionType.none]); + const RealmPropertyMetadata(this.key, this.objectType, this.propertyType, this.isNullable, [this.collectionType = RealmCollectionType.none]); } class RealmCoreAccessor implements RealmAccessor { @@ -129,12 +133,20 @@ class RealmCoreAccessor implements RealmAccessor { RealmCoreAccessor(this.metadata); @override - Object? get(RealmObject object, String name) { + Object? get(RealmObject object, String name) { try { final propertyMeta = metadata[name]; if (propertyMeta.collectionType == RealmCollectionType.list) { final handle = realmCore.getListProperty(object, propertyMeta.key); final listMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); + + // listMetadata is not null when we have list of RealmObjects. If the API was + // called with a generic object arg - get we construct a list of + // RealmObjects since we don't know the type of the object. + if (listMetadata != null && _isTypeGenericObject()) { + return object.realm.createList(handle, listMetadata); + } + return object.realm.createList(handle, listMetadata); } @@ -142,6 +154,13 @@ class RealmCoreAccessor implements RealmAccessor { if (value is RealmObjectHandle) { final targetMetadata = propertyMeta.objectType != null ? object.realm.metadata.getByName(propertyMeta.objectType!) : object.realm.metadata.getByType(T); + + // If we have an object but the user called the API without providing a generic + // arg, we construct a RealmObject since we don't know the type of the object. + if (_isTypeGenericObject()) { + return object.realm.createObject(RealmObject, value, targetMetadata); + } + return object.realm.createObject(T, value, targetMetadata); } @@ -199,11 +218,14 @@ mixin RealmObject on RealmEntity implements Finalizable { RealmObjectHandle? _handle; RealmAccessor _accessor = RealmValuesAccessor(); static final Map _factories = { - RealmObject: () => DynamicRealmObject._(), + // Register default factories for `RealmObject` and `RealmObject?`. Whenever the user + // asks for these types, we'll use the ConcreteRealmObject implementation. + RealmObject: () => _ConcreteRealmObject(), + _typeOf(): () => _ConcreteRealmObject(), }; /// @nodoc - static Object? get(RealmObject object, String name) { + static Object? get(RealmObject object, String name) { return object._accessor.get(object, name); } @@ -213,7 +235,12 @@ mixin RealmObject on RealmEntity implements Finalizable { } /// @nodoc - static void registerFactory(T Function() factory) => _factories.putIfAbsent(T, () => factory); + static void registerFactory(T Function() factory) { + // We register a factory for both the type itself, but also the nullable + // version of the type. + _factories.putIfAbsent(T, () => factory); + _factories.putIfAbsent(_typeOf(), () => factory); + } /// @nodoc static T create() { @@ -261,6 +288,31 @@ mixin RealmObject on RealmEntity implements Finalizable { final controller = RealmObjectNotificationsController(object); return controller.createStream(); } + + // invocation.memberName in noSuchMethod is a Symbol, which hides its _name field. The idiomatic + // way to obtain it is via Mirrors, which is not available in Flutter. Symbol.toString returns + // Symbol("name"), so we use a simple regex to extract the symbol name. This is a bit fragile, but + // is the approach used by the Flutter team as well: https://github.com/dart-lang/sdk/issues/28372. + // If it turns out not to be reliable, we can instead construct symbols from the property names in + // the Accessor metadata and compare symbols directly. + static final RegExp _symbolRegex = RegExp('Symbol\\("(?.*)"\\)'); + + @override + DartDynamic noSuchMethod(Invocation invocation) { + if (invocation.isGetter) { + final name = _symbolRegex.firstMatch(invocation.memberName.toString())?.namedGroup("symbolName"); + if (name == null) { + throw RealmError( + "Could not find symbol name for ${invocation.memberName}. This is likely a bug in the Realm SDK - please file an issue at https://github.com/realm/realm-dart/issues"); + } + + return get(this, name); + } + + return super.noSuchMethod(invocation); + } + + late final DynamicRealmObject dynamic = DynamicRealmObject._(this); } /// @nodoc @@ -379,6 +431,88 @@ class RealmObjectNotificationsController extends Notifica } /// @nodoc -class DynamicRealmObject with RealmEntity, RealmObject { - DynamicRealmObject._(); +class _ConcreteRealmObject with RealmEntity, RealmObject {} + +// This is necessary whenever we need to pass T? as the type. +Type _typeOf() => T; + +bool _isTypeGenericObject() => T == Object || T == _typeOf(); + +/// Exposes a set of dynamic methods on the RealmObject type. These allow you to +/// access properties by name rather than via the strongly typed API. +/// +/// {@category Realm} +class DynamicRealmObject { + final RealmObject _obj; + + DynamicRealmObject._(this._obj); + + /// Gets a property by its name. If a generic type is specified, the property + /// type will be validated against the type. Otherwise the result will be wrapped + /// in [Object]. + T get(String name) { + _validatePropertyType(name, RealmCollectionType.none); + return RealmObject.get(_obj, name) as T; + } + + /// Gets a list by the property name. If a generic type is specified, the property + /// type will be validated against the type. Otherwise, a `List` will be + /// returned. + List getList(String name) { + _validatePropertyType(name, RealmCollectionType.list); + return RealmObject.get(_obj, name) as List; + } + + RealmPropertyMetadata? _validatePropertyType(String name, RealmCollectionType expectedCollectionType) { + final accessor = _obj.accessor; + if (accessor is RealmCoreAccessor) { + final prop = accessor.metadata._propertyKeys[name]; + if (prop == null) { + throw RealmException("Property '$name' does not exist on class '${accessor.metadata.name}'"); + } + + if (prop.collectionType != expectedCollectionType) { + throw RealmException( + "Property '$name' on class '${accessor.metadata.name}' is '${prop.collectionType}' but the method used to access it expected '$expectedCollectionType'."); + } + + // If the user passed in a type argument, we should validate its nullability; if they invoked + // the method without a type arg, we don't + if (T != _typeOf() && prop.isNullable != null is T) { + throw RealmException( + "Property '$name' on class '${accessor.metadata.name}' is ${prop.isNullable ? 'nullable' : 'required'} but the generic argument passed to get is $T."); + } + + final targetType = _getPropertyType(); + if (targetType != null && targetType != prop.propertyType) { + throw RealmException( + "Property '$name' on class '${accessor.metadata.name}' is not the correct type. Expected '$targetType', got '${prop.propertyType}'."); + } + + return prop; + } + + return null; + } + + static final _propertyTypeMap = { + int: RealmPropertyType.int, + _typeOf(): RealmPropertyType.int, + double: RealmPropertyType.double, + _typeOf(): RealmPropertyType.double, + String: RealmPropertyType.string, + _typeOf(): RealmPropertyType.string, + bool: RealmPropertyType.bool, + _typeOf(): RealmPropertyType.bool, + DateTime: RealmPropertyType.timestamp, + _typeOf(): RealmPropertyType.timestamp, + ObjectId: RealmPropertyType.objectid, + _typeOf(): RealmPropertyType.objectid, + Uuid: RealmPropertyType.uuid, + _typeOf(): RealmPropertyType.uuid, + RealmObject: RealmPropertyType.object, + _typeOf(): RealmPropertyType.object, + }; + + RealmPropertyType? _getPropertyType() => _propertyTypeMap[T]; } diff --git a/test/dynamic_realm_test.dart b/test/dynamic_realm_test.dart index dac25db2f..2948b2450 100644 --- a/test/dynamic_realm_test.dart +++ b/test/dynamic_realm_test.dart @@ -69,8 +69,117 @@ Future main([List? args]) async { expect(dynamic1, same(dynamic2)); }); + final date = DateTime.now().toUtc(); + final objectId = ObjectId(); + final uuid = Uuid.v4(); + + AllTypes _getPopulatedAllTypes() => AllTypes('abc', true, date, -123.456, objectId, uuid, -987, + nullableStringProp: 'def', + nullableBoolProp: true, + nullableDateProp: date, + nullableDoubleProp: -123.456, + nullableObjectIdProp: objectId, + nullableUuidProp: uuid, + nullableIntProp: 123); + + AllTypes _getEmptyAllTypes() => AllTypes('', false, DateTime(0).toUtc(), 0, objectId, uuid, 0); + + AllCollections _getPopulatedAllCollections() => AllCollections( + strings: ['abc', 'def'], + bools: [true, false], + dates: [date, DateTime(0).toUtc()], + doubles: [-123.456, 555.666], + objectIds: [objectId, objectId], + uuids: [uuid, uuid], + ints: [-987, 123]); + + void _validateDynamic(RealmObject actual, AllTypes expected) { + expect(actual.dynamic.get('stringProp'), expected.stringProp); + expect(actual.dynamic.get('stringProp'), expected.stringProp); + expect(actual.dynamic.get('nullableStringProp'), expected.nullableStringProp); + expect(actual.dynamic.get('nullableStringProp'), expected.nullableStringProp); + + expect(actual.dynamic.get('boolProp'), expected.boolProp); + expect(actual.dynamic.get('boolProp'), expected.boolProp); + expect(actual.dynamic.get('nullableBoolProp'), expected.nullableBoolProp); + expect(actual.dynamic.get('nullableBoolProp'), expected.nullableBoolProp); + + expect(actual.dynamic.get('dateProp'), expected.dateProp); + expect(actual.dynamic.get('dateProp'), expected.dateProp); + expect(actual.dynamic.get('nullableDateProp'), expected.nullableDateProp); + expect(actual.dynamic.get('nullableDateProp'), expected.nullableDateProp); + + expect(actual.dynamic.get('doubleProp'), expected.doubleProp); + expect(actual.dynamic.get('doubleProp'), expected.doubleProp); + expect(actual.dynamic.get('nullableDoubleProp'), expected.nullableDoubleProp); + expect(actual.dynamic.get('nullableDoubleProp'), expected.nullableDoubleProp); + + expect(actual.dynamic.get('objectIdProp'), expected.objectIdProp); + expect(actual.dynamic.get('objectIdProp'), expected.objectIdProp); + expect(actual.dynamic.get('nullableObjectIdProp'), expected.nullableObjectIdProp); + expect(actual.dynamic.get('nullableObjectIdProp'), expected.nullableObjectIdProp); + + expect(actual.dynamic.get('uuidProp'), expected.uuidProp); + expect(actual.dynamic.get('uuidProp'), expected.uuidProp); + expect(actual.dynamic.get('nullableUuidProp'), expected.nullableUuidProp); + expect(actual.dynamic.get('nullableUuidProp'), expected.nullableUuidProp); + + expect(actual.dynamic.get('intProp'), expected.intProp); + expect(actual.dynamic.get('intProp'), expected.intProp); + expect(actual.dynamic.get('nullableIntProp'), expected.nullableIntProp); + expect(actual.dynamic.get('nullableIntProp'), expected.nullableIntProp); + + dynamic actualDynamic = actual; + expect(actualDynamic.stringProp, expected.stringProp); + expect(actualDynamic.nullableStringProp, expected.nullableStringProp); + expect(actualDynamic.boolProp, expected.boolProp); + expect(actualDynamic.nullableBoolProp, expected.nullableBoolProp); + expect(actualDynamic.dateProp, expected.dateProp); + expect(actualDynamic.nullableDateProp, expected.nullableDateProp); + expect(actualDynamic.doubleProp, expected.doubleProp); + expect(actualDynamic.nullableDoubleProp, expected.nullableDoubleProp); + expect(actualDynamic.objectIdProp, expected.objectIdProp); + expect(actualDynamic.nullableObjectIdProp, expected.nullableObjectIdProp); + expect(actualDynamic.uuidProp, expected.uuidProp); + expect(actualDynamic.nullableUuidProp, expected.nullableUuidProp); + expect(actualDynamic.intProp, expected.intProp); + expect(actualDynamic.nullableIntProp, expected.nullableIntProp); + } + + void _validateDynamicLists(RealmObject actual, AllCollections expected) { + expect(actual.dynamic.getList('strings'), expected.strings); + expect(actual.dynamic.getList('strings'), expected.strings); + + expect(actual.dynamic.getList('bools'), expected.bools); + expect(actual.dynamic.getList('bools'), expected.bools); + + expect(actual.dynamic.getList('dates'), expected.dates); + expect(actual.dynamic.getList('dates'), expected.dates); + + expect(actual.dynamic.getList('doubles'), expected.doubles); + expect(actual.dynamic.getList('doubles'), expected.doubles); + + expect(actual.dynamic.getList('objectIds'), expected.objectIds); + expect(actual.dynamic.getList('objectIds'), expected.objectIds); + + expect(actual.dynamic.getList('uuids'), expected.uuids); + expect(actual.dynamic.getList('uuids'), expected.uuids); + + expect(actual.dynamic.getList('ints'), expected.ints); + expect(actual.dynamic.getList('ints'), expected.ints); + + dynamic actualDynamic = actual; + expect(actualDynamic.strings, expected.strings); + expect(actualDynamic.bools, expected.bools); + expect(actualDynamic.dates, expected.dates); + expect(actualDynamic.doubles, expected.doubles); + expect(actualDynamic.objectIds, expected.objectIds); + expect(actualDynamic.uuids, expected.uuids); + expect(actualDynamic.ints, expected.ints); + } + for (var isDynamic in [true, false]) { - Realm getDynamicRealm(Realm original) { + Realm _getDynamicRealm(Realm original) { if (isDynamic) { original.close(); return getRealm(Configuration.local([])); @@ -79,119 +188,430 @@ Future main([List? args]) async { return original; } - test('dynamic.all (dynamic=$isDynamic) returns empty collection', () { - final config = Configuration.local([Car.schema]); - final staticRealm = getRealm(config); + group('Realm.dynamic when isDynamic=$isDynamic', () { + test('all returns empty collection', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); - final realm = getDynamicRealm(staticRealm); - final allCars = realm.dynamic.all(Car.schema.name); - expect(allCars.length, 0); - }); + final realm = _getDynamicRealm(staticRealm); + final allCars = realm.dynamic.all(Car.schema.name); + expect(allCars.length, 0); + }); + + test('all returns non-empty collection', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(Car('Honda')); + }); - test('dynamic.all (dynamic=$isDynamic) returns non-empty collection', () { - final config = Configuration.local([Car.schema]); - final staticRealm = getRealm(config); - staticRealm.write(() { - staticRealm.add(Car('Honda')); + final realm = _getDynamicRealm(staticRealm); + final allCars = realm.dynamic.all(Car.schema.name); + expect(allCars.length, 1); + + final car = allCars[0]; + expect(car.dynamic.get('make'), 'Honda'); }); - final realm = getDynamicRealm(staticRealm); - final allCars = realm.dynamic.all(Car.schema.name); - expect(allCars.length, 1); + test('all throws for non-existent type', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); - final car = allCars[0]; - expect(RealmObject.get(car, 'make'), 'Honda'); - }); + final dynamicRealm = _getDynamicRealm(staticRealm); - test('dynamic.all (dynamic=$isDynamic) throws for non-existent type', () { - final config = Configuration.local([Car.schema]); - final staticRealm = getRealm(config); + expect(() => dynamicRealm.dynamic.all('i-dont-exist'), throws("Object type i-dont-exist not configured in the current Realm's schema")); + }); - final dynamicRealm = getDynamicRealm(staticRealm); + test('all can follow links', () { + final config = Configuration.local([LinksClass.schema]); + final staticRealm = getRealm(config); - expect(() => dynamicRealm.dynamic.all('i-dont-exist'), throws("Object type i-dont-exist not configured in the current Realm's schema")); - }); + final id1 = Uuid.v4(); + final id2 = Uuid.v4(); + final id3 = Uuid.v4(); + + staticRealm.write(() { + final obj1 = staticRealm.add(LinksClass(id1)); + final obj2 = staticRealm.add(LinksClass(id2)); + final obj3 = staticRealm.add(LinksClass(id3)); + + obj1.link = obj2; + obj2.link = obj3; + + obj1.list.addAll([obj1, obj2, obj3]); + }); - test('dynamic.all (dynamic=$isDynamic) can follow links', () { - final config = Configuration.local([LinksClass.schema]); - final staticRealm = getRealm(config); + final dynamicRealm = _getDynamicRealm(staticRealm); - final id1 = Uuid.v4(); - final id2 = Uuid.v4(); - final id3 = Uuid.v4(); + final objects = dynamicRealm.dynamic.all(LinksClass.schema.name); + final obj1 = objects.singleWhere((o) => o.dynamic.get('id') == id1); + final obj2 = objects.singleWhere((o) => o.dynamic.get('id') == id2); + final obj3 = objects.singleWhere((o) => o.dynamic.get('id') == id3); - staticRealm.write(() { - final obj1 = staticRealm.add(LinksClass(id1)); - final obj2 = staticRealm.add(LinksClass(id2)); - final obj3 = staticRealm.add(LinksClass(id3)); + expect(obj1.dynamic.get('link'), obj2); + expect(obj2.dynamic.get('link'), obj3); - obj1.link = obj2; - obj2.link = obj3; + final list = obj1.dynamic.getList('list'); - obj1.list.addAll([obj1, obj2, obj3]); + expect(list[0], obj1); + expect(list[1], obj2); + expect(list[2], obj3); }); - final dynamicRealm = getDynamicRealm(staticRealm); + test('all can be filtered', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); - final objects = dynamicRealm.dynamic.all(LinksClass.schema.name); - final obj1 = objects.singleWhere((o) => RealmObject.get(o, 'id') as Uuid == id1); - final obj2 = objects.singleWhere((o) => RealmObject.get(o, 'id') as Uuid == id2); - final obj3 = objects.singleWhere((o) => RealmObject.get(o, 'id') as Uuid == id3); + staticRealm.write(() { + staticRealm.add(Car('Honda')); + staticRealm.add(Car('Hyundai')); + staticRealm.add(Car('Suzuki')); + staticRealm.add(Car('Toyota')); + }); - expect(RealmObject.get(obj1, 'link'), obj2); - expect(RealmObject.get(obj2, 'link'), obj3); + final dynamicRealm = _getDynamicRealm(staticRealm); - final list = RealmObject.get(obj1, 'list') as List; + final carsWithH = dynamicRealm.dynamic.all(Car.schema.name).query('make BEGINSWITH "H"'); + expect(carsWithH.length, 2); + }); - expect(list[0], obj1); - expect(list[1], obj2); - expect(list[2], obj3); - }); + test('find can find by primary key', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); + + staticRealm.write(() { + staticRealm.add(Car('Honda')); + staticRealm.add(Car('Hyundai')); + }); - test('dynamic.all (dynamic=$isDynamic) can be filtered', () { - final config = Configuration.local([Car.schema]); - final staticRealm = getRealm(config); + final dynamicRealm = _getDynamicRealm(staticRealm); - staticRealm.write(() { - staticRealm.add(Car('Honda')); - staticRealm.add(Car('Hyundai')); - staticRealm.add(Car('Suzuki')); - staticRealm.add(Car('Toyota')); + final car = dynamicRealm.dynamic.find(Car.schema.name, 'Honda'); + expect(car, isNotNull); + expect(car!.dynamic.get('make'), 'Honda'); + + final nonExistent = dynamicRealm.dynamic.find(Car.schema.name, 'i-dont-exist'); + expect(nonExistent, isNull); }); - final dynamicRealm = getDynamicRealm(staticRealm); + test('find fails to find non-existent type', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); + + final dynamicRealm = _getDynamicRealm(staticRealm); - final carsWithH = dynamicRealm.dynamic.all(Car.schema.name).query('make BEGINSWITH "H"'); - expect(carsWithH.length, 2); + expect(() => dynamicRealm.dynamic.find('i-dont-exist', 'i-dont-exist'), + throws("Object type i-dont-exist not configured in the current Realm's schema")); + }); }); - test('dynamic.find (dynamic=$isDynamic) can find by primary key', () { - final config = Configuration.local([Car.schema]); - final staticRealm = getRealm(config); + group('RealmObject.dynamic.get when isDynamic=$isDynamic', () { + test('gets all property types', () { + final config = Configuration.local([AllTypes.schema]); + final staticRealm = getRealm(config); - staticRealm.write(() { - staticRealm.add(Car('Honda')); - staticRealm.add(Car('Hyundai')); + final nonEmpty = _getPopulatedAllTypes(); + final empty = _getEmptyAllTypes(); + + staticRealm.write(() { + staticRealm.add(_getPopulatedAllTypes()); + staticRealm.add(_getEmptyAllTypes()); + }); + + final dynamicRealm = _getDynamicRealm(staticRealm); + final objects = dynamicRealm.dynamic.all(AllTypes.schema.name); + + final obj1 = objects.singleWhere((o) => o.dynamic.get('stringProp') == nonEmpty.stringProp); + final obj2 = objects.singleWhere((o) => o.dynamic.get('stringProp') == empty.stringProp); + + _validateDynamic(obj1, _getPopulatedAllTypes()); + _validateDynamic(obj2, _getEmptyAllTypes()); }); - final dynamicRealm = getDynamicRealm(staticRealm); + test('gets normal links', () { + final config = Configuration.local([LinksClass.schema]); + final staticRealm = getRealm(config); + + final uuid1 = Uuid.v4(); + final uuid2 = Uuid.v4(); + + staticRealm.write(() { + final obj1 = staticRealm.add(LinksClass(uuid1)); + staticRealm.add(LinksClass(uuid2, link: obj1)); + }); + + final dynamicRealm = _getDynamicRealm(staticRealm); - final car = dynamicRealm.dynamic.find(Car.schema.name, 'Honda'); - expect(car, isNotNull); - expect(RealmObject.get(car!, 'make'), 'Honda'); + final obj1 = dynamicRealm.dynamic.find(LinksClass.schema.name, uuid1)!; + final obj2 = dynamicRealm.dynamic.find(LinksClass.schema.name, uuid2)!; - final nonExistent = dynamicRealm.dynamic.find(Car.schema.name, 'i-dont-exist'); - expect(nonExistent, isNull); + expect(obj1.dynamic.get('link'), isNull); + expect(obj1.dynamic.get('link'), isNull); + + expect(obj2.dynamic.get('link'), obj1); + expect(obj2.dynamic.get('link'), obj1); + expect(obj2.dynamic.get('link')?.dynamic.get('id'), uuid1); + + dynamic dynamicObj1 = obj1; + dynamic dynamicObj2 = obj2; + + expect(dynamicObj1.link, isNull); + + expect(dynamicObj2.link, obj1); + expect(dynamicObj2.link.id, uuid1); + }); + + test('fails with non-existent property', () { + final config = Configuration.local([AllTypes.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(_getEmptyAllTypes()); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.all(AllTypes.schema.name).single; + dynamic dynamicObj = obj; + expect(() => obj.dynamic.get('i-dont-exist'), throws("Property 'i-dont-exist' does not exist on class 'AllTypes'")); + expect(() => dynamicObj.idontexist, throws("Property idontexist does not exist on class AllTypes")); + }); + + test('fails with wrong type', () { + final config = Configuration.local([AllTypes.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(_getEmptyAllTypes()); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.all(AllTypes.schema.name).single; + + expect( + () => obj.dynamic.get('stringProp'), + throws( + "Property 'stringProp' on class 'AllTypes' is not the correct type. Expected 'RealmPropertyType.int', got 'RealmPropertyType.string'.")); + + expect( + () => obj.dynamic.get('nullableStringProp'), + throws( + "Property 'nullableStringProp' on class 'AllTypes' is not the correct type. Expected 'RealmPropertyType.int', got 'RealmPropertyType.string'.")); + + expect(() => obj.dynamic.get('nullableIntProp'), + throws("Property 'nullableIntProp' on class 'AllTypes' is nullable but the generic argument passed to get is int.")); + + expect(() => obj.dynamic.get('intProp'), + throws("Property 'intProp' on class 'AllTypes' is required but the generic argument passed to get is int?.")); + }); + + test('fails on collection properties', () { + final config = Configuration.local([AllCollections.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(AllCollections()); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.all(AllCollections.schema.name).single; + expect( + () => obj.dynamic.get('strings'), + throws( + "Property 'strings' on class 'AllCollections' is 'RealmCollectionType.list' but the method used to access it expected 'RealmCollectionType.none'.")); + + expect( + () => obj.dynamic.get('strings'), + throws( + "Property 'strings' on class 'AllCollections' is 'RealmCollectionType.list' but the method used to access it expected 'RealmCollectionType.none'.")); + + expect( + () => obj.dynamic.get('strings'), + throws( + "Property 'strings' on class 'AllCollections' is 'RealmCollectionType.list' but the method used to access it expected 'RealmCollectionType.none'.")); + }); }); - test('dynamic.find (dynamic=$isDynamic) fails to find non-existent type', () { - final config = Configuration.local([Car.schema]); - final staticRealm = getRealm(config); + group('RealmObject.dynamic.getList', () { + test('gets all list types', () { + final config = Configuration.local([AllCollections.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(_getPopulatedAllCollections()); + staticRealm.add(AllCollections()); + }); + + final dynamicRealm = _getDynamicRealm(staticRealm); + final objects = dynamicRealm.dynamic.all(AllCollections.schema.name); + final obj1 = objects.singleWhere((element) => element.dynamic.getList('strings').isNotEmpty); + final obj2 = objects.singleWhere((element) => element.dynamic.getList('strings').isEmpty); + + _validateDynamicLists(obj1, _getPopulatedAllCollections()); + _validateDynamicLists(obj2, AllCollections()); + }); + + test('gets collections of objects', () { + final config = Configuration.local([LinksClass.schema]); + final staticRealm = getRealm(config); + + final uuid1 = Uuid.v4(); + final uuid2 = Uuid.v4(); + + staticRealm.write(() { + final obj1 = staticRealm.add(LinksClass(uuid1)); + staticRealm.add(LinksClass(uuid2, list: [obj1, obj1])); + }); - final dynamicRealm = getDynamicRealm(staticRealm); + final dynamicRealm = _getDynamicRealm(staticRealm); - expect(() => dynamicRealm.dynamic.find('i-dont-exist', 'i-dont-exist'), - throws("Object type i-dont-exist not configured in the current Realm's schema")); + final obj1 = dynamicRealm.dynamic.find(LinksClass.schema.name, uuid1)!; + final obj2 = dynamicRealm.dynamic.find(LinksClass.schema.name, uuid2)!; + + expect(obj1.dynamic.getList('list'), isEmpty); + expect(obj1.dynamic.getList('list'), isEmpty); + + expect(obj2.dynamic.getList('list'), [obj1, obj1]); + expect(obj2.dynamic.getList('list'), [obj1, obj1]); + expect(obj2.dynamic.getList('list')[0].dynamic.get('id'), uuid1); + + dynamic dynamicObj1 = obj1; + dynamic dynamicObj2 = obj2; + + expect(dynamicObj1.list, isEmpty); + + expect(dynamicObj2.list, [obj1, obj1]); + expect(dynamicObj2.list[0].id, uuid1); + }); + + test('fails with non-existent property', () { + final config = Configuration.local([AllCollections.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(AllCollections()); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.all(AllCollections.schema.name).single; + expect(() => obj.dynamic.getList('i-dont-exist'), throws("Property 'i-dont-exist' does not exist on class 'AllCollections'")); + }); + + test('fails with wrong type', () { + final config = Configuration.local([AllCollections.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(AllCollections()); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.all(AllCollections.schema.name).single; + + expect( + () => obj.dynamic.getList('strings'), + throws( + "Property 'strings' on class 'AllCollections' is not the correct type. Expected 'RealmPropertyType.int', got 'RealmPropertyType.string'")); + }); + + test('fails on non-collection properties', () { + final config = Configuration.local([AllTypes.schema]); + final staticRealm = getRealm(config); + staticRealm.write(() { + staticRealm.add(_getEmptyAllTypes()); + }); + final dynamicRealm = _getDynamicRealm(staticRealm); + + final obj = dynamicRealm.dynamic.all(AllTypes.schema.name).single; + expect( + () => obj.dynamic.getList('intProp'), + throws( + "Property 'intProp' on class 'AllTypes' is 'RealmCollectionType.none' but the method used to access it expected 'RealmCollectionType.list'.")); + }); }); } + + test('RealmObject.dynamic.get when static can get all property types', () { + final config = Configuration.local([AllTypes.schema]); + final staticRealm = getRealm(config); + + staticRealm.write(() { + staticRealm.add(_getPopulatedAllTypes()); + staticRealm.add(_getEmptyAllTypes()); + }); + + for (var obj in staticRealm.all()) { + _validateDynamic(obj, obj); + } + }); + + test('RealmObject.dynamic.getList when static can get all list types', () { + final config = Configuration.local([AllCollections.schema]); + final realm = getRealm(config); + + realm.write(() { + realm.add(_getPopulatedAllCollections()); + + realm.add(AllCollections()); + }); + + for (final obj in realm.all()) { + _validateDynamicLists(obj, obj); + } + }); + + test('RealmObject.dynamic.get when static can get links', () { + final config = Configuration.local([LinksClass.schema]); + final realm = getRealm(config); + + final uuid1 = Uuid.v4(); + final uuid2 = Uuid.v4(); + + realm.write(() { + final obj1 = realm.add(LinksClass(uuid1)); + realm.add(LinksClass(uuid2, link: obj1)); + }); + + final obj1 = realm.find(uuid1)!; + final obj2 = realm.find(uuid2)!; + + expect(obj1.dynamic.get('link'), isNull); + expect(obj1.dynamic.get('link'), isNull); + + expect(obj2.dynamic.get('link'), obj1); + expect(obj2.dynamic.get('link'), obj1); + expect(obj2.dynamic.get('link')?.dynamic.get('id'), uuid1); + + dynamic dynamicObj1 = obj1; + dynamic dynamicObj2 = obj2; + + expect(dynamicObj1.link, isNull); + + expect(dynamicObj2.link, obj1); + expect(dynamicObj2.link.id, uuid1); + }); + + test('RealmObject.dynamic.getList when static can get links', () { + final config = Configuration.local([LinksClass.schema]); + final realm = getRealm(config); + + final uuid1 = Uuid.v4(); + final uuid2 = Uuid.v4(); + + realm.write(() { + final obj1 = realm.add(LinksClass(uuid1)); + realm.add(LinksClass(uuid2, list: [obj1, obj1])); + }); + + final obj1 = realm.find(uuid1)!; + final obj2 = realm.find(uuid2)!; + + expect(obj1.dynamic.getList('list'), isEmpty); + expect(obj1.dynamic.getList('list'), isEmpty); + + expect(obj2.dynamic.getList('list'), [obj1, obj1]); + expect(obj2.dynamic.getList('list'), [obj1, obj1]); + expect(obj2.dynamic.getList('list')[0].dynamic.get('id'), uuid1); + + dynamic dynamicObj1 = obj1; + dynamic dynamicObj2 = obj2; + + expect(dynamicObj1.list, isEmpty); + + expect(dynamicObj2.list, [obj1, obj1]); + expect(dynamicObj2.list[0].id, uuid1); + }); }