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

Backlink support #996

Merged
merged 10 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dart.lineLength": 160,
"cSpell.words": [
"apikeys",
"backlinks",
"BEGINSWITH",
"bson",
"deallocated",
Expand All @@ -31,7 +32,8 @@
"unmanaged",
"upsert",
"usercode",
"userdata"
"userdata",
"writeln"
],
"cmake.statusbar.advanced": {
"ctest": {
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>`. (Issue [#162](https://github.com/realm/realm-dart/issues/162))
* Support notifications on all managed realm lists, including list of primitives, ie. `RealmList<int>.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(#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.
Expand Down
8 changes: 8 additions & 0 deletions common/lib/src/realm_common_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,11 @@ class Indexed {
class Ignored {
const Ignored();
}

/// Indicates that the field it decorates is the inverse end of a relationship.
/// {@category Annotations}
class Backlink {
/// The name of the field in the other class that links to this class.
blagoev marked this conversation as resolved.
Show resolved Hide resolved
final Symbol fieldName;
const Backlink(this.fieldName);
}
18 changes: 11 additions & 7 deletions generator/lib/src/class_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ extension on Iterable<FieldElement> {
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!: ''},
);
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -149,7 +152,8 @@ extension ClassElementEx on ClassElement {

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

final mappedFields = fields.realmInfo.toList();
// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure this sorting is super readable. How about:

Suggested change
final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1);
final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed == b.isComputed ? 0 : (a.isComputed ? 1 : -1));

Or even simpler:

Suggested change
final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1);
final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed ? 1 : -1);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a dealbreaker though - it should still be correct, just feels a little complicated to understand.

Copy link
Contributor Author

@nielsenko nielsenko Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like to read it like: If a and b are different wrt. computed, then if a is computed, a goes last, otherwise a goes first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter that they're different though? I think my last suggestion reads like: "if a is computed, it goes last", which would be equivalent to "sort this array of bools in ascending order".


if (objectType == ObjectType.embeddedObject && mappedFields.any((field) => field.isPrimaryKey)) {
final pkSpan = fields.firstWhere((field) => field.realmInfo?.isPrimaryKey == true).span;
Expand All @@ -167,7 +171,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',
Expand Down
23 changes: 16 additions & 7 deletions generator/lib/src/dart_type_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,12 +31,12 @@ extension DartTypeEx on DartType {
bool isExactly<T>() => 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 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;
Expand All @@ -49,10 +50,11 @@ extension DartTypeEx on DartType {
DartType? get nullIfDynamic => isDynamic ? null : this;

DartType get basicType {
if (isRealmCollection) {
return (this as ParameterizedType).typeArguments.last;
final self = this;
blagoev marked this conversation as resolved.
Show resolved Hide resolved
if (self is ParameterizedType && (isRealmCollection || isDartCoreIterable)) {
return self.typeArguments.last;
}
return asNonNullable;
return this;
}

String get basicMappedName => basicType.mappedName;
Expand All @@ -61,9 +63,9 @@ extension DartTypeEx on DartType {
final self = this;
blagoev marked this conversation as resolved.
Show resolved Hide resolved
if (isRealmCollection) {
if (self is ParameterizedType) {
final provider = session.typeProvider;
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);
Expand All @@ -78,6 +80,13 @@ extension DartTypeEx on DartType {
}
}
}
} else if (isDartCoreIterable) {
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, ''),
Expand Down Expand Up @@ -105,7 +114,7 @@ extension DartTypeEx on DartType {
if (isDartCoreNum || isDartCoreDouble) return RealmPropertyType.double;
if (isExactly<Decimal128>()) return RealmPropertyType.decimal128;
if (isRealmModel) return RealmPropertyType.object;
if (isRealmBacklink) return RealmPropertyType.linkingObjects;
if (isDartCoreIterable) return RealmPropertyType.linkingObjects;
if (isExactly<ObjectId>()) return RealmPropertyType.objectid;
if (isExactly<Uuid>()) return RealmPropertyType.uuid;

Expand Down
1 change: 0 additions & 1 deletion generator/lib/src/element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion generator/lib/src/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}'",
});
Expand Down
75 changes: 63 additions & 12 deletions generator/lib/src/field_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
// 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';
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';

Expand All @@ -33,7 +35,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;
Expand All @@ -44,9 +45,11 @@ 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;
Expression? get initializerExpression => declarationAstNode.fields.variables.singleWhere((v) => v.name2.toString() == name).initializer;

FileSpan? typeSpan(SourceFile file) => ExpandedContextSpan(
ExpandedContextSpan(
Expand Down Expand Up @@ -76,6 +79,7 @@ extension FieldElementEx on FieldElement {

final primaryKey = primaryKeyInfo;
final indexed = indexedInfo;
final backlink = backlinkInfo;

// Check for as-of-yet unsupported type
if (type.isDartCoreSet || //
Expand Down Expand Up @@ -112,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 outcommented for later.
// in dart. Therefore we keep the code out-commented for later.
/*
if (!isFinal) {
throw RealmInvalidGenerationSourceError(
Expand Down Expand Up @@ -146,12 +150,14 @@ extension FieldElementEx on FieldElement {
);
}

String? linkOriginProperty;

// 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 = //
Expand All @@ -169,18 +175,19 @@ 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 collections
if (type.isRealmCollection) {
// Validate collections and backlinks
if (type.isRealmCollection || backlink != null) {
final typeDescription = type.isRealmCollection ? 'collections' : 'backlinks';
if (type.isNullable) {
throw RealmInvalidGenerationSourceError(
'Realm collections cannot be nullable',
'Realm $typeDescription cannot be nullable',
primarySpan: typeSpan(file),
primaryLabel: 'is nullable',
todo: '',
Expand All @@ -189,16 +196,59 @@ 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,
todo: 'Ensure element type is non-nullable');
}
}

// Validate backlinks
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('fieldName')?.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 backlink type',
primarySpan: typeSpan(file),
primaryLabel: "$sourceType.$sourceFieldName is not a '$linkType' or '$listOf'",
todo: '',
element: this,
);
}

// 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',
Expand All @@ -217,13 +267,14 @@ extension FieldElementEx on FieldElement {
isPrimaryKey: primaryKey != null,
mapTo: remappedRealmName,
realmType: realmType,
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',
Expand Down
2 changes: 1 addition & 1 deletion generator/lib/src/format_spans.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ String formatSpans(
color: color,
);
buffer
..write('\n' * 2 + 'in: ')
..write('${'\n' * 2}in: ')
..writeln(span.start.toolString)
..write(formatted);
}
Expand Down
10 changes: 5 additions & 5 deletions generator/lib/src/pseudo_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ class PseudoType extends TypeImpl {

// Private Symbols are suffixed with a secret '@<some int>'
// .. hence this ugly trick ヽ(ಠ_ಠ)ノ
final _writeSymbol = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_write"'));
im.invoke(_writeSymbol, <dynamic>[_name]); // #_write won't work
final writeSymbol = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_write"'));
im.invoke(writeSymbol, <dynamic>[_name]); // #_write won't work

final _writeNullability = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_writeNullability"'));
im.invoke(_writeNullability, <dynamic>[nullabilitySuffix]); // #_writeNullability won't work
final writeNullability = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_writeNullability"'));
im.invoke(writeNullability, <dynamic>[nullabilitySuffix]); // #_writeNullability won't work
}

@override
Expand All @@ -52,5 +52,5 @@ class PseudoType extends TypeImpl {
}

@override
Element? get element2 => throw UnimplementedError();
Element? get element2 => null;
}
Loading