From 09168af943baf444c84a90ef6a0b47ab02e1dd49 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 25 Apr 2022 22:48:18 +0200 Subject: [PATCH] Expose dynamic Realm API Delete unneeded test Update changelog --- CHANGELOG.md | 8 + .../test/good_test_data/primary_key.dart | 3 +- lib/src/configuration.dart | 29 ++- lib/src/list.dart | 21 +- lib/src/native/realm_core.dart | 76 ++++++- lib/src/realm_class.dart | 151 ++++++++++---- lib/src/realm_object.dart | 50 +++-- lib/src/results.dart | 14 +- test/configuration_test.dart | 4 - test/dynamic_realm_test.dart | 197 ++++++++++++++++++ test/test.dart | 17 ++ test/test.g.dart | 130 +++++++++++- 12 files changed, 591 insertions(+), 109 deletions(-) create mode 100644 test/dynamic_realm_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c3209e8..f2b543746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,19 @@ ## vNext **This project is in the Beta stage. The API should be quite stable, but occasional breaking changes may be made.** + +### Breaking Changes +* Changed the name of `Configuration.schema` to `Configuration.schemaObjects` and changed its type to `Iterable`. You can now access the Realm's schema via the new `Realm.schema` property. [#495](https://github.com/realm/realm-dart/pull/495)) + ### Enhancements * Added `DisconnectedSyncConfiguration` for opening a synchronized realm in a disconnected state. This configuration allows a synchronized realm to be opened by a secondary process, while a primary process handles synchronization. ([#621](https://github.com/realm/realm-dart/pull/621)) * Support better default paths on Flutter. ([#665](https://github.com/realm/realm-dart/pull/665)) * Support `Configuration.defaultRealmName` for setting the default realm name. ([#665](https://github.com/realm/realm-dart/pull/665)) * Support `Configuration.defaultRealmPath` for setting a custom default path for realms. ([#665](https://github.com/realm/realm-dart/pull/665)) * Support `Configuration.defaultStoragePath ` for getting the platform specific storage paths. ([#665](https://github.com/realm/realm-dart/pull/665)) +* Expose an API for string-based access to the objects in the `Realm`. Those are primarily intended to be used during migrations, but are available at all times for advanced use cases. [#495](https://github.com/realm/realm-dart/pull/495)) +* Expose an API for string-based access to the objects in the `Realm`. Those are primarily intended to be used during migrations, but are available at all times for advanced use cases. [#495](https://github.com/realm/realm-dart/pull/495)) +* Added `Realm.schema` property exposing the Realm's schema as passed through the Configuration or read from disk. [#495](https://github.com/realm/realm-dart/pull/495)) ## 0.3.1+beta (2022-06-07) @@ -73,6 +80,7 @@ * Support SyncErrorHandler in FlexibleSyncConfiguration. ([#577](https://github.com/realm/realm-dart/pull/577)) * Support SyncClientResetHandler in FlexibleSyncConfiguration. ([#608](https://github.com/realm/realm-dart/pull/608)) * [Dart] Added `Realm.Shutdown` method to allow normal process exit in Dart applications. ([#617](https://github.com/realm/realm-dart/pull/617)) +* Support logout user. ([#476](https://github.com/realm/realm-dart/pull/476)) ### Fixed * Fixed an issue that would result in the wrong transaction being rolled back if you start a write transaction inside a write transaction. ([#442](https://github.com/realm/realm-dart/issues/442)) diff --git a/generator/test/good_test_data/primary_key.dart b/generator/test/good_test_data/primary_key.dart index d87c9039b..f6e3b531a 100644 --- a/generator/test/good_test_data/primary_key.dart +++ b/generator/test/good_test_data/primary_key.dart @@ -5,6 +5,7 @@ class _IntPK { @PrimaryKey() late int id; } + @RealmModel() class _StringPK { @PrimaryKey() @@ -21,4 +22,4 @@ class _ObjectIdPK { class _UuidPK { @PrimaryKey() late Uuid id; -} \ No newline at end of file +} diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 055062fe8..b2e5414e5 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -48,11 +48,13 @@ typedef InitialDataCallback = void Function(Realm realm); /// Configuration used to create a [Realm] instance /// {@category Configuration} abstract class Configuration { - - /// The default realm filename to be used. + /// The default realm filename to be used. static String get defaultRealmName => _path.basename(defaultRealmPath); static set defaultRealmName(String name) => defaultRealmPath = _path.join(_path.dirname(defaultRealmPath), _path.basename(name)); - + + /// A collection of [SchemaObject] that will be used to construct the + /// [RealmSchema] once the Realm is opened. + final Iterable schemaObjects; /// The platform dependent path used to store realm files /// @@ -63,23 +65,23 @@ abstract class Configuration { /// On Dart standalone Windows, macOS and Linux this is the current directory. static String get defaultStoragePath { if (isFlutterPlatform) { - return realmCore.getAppDirectory(); + return realmCore.getAppDirectory(); } return Directory.current.path; } /// The platform dependent path to the default realm file. - /// + /// /// If set it should contain the path and the name of the realm file. Ex. "~/mypath/myrealm.realm" /// [defaultStoragePath] can be used to build this path. static late String defaultRealmPath = _path.join(defaultStoragePath, 'default.realm'); Configuration._( - List schemaObjects, { + this.schemaObjects, { String? path, this.fifoFilesFallbackPath, - }) : schema = RealmSchema(schemaObjects) { + }) { this.path = path ?? _path.join(_path.dirname(_defaultPath), _path.basename(defaultRealmName)); } @@ -101,9 +103,6 @@ abstract class Configuration { /// If omitted the [defaultPath] for the platform will be used. late final String path; - /// The [RealmSchema] for this [Configuration] - final RealmSchema schema; - //TODO: Not supported yet. // /// The key used to encrypt the entire [Realm]. // /// @@ -286,7 +285,7 @@ extension FlexibleSyncConfigurationInternal on FlexibleSyncConfiguration { /// [DisconnectedSyncConfiguration] is used to open [Realm] instances that are synchronized /// with MongoDB Atlas, without establishing a connection to Atlas App Services. This allows -/// for the synchronized realm to be opened in multiple processes concurrently, as long as +/// for the synchronized realm to be opened in multiple processes concurrently, as long as /// only one of them uses a [FlexibleSyncConfiguration] to sync changes. /// {@category Configuration} class DisconnectedSyncConfiguration extends Configuration { @@ -332,12 +331,8 @@ class RealmSchema extends Iterable { late final List _schema; /// Initializes [RealmSchema] instance representing ```schemaObjects``` collection - RealmSchema(List schemaObjects) { - if (schemaObjects.isEmpty) { - throw RealmError("No schema specified"); - } - - _schema = schemaObjects; + RealmSchema(Iterable schemaObjects) { + _schema = schemaObjects.toList(); } @override diff --git a/lib/src/list.dart b/lib/src/list.dart index 0555e4e95..18f15ae8c 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -30,6 +30,8 @@ import 'results.dart'; /// /// {@category Realm} abstract class RealmList with RealmEntity implements List { + late final RealmObjectMetadata? _metadata; + /// Gets a value indicating whether this collection is still valid to use. /// /// Indicates whether the [Realm] instance hasn't been closed, @@ -37,14 +39,17 @@ abstract class RealmList with RealmEntity implements List { /// and it's parent object hasn't been deleted. bool get isValid; - factory RealmList._(RealmListHandle handle, Realm realm) => ManagedRealmList._(handle, realm); + factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); factory RealmList(Iterable items) => UnmanagedRealmList(items); } class ManagedRealmList extends collection.ListBase with RealmEntity implements RealmList { final RealmListHandle _handle; - ManagedRealmList._(this._handle, Realm realm) { + @override + late final RealmObjectMetadata? _metadata; + + ManagedRealmList._(this._handle, Realm realm, this._metadata) { setRealm(realm); } @@ -67,7 +72,7 @@ class ManagedRealmList extends collection.ListBase with Rea final value = realmCore.listGetElementAt(this, index); if (value is RealmObjectHandle) { - return realm.createObject(T, value) as T; + return realm.createObject(T, value, _metadata!) as T; } return value as T; @@ -100,6 +105,12 @@ class UnmanagedRealmList extends collection.ListBase with R } } + @override + RealmObjectMetadata? get _metadata => throw RealmException("Unmanaged lists don't have metadata associated with them."); + + @override + set _metadata(RealmObjectMetadata? _) => throw RealmException("Unmanaged lists don't have metadata associated with them."); + @override int get length => _unmanaged.length; @@ -132,7 +143,7 @@ extension RealmListOfObject on RealmList { RealmResults query(String query, [List arguments = const []]) { final managedList = asManaged(); final handle = realmCore.queryList(managedList, query, arguments); - return RealmResultsInternal.create(handle, realm); + return RealmResultsInternal.create(handle, realm, _metadata); } /// Allows listening for changes when the contents of this collection changes. @@ -149,7 +160,7 @@ extension RealmListInternal on RealmList { RealmListHandle get handle => asManaged()._handle; - static RealmList create(RealmListHandle handle, Realm realm) => RealmList._(handle, realm); + static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); static void setValue(RealmListHandle handle, Realm realm, int index, Object? value) { if (index < 0) { diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 65f0db32c..4767322b4 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -148,11 +148,14 @@ class _RealmCore { ConfigHandle _createConfig(Configuration config) { return using((Arena arena) { - final schemaHandle = _createSchema(config.schema); final configPtr = _realmLib.realm_config_new(); final configHandle = ConfigHandle._(configPtr); - _realmLib.realm_config_set_schema(configHandle._pointer, schemaHandle._pointer); + if (config.schemaObjects.isNotEmpty) { + final schemaHandle = _createSchema(config.schemaObjects); + _realmLib.realm_config_set_schema(configHandle._pointer, schemaHandle._pointer); + } + _realmLib.realm_config_set_path(configHandle._pointer, config.path.toUtf8Ptr(arena)); _realmLib.realm_config_set_scheduler(configHandle._pointer, scheduler.handle._pointer); @@ -450,6 +453,53 @@ class _RealmCore { return RealmHandle._(realmPtr); } + RealmSchema readSchema(Realm realm) { + return using((Arena arena) { + final classKeys = _getValues( + arena, + (size) => arena(size), + (valuesPtr, size, outSize) => _realmLib.realm_get_class_keys(realm.handle._pointer, valuesPtr, size, outSize), + (values, index) => values.elementAt(index).value); + + final result = classKeys.map((e) => _getSchemaForClassKey(realm, e, arena)).toList(); + return RealmSchema(result); + }); + } + + SchemaObject _getSchemaForClassKey(Realm realm, int classKey, Arena arena) { + final classInfo = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_get_class(realm.handle._pointer, classKey, classInfo)); + + final name = classInfo.ref.name.cast().toDartString(); + + final nativeProperties = _getValues( + arena, + (size) => arena(size), + (valuesPtr, size, outSize) => _realmLib.realm_get_class_properties(realm.handle._pointer, classKey, valuesPtr, size, outSize), + (values, index) => values.elementAt(index).ref); + + final properties = nativeProperties.map((e) => e.toSchemaProperty()).toList(); + + return SchemaObject(RealmObject, name, properties); + } + + List _getValues(Arena arena, Pointer Function(int size) createPtr, + bool Function(Pointer valuesPtr, int size, Pointer outSize) getValue, TReturn Function(Pointer values, int index) getAtIndex) { + // TODO: this is a hack to get the size of the array and should not be needed once https://github.com/realm/realm-core/issues/5430 is addressed. + final valuesCount = arena(); + _realmLib.invokeGetBool(() => getValue(nullptr, 0, valuesCount)); + + final valuesPtr = createPtr(valuesCount.value); + _realmLib.invokeGetBool(() => getValue(valuesPtr, valuesCount.value, valuesCount)); + + final result = []; + for (var i = 0; i < valuesCount.value; i++) { + result.add(getAtIndex(valuesPtr, i)); + } + + return result; + } + void deleteRealmFiles(String path) { using((Arena arena) { final realm_deleted = arena(); @@ -489,7 +539,7 @@ class _RealmCore { _realmLib.invokeGetBool(() => _realmLib.realm_refresh(realm.handle._pointer), "Could not refresh"); } - RealmClassMetadata getClassMetadata(Realm realm, String className, Type classType) { + RealmObjectMetadata getObjectMedata(Realm realm, String className, Type classType) { return using((Arena arena) { final found = arena(); final classInfo = arena(); @@ -501,11 +551,11 @@ class _RealmCore { } final primaryKey = classInfo.ref.primary_key.cast().toRealmDartString(treatEmptyAsNull: true); - return RealmClassMetadata(classType, classInfo.ref.key, primaryKey); + return RealmObjectMetadata(className, classType, primaryKey, classInfo.ref.key, _getPropertyMetadata(realm, classInfo.ref.key)); }); } - Map getPropertyMetadata(Realm realm, int classKey) { + Map _getPropertyMetadata(Realm realm, int classKey) { return using((Arena arena) { final propertyCountPtr = arena(); _realmLib.invokeGetBool( @@ -521,7 +571,8 @@ class _RealmCore { for (var i = 0; i < propertyCount; i++) { final property = propertiesPtr.elementAt(i); final propertyName = property.ref.name.cast().toRealmDartString()!; - final propertyMeta = RealmPropertyMetadata(property.ref.key, RealmCollectionType.values.elementAt(property.ref.collection_type)); + final objectType = property.ref.link_target.cast().toRealmDartString(treatEmptyAsNull: true); + final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, RealmCollectionType.values.elementAt(property.ref.collection_type)); result[propertyName] = propertyMeta; } return result; @@ -2017,6 +2068,17 @@ extension on List { } } +extension on realm_property_info { + SchemaProperty toSchemaProperty() { + final linkTarget = link_target == nullptr ? null : link_target.cast().toDartString(); + return SchemaProperty(name.cast().toDartString(), RealmPropertyType.values[type], + optional: flags & realm_property_flags.RLM_PROPERTY_NULLABLE == realm_property_flags.RLM_PROPERTY_NULLABLE, + primaryKey: flags & realm_property_flags.RLM_PROPERTY_PRIMARY_KEY == realm_property_flags.RLM_PROPERTY_PRIMARY_KEY, + linkTarget: linkTarget == null || linkTarget.isEmpty ? null : linkTarget, + collectionType: RealmCollectionType.values[collection_type]); + } +} + enum _CustomErrorCode { noError(0), unknownHttp(998), @@ -2118,7 +2180,7 @@ extension LevelExt on Level { } extension PlatformEx on Platform { - static String fromEnvironment(String name, {String defaultValue = "" }) { + static String fromEnvironment(String name, {String defaultValue = ""}) { final result = Platform.environment[name]; if (result == null) { return defaultValue; diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index e1ccf13a5..a7065e7dd 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -21,6 +21,7 @@ import 'dart:io'; import 'package:logging/logging.dart'; import 'package:realm_common/realm_common.dart'; +import 'package:collection/collection.dart'; import 'configuration.dart'; import 'list.dart'; @@ -85,12 +86,20 @@ export 'session.dart' show Session, SessionState, ConnectionState, ProgressDirec /// /// {@category Realm} class Realm { - final Map _metadata = {}; - final RealmHandle _handle; + late final RealmMetadata _metadata; + late final RealmHandle _handle; + + /// An object encompassing this `Realm` instance's dynamic API. + late final DynamicRealm dynamic = DynamicRealm._(this); /// The [Configuration] object used to open this [Realm] final Configuration config; + /// The schema of this [Realm]. If the [Configuration] was created with a + /// non-empty list of schemas, this will match the collection. Otherwise, + /// the schema will be read from the file. + late final RealmSchema schema; + /// Opens a `Realm` using a [Configuration] object. Realm(Configuration config) : this._(config); @@ -107,12 +116,8 @@ class Realm { } void _populateMetadata() { - for (var realmClass in config.schema) { - final classMeta = realmCore.getClassMetadata(this, realmClass.name, realmClass.type); - final propertyMeta = realmCore.getPropertyMetadata(this, classMeta.key); - final metadata = RealmMetadata(classMeta, propertyMeta); - _metadata[realmClass.type] = metadata; - } + schema = config.schemaObjects.isNotEmpty ? RealmSchema(config.schemaObjects) : realmCore.readSchema(this); + _metadata = RealmMetadata._(schema.map((c) => realmCore.getObjectMedata(this, c.name, c.type))); } /// Deletes all files associated with a `Realm` located at given [path] @@ -156,15 +161,10 @@ class Realm { return object; } - final metadata = _metadata[object.runtimeType]; - if (metadata == null) { - throw RealmError("Object type ${object.runtimeType} not configured in the current Realm's schema." - " Add type ${object.runtimeType} to your config before opening the Realm"); - } - - final handle = metadata.class_.primaryKey == null - ? realmCore.createRealmObject(this, metadata.class_.key) - : realmCore.createRealmObjectWithPrimaryKey(this, metadata.class_.key, object.accessor.get(object, metadata.class_.primaryKey!)!); + final metadata = _metadata.getByType(object.runtimeType); + final handle = metadata.primaryKey == null + ? realmCore.createRealmObject(this, metadata.tableKey) + : realmCore.createRealmObjectWithPrimaryKey(this, metadata.tableKey, object.accessor.get(object, metadata.primaryKey!)!); final accessor = RealmCoreAccessor(metadata); object.manage(this, handle, accessor); @@ -245,9 +245,9 @@ class Realm { /// Fast lookup for a [RealmObject] with the specified [primaryKey]. T? find(Object primaryKey) { - RealmMetadata metadata = _getMetadata(T); + final metadata = _metadata.getByType(T); - final handle = realmCore.find(this, metadata.class_.key, primaryKey); + final handle = realmCore.find(this, metadata.tableKey, primaryKey); if (handle == null) { return null; } @@ -257,22 +257,13 @@ class Realm { return object as T; } - RealmMetadata _getMetadata(Type type) { - final metadata = _metadata[type]; - if (metadata == null) { - throw RealmError("Object type $type not configured in the current Realm's schema. Add type $type to your config before opening the Realm"); - } - - return metadata; - } - /// Returns all [RealmObject]s of type `T` in the `Realm` /// /// The returned [RealmResults] allows iterating all the values without further filtering. RealmResults all() { - RealmMetadata metadata = _getMetadata(T); - final handle = realmCore.findAll(this, metadata.class_.key); - return RealmResultsInternal.create(handle, this); + final metadata = _metadata.getByType(T); + final handle = realmCore.findAll(this, metadata.tableKey); + return RealmResultsInternal.create(handle, this, metadata); } /// Returns all [RealmObject]s that match the specified [query]. @@ -280,9 +271,9 @@ class Realm { /// 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 []]) { - RealmMetadata metadata = _getMetadata(T); - final handle = realmCore.queryClass(this, metadata.class_.key, query, args); - return RealmResultsInternal.create(handle, this); + final metadata = _metadata.getByType(T); + final handle = realmCore.queryClass(this, metadata.tableKey, query, args); + return RealmResultsInternal.create(handle, this, metadata); } /// Deletes all [RealmObject]s of type `T` in the `Realm` @@ -327,9 +318,9 @@ class Realm { ..level = RealmLogLevel.info ..onRecord.listen((event) => print(event)); - /// Used to shutdown Realm and allow the process to correctly release native resources and exit. - /// - /// Disclaimer: This method is mostly needed on Dart standalone and if not called the Dart probram will hang and not exit. + /// Used to shutdown Realm and allow the process to correctly release native resources and exit. + /// + /// Disclaimer: This method is mostly needed on Dart standalone and if not called the Dart probram will hang and not exit. /// 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(); } @@ -370,20 +361,17 @@ extension RealmInternal on Realm { return Realm._(config, handle); } - RealmObject createObject(Type type, RealmObjectHandle handle) { - RealmMetadata metadata = _getMetadata(type); - + RealmObject createObject(Type type, RealmObjectHandle handle, RealmObjectMetadata metadata) { final accessor = RealmCoreAccessor(metadata); - var object = RealmObjectInternal.create(type, this, handle, accessor); - return object; + return RealmObjectInternal.create(type, this, handle, accessor); } - RealmList createList(RealmListHandle handle) { - return RealmListInternal.create(handle, this); + RealmList createList(RealmListHandle handle, RealmObjectMetadata? metadata) { + return RealmListInternal.create(handle, this, metadata); } List getPropertyNames(Type type, List propertyKeys) { - RealmMetadata metadata = _getMetadata(type); + final metadata = _metadata.getByType(type); final result = []; for (var key in propertyKeys) { final name = metadata.getPropertyName(key); @@ -393,6 +381,8 @@ extension RealmInternal on Realm { } return result; } + + RealmMetadata get metadata => _metadata; } /// @nodoc @@ -474,3 +464,74 @@ class RealmLogLevel { /// Same as [Level.OFF]; static const off = Level.OFF; } + +/// @nodoc +class RealmMetadata { + final Map _typeMap = {}; + final Map _stringMap = {}; + + RealmMetadata._(Iterable objects) { + for (final meta in objects) { + if (meta.type != RealmObject) { + _typeMap[meta.type] = meta; + } else { + _stringMap[meta.name] = meta; + } + } + } + + RealmObjectMetadata getByType(Type type) { + final metadata = _typeMap[type]; + if (metadata == null) { + throw RealmException("Object type $type not configured in the current Realm's schema. Add type $type to your config before opening the Realm"); + } + + return metadata; + } + + RealmObjectMetadata getByName(String type) { + var metadata = _stringMap[type]; + if (metadata == null) { + metadata = _typeMap.values.firstWhereOrNull((v) => v.name == type); + if (metadata == null) { + throw RealmException("Object type $type not configured in the current Realm's schema. Add type $type to your config before opening the Realm"); + } + + _stringMap[type] = metadata; + } + + return metadata; + } +} + +/// Exposes a set of dynamic methods on the Realm object. These don't use strongly typed +/// classes and instead lookup objects by string name. +/// +/// {@category Realm} +class DynamicRealm { + final Realm _realm; + + DynamicRealm._(this._realm); + + /// Returns all [RealmObject]s of type [className] in the `Realm` + /// + /// The returned [RealmResults] allows iterating all the values without further filtering. + RealmResults all(String className) { + final metadata = _realm._metadata.getByName(className); + final handle = realmCore.findAll(_realm, metadata.tableKey); + return RealmResultsInternal.create(handle, _realm, metadata); + } + + /// Fast lookup for a [RealmObject] of type [className] with the specified [primaryKey]. + RealmObject? find(String className, Object primaryKey) { + final metadata = _realm._metadata.getByName(className); + + final handle = realmCore.find(_realm, metadata.tableKey, primaryKey); + if (handle == null) { + return null; + } + + final accessor = RealmCoreAccessor(metadata); + return RealmObjectInternal.create(RealmObject, _realm, handle, accessor); + } +} diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index a6acf3fa3..70e37ad0e 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -90,14 +90,20 @@ class RealmValuesAccessor implements RealmAccessor { } } -class RealmMetadata { - RealmClassMetadata class_; +class RealmObjectMetadata { + final int tableKey; + final String name; + final Type type; + final String? primaryKey; + final Map _propertyKeys; - RealmMetadata(this.class_, this._propertyKeys); + String get _nameForExceptions => type == RealmObject ? name : type.toString(); + + RealmObjectMetadata(this.name, this.type, this.primaryKey, this.tableKey, this._propertyKeys); RealmPropertyMetadata operator [](String propertyName) => - _propertyKeys[propertyName] ?? (throw RealmException("Property $propertyName does not exists on class ${class_.type.runtimeType}")); + _propertyKeys[propertyName] ?? (throw RealmException("Property $propertyName does not exists on class $_nameForExceptions")); String? getPropertyName(int propertyKey) { for (final entry in _propertyKeys.entries) { @@ -109,22 +115,15 @@ class RealmMetadata { } } -class RealmClassMetadata { - final int key; - final Type type; - final String? primaryKey; - - RealmClassMetadata(this.type, int classKey, [this.primaryKey]) : key = classKey; -} - class RealmPropertyMetadata { final int key; final RealmCollectionType collectionType; - const RealmPropertyMetadata(this.key, [this.collectionType = RealmCollectionType.none]); + final String? objectType; + const RealmPropertyMetadata(this.key, this.objectType, [this.collectionType = RealmCollectionType.none]); } class RealmCoreAccessor implements RealmAccessor { - final RealmMetadata metadata; + final RealmObjectMetadata metadata; RealmCoreAccessor(this.metadata); @@ -134,18 +133,20 @@ class RealmCoreAccessor implements RealmAccessor { final propertyMeta = metadata[name]; if (propertyMeta.collectionType == RealmCollectionType.list) { final handle = realmCore.getListProperty(object, propertyMeta.key); - return object.realm.createList(handle); + final listMeta = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); + return object.realm.createList(handle, listMeta); } Object? value = realmCore.getProperty(object, propertyMeta.key); if (value is RealmObjectHandle) { - return object.realm.createObject(T, value); + final targetMetadata = propertyMeta.objectType != null ? object.realm.metadata.getByName(propertyMeta.objectType!) : object.realm.metadata.getByType(T); + return object.realm.createObject(T, value, targetMetadata); } return value; } on Exception catch (e) { - throw RealmException("Error getting property ${metadata.class_.type}.$name Error: $e"); + throw RealmException("Error getting property ${metadata._nameForExceptions}.$name Error: $e"); } } @@ -173,7 +174,7 @@ class RealmCoreAccessor implements RealmAccessor { realmCore.setProperty(object, propertyMeta.key, value, isDefault); } on Exception catch (e) { - throw RealmException("Error setting property ${metadata.class_.type}.$name Error: $e"); + throw RealmException("Error setting property ${metadata._nameForExceptions}.$name Error: $e"); } } } @@ -203,7 +204,9 @@ extension RealmEntityInternal on RealmEntity { mixin RealmObject on RealmEntity { RealmObjectHandle? _handle; RealmAccessor _accessor = RealmValuesAccessor(); - static final Map _factories = {}; + static final Map _factories = { + RealmObject: () => DynamicRealmObject._(), + }; /// @nodoc static Object? get(RealmObject object, String name) { @@ -291,9 +294,7 @@ extension RealmObjectInternal on RealmObject { } final object = RealmObject._factories[type]!(); - object._handle = handle; - object._accessor = accessor; - object._realm = realm; + object.manage(realm, handle, accessor); return object; } @@ -366,3 +367,8 @@ class RealmObjectNotificationsController extends Notifica streamController.addError(error); } } + +/// @nodoc +class DynamicRealmObject with RealmEntity, RealmObject { + DynamicRealmObject._(); +} diff --git a/lib/src/results.dart b/lib/src/results.dart index 1cd2f6e32..f30b340d5 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -22,12 +22,14 @@ import 'dart:collection' as collection; import 'collections.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; +import 'realm_object.dart' show RealmObjectMetadata; /// Instances of this class are live collections and will update as new elements are either /// added to or deleted from the Realm that match the underlying query. /// /// {@category Realm} class RealmResults extends collection.IterableBase { + final RealmObjectMetadata? _metadata; final RealmResultsHandle _handle; /// The Realm instance this collection belongs to. @@ -35,12 +37,12 @@ class RealmResults extends collection.IterableBase { final _supportsSnapshot = [] is List; - RealmResults._(this._handle, this.realm); + RealmResults._(this._handle, this.realm, this._metadata); /// 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) as T; + return realm.createObject(T, handle, _metadata!) as T; } /// Returns a new [RealmResults] filtered according to the provided query. @@ -49,7 +51,7 @@ class RealmResults extends collection.IterableBase { /// 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); + return RealmResultsInternal.create(handle, realm, _metadata); } /// `true` if the `Results` collection is empty. @@ -62,7 +64,7 @@ class RealmResults extends collection.IterableBase { var results = this; if (_supportsSnapshot) { final handle = realmCore.resultsSnapshot(this); - results = RealmResults._(handle, realm); + results = RealmResultsInternal.create(handle, realm, _metadata); } return _RealmResultsIterator(results); } @@ -83,8 +85,8 @@ class RealmResults extends collection.IterableBase { extension RealmResultsInternal on RealmResults { RealmResultsHandle get handle => _handle; - static RealmResults create(RealmResultsHandle handle, Realm realm) { - return RealmResults._(handle, realm); + static RealmResults create(RealmResultsHandle handle, Realm realm, RealmObjectMetadata? metadata) { + return RealmResults._(handle, realm, metadata); } } diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 855bde444..6d737b018 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -31,10 +31,6 @@ Future main([List? args]) async { Configuration.local([Car.schema]); }); - test('Configuration exception if no schema', () { - expect(() => Configuration.local([]), throws()); - }); - test('Configuration default path', () { final config = Configuration.local([Car.schema]); if (Platform.isAndroid || Platform.isIOS) { diff --git a/test/dynamic_realm_test.dart b/test/dynamic_realm_test.dart new file mode 100644 index 000000000..c5b16d1ce --- /dev/null +++ b/test/dynamic_realm_test.dart @@ -0,0 +1,197 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2022 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +// ignore_for_file: unused_local_variable, avoid_relative_lib_imports + +import 'dart:io'; +import 'package:test/test.dart' hide test, throws; +import '../lib/realm.dart'; + +import 'test.dart'; + +Future main([List? args]) async { + print("Current PID $pid"); + + await setupTests(args); + + _assertSchemaExists(Realm realm, SchemaObject expected) { + final foundSchema = realm.schema.singleWhere((e) => e.name == expected.name); + expect(foundSchema.properties.length, expected.properties.length); + + for (final prop in foundSchema.properties) { + final expectedProp = expected.properties.singleWhere((e) => e.name == prop.name); + expect(prop.collectionType, expectedProp.collectionType); + expect(prop.linkTarget, expectedProp.linkTarget); + expect(prop.optional, expectedProp.optional); + expect(prop.primaryKey, expectedProp.primaryKey); + expect(prop.propertyType, expectedProp.propertyType); + } + } + + test('schema is read from disk', () { + final config = Configuration.local([Car.schema, Dog.schema, Person.schema, AllTypes.schema, LinksClass.schema]); + getRealm(config).close(); + + final dynamicConfig = Configuration.local([]); + final realm = getRealm(dynamicConfig); + + expect(realm.schema.length, 5); + + _assertSchemaExists(realm, Car.schema); + _assertSchemaExists(realm, Dog.schema); + _assertSchemaExists(realm, Person.schema); + _assertSchemaExists(realm, AllTypes.schema); + _assertSchemaExists(realm, LinksClass.schema); + }); + + test('dynamic is always the same', () { + final config = Configuration.local([Car.schema]); + final realm = getRealm(config); + + final dynamic1 = realm.dynamic; + final dynamic2 = realm.dynamic; + + expect(dynamic1, same(dynamic2)); + }); + + for (var isDynamic in [true, false]) { + Realm getDynamicRealm(Realm original) { + if (isDynamic) { + original.close(); + return getRealm(Configuration.local([])); + } + + return original; + } + + test('dynamic.all (dynamic=$isDynamic) 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); + }); + + 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(RealmObject.get(car, 'make'), 'Honda'); + }); + + test('dynamic.all (dynamic=$isDynamic) throws for non-existent type', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); + + final dynamicRealm = getDynamicRealm(staticRealm); + + expect(() => dynamicRealm.dynamic.all('i-dont-exist'), throws("Object type i-dont-exist not configured in the current Realm's schema")); + }); + + test('dynamic.all (dynamic=$isDynamic) can follow links', () { + final config = Configuration.local([LinksClass.schema]); + final staticRealm = getRealm(config); + + 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]); + }); + + final dynamicRealm = getDynamicRealm(staticRealm); + + 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); + + expect(RealmObject.get(obj1, 'link'), obj2); + expect(RealmObject.get(obj2, 'link'), obj3); + + final list = RealmObject.get(obj1, 'list') as List; + + expect(list[0], obj1); + expect(list[1], obj2); + expect(list[2], obj3); + }); + + test('dynamic.all (dynamic=$isDynamic) can be filtered', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); + + staticRealm.write(() { + staticRealm.add(Car('Honda')); + staticRealm.add(Car('Hyundai')); + staticRealm.add(Car('Suzuki')); + staticRealm.add(Car('Toyota')); + }); + + final dynamicRealm = getDynamicRealm(staticRealm); + + final carsWithH = dynamicRealm.dynamic.all(Car.schema.name).query('make BEGINSWITH "H"'); + expect(carsWithH.length, 2); + }); + + test('dynamic.find (dynamic=$isDynamic) 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')); + }); + + final dynamicRealm = getDynamicRealm(staticRealm); + + final car = dynamicRealm.dynamic.find(Car.schema.name, 'Honda'); + expect(car, isNotNull); + expect(RealmObject.get(car!, 'make'), 'Honda'); + + final nonExistent = dynamicRealm.dynamic.find(Car.schema.name, 'i-dont-exist'); + expect(nonExistent, isNull); + }); + + test('dynamic.find (dynamic=$isDynamic) fails to find non-existent type', () { + final config = Configuration.local([Car.schema]); + final staticRealm = getRealm(config); + + 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")); + }); + } +} diff --git a/test/test.dart b/test/test.dart index d518600bb..90ce2c2b0 100644 --- a/test/test.dart +++ b/test/test.dart @@ -114,6 +114,23 @@ class _AllTypes { late ObjectId objectIdProp; late Uuid uuidProp; late int intProp; + + late String? nullableStringProp; + late bool? nullableBoolProp; + late DateTime? nullableDateProp; + late double? nullableDoubleProp; + late ObjectId? nullableObjectIdProp; + late Uuid? nullableUuidProp; + late int? nullableIntProp; +} + +@RealmModel() +class _LinksClass { + @PrimaryKey() + late Uuid id; + + late _LinksClass? link; + late List<_LinksClass> list; } @RealmModel() diff --git a/test/test.g.dart b/test/test.g.dart index 9d581f626..d09071bb1 100644 --- a/test/test.g.dart +++ b/test/test.g.dart @@ -405,8 +405,15 @@ class AllTypes extends _AllTypes with RealmEntity, RealmObject { double doubleProp, ObjectId objectIdProp, Uuid uuidProp, - int intProp, - ) { + int intProp, { + String? nullableStringProp, + bool? nullableBoolProp, + DateTime? nullableDateProp, + double? nullableDoubleProp, + ObjectId? nullableObjectIdProp, + Uuid? nullableUuidProp, + int? nullableIntProp, + }) { RealmObject.set(this, 'stringProp', stringProp); RealmObject.set(this, 'boolProp', boolProp); RealmObject.set(this, 'dateProp', dateProp); @@ -414,6 +421,13 @@ class AllTypes extends _AllTypes with RealmEntity, RealmObject { RealmObject.set(this, 'objectIdProp', objectIdProp); RealmObject.set(this, 'uuidProp', uuidProp); RealmObject.set(this, 'intProp', intProp); + RealmObject.set(this, 'nullableStringProp', nullableStringProp); + RealmObject.set(this, 'nullableBoolProp', nullableBoolProp); + RealmObject.set(this, 'nullableDateProp', nullableDateProp); + RealmObject.set(this, 'nullableDoubleProp', nullableDoubleProp); + RealmObject.set(this, 'nullableObjectIdProp', nullableObjectIdProp); + RealmObject.set(this, 'nullableUuidProp', nullableUuidProp); + RealmObject.set(this, 'nullableIntProp', nullableIntProp); } AllTypes._(); @@ -458,6 +472,55 @@ class AllTypes extends _AllTypes with RealmEntity, RealmObject { @override set intProp(int value) => RealmObject.set(this, 'intProp', value); + @override + String? get nullableStringProp => + RealmObject.get(this, 'nullableStringProp') as String?; + @override + set nullableStringProp(String? value) => + RealmObject.set(this, 'nullableStringProp', value); + + @override + bool? get nullableBoolProp => + RealmObject.get(this, 'nullableBoolProp') as bool?; + @override + set nullableBoolProp(bool? value) => + RealmObject.set(this, 'nullableBoolProp', value); + + @override + DateTime? get nullableDateProp => + RealmObject.get(this, 'nullableDateProp') as DateTime?; + @override + set nullableDateProp(DateTime? value) => + RealmObject.set(this, 'nullableDateProp', value); + + @override + double? get nullableDoubleProp => + RealmObject.get(this, 'nullableDoubleProp') as double?; + @override + set nullableDoubleProp(double? value) => + RealmObject.set(this, 'nullableDoubleProp', value); + + @override + ObjectId? get nullableObjectIdProp => + RealmObject.get(this, 'nullableObjectIdProp') as ObjectId?; + @override + set nullableObjectIdProp(ObjectId? value) => + RealmObject.set(this, 'nullableObjectIdProp', value); + + @override + Uuid? get nullableUuidProp => + RealmObject.get(this, 'nullableUuidProp') as Uuid?; + @override + set nullableUuidProp(Uuid? value) => + RealmObject.set(this, 'nullableUuidProp', value); + + @override + int? get nullableIntProp => + RealmObject.get(this, 'nullableIntProp') as int?; + @override + set nullableIntProp(int? value) => + RealmObject.set(this, 'nullableIntProp', value); + @override Stream> get changes => RealmObject.getChanges(this); @@ -474,6 +537,69 @@ class AllTypes extends _AllTypes with RealmEntity, RealmObject { SchemaProperty('objectIdProp', RealmPropertyType.objectid), SchemaProperty('uuidProp', RealmPropertyType.uuid), SchemaProperty('intProp', RealmPropertyType.int), + SchemaProperty('nullableStringProp', RealmPropertyType.string, + optional: true), + SchemaProperty('nullableBoolProp', RealmPropertyType.bool, + optional: true), + SchemaProperty('nullableDateProp', RealmPropertyType.timestamp, + optional: true), + SchemaProperty('nullableDoubleProp', RealmPropertyType.double, + optional: true), + SchemaProperty('nullableObjectIdProp', RealmPropertyType.objectid, + optional: true), + SchemaProperty('nullableUuidProp', RealmPropertyType.uuid, + optional: true), + SchemaProperty('nullableIntProp', RealmPropertyType.int, optional: true), + ]); + } +} + +class LinksClass extends _LinksClass with RealmEntity, RealmObject { + LinksClass( + Uuid id, { + LinksClass? link, + Iterable list = const [], + }) { + RealmObject.set(this, 'id', id); + RealmObject.set(this, 'link', link); + RealmObject.set>( + this, 'list', RealmList(list)); + } + + LinksClass._(); + + @override + Uuid get id => RealmObject.get(this, 'id') as Uuid; + @override + set id(Uuid value) => throw RealmUnsupportedSetError(); + + @override + LinksClass? get link => + RealmObject.get(this, 'link') as LinksClass?; + @override + set link(covariant LinksClass? value) => RealmObject.set(this, 'link', value); + + @override + RealmList get list => + RealmObject.get(this, 'list') as RealmList; + @override + set list(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObject.getChanges(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObject.registerFactory(LinksClass._); + return const SchemaObject(LinksClass, 'LinksClass', [ + SchemaProperty('id', RealmPropertyType.uuid, primaryKey: true), + SchemaProperty('link', RealmPropertyType.object, + optional: true, linkTarget: 'LinksClass'), + SchemaProperty('list', RealmPropertyType.object, + linkTarget: 'LinksClass', collectionType: RealmCollectionType.list), ]); } }