Skip to content

Commit

Permalink
kn/asymmetric sync (#1400)
Browse files Browse the repository at this point in the history
* Support asymmetric objects

* Add asymmetric tests

* Add generator tests

* Update CHANGELOG

* Update doc comments

* PR feedback

* Fix spelling mistake

* Add test for use of asymmetric objects in local realms

* Add test for use of asymmetric objects with disconnectedSync

* Fix bug
  • Loading branch information
nielsenko authored Sep 18, 2023
1 parent 70ebe26 commit d11aefa
Show file tree
Hide file tree
Showing 24 changed files with 615 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
### Enhancements
* Support efficient `skip` on `RealmResults` ([#1391](https://github.com/realm/realm-dart/pull/1391))
* Support efficient `indexOf` and `contains` on `RealmResults` ([#1394](https://github.com/realm/realm-dart/pull/1394))
* Support asymmetric objects. ([#1400](https://github.com/realm/realm-dart/pull/1400))

### Fixed
* None

Expand Down
2 changes: 1 addition & 1 deletion common/lib/src/realm_common_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ enum ObjectType {
/// A special type of object used to facilitate unidirectional synchronization
/// with Atlas App Services. It is used to push data to Realm without the ability
/// to query or modify it.
_asymmetricObject('AsymmetricObject', 2);
asymmetricObject('AsymmetricObject', 2);

const ObjectType([this._className = 'Unknown', this._flags = -1]);

Expand Down
3 changes: 3 additions & 0 deletions common/lib/src/realm_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ abstract class RealmObjectMarker implements RealmObjectBaseMarker {}
/// @nodoc
abstract class EmbeddedObjectMarker implements RealmObjectBaseMarker {}

/// @nodoc
abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {}

/// A type that can represent any valid realm data type, except collections and embedded objects.
///
/// You can use [RealmValue] to declare fields on realm models, in which case it must be non-nullable,
Expand Down
56 changes: 55 additions & 1 deletion generator/lib/src/class_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:realm_common/realm_common.dart';
import 'package:realm_generator/src/dart_type_ex.dart';
import 'package:source_gen/source_gen.dart';
import 'annotation_value.dart';
import 'error.dart';
Expand Down Expand Up @@ -150,7 +151,7 @@ extension ClassElementEx on ClassElement {
);
}

final objectType = ObjectType.values[modelInfo.value.getField('type')!.getField('index')!.toIntValue()!];
final objectType = thisType.realmObjectType!;

// Realm Core requires computed properties at the end so we sort them at generation time versus doing it at runtime every time.
final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1);
Expand All @@ -165,6 +166,59 @@ extension ClassElementEx on ClassElement {
todo: 'Remove the @PrimaryKey annotation from the field or set the model type to a value different from ObjectType.embeddedObject.');
}

// TODO:
// What follows is the least intrusive handling of invariants for asymmetric
// objects I could come up with.
//
// Really this calls for a bigger refactoring of the generator code where we
// build a graph of RealmModelInfo and RealmFieldInfo, but I have multiple
// PRs inflight that touches this code, so I will defer the refactoring until
// they have landed.

// Check that no objects have links to asymmetric objects.
for (final field in mappedFields) {
final fieldElement = field.fieldElement;
final classElement = fieldElement.type.basicType.element as ClassElement;
if (classElement.thisType.isRealmModelOfType(ObjectType.asymmetricObject)) {
throw RealmInvalidGenerationSourceError(
'Linking to asymmetric objects is not allowed',
todo: 'Remove the field',
element: fieldElement,
);
}
}

// Check that asymmetric objects:
// 1) only have links to embedded objects.
// 2) have a primary key named _id.
if (objectType == ObjectType.asymmetricObject) {
var hasPrimaryKey = false;
for (final field in mappedFields) {
final fieldElement = field.fieldElement;
final classElement = fieldElement.type.basicType.element as ClassElement;
if (field.type.basicType.isRealmModel && !classElement.thisType.isRealmModelOfType(ObjectType.embeddedObject)) {
throw RealmInvalidGenerationSourceError('Asymmetric objects cannot link to non-embedded objects', todo: '', element: fieldElement);
}
if (field.isPrimaryKey) {
hasPrimaryKey = true;
if (field.realmName != '_id') {
throw RealmInvalidGenerationSourceError(
'Asymmetric objects must have a primary key named _id',
todo: 'Add @MapTo("_id") to the @PrimaryKey field',
element: fieldElement,
);
}
}
}
if (!hasPrimaryKey) {
throw RealmInvalidGenerationSourceError(
'Asymmetric objects must have a primary key named _id',
todo: 'Add a primary key named _id',
element: this,
);
}
}

return RealmModelInfo(name, modelName, realmName, mappedFields, objectType);
} on InvalidGenerationSourceError catch (_) {
rethrow;
Expand Down
15 changes: 13 additions & 2 deletions generator/lib/src/dart_type_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,18 @@ extension DartTypeEx on DartType {
bool get isRealmValue => const TypeChecker.fromRuntime(RealmValue).isAssignableFromType(this);
bool get isRealmCollection => realmCollectionType != RealmCollectionType.none;
bool get isRealmSet => realmCollectionType == RealmCollectionType.set;
bool get isRealmModel => element2 != null ? realmModelChecker.annotationsOfExact(element2!).isNotEmpty : false;

ObjectType? get realmObjectType {
if (element == null) return null;
final realmModelAnnotation = realmModelChecker.firstAnnotationOfExact(element!);
if (realmModelAnnotation == null) return null; // not a RealmModel
final index = realmModelAnnotation.getField('type')!.getField('index')!.toIntValue()!;
return ObjectType.values[index];
}

bool get isRealmModel => realmObjectType != null;
bool isRealmModelOfType(ObjectType type) => realmObjectType == type;

bool get isUint8List => isExactly<Uint8List>();

bool get isNullable => session.typeSystem.isNullable(this);
Expand All @@ -50,7 +61,7 @@ extension DartTypeEx on DartType {
return RealmCollectionType.none;
}

DartType? get nullIfDynamic => isDynamic ? null : this;
DartType? get nullIfDynamic => this is DynamicType ? null : this;

DartType get basicType {
final self = this;
Expand Down
2 changes: 2 additions & 0 deletions generator/lib/src/field_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import 'type_checkers.dart';
extension FieldElementEx on FieldElement {
static const realmSetUnsupportedRealmTypes = [RealmPropertyType.linkingObjects];

ClassElement get enclosingClassElement => enclosingElement as ClassElement;

FieldDeclaration get declarationAstNode => getDeclarationFromElement(this)!.node.parent!.parent as FieldDeclaration;

AnnotationValue? get ignoredInfo => annotationInfoOfExact(ignoredChecker);
Expand Down
13 changes: 13 additions & 0 deletions generator/test/error_test_data/asymmetric_external_link.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:realm_common/realm_common.dart';

@RealmModel(ObjectType.asymmetricObject)
class _Asymmetric {
@PrimaryKey()
@MapTo('_id')
late ObjectId id;
}

@RealmModel()
class _Bad {
_Asymmetric? asymmetric;
}
12 changes: 12 additions & 0 deletions generator/test/error_test_data/asymmetric_external_link.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Linking to asymmetric objects is not allowed

in: asset:pkg/test/error_test_data/asymmetric_external_link.dart:12:16
10 │ @RealmModel()
11 │ class _Bad {
│ ━━━━ in realm model for 'Bad'
12 │ _Asymmetric? asymmetric;
│ ^^^^^^^^^^ !
Remove the field

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:realm_common/realm_common.dart';

@RealmModel()
class _Symmetric {}

@RealmModel(ObjectType.asymmetricObject)
class _Asymmetric {
@PrimaryKey()
@MapTo('_id')
late ObjectId id;

_Symmetric? illegal;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Asymmetric objects cannot link to non-embedded objects

in: asset:pkg/test/error_test_data/asymmetric_link_to_non_embedded.dart:12:15
6 │ @RealmModel(ObjectType.asymmetricObject)
7 │ class _Asymmetric {
│ ━━━━━━━━━━━ in realm model for 'Asymmetric'
... │
12 │ _Symmetric? illegal;
│ ^^^^^^^ !
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:realm_common/realm_common.dart';

@RealmModel()
class _Symmetric {}

@RealmModel(ObjectType.asymmetricObject)
class _Asymmetric {
@PrimaryKey()
@MapTo('_id')
late ObjectId id;

late List<_Symmetric> illegal;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Asymmetric objects cannot link to non-embedded objects

in: asset:pkg/test/error_test_data/asymmetric_link_to_non_embedded_list.dart:12:25
6 │ @RealmModel(ObjectType.asymmetricObject)
7 │ class _Asymmetric {
│ ━━━━━━━━━━━ in realm model for 'Asymmetric'
... │
12 │ late List<_Symmetric> illegal;
│ ^^^^^^^ !
8 changes: 8 additions & 0 deletions generator/test/error_test_data/asymmetric_missing_pk.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:realm_common/realm_common.dart';

@RealmModel(ObjectType.asymmetricObject)
class _BadAsymmetric {
// missing @PrimaryKey()
@MapTo('_id')
late ObjectId id;
}
10 changes: 10 additions & 0 deletions generator/test/error_test_data/asymmetric_missing_pk.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Asymmetric objects must have a primary key named _id

in: asset:pkg/test/error_test_data/asymmetric_missing_pk.dart:4:7
3 │ @RealmModel(ObjectType.asymmetricObject)
4 │ class _BadAsymmetric {
│ ^^^^^^^^^^^^^^
Add a primary key named _id

7 changes: 7 additions & 0 deletions generator/test/error_test_data/asymmetric_wrong_pk.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:realm_common/realm_common.dart';

@RealmModel(ObjectType.asymmetricObject)
class _BadAsymmetric {
@PrimaryKey()
late ObjectId wrongName;
}
13 changes: 13 additions & 0 deletions generator/test/error_test_data/asymmetric_wrong_pk.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Asymmetric objects must have a primary key named _id

in: asset:pkg/test/error_test_data/asymmetric_wrong_pk.dart:6:17
3 │ @RealmModel(ObjectType.asymmetricObject)
4 │ class _BadAsymmetric {
│ ━━━━━━━━━━━━━━ in realm model for 'BadAsymmetric'
5 │ @PrimaryKey()
6 │ late ObjectId wrongName;
│ ^^^^^^^^^ !
Add @MapTo("_id") to the @PrimaryKey field

18 changes: 18 additions & 0 deletions generator/test/good_test_data/asymmetric_object.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:realm_common/realm_common.dart';

@RealmModel()
class _Asymmetric {
@PrimaryKey()
@MapTo('_id')
late ObjectId id;

late List<_Embedded> children;
late _Embedded? father;
late _Embedded? mother;
}

@RealmModel(ObjectType.embeddedObject)
class _Embedded {
late String name;
late int age;
}
112 changes: 112 additions & 0 deletions generator/test/good_test_data/asymmetric_object.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// **************************************************************************
// RealmObjectGenerator
// **************************************************************************

class Asymmetric extends _Asymmetric
with RealmEntity, RealmObjectBase, RealmObject {
Asymmetric(
ObjectId id, {
Embedded? father,
Embedded? mother,
Iterable<Embedded> children = const [],
}) {
RealmObjectBase.set(this, '_id', id);
RealmObjectBase.set(this, 'father', father);
RealmObjectBase.set(this, 'mother', mother);
RealmObjectBase.set<RealmList<Embedded>>(
this, 'children', RealmList<Embedded>(children));
}

Asymmetric._();

@override
ObjectId get id => RealmObjectBase.get<ObjectId>(this, '_id') as ObjectId;
@override
set id(ObjectId value) => RealmObjectBase.set(this, '_id', value);

@override
RealmList<Embedded> get children =>
RealmObjectBase.get<Embedded>(this, 'children') as RealmList<Embedded>;
@override
set children(covariant RealmList<Embedded> value) =>
throw RealmUnsupportedSetError();

@override
Embedded? get father =>
RealmObjectBase.get<Embedded>(this, 'father') as Embedded?;
@override
set father(covariant Embedded? value) =>
RealmObjectBase.set(this, 'father', value);

@override
Embedded? get mother =>
RealmObjectBase.get<Embedded>(this, 'mother') as Embedded?;
@override
set mother(covariant Embedded? value) =>
RealmObjectBase.set(this, 'mother', value);

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

@override
Asymmetric freeze() => RealmObjectBase.freezeObject<Asymmetric>(this);

static SchemaObject get schema => _schema ??= _initSchema();
static SchemaObject? _schema;
static SchemaObject _initSchema() {
RealmObjectBase.registerFactory(Asymmetric._);
return const SchemaObject(
ObjectType.realmObject, Asymmetric, 'Asymmetric', [
SchemaProperty('id', RealmPropertyType.objectid,
mapTo: '_id', primaryKey: true),
SchemaProperty('children', RealmPropertyType.object,
linkTarget: 'Embedded', collectionType: RealmCollectionType.list),
SchemaProperty('father', RealmPropertyType.object,
optional: true, linkTarget: 'Embedded'),
SchemaProperty('mother', RealmPropertyType.object,
optional: true, linkTarget: 'Embedded'),
]);
}
}

class Embedded extends _Embedded
with RealmEntity, RealmObjectBase, EmbeddedObject {
Embedded(
String name,
int age,
) {
RealmObjectBase.set(this, 'name', name);
RealmObjectBase.set(this, 'age', age);
}

Embedded._();

@override
String get name => RealmObjectBase.get<String>(this, 'name') as String;
@override
set name(String value) => RealmObjectBase.set(this, 'name', value);

@override
int get age => RealmObjectBase.get<int>(this, 'age') as int;
@override
set age(int value) => RealmObjectBase.set(this, 'age', value);

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

@override
Embedded freeze() => RealmObjectBase.freezeObject<Embedded>(this);

static SchemaObject get schema => _schema ??= _initSchema();
static SchemaObject? _schema;
static SchemaObject _initSchema() {
RealmObjectBase.registerFactory(Embedded._);
return const SchemaObject(ObjectType.embeddedObject, Embedded, 'Embedded', [
SchemaProperty('name', RealmPropertyType.string),
SchemaProperty('age', RealmPropertyType.int),
]);
}
}

Loading

0 comments on commit d11aefa

Please sign in to comment.