Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

RDART-1021: Wire up some basic dynamic setting and change notifications #1669

Merged
merged 8 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
## vNext (TBD)

### Enhancements
* None
* Added support for creating and storing a RealmObject using the `Realm.dynamic` API: `realm.dynamic.create("Person", primaryKey: 123)`. (PR [#1669](https://github.com/realm/realm-dart/pull/1669))
* Added support for setting properties on a RealmObject using the dynamic API: `obj.dynamic.set("name", "Peter")`. (PR [#1669](https://github.com/realm/realm-dart/pull/1669))

### Fixed
* Listening for `.changes` on a dynamic object (obtained via the `realm.dynamic` API) no longer throws. (Issue [#1668](https://github.com/realm/realm-dart/issues/1668))
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
* Private fields did not work with default values. (Issue [#1663](https://github.com/realm/realm-dart/issues/1663))

### Compatibility
Expand Down
29 changes: 27 additions & 2 deletions packages/realm_dart/lib/src/realm_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,8 @@ extension RealmInternal on Realm {
return RealmMapInternal.create<T>(handle, this, metadata);
}

List<String> getPropertyNames(Type type, List<int> propertyKeys) {
final metadata = _metadata.getByType(type);
List<String> getPropertyNames(RealmObjectBase obj, List<int> propertyKeys) {
final metadata = _metadata.tryGetByType(obj.runtimeType) ?? _metadata.getByName(obj.objectSchema.name);
final result = <String>[];
for (var key in propertyKeys) {
final name = metadata.getPropertyName(key);
Expand Down Expand Up @@ -907,6 +907,8 @@ class RealmMetadata {
return metadata;
}

RealmObjectMetadata? tryGetByType(Type type) => _typeMap[type];

RealmObjectMetadata getByName(String type) {
var metadata = _stringMap[type];
if (metadata == null) {
Expand Down Expand Up @@ -963,6 +965,29 @@ class DynamicRealm {
final accessor = RealmCoreAccessor(metadata, _realm._isInMigration);
return RealmObjectInternal.create(RealmObject, _realm, handle, accessor) as RealmObject;
}

/// Creates a managed RealmObject with the specified [className] and [primaryKey].
RealmObject create(String className, {Object? primaryKey}) {
final metadata = _realm._metadata.getByName(className);

RealmObjectHandle handle;
if (metadata.primaryKey != null) {
if (primaryKey == null) {
throw RealmError("The class $className has primary key defined, but you didn't pass one");
}

handle = realmCore.createRealmObjectWithPrimaryKey(_realm, metadata.classKey, primaryKey);
} else {
if (primaryKey != null) {
throw RealmError("The class $className doesn't have primary key defined, but you passed $primaryKey");
}

handle = realmCore.createRealmObject(_realm, metadata.classKey);
}

final accessor = RealmCoreAccessor(metadata, _realm._isInMigration);
return RealmObjectInternal.create(RealmObject, _realm, handle, accessor) as RealmObject;
}
}

/// A class used during a migration callback. It exposes a set of dynamic API as
Expand Down
42 changes: 34 additions & 8 deletions packages/realm_dart/lib/src/realm_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class RealmCoreAccessor implements RealmAccessor {
value = object.realm.createObject(type, value, targetMetadata);
}

if (T == RealmValue) {
if (T == RealmValue || (propertyMeta.propertyType == RealmPropertyType.mixed && _isTypeGenericObject<T>())) {
value = RealmValue.from(value);
}

Expand Down Expand Up @@ -381,7 +381,7 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab
}

/// @nodoc
static void set<T extends Object>(RealmObjectBase object, String name, T? value, {bool update = false}) {
static void set<T>(RealmObjectBase object, String name, T value, {bool update = false}) {
object._accessor.set(object, name, value, update: update);
}

Expand Down Expand Up @@ -605,10 +605,16 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab
}

/// @nodoc
mixin RealmObject on RealmObjectBase implements RealmObjectMarker {}
mixin RealmObject on RealmObjectBase implements RealmObjectMarker {
@override
Stream<RealmObjectChanges<RealmObject>> get changes => throw RealmError("Invalid usage. Use the generated inheritors of RealmObject");
}

/// @nodoc
mixin EmbeddedObject on RealmObjectBase implements EmbeddedObjectMarker {}
mixin EmbeddedObject on RealmObjectBase implements EmbeddedObjectMarker {
@override
Stream<RealmObjectChanges<EmbeddedObject>> get changes => throw RealmError("Invalid usage. Use the generated inheritors of EmbeddedObject");
}

/// Base for any object that can be persisted in a [Realm], but cannot be retrieved,
/// hence cannot be modified.
Expand Down Expand Up @@ -731,7 +737,7 @@ class RealmObjectChanges<T extends RealmObjectBase> implements Finalizable {
List<String> get properties {
final propertyKeys = realmCore.getObjectChangesProperties(_handle);
return object.realm
.getPropertyNames(object.runtimeType, propertyKeys)
.getPropertyNames(object, propertyKeys)
.map((e) => object.objectSchema.firstWhere((element) => element.mapTo == e || element.name == e).name)
.toList();
}
Expand Down Expand Up @@ -794,12 +800,18 @@ class RealmObjectNotificationsController<T extends RealmObjectBase> extends Noti
class _ConcreteRealmObject with RealmEntity, RealmObjectBase, RealmObject {
@override
SchemaObject get objectSchema => RealmObjectBase.getSchema(this)!; // _ConcreteRealmObject should only ever be created for managed objects

@override
Stream<RealmObjectChanges<RealmObject>> get changes => RealmObjectBase.getChanges<RealmObject>(this);
}

/// @nodoc
class _ConcreteEmbeddedObject with RealmEntity, RealmObjectBase, EmbeddedObject {
@override
SchemaObject get objectSchema => RealmObjectBase.getSchema(this)!; // _ConcreteEmbeddedObject should only ever be created for managed objects

@override
Stream<RealmObjectChanges<EmbeddedObject>> get changes => RealmObjectBase.getChanges<EmbeddedObject>(this);
}

// This is necessary whenever we need to pass T? as the type.
Expand All @@ -824,6 +836,13 @@ class DynamicRealmObject {
return RealmObjectBase.get<T>(_obj, name) as T;
}

/// Sets a property by its name. The supplied [value] must be assignable
/// to the property type, otherwise an exception will be thrown.
void set<T extends Object?>(String name, T value) {
_validatePropertyType<T>(name, RealmCollectionType.none, relaxedNullability: true);
RealmObjectBase.set(_obj, name, value);
}

/// 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<Object>` will be
/// returned.
Expand All @@ -848,7 +867,7 @@ class DynamicRealmObject {
return RealmObjectBase.get<T>(_obj, name) as RealmMap<T>;
}

RealmPropertyMetadata? _validatePropertyType<T extends Object?>(String name, RealmCollectionType expectedCollectionType) {
RealmPropertyMetadata? _validatePropertyType<T extends Object?>(String name, RealmCollectionType expectedCollectionType, {bool relaxedNullability = false}) {
final accessor = _obj.accessor;
if (accessor is RealmCoreAccessor) {
final prop = accessor.metadata._propertyKeys[name];
Expand All @@ -864,8 +883,15 @@ class DynamicRealmObject {
// 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<RealmValue>() && T != _typeOf<Object?>() && prop.isNullable != null is T) {
throw RealmException(
"Property '$name' on class '${accessor.metadata.schema.name}' is ${prop.isNullable ? 'nullable' : 'required'} but the generic argument passed to get<T> is $T.");
if (relaxedNullability && prop.isNullable) {
// We're relaxing nullability requirements when setting a property - in that case, we accept
// a non-null generic type argument, even if the property is nullable to allow users to invoke
// .set without a generic argument (i.e. have the compiler infer the generic based on the value
// argument).
} else {
throw RealmException(
"Property '$name' on class '${accessor.metadata.schema.name}' is ${prop.isNullable ? 'nullable' : 'required'} but the generic argument supplied is $T.");
}
}

final targetType = _getPropertyType<T>();
Expand Down
Loading
Loading