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

kn/asymmetric sync #1400

Merged
merged 10 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
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.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
desistefanova marked this conversation as resolved.
Show resolved Hide resolved

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;
│ ^^^^^^^ !
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),
]);
}
}

3 changes: 3 additions & 0 deletions lib/src/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,9 @@ class _RealmCore {
case ObjectType.embeddedObject:
type = EmbeddedObject;
break;
case ObjectType.asymmetricObject:
type = AsymmetricObject;
break;
default:
throw RealmError('$baseType is not supported yet');
}
Expand Down
Loading