From c829b73524ad78c53bd190a7d4dda0a7fb39e3e1 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Fri, 10 May 2024 20:52:09 +0000 Subject: [PATCH] chore: Conform custom types in where clause (#68) * 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 --- _tests_/lib/src/models.dart | 11 ++++- _tests_/pubspec.yaml | 1 + _tests_/test/integration/e2e_relation.dart | 40 +++++++++++------- lib/src/builder/generator.dart | 41 ++++++++++++++++++- lib/src/builder/utils.dart | 3 ++ lib/src/database/entity/converter.dart | 2 +- lib/src/database/entity/entity.dart | 10 ++--- lib/src/database/entity/relations.dart | 13 +++++- lib/src/migration.dart | 4 ++ lib/src/primitives/where.dart | 47 +++++++++++++++++----- lib/src/query/query.dart | 4 +- lib/src/reflection.dart | 3 ++ 12 files changed, 142 insertions(+), 37 deletions(-) diff --git a/_tests_/lib/src/models.dart b/_tests_/lib/src/models.dart index b1b3c9aa..10ca89b3 100644 --- a/_tests_/lib/src/models.dart +++ b/_tests_/lib/src/models.dart @@ -58,12 +58,19 @@ class Post extends Entity { @table class PostComment extends Entity { - @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 toJson() => { + 'id': id, + 'comment': comment, + 'postId': postId, + }; } diff --git a/_tests_/pubspec.yaml b/_tests_/pubspec.yaml index 0226e132..bb6d0fd1 100644 --- a/_tests_/pubspec.yaml +++ b/_tests_/pubspec.yaml @@ -9,5 +9,6 @@ environment: dependencies: build_runner: ^2.4.9 + uuid: ^4.4.0 yaroorm: path: ../ \ No newline at end of file diff --git a/_tests_/test/integration/e2e_relation.dart b/_tests_/test/integration/e2e_relation.dart index b32b212c..fc6d47cc 100644 --- a/_tests_/test/integration/e2e_relation.dart +++ b/_tests_/test/integration/e2e_relation.dart @@ -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'; @@ -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); @@ -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 { @@ -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)); diff --git a/lib/src/builder/generator.dart b/lib/src/builder/generator.dart index 6af3aec3..8a5042a3 100644 --- a/lib/src/builder/generator.dart +++ b/lib/src/builder/generator.dart @@ -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!)), ])), @@ -351,7 +353,7 @@ return switch(field) { List _generateWhereClauseForRelations(ParsedEntityClass parsed) { final result = []; - 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); @@ -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, diff --git a/lib/src/builder/utils.dart b/lib/src/builder/utils.dart index 053028a4..5cc94ba5 100644 --- a/lib/src/builder/utils.dart +++ b/lib/src/builder/utils.dart @@ -184,6 +184,9 @@ final class ParsedEntityClass { List get belongsToGetters => getters.where((getter) => typeChecker(entity.BelongsTo).isExactlyType(getter.type)).toList(); + List get hasOneGetters => + getters.where((getter) => typeChecker(entity.HasOne).isExactlyType(getter.type)).toList(); + bool get hasAutoIncrementingPrimaryKey { return primaryKey.reader.peek('autoIncrement')!.boolValue; } diff --git a/lib/src/database/entity/converter.dart b/lib/src/database/entity/converter.dart index dd713780..48ab076f 100644 --- a/lib/src/database/entity/converter.dart +++ b/lib/src/database/entity/converter.dart @@ -75,7 +75,7 @@ UnmodifiableMapView entityMapToDbData>( bool onlyPropertiesPassed = false, }) { final entity = Query.getEntity(); - final editableFields = entity.editableColumns; + final editableFields = entity.fieldsRequiredForCreate; final resultsMap = {}; diff --git a/lib/src/database/entity/entity.dart b/lib/src/database/entity/entity.dart index 8eb680fa..005617e3 100644 --- a/lib/src/database/entity/entity.dart +++ b/lib/src/database/entity/entity.dart @@ -22,11 +22,7 @@ abstract class Entity> { DriverContract _driver = DB.defaultDriver; - EntityTypeDefinition? _typeDataCache; - EntityTypeDefinition get typeData { - if (_typeDataCache != null) return _typeDataCache!; - return _typeDataCache = Query.getEntity(); - } + EntityTypeDefinition get _typeDef => Query.getEntity(); String? get connection => null; @@ -97,8 +93,8 @@ abstract class Entity> { Symbol? foreignKey, }) { final parentFieldName = Query.getEntity().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._( parentFieldName, diff --git a/lib/src/database/entity/relations.dart b/lib/src/database/entity/relations.dart index 64dd0b4b..fd714c9d 100644 --- a/lib/src/database/entity/relations.dart +++ b/lib/src/database/entity/relations.dart @@ -8,7 +8,7 @@ abstract class EntityRelation, RelatedModel extend EntityRelation(this.parent) : _query = Query.table().driver(parent._driver); Object get parentId { - final typeInfo = parent.typeData; + final typeInfo = parent._typeDef; return typeInfo.mirror.call(parent).get(typeInfo.primaryKey.dartName)!; } @@ -20,6 +20,8 @@ abstract class EntityRelation, RelatedModel extend get({bool refresh = false}); + bool get loaded; + delete(); } @@ -36,6 +38,9 @@ final class HasOne, RelatedModel extends Entity _cache != null; + @override RelatedModel? get value { if (_cache == null) { @@ -75,6 +80,9 @@ final class HasMany, RelatedModel extends Entity get $readQuery => _query.where((q) => q.$equal(foreignKey, parentId)); + @override + bool get loaded => _cache != null; + @override List get value { if (_cache == null) { @@ -140,6 +148,9 @@ final class BelongsTo, RelatedModel extends Entity final String foreignKey; final dynamic foreignKeyValue; + @override + bool get loaded => _cache != null; + BelongsTo._( this.foreignKey, this.foreignKeyValue, diff --git a/lib/src/migration.dart b/lib/src/migration.dart index 1f500313..714767a0 100644 --- a/lib/src/migration.dart +++ b/lib/src/migration.dart @@ -202,9 +202,13 @@ abstract class Schema { return CreateSchema._( 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)) { diff --git a/lib/src/primitives/where.dart b/lib/src/primitives/where.dart index 681f5943..fba122cb 100644 --- a/lib/src/primitives/where.dart +++ b/lib/src/primitives/where.dart @@ -59,6 +59,8 @@ abstract interface class WhereClause { @internal void validate(List joins); + + void _withConverters(Map converters); } class $AndGroup extends WhereClause { @@ -71,6 +73,14 @@ class $AndGroup extends WhereClause { val.validate(joins); } } + + @override + void _withConverters(Map converters) { + final clauseValues = values.whereType(); + for (final val in clauseValues) { + val._withConverters(converters); + } + } } class $OrGroup extends WhereClause { @@ -83,21 +93,32 @@ class $OrGroup extends WhereClause { val.validate(joins); } } + + @override + void _withConverters(Map converters) { + final clauseValues = values.whereType(); + for (final val in clauseValues) { + val._withConverters(converters); + } + } } class WhereClauseValue extends WhereClause { final String field; final Operator operator; - final ValueType value; + final dynamic _value; final String? table; + final Map _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( @@ -108,6 +129,12 @@ class WhereClauseValue extends WhereClause { } } + dynamic get value { + final typeConverter = _converters[ValueType]; + if (typeConverter == null) return _value; + return typeConverter.toDbType(_value); + } + @override void validate(List joins) { if (table == null) return; @@ -118,6 +145,13 @@ class WhereClauseValue extends WhereClause { ); } } + + @override + void _withConverters(Map converters) { + _converters + ..clear() + ..addAll(converters); + } } class WhereClauseBuilder> with WhereOperation { @@ -182,12 +216,7 @@ class WhereClauseBuilder> with WhereOperation { @override WhereClauseValue> $isNotBetween(String field, List 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) { diff --git a/lib/src/query/query.dart b/lib/src/query/query.dart index 20893cca..085c6649 100644 --- a/lib/src/query/query.dart +++ b/lib/src/query/query.dart @@ -148,7 +148,9 @@ final class Query> ReadQuery where(WhereBuilder builder) { final whereClause = builder.call(WhereClauseBuilder()); - whereClause.validate(_joins); + whereClause + ..validate(_joins) + .._withConverters(converters); return ReadQuery._(this, whereClause: whereClause); } diff --git a/lib/src/reflection.dart b/lib/src/reflection.dart index 8311ca19..e2fc7333 100644 --- a/lib/src/reflection.dart +++ b/lib/src/reflection.dart @@ -47,6 +47,9 @@ final class EntityTypeDefinition> { UpdatedAtField? get updatedAtField => !timestampsEnabled ? null : columns.firstWhereOrNull((e) => e is UpdatedAtField) as UpdatedAtField?; + Iterable get fieldsRequiredForCreate => + primaryKey.autoIncrement ? columns.where((e) => e != primaryKey) : columns; + Iterable get editableColumns => columns.where((e) => e != primaryKey); const EntityTypeDefinition(