Skip to content

Commit

Permalink
chore: Conform custom types in where clause (#68)
Browse files Browse the repository at this point in the history
* include hasOne in where clause generation

* hasOne relation fixes

* conform custom types to DB types in where clause

* tiny fix

* Use varchar when primaryKey type is String

* Use string id in test

* tiny fix

* fix failing test

* fix test

* fix failing tests
  • Loading branch information
codekeyz authored May 10, 2024
1 parent 434109c commit c829b73
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 37 deletions.
11 changes: 9 additions & 2 deletions _tests_/lib/src/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,19 @@ class Post extends Entity<Post> {

@table
class PostComment extends Entity<PostComment> {
@primaryKey
final int id;
@PrimaryKey(autoIncrement: false)
final String id;

final String comment;

@bindTo(Post, onDelete: ForeignKeyAction.cascade)
final int postId;

PostComment(this.id, this.comment, {required this.postId});

Map<String, dynamic> toJson() => {
'id': id,
'comment': comment,
'postId': postId,
};
}
1 change: 1 addition & 0 deletions _tests_/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ environment:

dependencies:
build_runner: ^2.4.9
uuid: ^4.4.0
yaroorm:
path: ../
40 changes: 25 additions & 15 deletions _tests_/test/integration/e2e_relation.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
import 'package:yaroorm/yaroorm.dart';
import 'package:yaroorm_tests/src/models.dart';
import 'package:yaroorm_tests/test_data.dart';
Expand All @@ -7,6 +8,8 @@ import 'package:yaroorm/src/reflection.dart';

import '../util.dart';

final uuid = Uuid();

void runRelationsE2ETest(String connectionName) {
final driver = DB.driver(connectionName);

Expand Down Expand Up @@ -75,24 +78,31 @@ void runRelationsE2ETest(String connectionName) {
var comments = await post!.comments.get();
expect(comments, isEmpty);

final firstId = uuid.v4();
final secondId = uuid.v4();

await post.comments.insertMany([
NewPostCommentForPost(comment: 'This post looks abit old'),
NewPostCommentForPost(comment: 'oh, another comment'),
NewPostCommentForPost(
id: firstId,
comment: 'A new post looks abit old',
),
NewPostCommentForPost(
id: secondId,
comment: 'Come, let us explore Dart',
),
]);

comments = await post.comments.get();
comments = await post.comments.get(orderBy: [
OrderPostCommentBy.comment(order: OrderDirection.desc),
]);

expect(comments.every((e) => e.postId == post.id), isTrue);
expect(
comments.map((c) => {
'id': c.id,
'comment': c.comment,
'postId': c.postId,
}),
[
{'id': 1, 'comment': 'This post looks abit old', 'postId': 1},
{'id': 2, 'comment': 'oh, another comment', 'postId': 1}
]);
expect(comments.map((e) => e.id), containsAll([firstId, secondId]));

expect(comments.map((c) => c.toJson()), [
{'id': secondId, 'comment': 'Come, let us explore Dart', 'postId': 1},
{'id': firstId, 'comment': 'A new post looks abit old', 'postId': 1},
]);
});

test('should add post for another user', () async {
Expand Down Expand Up @@ -120,8 +130,8 @@ void runRelationsE2ETest(String connectionName) {
expect(anotherUserPost.userId, anotherUser.id);

await anotherUserPost.comments.insertMany([
NewPostCommentForPost(comment: 'ah ah'),
NewPostCommentForPost(comment: 'oh oh'),
NewPostCommentForPost(id: uuid.v4(), comment: 'ah ah'),
NewPostCommentForPost(id: uuid.v4(), comment: 'oh oh'),
]);

expect(await anotherUserPost.comments.get(), hasLength(2));
Expand Down
41 changes: 40 additions & 1 deletion lib/src/builder/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ return switch(field) {
..methods.addAll([
if (parsedEntity.belongsToGetters.isNotEmpty)
...parsedEntity.belongsToGetters.map((field) => _generateJoinForBelongsTo(parsedEntity, field.getter!)),
if (parsedEntity.hasOneGetters.isNotEmpty)
...parsedEntity.hasOneGetters.map((field) => _generateJoinForHasOne(parsedEntity, field.getter!)),
if (parsedEntity.hasManyGetters.isNotEmpty)
...parsedEntity.hasManyGetters.map((field) => _generateJoinForHasMany(parsedEntity, field.getter!)),
])),
Expand Down Expand Up @@ -351,7 +353,7 @@ return switch(field) {
List<Method> _generateWhereClauseForRelations(ParsedEntityClass parsed) {
final result = <Method>[];

for (final (field) in [...parsed.hasManyGetters, ...parsed.belongsToGetters]) {
for (final (field) in [...parsed.hasManyGetters, ...parsed.belongsToGetters, ...parsed.hasOneGetters]) {
final hasMany = field.getter!.returnType as InterfaceType;
final referencedClass = hasMany.typeArguments.last.element as ClassElement;
final parsedReferenceClass = ParsedEntityClass.parse(referencedClass);
Expand Down Expand Up @@ -522,6 +524,43 @@ return switch(field) {
);
}

/// Generate JOIN for HasOne getters on Entity
Method _generateJoinForHasOne(
ParsedEntityClass parent,
PropertyAccessorElement getter,
) {
final hasOne = getter.returnType as InterfaceType;
final getterName = getter.name;
final relatedClass = ParsedEntityClass.parse(hasOne.typeArguments.last.element as ClassElement);

final bindings = parent.bindings;
if (bindings.isEmpty) {
throw InvalidGenerationSource(
'No bindings found to enable HasOne relation for ${relatedClass.className} in ${parent.className}. Did you forget to use `@bindTo` ?',
element: getter,
);
}

/// TODO(codekey): be able to specify binding to use
final bindingToUse = bindings.entries.firstWhere((e) => e.value.entity.element == relatedClass.element);
final field = parent.allFields.firstWhere((e) => Symbol(e.name) == bindingToUse.key);
final foreignField = relatedClass.allFields.firstWhere((e) => Symbol(e.name) == bindingToUse.value.field);

final joinClass = 'Join<${parent.className}, ${relatedClass.className}>';
return Method(
(m) => m
..name = getterName
..type = MethodType.getter
..lambda = true
..returns = refer(joinClass)
..body = Code('''$joinClass("$getterName",
origin: (table: "${parent.table}", column: "${getFieldDbName(field)}"),
on: (table: "${relatedClass.table}", column: "${getFieldDbName(foreignField)}"),
key: HasOne<${parent.className}, ${relatedClass.className}>,
)'''),
);
}

String _generateCodeForBinding(
String className,
String relatedClass,
Expand Down
3 changes: 3 additions & 0 deletions lib/src/builder/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ final class ParsedEntityClass {
List<FieldElement> get belongsToGetters =>
getters.where((getter) => typeChecker(entity.BelongsTo).isExactlyType(getter.type)).toList();

List<FieldElement> get hasOneGetters =>
getters.where((getter) => typeChecker(entity.HasOne).isExactlyType(getter.type)).toList();

bool get hasAutoIncrementingPrimaryKey {
return primaryKey.reader.peek('autoIncrement')!.boolValue;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/database/entity/converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ UnmodifiableMapView<String, dynamic> entityMapToDbData<T extends Entity<T>>(
bool onlyPropertiesPassed = false,
}) {
final entity = Query.getEntity<T>();
final editableFields = entity.editableColumns;
final editableFields = entity.fieldsRequiredForCreate;

final resultsMap = <String, dynamic>{};

Expand Down
10 changes: 3 additions & 7 deletions lib/src/database/entity/entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ abstract class Entity<Parent extends Entity<Parent>> {

DriverContract _driver = DB.defaultDriver;

EntityTypeDefinition<Parent>? _typeDataCache;
EntityTypeDefinition<Parent> get typeData {
if (_typeDataCache != null) return _typeDataCache!;
return _typeDataCache = Query.getEntity<Parent>();
}
EntityTypeDefinition<Parent> get _typeDef => Query.getEntity<Parent>();

String? get connection => null;

Expand Down Expand Up @@ -97,8 +93,8 @@ abstract class Entity<Parent extends Entity<Parent>> {
Symbol? foreignKey,
}) {
final parentFieldName = Query.getEntity<RelatedModel>().primaryKey.columnName;
foreignKey ??= typeData.bindings.entries.firstWhere((e) => e.value.type == RelatedModel).key;
final referenceFieldValue = typeData.mirror(this as Parent).get(foreignKey);
foreignKey ??= _typeDef.bindings.entries.firstWhere((e) => e.value.type == RelatedModel).key;
final referenceFieldValue = _typeDef.mirror(this as Parent).get(foreignKey);

return BelongsTo<Parent, RelatedModel>._(
parentFieldName,
Expand Down
13 changes: 12 additions & 1 deletion lib/src/database/entity/relations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ abstract class EntityRelation<Parent extends Entity<Parent>, RelatedModel extend
EntityRelation(this.parent) : _query = Query.table<RelatedModel>().driver(parent._driver);

Object get parentId {
final typeInfo = parent.typeData;
final typeInfo = parent._typeDef;
return typeInfo.mirror.call(parent).get(typeInfo.primaryKey.dartName)!;
}

Expand All @@ -20,6 +20,8 @@ abstract class EntityRelation<Parent extends Entity<Parent>, RelatedModel extend

get({bool refresh = false});

bool get loaded;

delete();
}

Expand All @@ -36,6 +38,9 @@ final class HasOne<Parent extends Entity<Parent>, RelatedModel extends Entity<Re
this._cache,
);

@override
bool get loaded => _cache != null;

@override
RelatedModel? get value {
if (_cache == null) {
Expand Down Expand Up @@ -75,6 +80,9 @@ final class HasMany<Parent extends Entity<Parent>, RelatedModel extends Entity<R

ReadQuery<RelatedModel> get $readQuery => _query.where((q) => q.$equal(foreignKey, parentId));

@override
bool get loaded => _cache != null;

@override
List<RelatedModel> get value {
if (_cache == null) {
Expand Down Expand Up @@ -140,6 +148,9 @@ final class BelongsTo<Parent extends Entity<Parent>, RelatedModel extends Entity
final String foreignKey;
final dynamic foreignKeyValue;

@override
bool get loaded => _cache != null;

BelongsTo._(
this.foreignKey,
this.foreignKeyValue,
Expand Down
4 changes: 4 additions & 0 deletions lib/src/migration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,13 @@ abstract class Schema {
return CreateSchema<T>._(
entity.tableName,
(table) {
final primaryKey = entity.primaryKey;
table.id(
name: entity.primaryKey.columnName,
autoIncrement: entity.primaryKey.autoIncrement,

/// TODO(codekeyz): is this the right way to do this?
type: primaryKey.type == String ? 'VARCHAR(255)' : null,
);

for (final prop in entity.columns.where((e) => !e.isPrimaryKey)) {
Expand Down
47 changes: 38 additions & 9 deletions lib/src/primitives/where.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ abstract interface class WhereClause {

@internal
void validate(List<Join> joins);

void _withConverters(Map<Type, EntityTypeConverter> converters);
}

class $AndGroup extends WhereClause {
Expand All @@ -71,6 +73,14 @@ class $AndGroup extends WhereClause {
val.validate(joins);
}
}

@override
void _withConverters(Map<Type, EntityTypeConverter> converters) {
final clauseValues = values.whereType<WhereClauseValue>();
for (final val in clauseValues) {
val._withConverters(converters);
}
}
}

class $OrGroup extends WhereClause {
Expand All @@ -83,21 +93,32 @@ class $OrGroup extends WhereClause {
val.validate(joins);
}
}

@override
void _withConverters(Map<Type, EntityTypeConverter> converters) {
final clauseValues = values.whereType<WhereClauseValue>();
for (final val in clauseValues) {
val._withConverters(converters);
}
}
}

class WhereClauseValue<ValueType> extends WhereClause {
final String field;
final Operator operator;
final ValueType value;
final dynamic _value;

final String? table;

final Map<Type, EntityTypeConverter> _converters = {};

WhereClauseValue._(
this.field,
this.operator,
this.value, {
ValueType value, {
this.table,
}) : super(const []) {
}) : _value = value,
super(const []) {
if ([Operator.BETWEEN, Operator.NOT_BETWEEN].contains(operator)) {
if (value is! Iterable || (value as Iterable).length != 2) {
throw ArgumentError(
Expand All @@ -108,6 +129,12 @@ class WhereClauseValue<ValueType> extends WhereClause {
}
}

dynamic get value {
final typeConverter = _converters[ValueType];
if (typeConverter == null) return _value;
return typeConverter.toDbType(_value);
}

@override
void validate(List<Join> joins) {
if (table == null) return;
Expand All @@ -118,6 +145,13 @@ class WhereClauseValue<ValueType> extends WhereClause {
);
}
}

@override
void _withConverters(Map<Type, EntityTypeConverter> converters) {
_converters
..clear()
..addAll(converters);
}
}

class WhereClauseBuilder<T extends Entity<T>> with WhereOperation {
Expand Down Expand Up @@ -182,12 +216,7 @@ class WhereClauseBuilder<T extends Entity<T>> with WhereOperation {
@override
WhereClauseValue<List<V>> $isNotBetween<V>(String field, List<V> values) {
_ensureHasField(field);
return WhereClauseValue._(
field,
Operator.NOT_BETWEEN,
values,
table: table,
);
return WhereClauseValue._(field, Operator.NOT_BETWEEN, values, table: table);
}

void _ensureHasField(String field) {
Expand Down
4 changes: 3 additions & 1 deletion lib/src/query/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ final class Query<T extends Entity<T>>

ReadQuery<T> where(WhereBuilder<T> builder) {
final whereClause = builder.call(WhereClauseBuilder<T>());
whereClause.validate(_joins);
whereClause
..validate(_joins)
.._withConverters(converters);

return ReadQuery<T>._(this, whereClause: whereClause);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/src/reflection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ final class EntityTypeDefinition<T extends Entity<T>> {
UpdatedAtField? get updatedAtField =>
!timestampsEnabled ? null : columns.firstWhereOrNull((e) => e is UpdatedAtField) as UpdatedAtField?;

Iterable<DBEntityField> get fieldsRequiredForCreate =>
primaryKey.autoIncrement ? columns.where((e) => e != primaryKey) : columns;

Iterable<DBEntityField> get editableColumns => columns.where((e) => e != primaryKey);

const EntityTypeDefinition(
Expand Down

0 comments on commit c829b73

Please sign in to comment.