From 08a9e9eff5ac9e3e09243b604444718f30343cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Mon, 26 Sep 2022 10:22:05 +0200 Subject: [PATCH 01/10] Add Backlink annotation --- common/lib/src/realm_common_base.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/lib/src/realm_common_base.dart b/common/lib/src/realm_common_base.dart index 23ecbf52e..b2aa4700f 100644 --- a/common/lib/src/realm_common_base.dart +++ b/common/lib/src/realm_common_base.dart @@ -97,3 +97,10 @@ class Indexed { class Ignored { const Ignored(); } + +/// Indicates a backlink property. +/// {@category Annotations} +class Backlink { + final Symbol symbol; + const Backlink(this.symbol); +} From 8f311dd941c8d82466eb56de54e64945ee92c01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Mon, 26 Sep 2022 11:06:16 +0200 Subject: [PATCH 02/10] Support back-links in generator --- generator/lib/src/class_element_ex.dart | 3 +++ generator/lib/src/dart_type_ex.dart | 19 ++++++++++++++----- generator/lib/src/field_element_ex.dart | 13 +++++++++---- generator/lib/src/realm_field_info.dart | 22 +++++++++++++++------- generator/lib/src/realm_model_info.dart | 17 +++++++++++------ generator/lib/src/session.dart | 2 +- generator/lib/src/type_checkers.dart | 2 ++ 7 files changed, 55 insertions(+), 23 deletions(-) diff --git a/generator/lib/src/class_element_ex.dart b/generator/lib/src/class_element_ex.dart index 081389ab9..1ecddea61 100644 --- a/generator/lib/src/class_element_ex.dart +++ b/generator/lib/src/class_element_ex.dart @@ -161,6 +161,9 @@ extension ClassElementEx on ClassElement { todo: 'Remove the @PrimaryKey annotation from the field or set the model type to a value different from ObjectType.embeddedObject.'); } + // Computed fields go last. This is important for the schema generation. + mappedFields.sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1); + return RealmModelInfo(name, modelName, realmName, mappedFields, objectType); } on InvalidGenerationSourceError catch (_) { rethrow; diff --git a/generator/lib/src/dart_type_ex.dart b/generator/lib/src/dart_type_ex.dart index dac8950a9..6da5c049b 100644 --- a/generator/lib/src/dart_type_ex.dart +++ b/generator/lib/src/dart_type_ex.dart @@ -18,6 +18,7 @@ import 'dart:ffi'; import 'dart:typed_data'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:realm_common/realm_common.dart'; import 'package:realm_generator/src/pseudo_type.dart'; @@ -30,7 +31,7 @@ extension DartTypeEx on DartType { bool isExactly() => TypeChecker.fromRuntime(T).isExactlyType(this); bool get isRealmAny => const TypeChecker.fromRuntime(RealmAny).isAssignableFromType(this); - bool get isRealmBacklink => false; // TODO: Implement Backlink support https://github.com/realm/realm-dart/issues/693 + bool get isRealmBacklink => isDartCoreIterable; // && element2 != null ? backlinkChecker.annotationsOfExact(element2!).isNotEmpty : false; bool get isRealmCollection => realmCollectionType != RealmCollectionType.none; bool get isRealmModel => element != null ? realmModelChecker.annotationsOfExact(element!).isNotEmpty : false; @@ -49,19 +50,20 @@ extension DartTypeEx on DartType { DartType? get nullIfDynamic => isDynamic ? null : this; DartType get basicType { - if (isRealmCollection) { - return (this as ParameterizedType).typeArguments.last; + final self = this; + if (self is ParameterizedType && (isRealmCollection || isRealmBacklink)) { + return self.typeArguments.last; } - return asNonNullable; + return this; } String get basicMappedName => basicType.mappedName; DartType get mappedType { final self = this; + final provider = session.typeProvider; if (isRealmCollection) { if (self is ParameterizedType) { - final provider = session.typeProvider; final mapped = self.typeArguments.last.mappedType; if (self != mapped) { if (self.isDartCoreList) { @@ -78,6 +80,13 @@ extension DartTypeEx on DartType { } } } + } else if (isRealmBacklink) { + if (self is ParameterizedType) { + final mapped = self.typeArguments.last.mappedType; + if (self != mapped) { + return PseudoType('RealmResults<${mapped.basicMappedName}>', nullabilitySuffix: NullabilitySuffix.none); + } + } } else if (isRealmModel) { return PseudoType( getDisplayString(withNullability: false).replaceAll(session.prefix, ''), diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index 8b5ff8853..654046247 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -44,6 +44,8 @@ extension FieldElementEx on FieldElement { AnnotationValue? get indexedInfo => annotationInfoOfExact(indexedChecker); + AnnotationValue? get backlinkInfo => annotationInfoOfExact(backlinkChecker); + TypeAnnotation? get typeAnnotation => declarationAstNode.fields.type; Expression? get initializerExpression => declarationAstNode.fields.variables.singleWhere((v) => v.name.name == name).initializer; @@ -76,6 +78,7 @@ extension FieldElementEx on FieldElement { final primaryKey = primaryKeyInfo; final indexed = indexedInfo; + final backlink = backlinkInfo; // Check for as-of-yet unsupported type if (type.isDartCoreSet || // @@ -176,11 +179,12 @@ extension FieldElementEx on FieldElement { todo: todo, ); } else { - // Validate collections - if (type.isRealmCollection) { + // Validate collections and back-links + if (type.isRealmCollection || type.isRealmBacklink) { + final typeDescription = type.isRealmCollection ? 'collections' : 'back-links'; if (type.isNullable) { throw RealmInvalidGenerationSourceError( - 'Realm collections cannot be nullable', + 'Realm $typeDescription cannot be nullable', primarySpan: typeSpan(file), primaryLabel: 'is nullable', todo: '', @@ -189,7 +193,7 @@ extension FieldElementEx on FieldElement { } final itemType = type.basicType; if (itemType.isRealmModel && itemType.isNullable) { - throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in collections', + throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in $typeDescription', primarySpan: typeSpan(file), primaryLabel: 'which has a nullable realm object element type', element: this, @@ -217,6 +221,7 @@ extension FieldElementEx on FieldElement { isPrimaryKey: primaryKey != null, mapTo: remappedRealmName, realmType: realmType, + linkOriginProperty: backlink?.value.getField('symbol')?.toSymbolValue(), ); } on InvalidGenerationSourceError catch (_) { rethrow; diff --git a/generator/lib/src/realm_field_info.dart b/generator/lib/src/realm_field_info.dart index d79ce80fc..19eb205ea 100644 --- a/generator/lib/src/realm_field_info.dart +++ b/generator/lib/src/realm_field_info.dart @@ -20,8 +20,8 @@ import 'package:analyzer/dart/element/type.dart'; import 'package:realm_common/realm_common.dart'; import 'dart_type_ex.dart'; -import 'field_element_ex.dart'; import 'element.dart'; +import 'field_element_ex.dart'; class RealmFieldInfo { final FieldElement fieldElement; @@ -29,6 +29,7 @@ class RealmFieldInfo { final bool isPrimaryKey; final bool indexed; final RealmPropertyType realmType; + final String? linkOriginProperty; RealmFieldInfo({ required this.fieldElement, @@ -36,23 +37,29 @@ class RealmFieldInfo { required this.isPrimaryKey, required this.indexed, required this.realmType, + required this.linkOriginProperty, }); DartType get type => fieldElement.type; bool get isFinal => fieldElement.isFinal; - bool get isRealmCollection => fieldElement.type.isRealmCollection; + bool get isRealmCollection => type.isRealmCollection; + bool get isRealmBacklink => type.isRealmBacklink; bool get isLate => fieldElement.isLate; bool get hasDefaultValue => fieldElement.hasInitializer; - bool get optional => type.isNullable || (type.isRealmCollection && (type as ParameterizedType).typeArguments.last.isNullable); + bool get optional => type.basicType.isNullable; bool get isRequired => !(hasDefaultValue || optional); + bool get isComputed => realmType == RealmPropertyType.linkingObjects; // only computed, so far String get name => fieldElement.name; String get realmName => mapTo ?? name; String get basicMappedTypeName => type.basicMappedName; - String get basicRealmTypeName => fieldElement.modelType.basicType.element?.remappedRealmName ?? fieldElement.modelType.basicMappedName; + String get basicNonNullableMappedTypeName => type.basicType.asNonNullable.mappedName; + + String get basicRealmTypeName => + fieldElement.modelType.basicType.asNonNullable.element2?.remappedRealmName ?? fieldElement.modelType.asNonNullable.basicMappedName; String get modelTypeName => fieldElement.modelTypeName; @@ -61,14 +68,15 @@ class RealmFieldInfo { RealmCollectionType get realmCollectionType => type.realmCollectionType; Iterable toCode() sync* { + final getTypeName = type.isRealmCollection ? basicMappedTypeName : basicNonNullableMappedTypeName; yield '@override'; - yield "$mappedTypeName get $name => RealmObjectBase.get<$basicMappedTypeName>(this, '$realmName') as $mappedTypeName;"; - bool generateSetter = !isFinal && !isRealmCollection; + yield "$mappedTypeName get $name => RealmObjectBase.get<$getTypeName>(this, '$realmName') as $mappedTypeName;"; + bool generateSetter = !isFinal && !isRealmCollection && !isRealmBacklink; if (generateSetter) { yield '@override'; yield "set $name(${mappedTypeName != modelTypeName ? 'covariant ' : ''}$mappedTypeName value) => RealmObjectBase.set(this, '$realmName', value);"; } else { - bool generateThrowError = isLate || isRealmCollection; + bool generateThrowError = isLate || isRealmCollection || isRealmBacklink; if (generateThrowError) { yield '@override'; yield "set $name(${mappedTypeName != modelTypeName ? 'covariant ' : ''}$mappedTypeName value) => throw RealmUnsupportedSetError();"; diff --git a/generator/lib/src/realm_model_info.dart b/generator/lib/src/realm_model_info.dart index 875896be8..9ee34e233 100644 --- a/generator/lib/src/realm_model_info.dart +++ b/generator/lib/src/realm_model_info.dart @@ -29,14 +29,14 @@ class RealmModelInfo { final List fields; final ObjectType baseType; - RealmModelInfo(this.name, this.modelName, this.realmName, this.fields, this.baseType); + const RealmModelInfo(this.name, this.modelName, this.realmName, this.fields, this.baseType); Iterable toCode() sync* { yield 'class $name extends $modelName with RealmEntity, RealmObjectBase, ${baseType.className} {'; { - final allExceptCollections = fields.where((f) => !f.type.isRealmCollection).toList(); + final allSettable = fields.where((f) => !f.type.isRealmCollection && !f.type.isRealmBacklink).toList(); - final hasDefaults = allExceptCollections.where((f) => f.hasDefaultValue).toList(); + final hasDefaults = allSettable.where((f) => f.hasDefaultValue).toList(); if (hasDefaults.isNotEmpty) { yield 'static var _defaultsSet = false;'; yield ''; @@ -44,10 +44,10 @@ class RealmModelInfo { yield '$name('; { - final required = allExceptCollections.where((f) => f.isRequired || f.isPrimaryKey); + final required = allSettable.where((f) => f.isRequired || f.isPrimaryKey); yield* required.map((f) => '${f.mappedTypeName} ${f.name},'); - final notRequired = allExceptCollections.where((f) => !f.isRequired && !f.isPrimaryKey); + final notRequired = allSettable.where((f) => !f.isRequired && !f.isPrimaryKey); final collections = fields.where((f) => f.type.isRealmCollection).toList(); if (notRequired.isNotEmpty || collections.isNotEmpty) { yield '{'; @@ -66,7 +66,7 @@ class RealmModelInfo { yield '}'; } - yield* allExceptCollections.map((f) { + yield* allSettable.map((f) { return "RealmObjectBase.set(this, '${f.realmName}', ${f.name});"; }); @@ -105,6 +105,11 @@ class RealmModelInfo { if (f.optional) 'optional': f.optional, if (f.isPrimaryKey) 'primaryKey': f.isPrimaryKey, if (f.realmType == RealmPropertyType.object) 'linkTarget': f.basicRealmTypeName, + if (f.realmType == RealmPropertyType.linkingObjects) ...{ + 'linkOriginProperty': f.linkOriginProperty!, + 'collectionType': RealmCollectionType.list, + 'linkTarget': f.basicRealmTypeName, + }, if (f.realmCollectionType != RealmCollectionType.none) 'collectionType': f.realmCollectionType, }; return "SchemaProperty('${f.realmName}', ${f.realmType}${namedArgs.isNotEmpty ? ', ${namedArgs.toArgsString()}' : ''}),"; diff --git a/generator/lib/src/session.dart b/generator/lib/src/session.dart index e5cf51e29..72141838c 100644 --- a/generator/lib/src/session.dart +++ b/generator/lib/src/session.dart @@ -42,7 +42,7 @@ Future scopeSession( ); return await runZonedGuarded( fn, - (e, st) => throw e, + (e, st) => Error.throwWithStackTrace(e, st), zoneValues: {_sessionKey: s}, )!; } diff --git a/generator/lib/src/type_checkers.dart b/generator/lib/src/type_checkers.dart index 0e9232d77..96890c087 100644 --- a/generator/lib/src/type_checkers.dart +++ b/generator/lib/src/type_checkers.dart @@ -27,6 +27,8 @@ const mapToChecker = TypeChecker.fromRuntime(MapTo); const primaryKeyChecker = TypeChecker.fromRuntime(PrimaryKey); +const backlinkChecker = TypeChecker.fromRuntime(Backlink); + const realmAnnotationChecker = TypeChecker.any([ ignoredChecker, indexedChecker, From 8d5a8177bbabe5dfdfbc2ad858b631cc5f79add0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Mon, 26 Sep 2022 11:13:13 +0200 Subject: [PATCH 03/10] Test back-link generator support --- generator/test/good_test_data/all_types.dart | 4 ++++ .../test/good_test_data/all_types.expected | 20 +++++++++++++++++++ generator/test/test_util.dart | 12 +++++------ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/generator/test/good_test_data/all_types.dart b/generator/test/good_test_data/all_types.dart index de75979c3..b0f45f446 100644 --- a/generator/test/good_test_data/all_types.dart +++ b/generator/test/good_test_data/all_types.dart @@ -6,6 +6,7 @@ import 'package:realm_common/realm_common.dart'; @MapTo('MyFoo') class _Foo { int x = 0; + late _Bar? bar; } @RealmModel() @@ -32,6 +33,9 @@ class _Bar { String? anOptionalString; late ObjectId objectId; + + @Backlink(#bar) + late Iterable<_Foo> foos; } @RealmModel() diff --git a/generator/test/good_test_data/all_types.expected b/generator/test/good_test_data/all_types.expected index e06c1a152..e5f3a9e4b 100644 --- a/generator/test/good_test_data/all_types.expected +++ b/generator/test/good_test_data/all_types.expected @@ -7,6 +7,7 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { Foo({ int x = 0, + Bar? bar, }) { if (!_defaultsSet) { _defaultsSet = RealmObjectBase.setDefaults({ @@ -14,6 +15,7 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { }); } RealmObjectBase.set(this, 'x', x); + RealmObjectBase.set(this, 'bar', bar); } Foo._(); @@ -23,6 +25,11 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { @override set x(int value) => RealmObjectBase.set(this, 'x', value); + @override + Bar? get bar => RealmObjectBase.get(this, 'bar') as Bar?; + @override + set bar(covariant Bar? value) => RealmObjectBase.set(this, 'bar', value); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -36,6 +43,8 @@ class Foo extends _Foo with RealmEntity, RealmObjectBase, RealmObject { RealmObjectBase.registerFactory(Foo._); return const SchemaObject(ObjectType.realmObject, Foo, 'MyFoo', [ SchemaProperty('x', RealmPropertyType.int), + SchemaProperty('bar', RealmPropertyType.object, + optional: true, linkTarget: 'Bar'), ]); } } @@ -133,6 +142,13 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { @override set objectId(ObjectId value) => RealmObjectBase.set(this, 'objectId', value); + @override + RealmResults get foos => + RealmObjectBase.get(this, 'foos') as RealmResults; + @override + set foos(covariant RealmResults value) => + throw RealmUnsupportedSetError(); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -159,6 +175,10 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { SchemaProperty('anOptionalString', RealmPropertyType.string, optional: true), SchemaProperty('objectId', RealmPropertyType.objectid), + SchemaProperty('foos', RealmPropertyType.linkingObjects, + linkOriginProperty: 'bar', + collectionType: RealmCollectionType.list, + linkTarget: 'MyFoo'), ]); } } diff --git a/generator/test/test_util.dart b/generator/test/test_util.dart index f700e026e..b641519f3 100644 --- a/generator/test/test_util.dart +++ b/generator/test/test_util.dart @@ -15,7 +15,7 @@ Map getListOfTestFiles(String directory) { for (var file in files) { if (_path.extension(file.path) == '.dart' && !file.path.endsWith('g.dart')) { var expectedFileName = _path.setExtension(file.path, '.expected'); - if (!files.any((f) => f.path == expectedFileName)) { + if (!files.any((f) => f.path == expectedFileName)) { throw "Expected file not found. $expectedFileName"; } result.addAll({_path.basename(file.path): _path.basename(expectedFileName)}); @@ -53,11 +53,6 @@ class LinesEqualsMatcher extends Matcher { final actualValue = utf8.decode(actual as List); final actualLines = actualValue.split("\n"); - if (actualLines.length > expectedLines.length) { - matchState["Error"] = "Different number of lines. \nExpected: ${expectedLines.length}\nActual: ${actualLines.length}"; - return false; - } - for (var i = 0; i < expectedLines.length - 1; i++) { if (i >= actualLines.length) { matchState["Error"] = "Difference at line ${i + 1}. \nExpected: ${expectedLines[i]}.\n Actual: empty"; @@ -70,6 +65,11 @@ class LinesEqualsMatcher extends Matcher { } } + if (actualLines.length > expectedLines.length) { + matchState["Error"] = "Different number of lines. \nExpected: ${expectedLines.length}\nActual: ${actualLines.length}"; + return false; + } + return true; } From 04746dd7e69fe3603929b0df8c54c689d27231a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 27 Oct 2022 09:48:06 +0200 Subject: [PATCH 04/10] Support backlinks --- lib/src/native/realm_core.dart | 21 +++++++++++----- lib/src/realm_object.dart | 45 ++++++++++++++++++++-------------- lib/src/realm_property.dart | 17 +++++++++++-- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index bd768597e..7c6bddfed 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -24,6 +24,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:cancellation_token/cancellation_token.dart'; +import 'package:collection/collection.dart'; // Hide StringUtf8Pointer.toNativeUtf8 and StringUtf16Pointer since these allows silently allocating memory. Use toUtf8Ptr instead import 'package:ffi/ffi.dart' hide StringUtf8Pointer, StringUtf16Pointer; import 'package:logging/logging.dart'; @@ -120,15 +121,17 @@ class _RealmCore { for (var i = 0; i < classCount; i++) { final schemaObject = schema.elementAt(i); final classInfo = schemaClasses.elementAt(i).ref; + final propertiesCount = schemaObject.properties.length; + final computedCount = schemaObject.properties.where((p) => p.isComputed).length; + final persistedCount = propertiesCount - computedCount; classInfo.name = schemaObject.name.toCharPtr(arena); classInfo.primary_key = "".toCharPtr(arena); - classInfo.num_properties = schemaObject.properties.length; - classInfo.num_computed_properties = 0; + classInfo.num_properties = persistedCount; + classInfo.num_computed_properties = computedCount; classInfo.key = RLM_INVALID_CLASS_KEY; classInfo.flags = schemaObject.baseType.flags; - final propertiesCount = schemaObject.properties.length; final properties = arena(propertiesCount); for (var j = 0; j < propertiesCount; j++) { @@ -138,7 +141,7 @@ class _RealmCore { //TODO: Assign the correct public name value https://github.com/realm/realm-dart/issues/697 propInfo.public_name = "".toCharPtr(arena); propInfo.link_target = (schemaProperty.linkTarget ?? "").toCharPtr(arena); - propInfo.link_origin_property_name = "".toCharPtr(arena); + propInfo.link_origin_property_name = (schemaProperty.linkOriginProperty ?? "").toCharPtr(arena); propInfo.type = schemaProperty.propertyType.index; propInfo.collection_type = schemaProperty.collectionType.index; propInfo.flags = realm_property_flags.RLM_PROPERTY_NORMAL; @@ -695,10 +698,11 @@ 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 linkOriginProperty = property.ref.link_origin_property_name.cast().toRealmDartString(treatEmptyAsNull: true); final isNullable = property.ref.flags & realm_property_flags.RLM_PROPERTY_NULLABLE != 0; final isPrimaryKey = propertyName == primaryKeyName; - final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, RealmPropertyType.values.elementAt(property.ref.type), isNullable, - isPrimaryKey, RealmCollectionType.values.elementAt(property.ref.collection_type)); + final propertyMeta = RealmPropertyMetadata(property.ref.key, objectType, linkOriginProperty, RealmPropertyType.values.elementAt(property.ref.type), + isNullable, isPrimaryKey, RealmCollectionType.values.elementAt(property.ref.collection_type)); result[propertyName] = propertyMeta; } return result; @@ -967,6 +971,11 @@ class _RealmCore { return RealmListHandle._(pointer, object.realm.handle); } + RealmResultsHandle getBacklinks(RealmObjectBase object, int sourceTableKey, int propertyKey) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_backlinks(object.handle._pointer, sourceTableKey, propertyKey)); + return RealmResultsHandle._(pointer, object.realm.handle); + } + int getListSize(RealmListHandle handle) { return using((Arena arena) { final size = arena(); diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index 735bb6904..797aab743 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -21,10 +21,11 @@ import 'dart:ffi'; import 'package:collection/collection.dart'; +import 'configuration.dart'; import 'list.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; -import 'configuration.dart'; +import 'results.dart'; typedef DartDynamic = dynamic; @@ -126,8 +127,9 @@ class RealmPropertyMetadata { final RealmPropertyType propertyType; final bool isNullable; final String? objectType; + final String? linkOriginProperty; final bool isPrimaryKey; - const RealmPropertyMetadata(this.key, this.objectType, this.propertyType, this.isNullable, this.isPrimaryKey, + const RealmPropertyMetadata(this.key, this.objectType, this.linkOriginProperty, this.propertyType, this.isNullable, this.isPrimaryKey, [this.collectionType = RealmCollectionType.none]); } @@ -142,24 +144,31 @@ class RealmCoreAccessor implements RealmAccessor { 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()) { - switch (listMetadata.schema.baseType) { - case ObjectType.realmObject: - return object.realm.createList(handle, listMetadata); - case ObjectType.embeddedObject: - return object.realm.createList(handle, listMetadata); - default: - throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); + if (propertyMeta.propertyType == RealmPropertyType.linkingObjects) { + final sourceMeta = object.realm.metadata.getByName(propertyMeta.objectType!); + final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; + final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); + return RealmResultsInternal.create(handle, object.realm, metadata); + } else { + 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()) { + switch (listMetadata.schema.baseType) { + case ObjectType.realmObject: + return object.realm.createList(handle, listMetadata); + case ObjectType.embeddedObject: + return object.realm.createList(handle, listMetadata); + default: + throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); + } } - } - return object.realm.createList(handle, listMetadata); + return object.realm.createList(handle, listMetadata); + } } Object? value = realmCore.getProperty(object, propertyMeta.key); diff --git a/lib/src/realm_property.dart b/lib/src/realm_property.dart index e33abf86a..0efa96c14 100644 --- a/lib/src/realm_property.dart +++ b/lib/src/realm_property.dart @@ -26,6 +26,8 @@ class SchemaProperty { final String? linkTarget; + final String? linkOriginProperty; + /// Defines the `Realm` collection type if this property is a collection final RealmCollectionType collectionType; @@ -35,6 +37,9 @@ class SchemaProperty { /// The `Realm` type of the property final RealmPropertyType propertyType; + /// `true` if the property is computed + bool get isComputed => propertyType == RealmPropertyType.linkingObjects; + /// `true` if the property is optional final bool optional; @@ -42,6 +47,14 @@ class SchemaProperty { final String? mapTo; /// @nodoc - const SchemaProperty(this.name, this.propertyType, - {this.optional = false, this.mapTo, this.primaryKey = false, this.linkTarget, this.collectionType = RealmCollectionType.none}); + const SchemaProperty( + this.name, + this.propertyType, { + this.optional = false, + this.mapTo, + this.primaryKey = false, + this.linkTarget, + this.linkOriginProperty, + this.collectionType = RealmCollectionType.none, + }); } From 55cea960d228763d22838c2e8ea76832e1de1ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 27 Oct 2022 09:47:19 +0200 Subject: [PATCH 05/10] Backlink tests --- .vscode/settings.json | 1 + test/backlinks_test.dart | 139 +++++++++++++++++++++++++++++++++++++ test/backlinks_test.g.dart | 128 ++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 test/backlinks_test.dart create mode 100644 test/backlinks_test.g.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 7fda08abe..2023b9060 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "dart.lineLength": 160, "cSpell.words": [ "apikeys", + "backlinks", "BEGINSWITH", "bson", "deallocated", diff --git a/test/backlinks_test.dart b/test/backlinks_test.dart new file mode 100644 index 000000000..52f1965ba --- /dev/null +++ b/test/backlinks_test.dart @@ -0,0 +1,139 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'package:realm_common/realm_common.dart'; +import 'package:test/expect.dart'; + +import '../lib/realm.dart'; +import 'test.dart'; + +part 'backlinks_test.g.dart'; + +@RealmModel() +class _Source { + String name = 'source'; + _Target? oneTarget; + List<_Target> manyTargets = []; +} + +@RealmModel() +class _Target { + @Backlink(#oneTarget) + late Iterable<_Source> oneToMany; // computed property, so must go last in generated class! + + String name = 'target'; + + @Backlink(#manyTargets) + late Iterable<_Source> manyToMany; // computed property, so must go last in generated class! +} + +Future main([List? args]) async { + await setupTests(args); + + test('Backlinks empty', () { + final config = Configuration.local([Target.schema, Source.schema]); + final realm = getRealm(config); + + final target = realm.write(() => realm.add(Target())); + + expect(target.oneToMany, isEmpty); + }); + + test('Backlinks 1-1(ish)', () { + final config = Configuration.local([Target.schema, Source.schema]); + final realm = getRealm(config); + + final target = Target(); + final source = realm.write(() => realm.add(Source(oneTarget: target))); + + expect(source.oneTarget, target); + expect(target.oneToMany, [source]); + }); + + test('Backlinks 1-many', () { + final config = Configuration.local([Target.schema, Source.schema]); + final realm = getRealm(config); + + final target = Target(); + final sources = List.generate(100, (i) => Source(oneTarget: target, name: '$i')); + realm.write(() => realm.addAll(sources)); + + expect(target.oneToMany, sources); + }); + + test('Backlinks many-many', () { + final config = Configuration.local([Target.schema, Source.schema]); + final realm = getRealm(config); + + final targets = List.generate(100, (i) => Target(name: '$i')); + final sources = List.generate(100, (i) => Source(manyTargets: targets)); + + realm.write(() => realm.addAll(sources)); + + for (final t in targets) { + expect(t.manyToMany, sources); + } + + for (final s in sources) { + expect(s.manyTargets, targets); + } + }); + + test('Backlinks query', () { + final config = Configuration.local([Target.schema, Source.schema]); + final realm = getRealm(config); + + final target = Target(); + final sources = List.generate(100, (i) => Source(oneTarget: target, name: '$i')); + realm.write(() => realm.addAll(sources)); + + final fortyTwo = realm.query(r'name == $0', ['42']).single; + expect(target.oneToMany[42], fortyTwo); + expect(target.oneToMany.query(r'name = $0', ['42']), [fortyTwo]); + }); + + test('Backlinks notifications', () { + final config = Configuration.local([Target.schema, Source.schema]); + final realm = getRealm(config); + + final target = realm.write(() => realm.add(Target())); + + expectLater( + target.oneToMany.changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>() // + .having((ch) => ch.inserted, 'inserted', [0, 2]) // is this surprising? + .having((ch) => ch.deleted, 'deleted', [0]) // + .having((ch) => ch.modified, 'modified', [1]), + ])); + + final first = realm.write(() => realm.add(Source(oneTarget: target))); + + final second = realm.write(() => realm.add(Source(oneTarget: target))); + + realm.write(() { + realm.add(Source(oneTarget: target)); + realm.add(Source(oneTarget: target)); + second.name = "changed second"; + realm.delete(first); + }); + }); +} diff --git a/test/backlinks_test.g.dart b/test/backlinks_test.g.dart new file mode 100644 index 000000000..1c100514d --- /dev/null +++ b/test/backlinks_test.g.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'backlinks_test.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + Source({ + String name = 'source', + Target? oneTarget, + Iterable manyTargets = const [], + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'name': 'source', + }); + } + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'oneTarget', oneTarget); + RealmObjectBase.set>( + this, 'manyTargets', RealmList(manyTargets)); + } + + Source._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + Target? get oneTarget => + RealmObjectBase.get(this, 'oneTarget') as Target?; + @override + set oneTarget(covariant Target? value) => + RealmObjectBase.set(this, 'oneTarget', value); + + @override + RealmList get manyTargets => + RealmObjectBase.get(this, 'manyTargets') as RealmList; + @override + set manyTargets(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Source freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Source._); + return const SchemaObject(ObjectType.realmObject, Source, 'Source', [ + SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('oneTarget', RealmPropertyType.object, + optional: true, linkTarget: 'Target'), + SchemaProperty('manyTargets', RealmPropertyType.object, + linkTarget: 'Target', collectionType: RealmCollectionType.list), + ]); + } +} + +class Target extends _Target with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + Target({ + String name = 'target', + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'name': 'target', + }); + } + RealmObjectBase.set(this, 'name', name); + } + + Target._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + RealmResults get oneToMany => + RealmObjectBase.get(this, 'oneToMany') as RealmResults; + @override + set oneToMany(covariant RealmResults value) => + throw RealmUnsupportedSetError(); + + @override + RealmResults get manyToMany => + RealmObjectBase.get(this, 'manyToMany') as RealmResults; + @override + set manyToMany(covariant RealmResults value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Target freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Target._); + return const SchemaObject(ObjectType.realmObject, Target, 'Target', [ + SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('oneToMany', RealmPropertyType.linkingObjects, + linkOriginProperty: 'oneTarget', + collectionType: RealmCollectionType.list, + linkTarget: 'Source'), + SchemaProperty('manyToMany', RealmPropertyType.linkingObjects, + linkOriginProperty: 'manyTargets', + collectionType: RealmCollectionType.list, + linkTarget: 'Source'), + ]); + } +} From a66c5a377a5e97e8c868663f33adcf1297a24ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 27 Oct 2022 11:11:54 +0200 Subject: [PATCH 06/10] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d753876..16a86c704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Added `User.functions`. This is the entry point for calling Atlas App functions. Functions allow you to define and execute server-side logic for your application. Atlas App functions are created on the server, written in modern JavaScript (ES6+) and executed in a serverless manner. When you call a function, you can dynamically access components of the current application as well as information about the request to execute the function and the logged in user that sent the request. ([#973](https://github.com/realm/realm-dart/pull/973)) * Support results of primitives, ie. `RealmResult`. (Issue [#162](https://github.com/realm/realm-dart/issues/162)) * Support notifications on all managed realm lists, including list of primitives, ie. `RealmList.changes` is supported. ([#893](https://github.com/realm/realm-dart/pull/893)) +* Support named backlinks on realm models. You can now add and annotate a realm object iterator field with `@Backlink(#symbolName)`. ([#996](https://github.com/realm/realm-dart/pull/996)) ### Fixed * Fixed a wrong mapping for `AuthProviderType` returned by `User.provider` for google, facebook and apple credentials. From ec0e3e5d776bf4a7eaf18b93c9e8c9d15719cda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 27 Oct 2022 18:46:09 +0200 Subject: [PATCH 07/10] Harden generator wrt. backlinks --- .vscode/settings.json | 3 +- generator/lib/src/class_element_ex.dart | 15 +++--- generator/lib/src/dart_type_ex.dart | 10 ++-- generator/lib/src/element.dart | 1 - generator/lib/src/error.dart | 2 +- generator/lib/src/field_element_ex.dart | 54 ++++++++++++++++--- generator/lib/src/format_spans.dart | 2 +- generator/lib/src/pseudo_type.dart | 10 ++-- generator/lib/src/realm_field_info.dart | 4 +- generator/lib/src/realm_model_info.dart | 2 +- generator/lib/src/session.dart | 11 ++-- generator/test/error_test.dart | 4 +- ...link_annotation_on_incompatible_field.dart | 7 +++ ..._annotation_on_incompatible_field.expected | 11 ++++ ...cklink_annotation_on_invalid_iterable.dart | 7 +++ ...nk_annotation_on_invalid_iterable.expected | 11 ++++ .../backlink_illegal_symbol.dart | 12 +++++ .../backlink_illegal_symbol.expected | 11 ++++ .../backlink_incompatible_type.dart | 15 ++++++ .../backlink_incompatible_type.expected | 11 ++++ .../backlink_unknown_symbol.dart | 12 +++++ .../backlink_unknown_symbol.expected | 11 ++++ .../invalid_model_name_mapping.expected | 2 +- .../error_test_data/missing_underscore.g.dart | 3 -- generator/test/test_util.dart | 4 +- 25 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 generator/test/error_test_data/backlink_annotation_on_incompatible_field.dart create mode 100644 generator/test/error_test_data/backlink_annotation_on_incompatible_field.expected create mode 100644 generator/test/error_test_data/backlink_annotation_on_invalid_iterable.dart create mode 100644 generator/test/error_test_data/backlink_annotation_on_invalid_iterable.expected create mode 100644 generator/test/error_test_data/backlink_illegal_symbol.dart create mode 100644 generator/test/error_test_data/backlink_illegal_symbol.expected create mode 100644 generator/test/error_test_data/backlink_incompatible_type.dart create mode 100644 generator/test/error_test_data/backlink_incompatible_type.expected create mode 100644 generator/test/error_test_data/backlink_unknown_symbol.dart create mode 100644 generator/test/error_test_data/backlink_unknown_symbol.expected delete mode 100644 generator/test/error_test_data/missing_underscore.g.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 2023b9060..57921f42e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,7 +32,8 @@ "unmanaged", "upsert", "usercode", - "userdata" + "userdata", + "writeln" ], "cmake.statusbar.advanced": { "ctest": { diff --git a/generator/lib/src/class_element_ex.dart b/generator/lib/src/class_element_ex.dart index 1ecddea61..90f733c9e 100644 --- a/generator/lib/src/class_element_ex.dart +++ b/generator/lib/src/class_element_ex.dart @@ -51,9 +51,7 @@ extension on Iterable { element: field, primarySpan: field.span!, primaryLabel: 'second primary key', - secondarySpans: { - ...{for (final p in primaryKeys..removeAt(1)) p.fieldElement.span!: ''}, - }, + secondarySpans: {for (final p in primaryKeys..removeAt(1)) p.fieldElement.span!: ''}, ); } } @@ -87,8 +85,13 @@ extension ClassElementEx on ClassElement { } if (!modelName.endsWith(suffix)) { - throw RealmInvalidGenerationSourceError('Missing suffix on realm model name', - element: this, primarySpan: span, primaryLabel: 'missing suffix', todo: 'Align class name to have suffix $suffix,'); + throw RealmInvalidGenerationSourceError( + 'Missing suffix on realm model name', + element: this, + primarySpan: span, + primaryLabel: 'missing suffix', + todo: 'Align class name to have suffix $suffix,', + ); } // Remove suffix and prefix, if any. @@ -170,7 +173,7 @@ extension ClassElementEx on ClassElement { } catch (e, s) { // Fallback. Not perfect, but better than just forwarding original error. throw RealmInvalidGenerationSourceError( - '$e \n $s', + '$e\n$s', todo: // 'Unexpected error. Please open an issue on: ' 'https://github.com/realm/realm-dart', diff --git a/generator/lib/src/dart_type_ex.dart b/generator/lib/src/dart_type_ex.dart index 6da5c049b..05602b662 100644 --- a/generator/lib/src/dart_type_ex.dart +++ b/generator/lib/src/dart_type_ex.dart @@ -31,12 +31,12 @@ extension DartTypeEx on DartType { bool isExactly() => TypeChecker.fromRuntime(T).isExactlyType(this); bool get isRealmAny => const TypeChecker.fromRuntime(RealmAny).isAssignableFromType(this); - bool get isRealmBacklink => isDartCoreIterable; // && element2 != null ? backlinkChecker.annotationsOfExact(element2!).isNotEmpty : false; bool get isRealmCollection => realmCollectionType != RealmCollectionType.none; - bool get isRealmModel => element != null ? realmModelChecker.annotationsOfExact(element!).isNotEmpty : false; + bool get isRealmModel => element2 != null ? realmModelChecker.annotationsOfExact(element2!).isNotEmpty : false; bool get isNullable => session.typeSystem.isNullable(this); DartType get asNonNullable => session.typeSystem.promoteToNonNull(this); + DartType get asNullable => session.typeSystem.leastUpperBound(this, session.typeProvider.nullType); RealmCollectionType get realmCollectionType { if (isDartCoreSet) return RealmCollectionType.set; @@ -51,7 +51,7 @@ extension DartTypeEx on DartType { DartType get basicType { final self = this; - if (self is ParameterizedType && (isRealmCollection || isRealmBacklink)) { + if (self is ParameterizedType && (isRealmCollection || isDartCoreIterable)) { return self.typeArguments.last; } return this; @@ -80,7 +80,7 @@ extension DartTypeEx on DartType { } } } - } else if (isRealmBacklink) { + } else if (isDartCoreIterable) { if (self is ParameterizedType) { final mapped = self.typeArguments.last.mappedType; if (self != mapped) { @@ -114,7 +114,7 @@ extension DartTypeEx on DartType { if (isDartCoreNum || isDartCoreDouble) return RealmPropertyType.double; if (isExactly()) return RealmPropertyType.decimal128; if (isRealmModel) return RealmPropertyType.object; - if (isRealmBacklink) return RealmPropertyType.linkingObjects; + if (isDartCoreIterable) return RealmPropertyType.linkingObjects; if (isExactly()) return RealmPropertyType.objectid; if (isExactly()) return RealmPropertyType.uuid; diff --git a/generator/lib/src/element.dart b/generator/lib/src/element.dart index 845554d9c..b6f30cb95 100644 --- a/generator/lib/src/element.dart +++ b/generator/lib/src/element.dart @@ -27,7 +27,6 @@ import 'package:realm_generator/src/expanded_context_span.dart'; import 'package:source_gen/source_gen.dart'; import 'package:source_span/source_span.dart'; -import 'annotation_value.dart'; import 'class_element_ex.dart'; import 'error.dart'; import 'field_element_ex.dart'; diff --git a/generator/lib/src/error.dart b/generator/lib/src/error.dart index 6e95367fa..23d72118a 100644 --- a/generator/lib/src/error.dart +++ b/generator/lib/src/error.dart @@ -43,7 +43,7 @@ class RealmInvalidGenerationSourceError extends InvalidGenerationSourceError { color = color ?? session.color, super(message, todo: todo, element: element) { if (element is FieldElement || element is ConstructorElement) { - final classElement = element.enclosingElement!; + final classElement = element.enclosingElement3!; this.secondarySpans.addAll({ classElement.span!: "in realm model for '${session.mapping.entries.where((e) => e.value == classElement).singleOrNull?.key}'", }); diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index 654046247..1a802af89 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -22,6 +22,7 @@ import 'package:build/build.dart'; import 'package:realm_common/realm_common.dart'; import 'package:realm_generator/src/expanded_context_span.dart'; import 'package:realm_generator/src/pseudo_type.dart'; +import 'package:realm_generator/src/utils.dart'; import 'package:source_gen/source_gen.dart'; import 'package:source_span/source_span.dart'; @@ -33,7 +34,6 @@ import 'format_spans.dart'; import 'realm_field_info.dart'; import 'session.dart'; import 'type_checkers.dart'; -import 'utils.dart'; extension FieldElementEx on FieldElement { FieldDeclaration get declarationAstNode => getDeclarationFromElement(this)!.node.parent!.parent as FieldDeclaration; @@ -48,7 +48,7 @@ extension FieldElementEx on FieldElement { TypeAnnotation? get typeAnnotation => declarationAstNode.fields.type; - Expression? get initializerExpression => declarationAstNode.fields.variables.singleWhere((v) => v.name.name == name).initializer; + Expression? get initializerExpression => declarationAstNode.fields.variables.singleWhere((v) => v.name2.toString() == name).initializer; FileSpan? typeSpan(SourceFile file) => ExpandedContextSpan( ExpandedContextSpan( @@ -115,7 +115,7 @@ extension FieldElementEx on FieldElement { // // However, this may change in the future. Either as the dart language team change this // blemish. Or perhaps we can avoid the late modifier, once static meta programming lands - // in dart. Therefor we keep the code outcommented for later. + // in dart. Therefor we keep the code out-commented for later. /* if (!isFinal) { throw RealmInvalidGenerationSourceError( @@ -150,11 +150,11 @@ extension FieldElementEx on FieldElement { } // Validate field type - final modelSpan = enclosingElement.span!; + final modelSpan = enclosingElement3.span!; final file = modelSpan.file; final realmType = type.realmType; if (realmType == null) { - final notARealmTypeSpan = type.element?.span; + final notARealmTypeSpan = type.element2?.span; String todo; if (notARealmTypeSpan != null) { todo = // @@ -172,15 +172,55 @@ extension FieldElementEx on FieldElement { primarySpan: typeSpan(file), primaryLabel: '$modelTypeName is not a realm model type', secondarySpans: { - modelSpan: "in realm model '${enclosingElement.displayName}'", + modelSpan: "in realm model '${enclosingElement3.displayName}'", // may go both above and below, or stem from another file if (notARealmTypeSpan != null) notARealmTypeSpan: '' }, todo: todo, ); } else { + // Validate back-links + if (backlink != null) { + if (!type.isDartCoreIterable || !(type as ParameterizedType).typeArguments.first.isRealmModel) { + throw RealmInvalidGenerationSourceError( + 'Backlink must be an iterable of realm objects', + primarySpan: typeSpan(file), + primaryLabel: '$modelTypeName is not an iterable of realm objects', + todo: '', + element: this, + ); + } + + final sourceFieldName = backlink.value.getField('symbol')?.toSymbolValue(); + final sourceType = (type as ParameterizedType).typeArguments.first; + final sourceField = (sourceType.element2 as ClassElement?)?.fields.where((f) => f.name == sourceFieldName).singleOrNull; + + if (sourceField == null) { + throw RealmInvalidGenerationSourceError( + 'Backlink must point to a valid field', + primarySpan: typeSpan(file), + primaryLabel: '$sourceType does not have a field named $sourceFieldName', + todo: '', + element: this, + ); + } + + final thisType = (enclosingElement3 as ClassElement).thisType; + final linkType = thisType.asNullable; + final listOf = session.typeProvider.listType(thisType); + if (sourceField.type != linkType && sourceField.type != listOf) { + throw RealmInvalidGenerationSourceError( + 'Incompatible back-link type', + primarySpan: typeSpan(file), + primaryLabel: "$sourceType.$sourceFieldName is not a '$linkType' or '$listOf'", + todo: '', + element: this, + ); + } + } + // Validate collections and back-links - if (type.isRealmCollection || type.isRealmBacklink) { + if (type.isRealmCollection || backlink != null) { final typeDescription = type.isRealmCollection ? 'collections' : 'back-links'; if (type.isNullable) { throw RealmInvalidGenerationSourceError( diff --git a/generator/lib/src/format_spans.dart b/generator/lib/src/format_spans.dart index 4aa098933..4473cfb04 100644 --- a/generator/lib/src/format_spans.dart +++ b/generator/lib/src/format_spans.dart @@ -39,7 +39,7 @@ String formatSpans( color: color, ); buffer - ..write('\n' * 2 + 'in: ') + ..write('${'\n' * 2}in: ') ..writeln(span.start.toolString) ..write(formatted); } diff --git a/generator/lib/src/pseudo_type.dart b/generator/lib/src/pseudo_type.dart index 11766fff5..e31ef0790 100644 --- a/generator/lib/src/pseudo_type.dart +++ b/generator/lib/src/pseudo_type.dart @@ -36,11 +36,11 @@ class PseudoType extends TypeImpl { // Private Symbols are suffixed with a secret '@' // .. hence this ugly trick ヽ(ಠ_ಠ)ノ - final _writeSymbol = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_write"')); - im.invoke(_writeSymbol, [_name]); // #_write won't work + final writeSymbol = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_write"')); + im.invoke(writeSymbol, [_name]); // #_write won't work - final _writeNullability = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_writeNullability"')); - im.invoke(_writeNullability, [nullabilitySuffix]); // #_writeNullability won't work + final writeNullability = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_writeNullability"')); + im.invoke(writeNullability, [nullabilitySuffix]); // #_writeNullability won't work } @override @@ -52,5 +52,5 @@ class PseudoType extends TypeImpl { } @override - Element? get element2 => throw UnimplementedError(); + Element? get element2 => null; } diff --git a/generator/lib/src/realm_field_info.dart b/generator/lib/src/realm_field_info.dart index 19eb205ea..6951f6245 100644 --- a/generator/lib/src/realm_field_info.dart +++ b/generator/lib/src/realm_field_info.dart @@ -44,12 +44,12 @@ class RealmFieldInfo { bool get isFinal => fieldElement.isFinal; bool get isRealmCollection => type.isRealmCollection; - bool get isRealmBacklink => type.isRealmBacklink; bool get isLate => fieldElement.isLate; bool get hasDefaultValue => fieldElement.hasInitializer; bool get optional => type.basicType.isNullable; bool get isRequired => !(hasDefaultValue || optional); - bool get isComputed => realmType == RealmPropertyType.linkingObjects; // only computed, so far + bool get isRealmBacklink => realmType == RealmPropertyType.linkingObjects; + bool get isComputed => isRealmBacklink; // only computed, so far String get name => fieldElement.name; String get realmName => mapTo ?? name; diff --git a/generator/lib/src/realm_model_info.dart b/generator/lib/src/realm_model_info.dart index 9ee34e233..0d8a785c8 100644 --- a/generator/lib/src/realm_model_info.dart +++ b/generator/lib/src/realm_model_info.dart @@ -34,7 +34,7 @@ class RealmModelInfo { Iterable toCode() sync* { yield 'class $name extends $modelName with RealmEntity, RealmObjectBase, ${baseType.className} {'; { - final allSettable = fields.where((f) => !f.type.isRealmCollection && !f.type.isRealmBacklink).toList(); + final allSettable = fields.where((f) => !f.type.isRealmCollection && !f.isRealmBacklink).toList(); final hasDefaults = allSettable.where((f) => f.hasDefaultValue).toList(); if (hasDefaults.isNotEmpty) { diff --git a/generator/lib/src/session.dart b/generator/lib/src/session.dart index 72141838c..438b42b0a 100644 --- a/generator/lib/src/session.dart +++ b/generator/lib/src/session.dart @@ -15,6 +15,7 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// + import 'dart:async'; import 'package:analyzer/dart/analysis/results.dart'; @@ -22,10 +23,10 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type_provider.dart'; import 'package:analyzer/dart/element/type_system.dart'; -const _sessionKey = #SessonKey; +const _sessionKey = #SessionKey; // in case multiple libs are processed concurrently, we make session zone local -_Session get session => Zone.current[_sessionKey] as _Session; +Session get session => Zone.current[_sessionKey] as Session; Future scopeSession( ResolvedLibraryResult resolvedLibrary, @@ -34,7 +35,7 @@ Future scopeSession( String? suffix, bool color = false, }) async { - final s = _Session( + final s = Session( resolvedLibrary, prefix: prefix, suffix: suffix, @@ -47,14 +48,14 @@ Future scopeSession( )!; } -class _Session { +class Session { final ResolvedLibraryResult resolvedLibrary; final Pattern prefix; final String suffix; final bool color; final mapping = {}; // shared - _Session(this.resolvedLibrary, {String? prefix, String? suffix, this.color = false}) + Session(this.resolvedLibrary, {String? prefix, String? suffix, this.color = false}) : prefix = prefix ?? RegExp(r'[_$]'), // defaults to _ or $ suffix = suffix ?? ''; diff --git a/generator/test/error_test.dart b/generator/test/error_test.dart index 1711f939f..b3e3fefd3 100644 --- a/generator/test/error_test.dart +++ b/generator/test/error_test.dart @@ -6,8 +6,8 @@ void main() { const directory = 'test/error_test_data'; getListOfTestFiles(directory).forEach((inputFile, outputFile) { executeTest(getTestName(inputFile), () async { - await expectLater( - () async => await generatorTestBuilder(directory, inputFile), + expectLater( + generatorTestBuilder(directory, inputFile), throwsA( isA().having( (e) => e.format(), diff --git a/generator/test/error_test_data/backlink_annotation_on_incompatible_field.dart b/generator/test/error_test_data/backlink_annotation_on_incompatible_field.dart new file mode 100644 index 000000000..452c30f03 --- /dev/null +++ b/generator/test/error_test_data/backlink_annotation_on_incompatible_field.dart @@ -0,0 +1,7 @@ +import 'package:realm_common/realm_common.dart'; + +@RealmModel() +class _Foo { + @Backlink(#bad) + late bool bad; +} diff --git a/generator/test/error_test_data/backlink_annotation_on_incompatible_field.expected b/generator/test/error_test_data/backlink_annotation_on_incompatible_field.expected new file mode 100644 index 000000000..ecb58f5c9 --- /dev/null +++ b/generator/test/error_test_data/backlink_annotation_on_incompatible_field.expected @@ -0,0 +1,11 @@ +Backlink must be an iterable of realm objects + +in: asset:pkg/test/error_test_data/backlink_annotation_on_incompatible_field.dart:6:8 + ╷ +3 │ @RealmModel() +4 │ class _Foo { + │ ━━━━ in realm model for 'Foo' +5 │ @Backlink(#bad) +6 │ late bool bad; + │ ^^^^ bool is not an iterable of realm objects + ╵ \ No newline at end of file diff --git a/generator/test/error_test_data/backlink_annotation_on_invalid_iterable.dart b/generator/test/error_test_data/backlink_annotation_on_invalid_iterable.dart new file mode 100644 index 000000000..fa1b2a4cc --- /dev/null +++ b/generator/test/error_test_data/backlink_annotation_on_invalid_iterable.dart @@ -0,0 +1,7 @@ +import 'package:realm_common/realm_common.dart'; + +@RealmModel() +class _Foo { + @Backlink(#bad) + late Iterable bad; +} diff --git a/generator/test/error_test_data/backlink_annotation_on_invalid_iterable.expected b/generator/test/error_test_data/backlink_annotation_on_invalid_iterable.expected new file mode 100644 index 000000000..4b651d5a0 --- /dev/null +++ b/generator/test/error_test_data/backlink_annotation_on_invalid_iterable.expected @@ -0,0 +1,11 @@ +Backlink must be an iterable of realm objects + +in: asset:pkg/test/error_test_data/backlink_annotation_on_invalid_iterable.dart:6:8 + ╷ +3 │ @RealmModel() +4 │ class _Foo { + │ ━━━━ in realm model for 'Foo' +5 │ @Backlink(#bad) +6 │ late Iterable bad; + │ ^^^^^^^^^^^^^ Iterable is not an iterable of realm objects + ╵ \ No newline at end of file diff --git a/generator/test/error_test_data/backlink_illegal_symbol.dart b/generator/test/error_test_data/backlink_illegal_symbol.dart new file mode 100644 index 000000000..df5d46bcc --- /dev/null +++ b/generator/test/error_test_data/backlink_illegal_symbol.dart @@ -0,0 +1,12 @@ +import 'package:realm_common/realm_common.dart'; + +@RealmModel() +class _NotASource { + late int notALink; +} + +@RealmModel() +class _Target { + @Backlink(#notALink) + late Iterable<_NotASource> backlinks; +} diff --git a/generator/test/error_test_data/backlink_illegal_symbol.expected b/generator/test/error_test_data/backlink_illegal_symbol.expected new file mode 100644 index 000000000..f3dfd94c3 --- /dev/null +++ b/generator/test/error_test_data/backlink_illegal_symbol.expected @@ -0,0 +1,11 @@ +Incompatible back-link type + +in: asset:pkg/test/error_test_data/backlink_illegal_symbol.dart:11:8 + ╷ +8 │ @RealmModel() +9 │ class _Target { + │ ━━━━━━━ in realm model for 'Target' +10 │ @Backlink(#notALink) +11 │ late Iterable<_NotASource> backlinks; + │ ^^^^^^^^^^^^^^^^^^^^^ _NotASource.notALink is not a '_Target?' or 'List<_Target>' + ╵ \ No newline at end of file diff --git a/generator/test/error_test_data/backlink_incompatible_type.dart b/generator/test/error_test_data/backlink_incompatible_type.dart new file mode 100644 index 000000000..81614538e --- /dev/null +++ b/generator/test/error_test_data/backlink_incompatible_type.dart @@ -0,0 +1,15 @@ +import 'package:realm_common/realm_common.dart'; + +@RealmModel() +class _IncompatibleSource { + _AnotherTarget? target; +} + +@RealmModel() +class _Target { + @Backlink(#target) + late Iterable<_IncompatibleSource> backlinks; +} + +@RealmModel() +class _AnotherTarget {} diff --git a/generator/test/error_test_data/backlink_incompatible_type.expected b/generator/test/error_test_data/backlink_incompatible_type.expected new file mode 100644 index 000000000..beea05d3f --- /dev/null +++ b/generator/test/error_test_data/backlink_incompatible_type.expected @@ -0,0 +1,11 @@ +Incompatible back-link type + +in: asset:pkg/test/error_test_data/backlink_incompatible_type.dart:11:8 + ╷ +8 │ @RealmModel() +9 │ class _Target { + │ ━━━━━━━ in realm model for 'Target' +10 │ @Backlink(#target) +11 │ late Iterable<_IncompatibleSource> backlinks; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ _IncompatibleSource.target is not a '_Target?' or 'List<_Target>' + ╵ \ No newline at end of file diff --git a/generator/test/error_test_data/backlink_unknown_symbol.dart b/generator/test/error_test_data/backlink_unknown_symbol.dart new file mode 100644 index 000000000..a65ee2bec --- /dev/null +++ b/generator/test/error_test_data/backlink_unknown_symbol.dart @@ -0,0 +1,12 @@ +import 'package:realm_common/realm_common.dart'; + +@RealmModel() +class _Source { + _Target? target; +} + +@RealmModel() +class _Target { + @Backlink(#unknownSymbol) + late Iterable<_Source> backlinks; +} diff --git a/generator/test/error_test_data/backlink_unknown_symbol.expected b/generator/test/error_test_data/backlink_unknown_symbol.expected new file mode 100644 index 000000000..a71ce9e21 --- /dev/null +++ b/generator/test/error_test_data/backlink_unknown_symbol.expected @@ -0,0 +1,11 @@ +Backlink must point to a valid field + +in: asset:pkg/test/error_test_data/backlink_unknown_symbol.dart:11:8 + ╷ +8 │ @RealmModel() +9 │ class _Target { + │ ━━━━━━━ in realm model for 'Target' +10 │ @Backlink(#unknownSymbol) +11 │ late Iterable<_Source> backlinks; + │ ^^^^^^^^^^^^^^^^^ _Source does not have a field named unknownSymbol + ╵ \ No newline at end of file diff --git a/generator/test/error_test_data/invalid_model_name_mapping.expected b/generator/test/error_test_data/invalid_model_name_mapping.expected index e3939463d..38cd3cfb7 100644 --- a/generator/test/error_test_data/invalid_model_name_mapping.expected +++ b/generator/test/error_test_data/invalid_model_name_mapping.expected @@ -10,5 +10,5 @@ in: asset:pkg/test/error_test_data/invalid_model_name_mapping.dart:7:8 10 │ class Bad {} │ ━━━ when generating realm object class for 'Bad' ╵ -We need a valid indentifier +We need a valid identifier diff --git a/generator/test/error_test_data/missing_underscore.g.dart b/generator/test/error_test_data/missing_underscore.g.dart deleted file mode 100644 index e65e6548c..000000000 --- a/generator/test/error_test_data/missing_underscore.g.dart +++ /dev/null @@ -1,3 +0,0 @@ - -part of 'missing_underscore.dart'; -class Other extends _Other {} diff --git a/generator/test/test_util.dart b/generator/test/test_util.dart index b641519f3..cc1b1eaf3 100644 --- a/generator/test/test_util.dart +++ b/generator/test/test_util.dart @@ -40,8 +40,8 @@ Future> getInputFileAsset(String inputFilePath) async { class LinesEqualsMatcher extends Matcher { late final List expectedLines; - LinesEqualsMatcher(String expexted) { - expectedLines = expexted.split("\n"); + LinesEqualsMatcher(String expected) { + expectedLines = expected.split("\n"); } @override From bb82cd18e12575b9a6f23e7e40d692ed36b22785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 27 Oct 2022 19:44:26 +0200 Subject: [PATCH 08/10] Handle @MapTo on backlink targets --- generator/lib/src/class_element_ex.dart | 6 +-- generator/lib/src/field_element_ex.dart | 54 ++++++++++++++----------- test/backlinks_test.dart | 1 + test/backlinks_test.g.dart | 12 +++--- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/generator/lib/src/class_element_ex.dart b/generator/lib/src/class_element_ex.dart index 90f733c9e..26fb301fa 100644 --- a/generator/lib/src/class_element_ex.dart +++ b/generator/lib/src/class_element_ex.dart @@ -152,7 +152,8 @@ extension ClassElementEx on ClassElement { final objectType = ObjectType.values[modelInfo.value.getField('type')!.getField('index')!.toIntValue()!]; - final mappedFields = fields.realmInfo.toList(); + // Computed fields go last. This is important for the schema generation. + final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1); if (objectType == ObjectType.embeddedObject && mappedFields.any((field) => field.isPrimaryKey)) { final pkSpan = fields.firstWhere((field) => field.realmInfo?.isPrimaryKey == true).span; @@ -164,9 +165,6 @@ extension ClassElementEx on ClassElement { todo: 'Remove the @PrimaryKey annotation from the field or set the model type to a value different from ObjectType.embeddedObject.'); } - // Computed fields go last. This is important for the schema generation. - mappedFields.sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1); - return RealmModelInfo(name, modelName, realmName, mappedFields, objectType); } on InvalidGenerationSourceError catch (_) { rethrow; diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index 1a802af89..8470b5135 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -15,6 +15,7 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// + import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; @@ -149,6 +150,8 @@ extension FieldElementEx on FieldElement { ); } + String? linkOriginProperty; + // Validate field type final modelSpan = enclosingElement3.span!; final file = modelSpan.file; @@ -179,6 +182,28 @@ extension FieldElementEx on FieldElement { todo: todo, ); } else { + // Validate collections and back-links + if (type.isRealmCollection || backlink != null) { + final typeDescription = type.isRealmCollection ? 'collections' : 'back-links'; + if (type.isNullable) { + throw RealmInvalidGenerationSourceError( + 'Realm $typeDescription cannot be nullable', + primarySpan: typeSpan(file), + primaryLabel: 'is nullable', + todo: '', + element: this, + ); + } + final itemType = type.basicType; + if (itemType.isRealmModel && itemType.isNullable) { + throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in $typeDescription', + primarySpan: typeSpan(file), + primaryLabel: 'which has a nullable realm object element type', + element: this, + todo: 'Ensure element type is non-nullable'); + } + } + // Validate back-links if (backlink != null) { if (!type.isDartCoreIterable || !(type as ParameterizedType).typeArguments.first.isRealmModel) { @@ -217,32 +242,13 @@ extension FieldElementEx on FieldElement { element: this, ); } - } - // Validate collections and back-links - if (type.isRealmCollection || backlink != null) { - final typeDescription = type.isRealmCollection ? 'collections' : 'back-links'; - if (type.isNullable) { - throw RealmInvalidGenerationSourceError( - 'Realm $typeDescription cannot be nullable', - primarySpan: typeSpan(file), - primaryLabel: 'is nullable', - todo: '', - element: this, - ); - } - final itemType = type.basicType; - if (itemType.isRealmModel && itemType.isNullable) { - throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in $typeDescription', - primarySpan: typeSpan(file), - primaryLabel: 'which has a nullable realm object element type', - element: this, - todo: 'Ensure element type is non-nullable'); - } + // everything is kosher, just need to account for @MapTo! + linkOriginProperty = sourceField.annotationInfoOfExact(mapToChecker)?.value.getField('name')?.toStringValue() ?? sourceField.name; } // Validate object references - else if (realmType == RealmPropertyType.object) { + else if (realmType == RealmPropertyType.object && !type.isRealmCollection) { if (!type.isNullable) { throw RealmInvalidGenerationSourceError( 'Realm object references must be nullable', @@ -261,14 +267,14 @@ extension FieldElementEx on FieldElement { isPrimaryKey: primaryKey != null, mapTo: remappedRealmName, realmType: realmType, - linkOriginProperty: backlink?.value.getField('symbol')?.toSymbolValue(), + linkOriginProperty: linkOriginProperty, ); } on InvalidGenerationSourceError catch (_) { rethrow; } catch (e, s) { // Fallback. Not perfect, but better than just forwarding original error. throw RealmInvalidGenerationSourceError( - '$e \n $s', + '$e\n$s', todo: // 'Unexpected error. Please open an issue on: ' 'https://github.com/realm/realm-dart', diff --git a/test/backlinks_test.dart b/test/backlinks_test.dart index 52f1965ba..4f3481ffa 100644 --- a/test/backlinks_test.dart +++ b/test/backlinks_test.dart @@ -27,6 +27,7 @@ part 'backlinks_test.g.dart'; @RealmModel() class _Source { String name = 'source'; + @MapTo('et mål') // to throw a curve ball.. _Target? oneTarget; List<_Target> manyTargets = []; } diff --git a/test/backlinks_test.g.dart b/test/backlinks_test.g.dart index 1c100514d..ae87596fe 100644 --- a/test/backlinks_test.g.dart +++ b/test/backlinks_test.g.dart @@ -20,7 +20,7 @@ class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { }); } RealmObjectBase.set(this, 'name', name); - RealmObjectBase.set(this, 'oneTarget', oneTarget); + RealmObjectBase.set(this, 'et mål', oneTarget); RealmObjectBase.set>( this, 'manyTargets', RealmList(manyTargets)); } @@ -34,10 +34,10 @@ class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { @override Target? get oneTarget => - RealmObjectBase.get(this, 'oneTarget') as Target?; + RealmObjectBase.get(this, 'et mål') as Target?; @override set oneTarget(covariant Target? value) => - RealmObjectBase.set(this, 'oneTarget', value); + RealmObjectBase.set(this, 'et mål', value); @override RealmList get manyTargets => @@ -59,8 +59,8 @@ class Source extends _Source with RealmEntity, RealmObjectBase, RealmObject { RealmObjectBase.registerFactory(Source._); return const SchemaObject(ObjectType.realmObject, Source, 'Source', [ SchemaProperty('name', RealmPropertyType.string), - SchemaProperty('oneTarget', RealmPropertyType.object, - optional: true, linkTarget: 'Target'), + SchemaProperty('et mål', RealmPropertyType.object, + mapTo: 'et mål', optional: true, linkTarget: 'Target'), SchemaProperty('manyTargets', RealmPropertyType.object, linkTarget: 'Target', collectionType: RealmCollectionType.list), ]); @@ -116,7 +116,7 @@ class Target extends _Target with RealmEntity, RealmObjectBase, RealmObject { return const SchemaObject(ObjectType.realmObject, Target, 'Target', [ SchemaProperty('name', RealmPropertyType.string), SchemaProperty('oneToMany', RealmPropertyType.linkingObjects, - linkOriginProperty: 'oneTarget', + linkOriginProperty: 'et mål', collectionType: RealmCollectionType.list, linkTarget: 'Source'), SchemaProperty('manyToMany', RealmPropertyType.linkingObjects, From 923bebffe0369263c570acb90b46c41089c3d9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 28 Oct 2022 10:49:16 +0200 Subject: [PATCH 09/10] Increase analyzer constraint to 4.7.0^ --- generator/pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index e7401d0d9..7f1e15e8a 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -15,7 +15,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - analyzer: ^4.0.0 + analyzer: ^4.7.0 build_resolvers: ^2.0.9 build: ^2.0.0 dart_style: ^2.2.0 @@ -26,6 +26,7 @@ dependencies: dev_dependencies: build_test: ^2.1.4 + lints: ^2.0.0 test: ^1.14.3 path: ^1.0.0 From 7bdd8ec021375c3a12fc919479d0087ebd170f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 28 Oct 2022 11:57:04 +0200 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: blagoev --- CHANGELOG.md | 2 +- common/lib/src/realm_common_base.dart | 7 ++-- generator/lib/src/class_element_ex.dart | 2 +- generator/lib/src/dart_type_ex.dart | 2 +- generator/lib/src/field_element_ex.dart | 12 +++---- .../backlink_illegal_symbol.expected | 2 +- .../backlink_incompatible_type.expected | 2 +- generator/test/test_util.dart | 2 +- lib/src/realm_object.dart | 34 +++++++++---------- test/backlinks_test.dart | 4 +-- 10 files changed, 34 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a86c704..a26d549c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * Added `User.functions`. This is the entry point for calling Atlas App functions. Functions allow you to define and execute server-side logic for your application. Atlas App functions are created on the server, written in modern JavaScript (ES6+) and executed in a serverless manner. When you call a function, you can dynamically access components of the current application as well as information about the request to execute the function and the logged in user that sent the request. ([#973](https://github.com/realm/realm-dart/pull/973)) * Support results of primitives, ie. `RealmResult`. (Issue [#162](https://github.com/realm/realm-dart/issues/162)) * Support notifications on all managed realm lists, including list of primitives, ie. `RealmList.changes` is supported. ([#893](https://github.com/realm/realm-dart/pull/893)) -* Support named backlinks on realm models. You can now add and annotate a realm object iterator field with `@Backlink(#symbolName)`. ([#996](https://github.com/realm/realm-dart/pull/996)) +* Support named backlinks on realm models. You can now add and annotate a realm object iterator field with `@Backlink(#fieldName)`. ([#996](https://github.com/realm/realm-dart/pull/996)) ### Fixed * Fixed a wrong mapping for `AuthProviderType` returned by `User.provider` for google, facebook and apple credentials. diff --git a/common/lib/src/realm_common_base.dart b/common/lib/src/realm_common_base.dart index b2aa4700f..1db0dbf58 100644 --- a/common/lib/src/realm_common_base.dart +++ b/common/lib/src/realm_common_base.dart @@ -98,9 +98,10 @@ class Ignored { const Ignored(); } -/// Indicates a backlink property. +/// Indicates that the field it decorates is the inverse end of a relationship. /// {@category Annotations} class Backlink { - final Symbol symbol; - const Backlink(this.symbol); + /// The name of the field in the other class that links to this class. + final Symbol fieldName; + const Backlink(this.fieldName); } diff --git a/generator/lib/src/class_element_ex.dart b/generator/lib/src/class_element_ex.dart index 26fb301fa..9f69db4db 100644 --- a/generator/lib/src/class_element_ex.dart +++ b/generator/lib/src/class_element_ex.dart @@ -152,7 +152,7 @@ extension ClassElementEx on ClassElement { final objectType = ObjectType.values[modelInfo.value.getField('type')!.getField('index')!.toIntValue()!]; - // Computed fields go last. This is important for the schema generation. + // 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); if (objectType == ObjectType.embeddedObject && mappedFields.any((field) => field.isPrimaryKey)) { diff --git a/generator/lib/src/dart_type_ex.dart b/generator/lib/src/dart_type_ex.dart index 05602b662..be1d34d6b 100644 --- a/generator/lib/src/dart_type_ex.dart +++ b/generator/lib/src/dart_type_ex.dart @@ -61,11 +61,11 @@ extension DartTypeEx on DartType { DartType get mappedType { final self = this; - final provider = session.typeProvider; if (isRealmCollection) { if (self is ParameterizedType) { final mapped = self.typeArguments.last.mappedType; if (self != mapped) { + final provider = session.typeProvider; if (self.isDartCoreList) { final mappedList = provider.listType(mapped); return PseudoType('Realm${mappedList.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedList.nullabilitySuffix); diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index 8470b5135..e1ecf9f70 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -116,7 +116,7 @@ extension FieldElementEx on FieldElement { // // However, this may change in the future. Either as the dart language team change this // blemish. Or perhaps we can avoid the late modifier, once static meta programming lands - // in dart. Therefor we keep the code out-commented for later. + // in dart. Therefore we keep the code out-commented for later. /* if (!isFinal) { throw RealmInvalidGenerationSourceError( @@ -182,9 +182,9 @@ extension FieldElementEx on FieldElement { todo: todo, ); } else { - // Validate collections and back-links + // Validate collections and backlinks if (type.isRealmCollection || backlink != null) { - final typeDescription = type.isRealmCollection ? 'collections' : 'back-links'; + final typeDescription = type.isRealmCollection ? 'collections' : 'backlinks'; if (type.isNullable) { throw RealmInvalidGenerationSourceError( 'Realm $typeDescription cannot be nullable', @@ -204,7 +204,7 @@ extension FieldElementEx on FieldElement { } } - // Validate back-links + // Validate backlinks if (backlink != null) { if (!type.isDartCoreIterable || !(type as ParameterizedType).typeArguments.first.isRealmModel) { throw RealmInvalidGenerationSourceError( @@ -216,7 +216,7 @@ extension FieldElementEx on FieldElement { ); } - final sourceFieldName = backlink.value.getField('symbol')?.toSymbolValue(); + final sourceFieldName = backlink.value.getField('fieldName')?.toSymbolValue(); final sourceType = (type as ParameterizedType).typeArguments.first; final sourceField = (sourceType.element2 as ClassElement?)?.fields.where((f) => f.name == sourceFieldName).singleOrNull; @@ -235,7 +235,7 @@ extension FieldElementEx on FieldElement { final listOf = session.typeProvider.listType(thisType); if (sourceField.type != linkType && sourceField.type != listOf) { throw RealmInvalidGenerationSourceError( - 'Incompatible back-link type', + 'Incompatible backlink type', primarySpan: typeSpan(file), primaryLabel: "$sourceType.$sourceFieldName is not a '$linkType' or '$listOf'", todo: '', diff --git a/generator/test/error_test_data/backlink_illegal_symbol.expected b/generator/test/error_test_data/backlink_illegal_symbol.expected index f3dfd94c3..f2a819e91 100644 --- a/generator/test/error_test_data/backlink_illegal_symbol.expected +++ b/generator/test/error_test_data/backlink_illegal_symbol.expected @@ -1,4 +1,4 @@ -Incompatible back-link type +Incompatible backlink type in: asset:pkg/test/error_test_data/backlink_illegal_symbol.dart:11:8 ╷ diff --git a/generator/test/error_test_data/backlink_incompatible_type.expected b/generator/test/error_test_data/backlink_incompatible_type.expected index beea05d3f..2055fa453 100644 --- a/generator/test/error_test_data/backlink_incompatible_type.expected +++ b/generator/test/error_test_data/backlink_incompatible_type.expected @@ -1,4 +1,4 @@ -Incompatible back-link type +Incompatible backlink type in: asset:pkg/test/error_test_data/backlink_incompatible_type.dart:11:8 ╷ diff --git a/generator/test/test_util.dart b/generator/test/test_util.dart index cc1b1eaf3..d2621eeaf 100644 --- a/generator/test/test_util.dart +++ b/generator/test/test_util.dart @@ -65,7 +65,7 @@ class LinesEqualsMatcher extends Matcher { } } - if (actualLines.length > expectedLines.length) { + if (actualLines.length != expectedLines.length) { matchState["Error"] = "Different number of lines. \nExpected: ${expectedLines.length}\nActual: ${actualLines.length}"; return false; } diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index 797aab743..08da7ae00 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -149,26 +149,24 @@ class RealmCoreAccessor implements RealmAccessor { final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); return RealmResultsInternal.create(handle, object.realm, metadata); - } else { - 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()) { - switch (listMetadata.schema.baseType) { - case ObjectType.realmObject: - return object.realm.createList(handle, listMetadata); - case ObjectType.embeddedObject: - return object.realm.createList(handle, listMetadata); - default: - throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); - } + } + 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()) { + switch (listMetadata.schema.baseType) { + case ObjectType.realmObject: + return object.realm.createList(handle, listMetadata); + case ObjectType.embeddedObject: + return object.realm.createList(handle, listMetadata); + default: + throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); } - - return object.realm.createList(handle, listMetadata); } + return object.realm.createList(handle, listMetadata); } Object? value = realmCore.getProperty(object, propertyMeta.key); diff --git a/test/backlinks_test.dart b/test/backlinks_test.dart index 4f3481ffa..d0b31e477 100644 --- a/test/backlinks_test.dart +++ b/test/backlinks_test.dart @@ -35,12 +35,12 @@ class _Source { @RealmModel() class _Target { @Backlink(#oneTarget) - late Iterable<_Source> oneToMany; // computed property, so must go last in generated class! + late Iterable<_Source> oneToMany; String name = 'target'; @Backlink(#manyTargets) - late Iterable<_Source> manyToMany; // computed property, so must go last in generated class! + late Iterable<_Source> manyToMany; } Future main([List? args]) async {