diff --git a/_tests_/lib/src/models.dart b/_tests_/lib/src/models.dart index 0862796b..31dbd3da 100644 --- a/_tests_/lib/src/models.dart +++ b/_tests_/lib/src/models.dart @@ -22,7 +22,7 @@ class User extends Entity { required this.homeAddress, }); - // HasMany get posts => hasMany(); + HasMany get posts => hasMany(); } @Table('posts') @@ -56,15 +56,19 @@ class Post extends Entity { required this.updatedAt, }); - // HasMany get comments => hasMany(); + HasMany get comments => hasMany(); + + BelongsTo get owner => belongsTo(); } @Table('post_comments') class PostComment extends Entity { @primaryKey - final String id; + final int id; final String comment; - final int? postId; - PostComment(this.id, this.comment, {this.postId}); + @reference(Post, name: 'post_id', onDelete: ForeignKeyAction.cascade) + final int postId; + + PostComment(this.id, this.comment, {required this.postId}); } diff --git a/_tests_/test/integration/e2e_relation.dart b/_tests_/test/integration/e2e_relation.dart index e5bef96e..d614eca8 100644 --- a/_tests_/test/integration/e2e_relation.dart +++ b/_tests_/test/integration/e2e_relation.dart @@ -1,168 +1,139 @@ -// import 'package:test/test.dart'; -// import 'package:yaroorm/yaroorm.dart'; - -// import 'fixtures/migrator.dart'; -// import 'fixtures/test_data.dart'; - -// void runRelationsE2ETest(String connectionName) { -// final driver = DB.driver(connectionName); - -// final tables = [User, Post, PostComment].map((e) => getEntityTableName(e)); - -// test('tables names', -// () => expect(tables, ['users', 'posts', 'post_comments'])); - -// return group('with ${driver.type.name} driver', () { -// User? testUser1, anotherUser; - -// setUpAll(() async { -// await driver.connect(); - -// expect(driver.isOpen, isTrue); - -// var hasTables = await Future.wait(tables.map(driver.hasTable)); -// expect(hasTables.every((e) => e), isFalse); - -// await runMigrator(connectionName, 'migrate'); - -// hasTables = await Future.wait(tables.map(driver.hasTable)); -// expect(hasTables.every((e) => e), isTrue); - -// testUser1 = await User( -// firstname: 'Baba', -// lastname: 'Tunde', -// age: 29, -// homeAddress: "Owerri, Nigeria") -// .withDriver(driver) -// .save(); -// expect( -// testUser1, isA().having((p0) => p0.id, 'has primary key', 1)); -// }); - -// test('should add many posts for User', () async { -// final postsToAdd = [ -// Post('Foo Bar 1', 'foo bar 4'), -// Post('Mee Moo 2', 'foo bar 5'), -// Post('Coo Kie 3', 'foo bar 6'), -// ]; - -// await testUser1!.posts.addAll(postsToAdd); - -// final posts = await testUser1!.posts.orderByAsc('title').get(); -// expect(posts, hasLength(3)); -// expect( -// posts.every((e) => -// e.createdAt != null && -// e.updatedAt != null && -// e.userId == testUser1!.id), -// isTrue); -// expect( -// posts.map((e) => { -// 'id': e.id, -// 'title': e.title, -// 'desc': e.description, -// 'userId': e.userId! -// }), -// [ -// {'id': 3, 'title': 'Coo Kie 3', 'desc': 'foo bar 6', 'userId': 1}, -// {'id': 1, 'title': 'Foo Bar 1', 'desc': 'foo bar 4', 'userId': 1}, -// {'id': 2, 'title': 'Mee Moo 2', 'desc': 'foo bar 5', 'userId': 1}, -// ]); -// }); - -// test('should add comments for post', () async { -// final post = await testUser1!.posts.first(); -// expect(post, isA()); - -// var comments = await post!.comments.get(); -// expect(comments, isEmpty); - -// final c = PostComment('this post looks abit old') -// ..id = 'some_random_uuid_32893782738'; -// await post.comments.add(c); - -// comments = await post.comments.get(); -// expect( -// comments.map( -// (e) => {'id': c.id, 'comment': c.comment, 'postId': e.postId}), -// [ -// { -// 'id': 'some_random_uuid_32893782738', -// 'comment': 'this post looks abit old', -// 'postId': 1 -// } -// ]); - -// await post.comments.add(PostComment('oh, another comment') -// ..id = 'jagaban_299488474773_uuid_3i848'); -// comments = await post.comments.orderByDesc('comment').get(); -// expect(comments, hasLength(2)); -// expect( -// comments.map( -// (e) => {'id': e.id, 'comment': e.comment, 'postId': e.postId}), -// [ -// { -// 'id': 'some_random_uuid_32893782738', -// 'comment': 'this post looks abit old', -// 'postId': 1 -// }, -// { -// 'id': 'jagaban_299488474773_uuid_3i848', -// 'comment': 'oh, another comment', -// 'postId': 1 -// } -// ]); -// }); - -// test('should add post for another user', () async { -// anotherUser = await usersList.last.withDriver(driver).save(); -// final user = anotherUser as User; - -// expect(user.id, isNotNull); -// expect(user.id != testUser1!.id, isTrue); - -// var anotherUserPosts = await user.posts.get(); -// expect(anotherUserPosts, isEmpty); - -// await user.posts.add(Post('Another Post', 'wham bamn')); -// anotherUserPosts = await user.posts.get(); -// expect(anotherUserPosts, hasLength(1)); - -// final anotherUserPost = anotherUserPosts.first; -// expect(anotherUserPost.userId!, anotherUser!.id!); - -// await anotherUserPost.comments.addAll([ -// PostComment('ah ah')..id = '_id_394', -// PostComment('oh oh')..id = '_id_394885', -// ]); - -// expect(await anotherUserPost.comments.get(), hasLength(2)); -// }); - -// test('should delete comments for post', () async { -// expect(testUser1, isNotNull); -// final posts = await testUser1!.posts.get(); -// expect(posts, hasLength(3)); - -// // ignore: curly_braces_in_flow_control_structures -// for (final post in posts) await post.comments.delete(); - -// expect( -// await Future.wait(posts.map((e) => e.comments.get())), [[], [], []]); - -// await testUser1!.posts.delete(); - -// expect(await testUser1!.posts.get(), isEmpty); -// }); - -// tearDownAll(() async { -// await runMigrator(connectionName, 'migrate:reset'); - -// final hasTables = await Future.wait(tables.map(driver.hasTable)); -// expect(hasTables.every((e) => e), isFalse); - -// await driver.disconnect(); -// expect(driver.isOpen, isFalse); -// }); -// }); -// } +import 'package:test/test.dart'; +import 'package:yaroorm/yaroorm.dart'; +import 'package:yaroorm_tests/src/models.dart'; +import 'package:yaroorm_tests/test_data.dart'; + +import '../util.dart'; + +void runRelationsE2ETest(String connectionName) { + final driver = DB.driver(connectionName); + + return group('with ${driver.type.name} driver', () { + late User testUser1, anotherUser; + + final tableNames = [ + getEntityTableName(), + getEntityTableName(), + getEntityTableName(), + ]; + + setUpAll(() async { + await driver.connect(); + + expect(driver.isOpen, isTrue); + + var hasTables = await Future.wait(tableNames.map(driver.hasTable)); + expect(hasTables.every((e) => e), isFalse); + + await runMigrator(connectionName, 'migrate'); + + hasTables = await Future.wait(tableNames.map(driver.hasTable)); + expect(hasTables.every((e) => e), isTrue); + + testUser1 = await UserQuery.driver(driver).create( + firstname: 'Baba', + lastname: 'Tunde', + age: 29, + homeAddress: 'Owerri, Nigeria', + ); + }); + + test('should add many posts for User', () async { + await testUser1.posts.add(title: 'Aoo bar 1', description: 'foo bar 4'); + await testUser1.posts.add(title: 'Bee Moo 2', description: 'foo bar 5'); + await testUser1.posts.add(title: 'Coo Kie 3', description: 'foo bar 6'); + + final posts = await testUser1.posts.get( + orderBy: [OrderPostBy.title(OrderDirection.desc)], + ); + expect(posts, hasLength(3)); + expect(posts.map((e) => {'id': e.id, 'title': e.title, 'desc': e.description, 'userId': e.userId}), [ + {'id': 3, 'title': 'Coo Kie 3', 'desc': 'foo bar 6', 'userId': 1}, + {'id': 2, 'title': 'Bee Moo 2', 'desc': 'foo bar 5', 'userId': 1}, + {'id': 1, 'title': 'Aoo bar 1', 'desc': 'foo bar 4', 'userId': 1} + ]); + }); + + test('should fetch posts with owners', () async { + final posts = await PostQuery.driver(driver).withRelations((post) => [post.owner]).findMany(); + final post = posts.first; + + final owner = await post.owner; + final result = await owner.get(); + expect(owner.isUsingEntityCache, isTrue); + expect(result, isA()); + }); + + test('should add comments for post', () async { + final post = await testUser1.posts.first!; + expect(post, isA()); + + var comments = await post!.comments.get(); + expect(comments, isEmpty); + + await post.comments.add(comment: 'This post looks abit old'); + + comments = await post.comments.get(); + expect(comments.map((c) => {'id': c.id, 'comment': c.comment, 'postId': c.postId}), [ + {'id': 1, 'comment': 'This post looks abit old', 'postId': post.id} + ]); + + await post.comments.add(comment: 'oh, another comment'); + }); + + test('should add post for another user', () async { + final testuser = usersList.last; + anotherUser = await UserQuery.driver(driver).create( + firstname: testuser.firstname, + lastname: testuser.lastname, + age: testuser.age, + homeAddress: testuser.homeAddress, + ); + + expect(anotherUser.id, isNotNull); + expect(anotherUser.id != testUser1.id, isTrue); + + var anotherUserPosts = await anotherUser.posts.get(); + expect(anotherUserPosts, isEmpty); + + await anotherUser.posts.add(title: 'Another Post', description: 'wham bamn'); + anotherUserPosts = await anotherUser.posts.get(); + expect(anotherUserPosts, hasLength(1)); + + final anotherUserPost = anotherUserPosts.first; + expect(anotherUserPost.userId, anotherUser.id); + + await anotherUserPost.comments.add(comment: 'ah ah'); + await anotherUserPost.comments.add(comment: 'oh oh'); + + expect(await anotherUserPost.comments.get(), hasLength(2)); + }); + + test('should delete comments for post', () async { + expect(testUser1, isNotNull); + final posts = await testUser1.posts.get(); + expect(posts, hasLength(3)); + + // ignore: curly_braces_in_flow_control_structures + for (final post in posts) await post.comments.delete(); + + for (final post in posts) { + expect(await post.comments.get(), []); + } + + await testUser1.posts.delete(); + + expect(await testUser1.posts.get(), isEmpty); + }); + + tearDownAll(() async { + await runMigrator(connectionName, 'migrate:reset'); + + final hasTables = await Future.wait(tableNames.map(driver.hasTable)); + expect(hasTables.every((e) => e), isFalse); + + await driver.disconnect(); + expect(driver.isOpen, isFalse); + }); + }); +} diff --git a/_tests_/test/integration/mariadb.e2e.dart b/_tests_/test/integration/mariadb.e2e.dart index 90282195..bc87e40f 100644 --- a/_tests_/test/integration/mariadb.e2e.dart +++ b/_tests_/test/integration/mariadb.e2e.dart @@ -1,14 +1,15 @@ import 'package:test/test.dart'; -import '../../database/database.dart'; +import '../../database/database.dart' as db; import 'e2e_basic.dart'; +import 'e2e_relation.dart'; void main() async { - initializeORM(); + db.initializeORM(); group('MariaDB', () { group('Basic E2E Test', () => runBasicE2ETest('bar_mariadb')); - // group('Relation E2E Test', () => runRelationsE2ETest('bar_mariadb')); + group('Relation E2E Test', () => runRelationsE2ETest('bar_mariadb')); }); } diff --git a/_tests_/test/integration/mysql.e2e.dart b/_tests_/test/integration/mysql.e2e.dart index 0f93ba84..89cc0074 100644 --- a/_tests_/test/integration/mysql.e2e.dart +++ b/_tests_/test/integration/mysql.e2e.dart @@ -1,14 +1,15 @@ import 'package:test/test.dart'; -import '../../database/database.dart'; +import '../../database/database.dart' as db; import 'e2e_basic.dart'; +import 'e2e_relation.dart'; void main() async { - initializeORM(); + db.initializeORM(); group('MySQL', () { group('Basic E2E Test', () => runBasicE2ETest('moo_mysql')); - // group('Relation E2E Test', () => runRelationsE2ETest('moo_mysql')); + group('Relation E2E Test', () => runRelationsE2ETest('moo_mysql')); }); } diff --git a/_tests_/test/integration/pgsql.e2e.dart b/_tests_/test/integration/pgsql.e2e.dart index d01d4b12..e3300e7b 100644 --- a/_tests_/test/integration/pgsql.e2e.dart +++ b/_tests_/test/integration/pgsql.e2e.dart @@ -1,13 +1,14 @@ import 'package:test/test.dart'; -import '../../database/database.dart'; +import '../../database/database.dart' as db; import 'e2e_basic.dart'; +import 'e2e_relation.dart'; void main() async { - initializeORM(); + db.initializeORM(); group('Postgres', () { group('Basic E2E Test', () => runBasicE2ETest('foo_pgsql')); - // group('Relation E2E Test', () => runRelationsE2ETest('foo_pgsql')); + group('Relation E2E Test', () => runRelationsE2ETest('foo_pgsql')); }); } diff --git a/_tests_/test/integration/sqlite.e2e.dart b/_tests_/test/integration/sqlite.e2e.dart index b9f78269..1e944c02 100644 --- a/_tests_/test/integration/sqlite.e2e.dart +++ b/_tests_/test/integration/sqlite.e2e.dart @@ -1,14 +1,15 @@ import 'package:test/test.dart'; -import '../../database/database.dart'; +import '../../database/database.dart' as db; import 'e2e_basic.dart'; +import 'e2e_relation.dart'; void main() async { - initializeORM(); + db.initializeORM(); group('SQLite', () { group('Basic E2E Test', () => runBasicE2ETest('foo_sqlite')); - // group('Relation E2E Test', () => runRelationsE2ETest('foo_sqlite')); + group('Relation E2E Test', () => runRelationsE2ETest('foo_sqlite')); }); } diff --git a/docker-compose.yaml b/docker-compose.yaml index 3a2c413d..528a0b9a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,7 +32,7 @@ services: POSTGRES_PASSWORD: 'password' POSTGRES_DB: test_db ports: - - "3002:3306" + - "3002:5432" volumes: - postgresdb_data:/var/lib/psql/data diff --git a/lib/src/builder/generator.dart b/lib/src/builder/generator.dart index cf8076f6..5ba18cfb 100644 --- a/lib/src/builder/generator.dart +++ b/lib/src/builder/generator.dart @@ -1,5 +1,6 @@ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; import 'package:code_builder/code_builder.dart'; import 'package:collection/collection.dart'; @@ -27,56 +28,27 @@ class EntityGenerator extends GeneratorForAnnotation { throw InvalidGenerationSourceError( 'Generator cannot target `$name`.', todo: 'Remove the [Table] annotation from `$name`.', + element: element, ); } return _implementClass(element, annotation); } - FieldData? _getFieldAnnotationByType( - List fields, - Type type, - ) { - for (final field in fields) { - final result = typeChecker(type).firstAnnotationOf(field, throwOnUnresolved: false); - if (result != null) { - return (field: field, reader: ConstantReader(result)); - } - } - return null; - } - String _implementClass(ClassElement classElement, ConstantReader annotation) { - final getterFields = classElement.fields.where((e) => e.getter?.isSynthetic == false); - final hasManyGetters = getterFields.where((getter) => typeChecker(entity.HasMany).isExactlyType(getter.type)); - - if (hasManyGetters.isNotEmpty) { - // final hasManyClass = hasManyGetters.first.type.element; - } - - final fields = classElement.fields.where(allowedTypes).toList(); + final parsedEntity = ParsedEntityClass.parse(classElement); final className = classElement.name; - final tableName = annotation.peek('name')!.stringValue; - - final primaryKey = _getFieldAnnotationByType(fields, entity.PrimaryKey); - final createdAtField = _getFieldAnnotationByType(fields, entity.CreatedAtColumn)?.field; - final updatedAtField = _getFieldAnnotationByType(fields, entity.UpdatedAtColumn)?.field; - - final converters = annotation.peek('converters')!.listValue; + final primaryKey = parsedEntity.primaryKey; + final fields = parsedEntity.allFields; + final createdAtField = parsedEntity.createdAtField?.field; + final updatedAtField = parsedEntity.updatedAtField?.field; if (primaryKey == null) { throw Exception("$className Entity doesn't have primary key"); } - final autoIncrementPrimaryKey = primaryKey.reader.peek('autoIncrement')!.boolValue; - final timestampsEnabled = (createdAtField ?? updatedAtField) != null; - - /// other properties aside primarykey, updatedAt and createdAt - final normalFields = fields.where((e) => ![createdAtField, updatedAtField, primaryKey.field].contains(e)); - - final creatableFields = [if (!autoIncrementPrimaryKey) primaryKey.field, ...normalFields]; - + /// Validate class constructor final primaryConstructor = classElement.constructors.firstWhereOrNull((e) => e.name == ""); if (primaryConstructor == null) { throw '$className Entity does not have a default constructor'; @@ -89,6 +61,14 @@ class EntityGenerator extends GeneratorForAnnotation { 'These props are not allowed in $className Entity default constructor: ${notAllowedProps.join(', ')}'); } + final normalFields = parsedEntity.normalFields; + + final converters = annotation.peek('converters')!.listValue; + + final autoIncrementPrimaryKey = primaryKey.reader.peek('autoIncrement')!.boolValue; + final timestampsEnabled = (createdAtField ?? updatedAtField) != null; + final creatableFields = parsedEntity.fieldsRequiredForCreate; + String generateCodeForField(FieldElement e) { final symbol = '#${e.name}'; final columnName = getFieldDbName(e); @@ -113,18 +93,41 @@ class EntityGenerator extends GeneratorForAnnotation { if (superType == null || !typeChecker(entity.Entity).isExactly(superType)) { throw InvalidGenerationSourceError( 'Generator cannot target field `${e.name}` on `$className` class.', + element: element, todo: 'Type passed to [reference] annotation must be a subtype of `Entity`.', ); } + final parsedClass = ParsedEntityClass.parse(element); + final onUpdate = metaReader.peek('onUpdate')?.objectValue.variable!.name; final onDelete = metaReader.peek('onDelete')?.objectValue.variable!.name; + final customPassedReferenceSymbol = metaReader.peek('field')?.symbolValue; + + late FieldElement referenceField; + if (customPassedReferenceSymbol != null) { + final foundField = + parsedClass.allFields.firstWhereOrNull((e) => Symbol(e.name) == customPassedReferenceSymbol); + if (foundField == null) { + throw InvalidGenerationSourceError( + 'Referenced Symbol `$customPassedReferenceSymbol` does not exist on `${element.name}` class.', + element: e, + ); + } + referenceField = foundField; + } else { + referenceField = parsedClass.primaryKey!.field; + } + return '''DBEntityField.referenced<${element.name}>( - "$columnName", $symbol - ${onUpdate == null ? '' : ', onUpdate: ForeignKeyAction.$onUpdate'} - ${onDelete == null ? '' : ', onDelete: ForeignKeyAction.$onDelete'} - ,)'''; + ${[ + '($symbol, "$columnName")', + '(#${referenceField.name}, "${getFieldDbName(referenceField)}")', + if (e.type.isNullable) 'nullable: true', + if (onUpdate != null) 'onUpdate: ForeignKeyAction.$onUpdate', + if (onDelete != null) 'onDelete: ForeignKeyAction.$onDelete', + ].join(', ')})'''; } } @@ -172,7 +175,7 @@ class EntityGenerator extends GeneratorForAnnotation { ..lambda = true ..body = Code( '''DBEntity<$className>( - "$tableName", + "${parsedEntity.table}", timestampsEnabled: $timestampsEnabled, columns: ${fields.map(generateCodeForField).toList()}, mirror: _\$${className}EntityMirror.new, @@ -294,6 +297,65 @@ return switch(field) { ])), /// Generate Extension for HasMany creations + if (parsedEntity.hasManyGetters.isNotEmpty) ...[ + ...parsedEntity.hasManyGetters.map( + (hasManyField) { + final hasManyClass = hasManyField.getter!.returnType as InterfaceType; + final relatedClass = hasManyClass.typeArguments.last.element as ClassElement; + final relatedClassName = relatedClass.name; + + final parsedRelatedEntity = ParsedEntityClass.parse(relatedClass); + final referenceField = parsedRelatedEntity.referencedFields + .firstWhereOrNull((e) => e.reader.peek('type')!.typeValue.element!.name == className) + ?.field; + if (referenceField == null) { + throw InvalidGenerationSourceError( + 'No reference field found for $className in $relatedClassName', + element: relatedClass, + todo: 'Did you forget to annotate with `@reference`', + ); + } + + final relatedEntityCreateFields = + parsedRelatedEntity.fieldsRequiredForCreate.where((field) => field != referenceField); + + return Extension((b) => b + ..name = '${className}HasMany${relatedClassName}Extension' + ..on = refer('HasMany<$className, $relatedClassName>') + ..methods.addAll([ + Method( + (m) => m + ..name = 'add' + ..returns = refer('Future<$relatedClassName>') + ..optionalParameters.addAll(relatedEntityCreateFields.map( + (field) => Parameter((p) => p + ..name = field.name + ..named = true + ..type = refer('${field.type}') + ..required = !field.type.isNullable), + )) + ..body = Code('''return \$readQuery.\$query.\$insert({ + ${[ + ...relatedEntityCreateFields.map((e) => '#${e.name}: ${e.name}'), + '#${referenceField.name}: parentId' + ].join(',')} + });'''), + ), + ])); + }, + ) + ], + + /// Generate Extension for loading relations + Extension((b) => b + ..name = '${className}RelationsBuilder' + ..on = refer('JoinBuilder<$className>') + ..methods.addAll([ + if (parsedEntity.belongsToGetters.isNotEmpty) + ...parsedEntity.belongsToGetters.map((field) => _generateJoinForBelongsTo(parsedEntity, field.getter!)), + if (parsedEntity.hasManyGetters.isNotEmpty) + ...parsedEntity.hasManyGetters.map((field) => _generateJoinForHasMany(parsedEntity, field.getter!)), + ])), ])); return DartFormatter().format([ @@ -302,10 +364,6 @@ return switch(field) { ].join('\n\n')); } - bool allowedTypes(FieldElement field) { - return field.getter?.isSynthetic ?? false; - } - String _generateConstructorCode(String className, ConstructorElement constructor) { final sb = StringBuffer()..write('$className('); @@ -381,4 +439,72 @@ return switch(field) { /// TODO(codekeyz): resolve constructor for TypeConverters throw UnsupportedError('Parameters for TypeConverters not yet supported'); } + + /// Generate JOIN for BelongsTo getters on Entity + Method _generateJoinForBelongsTo( + ParsedEntityClass parent, + PropertyAccessorElement getter, + ) { + final belongsToClass = getter.returnType as InterfaceType; + final getterName = getter.name; + final referencedClass = belongsToClass.typeArguments.last.element as ClassElement; + + // Field on :parent that establishes the relationship needed to make this work + final field = parent.referencedFields + .firstWhereOrNull((e) => e.reader.peek('type')!.typeValue.element!.name == referencedClass.name); + if (field == null) { + throw InvalidGenerationSourceError( + 'No reference field found to establish :BELONGS_TO_${referencedClass.name} relation on ${parent.className}', + element: referencedClass, + todo: 'Did you forget to annotate with `@reference`', + ); + } + + // Get the column on the foreign table which we're latching onto + final parsedReferenceClass = ParsedEntityClass.parse(referencedClass); + final referenceColumn = parsedReferenceClass.allFields + .firstWhereOrNull((e) => Symbol(e.name) == field.reader.peek('field')?.symbolValue) ?? + parsedReferenceClass.primaryKey!.field; + + final relationship = 'BelongsTo<${parent.className}, ${referencedClass.name}>'; + final joinClass = 'Join<${parent.className}, ${referencedClass.name}, $relationship>'; + return Method( + (m) => m + ..name = getterName + ..type = MethodType.getter + ..lambda = true + ..returns = refer(joinClass) + ..body = Code('''$joinClass("$getterName", + origin: (#${field.field.name}, "${getFieldDbName(field.field)}"), + on: (#${referenceColumn.name}, "${getFieldDbName(referenceColumn)}") + )'''), + ); + } + + /// Generate JOIN for HasMany getters on Entity + Method _generateJoinForHasMany( + ParsedEntityClass parent, + PropertyAccessorElement getter, + ) { + final hasMany = getter.returnType as InterfaceType; + final referencedClass = hasMany.typeArguments.last.element as ClassElement; + final parsedReferenceClass = ParsedEntityClass.parse(referencedClass); + + final referenceField = parsedReferenceClass.referencedFields + .firstWhere((e) => e.reader.peek('type')!.typeValue.element == parent.element); + + final relationship = 'HasMany<${parent.className}, ${referencedClass.name}>'; + final joinClass = 'Join<${parent.className}, ${referencedClass.name}, $relationship>'; + return Method( + (m) => m + ..name = getter.name + ..type = MethodType.getter + ..lambda = true + ..returns = refer(joinClass) + ..body = Code('''$joinClass("${getter.name}", + origin: (#${parent.primaryKey!.field.name}, "${getFieldDbName(parent.primaryKey!.field)}"), + on: (#${referenceField.field.name}, "${getFieldDbName(referenceField.field)}") + )'''), + ); + } } diff --git a/lib/src/builder/utils.dart b/lib/src/builder/utils.dart index ad59b533..666d2b9b 100644 --- a/lib/src/builder/utils.dart +++ b/lib/src/builder/utils.dart @@ -7,6 +7,7 @@ import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; import 'package:collection/collection.dart'; +import 'package:grammer/grammer.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:recase/recase.dart'; import 'package:source_gen/source_gen.dart'; @@ -20,13 +21,9 @@ class YaroormCliException implements Exception { YaroormCliException(this.message) : super(); @override - String toString() { - return 'ORM CLI Error: $message'; - } + String toString() => 'ORM CLI Error: $message'; } -typedef FieldData = ({FieldElement field, ConstantReader reader}); - TypeChecker typeChecker(Type type) => TypeChecker.fromRuntime(type); extension DartTypeExt on DartType { @@ -42,6 +39,11 @@ String getFieldDbName(FieldElement element) { return elementName; } +String getTableName(ClassElement element) { + final meta = typeChecker(entity.Table).firstAnnotationOf(element, throwOnUnresolved: false); + return ConstantReader(meta).peek('name')?.stringValue ?? element.name.toPlural().first.snakeCase.toLowerCase(); +} + String getTypeDefName(String className) { return '${className.snakeCase}TypeData'; } @@ -152,3 +154,94 @@ Stream<(ResolvedLibraryResult, String, String)> _libraries(AnalysisContextCollec } } } + +typedef FieldElementAndReader = ({FieldElement field, ConstantReader reader}); + +final class ParsedEntityClass { + final ClassElement element; + + final String table; + final String className; + + final List allFields; + + final List getters; + + final FieldElementAndReader? primaryKey, createdAtField, updatedAtField; + + List get referencedFields => _getFieldsAndReaders(normalFields, entity.reference); + + List get hasManyGetters => + getters.where((getter) => typeChecker(entity.HasMany).isExactlyType(getter.type)).toList(); + + List get belongsToGetters => + getters.where((getter) => typeChecker(entity.BelongsTo).isExactlyType(getter.type)).toList(); + + /// All other properties aside primarykey, updatedAt and createdAt. + List get normalFields => + allFields.where((e) => ![createdAtField?.field, updatedAtField?.field, primaryKey!.field].contains(e)).toList(); + + bool get hasAutoIncrementingPrimaryKey { + return primaryKey!.reader.peek('autoIncrement')!.boolValue; + } + + List get fieldsRequiredForCreate => [ + if (!hasAutoIncrementingPrimaryKey) primaryKey!.field, + ...normalFields, + ]; + + const ParsedEntityClass( + this.table, + this.className, + this.element, { + this.primaryKey, + this.createdAtField, + this.updatedAtField, + required this.allFields, + this.getters = const [], + }); + + factory ParsedEntityClass.parse(ClassElement element, {ConstantReader? reader}) { + final tableName = getTableName(element); + final fields = element.fields.where(_allowedTypes).toList(); + final primaryKey = _getFieldAnnotationByType(fields, entity.PrimaryKey); + final createdAt = _getFieldAnnotationByType(fields, entity.CreatedAtColumn); + final updatedAt = _getFieldAnnotationByType(fields, entity.UpdatedAtColumn); + + return ParsedEntityClass( + tableName, + element.name, + element, + allFields: fields, + getters: element.fields.where((e) => e.getter?.isSynthetic == false).toList(), + primaryKey: primaryKey, + createdAtField: createdAt, + updatedAtField: updatedAt, + ); + } + + static bool _allowedTypes(FieldElement field) { + return field.getter?.isSynthetic ?? false; + } + + static FieldElementAndReader? _getFieldAnnotationByType(List fields, Type type) { + for (final field in fields) { + final result = typeChecker(type).firstAnnotationOf(field, throwOnUnresolved: false); + if (result != null) { + return (field: field, reader: ConstantReader(result)); + } + } + return null; + } + + static List _getFieldsAndReaders(List fields, Type type) { + return fields + .map((field) { + final result = typeChecker(type).firstAnnotationOf(field, throwOnUnresolved: false); + if (result == null) return null; + return (field: field, reader: ConstantReader(result)); + }) + .whereNotNull() + .toList(); + } +} diff --git a/lib/src/database/driver/pgsql_driver.dart b/lib/src/database/driver/pgsql_driver.dart index ff7b4b22..d2c84cd1 100644 --- a/lib/src/database/driver/pgsql_driver.dart +++ b/lib/src/database/driver/pgsql_driver.dart @@ -67,9 +67,8 @@ final class PostgreSqlDriver implements DatabaseDriver { @override Future insert(InsertQuery query) async { if (!isOpen) await connect(); - final primaryKey = await _getPrimaryKeyColumn(query.tableName); final values = {...query.data}; - final sql = _pgsqlSerializer.acceptInsertQuery(query, primaryKey: primaryKey); + final sql = _pgsqlSerializer.acceptInsertQuery(query); final result = await db!.execute(pg.Sql.named(sql), parameters: values); return result[0][0]; } @@ -124,21 +123,6 @@ final class PostgreSqlDriver implements DatabaseDriver { return result?.expand((x) => x).toList(); } - Future _getPrimaryKeyColumn(String tableName) async { - final result = await db?.execute('''SELECT pg_attribute.attname -FROM pg_index, pg_class, pg_attribute, pg_namespace -WHERE - pg_class.oid = '"$tableName"'::regclass AND - indrelid = pg_class.oid AND - nspname = 'public' AND - pg_class.relnamespace = pg_namespace.oid AND - pg_attribute.attrelid = pg_class.oid AND - pg_attribute.attnum = any(pg_index.indkey) - AND indisprimary;'''); - - return result?[0][0] as String; - } - @override List get typeconverters => [booleanConverter]; } @@ -164,7 +148,7 @@ class _PgSqlDriverTransactor extends DriverTransactor { Future insert(InsertQuery query) async { final sql = _pgsqlSerializer.acceptInsertQuery(query); final result = await txn.execute(pg.Sql.named(sql), parameters: query.data); - return result.affectedRows; + return result.first.toColumnMap()[query.primaryKey]; } @override @@ -200,12 +184,10 @@ class PgSqlPrimitiveSerializer extends MySqlPrimitiveSerializer { const PgSqlPrimitiveSerializer(); @override - String acceptInsertQuery(InsertQuery query, {String? primaryKey}) { + String acceptInsertQuery(InsertQuery query) { final keys = query.data.keys; final parameters = keys.map((e) => '@$e').join(', '); - final sql = 'INSERT INTO ${query.tableName} (${keys.map(escapeStr).join(', ')}) VALUES ($parameters)'; - if (primaryKey == null) return '$sql$terminator'; - return '$sql RETURNING "$primaryKey"$terminator'; + return 'INSERT INTO ${query.tableName} (${keys.map(escapeStr).join(', ')}) VALUES ($parameters) RETURNING "${query.primaryKey}"$terminator'; } @override @@ -231,7 +213,7 @@ class PgSqlPrimitiveSerializer extends MySqlPrimitiveSerializer { final values = dataMap.values.map((value) => "'$value'").join(', '); return '($values)'; }).join(', '); - return 'INSERT INTO ${query.tableName} ($fields) VALUES $values$terminator'; + return 'INSERT INTO ${query.tableName} ($fields) VALUES $values RETURNING ${query.primaryKey}$terminator'; } @override diff --git a/lib/src/database/driver/sqlite_driver.dart b/lib/src/database/driver/sqlite_driver.dart index d6399850..46652d2e 100644 --- a/lib/src/database/driver/sqlite_driver.dart +++ b/lib/src/database/driver/sqlite_driver.dart @@ -207,9 +207,23 @@ class SqliteSerializer extends PrimitiveSerializer { final queryBuilder = StringBuffer(); /// SELECT - final selectStatement = acceptSelect(query.fieldSelections.toList()); + final tableName = escapeStr(query.tableName); + final selectStatement = acceptSelect(tableName, query.fieldSelections.toList()); queryBuilder.write(selectStatement); - queryBuilder.write('FROM ${escapeStr(query.tableName)}'); + + /// JOINS + if (query.joins.isNotEmpty) { + final selections = query.joins.map((e) => e.aliasedForeignSelections.join(', ')).join(', '); + queryBuilder.write(', $selections FROM $tableName'); + + for (final join in query.joins) { + final field = '${join.fromTable}.${join.origin.$2}'; + final referencedField = '${join.onTable}.${join.on.$2}'; + queryBuilder.writeln(' LEFT JOIN ${join.onTable} ON $field = $referencedField'); + } + } else { + queryBuilder.write(' FROM $tableName'); + } /// WHERE final whereClause = query.whereClause; @@ -311,8 +325,10 @@ class SqliteSerializer extends PrimitiveSerializer { } @override - String acceptSelect(List fields) { - return fields.isEmpty ? 'SELECT * ' : 'SELECT ${fields.map(escapeStr).join(', ')} '; + String acceptSelect(String tableName, List fields) { + return fields.isEmpty + ? 'SELECT $tableName.*' + : 'SELECT ${fields.map((e) => '$tableName.${escapeStr(e)}').join(', ')}'; } @override diff --git a/lib/src/database/entity/entity.dart b/lib/src/database/entity/entity.dart index b947647e..3a1e4120 100644 --- a/lib/src/database/entity/entity.dart +++ b/lib/src/database/entity/entity.dart @@ -1,5 +1,6 @@ // ignore_for_file: camel_case_types +import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; @@ -14,8 +15,11 @@ import '../driver/driver.dart'; part 'converter.dart'; part 'relations.dart'; +part 'joins.dart'; abstract class Entity> { + final Map _relationsPreloaded = {}; + DriverContract _driver = DB.defaultDriver; DBEntity? _typeDataCache; @@ -30,23 +34,38 @@ abstract class Entity> { if (connection != null) _driver = DB.driver(connection!); } - withDriver(DriverContract driver) { + Entity withDriver(DriverContract driver) { return this.._driver = driver; } + @internal + Entity withRelationsData(Map data) { + _relationsPreloaded + ..clear() + ..addAll(data); + return this; + } + @protected - HasMany hasMany>([String? foreignKey]) { + HasMany hasMany>() { final relatedModelTypeData = Query.getEntity(); final referenceField = relatedModelTypeData.referencedFields.firstWhere((e) => e.reference.dartType == Parent); + + var relation = _relationsPreloaded[HasMany]; + if (relation is Map && relation.isEmpty) { + relation = >[]; + } + return HasMany._( - foreignKey ?? referenceField.columnName, + referenceField.columnName, this as Parent, + relation, ); } @protected - BelongsTo belongsTo>([String? foreignKey]) { - final parentFieldName = foreignKey ?? Query.getEntity().primaryKey.columnName; + BelongsTo belongsTo>() { + final parentFieldName = Query.getEntity().primaryKey.columnName; final referenceField = typeData.referencedFields.firstWhere((e) => e.reference.dartType == RelatedModel); final referenceFieldValue = typeData.mirror(this as Parent).get(referenceField.dartName); @@ -54,6 +73,7 @@ abstract class Entity> { parentFieldName, this as Parent, referenceFieldValue, + _relationsPreloaded[BelongsTo], ); } } @@ -96,10 +116,11 @@ class UpdatedAtColumn extends TableColumn { /// Use this to reference other entities class reference extends TableColumn { final Type type; + final Symbol? field; final ForeignKeyAction? onUpdate, onDelete; - const reference(this.type, {super.name, this.onUpdate, this.onDelete}); + const reference(this.type, {this.field, super.name, this.onUpdate, this.onDelete}); } const primaryKey = PrimaryKey(); diff --git a/lib/src/database/entity/joins.dart b/lib/src/database/entity/joins.dart new file mode 100644 index 00000000..d5aea6c6 --- /dev/null +++ b/lib/src/database/entity/joins.dart @@ -0,0 +1,27 @@ +part of 'entity.dart'; + +abstract class JoinBuilder> {} + +class Join, Reference extends Entity, + Relationship extends EntityRelation> { + String get fromTable => Query.getEntity().tableName; + String get onTable => Query.getEntity().tableName; + + final Type relation; + + final Entry origin; + final Entry on; + + final String resultKey; + + Iterable get aliasedForeignSelections => + Query.getEntity().columns.map((e) => '$onTable.${e.columnName} as "$resultKey.${e.columnName}"'); + + Join( + this.resultKey, { + required this.origin, + required this.on, + }) : relation = Relationship; +} + +typedef Entry> = (Symbol symbol, String columnName); diff --git a/lib/src/database/entity/relations.dart b/lib/src/database/entity/relations.dart index e5b6fa8e..d5e1699c 100644 --- a/lib/src/database/entity/relations.dart +++ b/lib/src/database/entity/relations.dart @@ -7,14 +7,18 @@ abstract class EntityRelation, RelatedModel extend EntityRelation(this.parent) : _query = Query.table().driver(parent._driver); - Object get ownerId { + Object get parentId { final typeInfo = parent.typeData; return typeInfo.mirror.call(parent).get(typeInfo.primaryKey.dartName)!; } DriverContract get _driver => parent._driver; - get(); + bool _usingEntityCache = false; + + bool get isUsingEntityCache => _usingEntityCache; + + get({bool refresh = false}); delete(); } @@ -25,10 +29,10 @@ final class HasOne, RelatedModel extends Entity get $readQuery => _query.where((q) => q.$equal(foreignKey, ownerId)); + ReadQuery get $readQuery => _query.where((q) => q.$equal(foreignKey, parentId)); @override - Future get() => $readQuery.findOne(); + FutureOr get({bool refresh = false}) => $readQuery.findOne(); @override Future delete() => $readQuery.delete(); @@ -38,14 +42,35 @@ final class HasOne, RelatedModel extends Entity, RelatedModel extends Entity> extends EntityRelation { + final List>? _cache; final String foreignKey; - HasMany._(this.foreignKey, super.parent); + HasMany._(this.foreignKey, super.parent, this._cache); - ReadQuery get $readQuery => _query.where((q) => q.$equal(foreignKey, ownerId)); + ReadQuery get $readQuery => _query.where((q) => q.$equal(foreignKey, parentId)); @override - Future> get({int? limit, int? offset, List>? orderBy}) { + FutureOr> get({ + int? limit, + int? offset, + List>? orderBy, + bool refresh = false, + }) async { + if (_cache != null) { + _usingEntityCache = true; + if (_cache!.isEmpty) return []; + + final typeData = Query.getEntity(); + return _cache! + .map((data) => serializedPropsToEntity( + data, + typeData, + combineConverters(typeData.converters, _driver.typeconverters), + )) + .toList(); + } + + _usingEntityCache = false; return $readQuery.findMany( limit: limit, offset: offset, @@ -53,7 +78,7 @@ final class HasMany, RelatedModel extends Entity get first => $readQuery.findOne(); + FutureOr get first => $readQuery.findOne(); @override Future delete() => $readQuery.delete(); @@ -61,17 +86,33 @@ final class HasMany, RelatedModel extends Entity, RelatedModel extends Entity> extends EntityRelation { + final Map? _cache; final String foreignKey; final dynamic value; - BelongsTo._(this.foreignKey, super.parent, this.value); + BelongsTo._(this.foreignKey, super.parent, this.value, this._cache); ReadQuery get _readQuery { return Query.table().driver(_driver).where((q) => q.$equal(foreignKey, value)); } @override - Future get() => _readQuery.findOne(); + FutureOr get({bool refresh = false}) async { + if (_cache != null && !refresh) { + _usingEntityCache = true; + if (_cache!.isEmpty) return null; + + final typeData = Query.getEntity(); + return serializedPropsToEntity( + _cache!, + typeData, + combineConverters(typeData.converters, _driver.typeconverters), + ); + } + + _usingEntityCache = false; + return _readQuery.findOne(); + } @override Future delete() => _readQuery.delete(); diff --git a/lib/src/primitives/serializer.dart b/lib/src/primitives/serializer.dart index 026161e5..f24a04b0 100644 --- a/lib/src/primitives/serializer.dart +++ b/lib/src/primitives/serializer.dart @@ -20,7 +20,7 @@ abstract class PrimitiveSerializer { String acceptWhereClauseValue(WhereClauseValue clauseValue); - String acceptSelect(List fields); + String acceptSelect(String tableName, List fields); String acceptOrderBy(List orderBys); diff --git a/lib/src/query/query.dart b/lib/src/query/query.dart index 9d939225..494099eb 100644 --- a/lib/src/query/query.dart +++ b/lib/src/query/query.dart @@ -33,6 +33,12 @@ mixin UpdateOperation> { }); } +mixin RelationsOperation> { + withRelations(List>> Function(JoinBuilder builder) builder) { + return this; + } +} + mixin LimitOperation { Future> take(int limit); } @@ -62,9 +68,10 @@ sealed class QueryBase { } final class Query> - with ReadOperation, InsertOperation, UpdateOperation, AggregateOperation { + with ReadOperation, InsertOperation, UpdateOperation, AggregateOperation, RelationsOperation { final DBEntity entity; final String? database; + final List _joins; late final String tableName; @@ -74,7 +81,9 @@ final class Query> static final Map _typedatas = {}; - Query._({String? tableName, this.database}) : entity = Query.getEntity() { + Query._({String? tableName, this.database}) + : entity = Query.getEntity(), + _joins = [] { this.tableName = tableName ?? entity.tableName; } @@ -134,7 +143,11 @@ final class Query> } } final recordId = await runner.insert( - InsertQuery(this, data: entityMapToDbData(data, converters)), + InsertQuery( + this, + data: entityMapToDbData(data, converters), + primaryKey: entity.primaryKey.columnName, + ), ); return (await findOne(where: (q) => q.$equal(entity.primaryKey.columnName, recordId)))!; @@ -154,6 +167,7 @@ final class Query> offset: offset, whereClause: whereClause, orderByProps: orderBy?.toSet(), + joins: _joins, ); final results = await runner.query(readQ); @@ -164,7 +178,7 @@ final class Query> @override Future findOne({WhereBuilder? where}) async { final whereClause = where?.call(WhereClauseBuilder._()); - final readQ = ReadQuery._(this, limit: 1, whereClause: whereClause); + final readQ = ReadQuery._(this, limit: 1, whereClause: whereClause, joins: _joins); final results = await runner.query(readQ); if (results.isEmpty) return null; return results.map(_wrapRawResult).first; @@ -195,13 +209,24 @@ final class Query> } /// [T] is the expected type passed to [Query] via Query - T _wrapRawResult(Map? result) { - if (result == null) return result as dynamic; + T _wrapRawResult(Map result) { + final Map> joinResults = {}; + for (final join in _joins) { + final entries = result.entries + .where((e) => e.key.startsWith('${join.resultKey}.')) + .map((e) => MapEntry(e.key.replaceFirst('${join.resultKey}.', '').trim(), e.value)); + if (entries.every((e) => e.value == null)) { + joinResults[join.relation] = {}; + } else { + joinResults[join.relation] = {}..addEntries(entries); + } + } + return serializedPropsToEntity( result, entity, converters, - ); + ).withRelationsData(joinResults).withDriver(runner) as T; } ReadQuery get _readQuery => ReadQuery._(this); @@ -229,6 +254,14 @@ final class Query> @override Future sum(String field) => SumAggregate(_readQuery, field).get(); + + @override + Query withRelations(List>> Function(JoinBuilder builder) builder) { + _joins + ..clear() + ..addAll(builder.call(_JoinBuilderImpl())); + return this; + } } mixin AggregateOperation { @@ -259,10 +292,11 @@ final class UpdateQuery extends QueryBase { Future execute() => runner.update(this); } -final class ReadQuery> extends QueryBase with AggregateOperation { +final class ReadQuery> extends QueryBase with AggregateOperation, RelationsOperation { final Set fieldSelections; final Set>? orderByProps; final WhereClause? whereClause; + final List joins; final int? limit, offset; final Query $query; @@ -272,6 +306,7 @@ final class ReadQuery> extends QueryBase with Agg this.whereClause, this.orderByProps, this.fieldSelections = const {}, + this.joins = const [], this.limit, this.offset, }) : super($query); @@ -337,12 +372,29 @@ final class ReadQuery> extends QueryBase with Agg Future delete() async { return DeleteQuery(_query, whereClause: whereClause!).execute(); } + + @override + ReadQuery withRelations( + List>> Function(JoinBuilder builder) builder, + ) { + joins + ..clear() + ..addAll(builder.call(_JoinBuilderImpl())); + return this; + } } +final class _JoinBuilderImpl> extends JoinBuilder {} + final class InsertQuery extends QueryBase { final Map data; + final String primaryKey; - InsertQuery(super.tableName, {required this.data}); + InsertQuery( + super.tableName, { + required this.data, + required this.primaryKey, + }); @override Future execute() => runner.insert(this); @@ -352,9 +404,14 @@ final class InsertQuery extends QueryBase { } final class InsertManyQuery extends QueryBase { + final String primaryKey; final List> values; - InsertManyQuery(super.tableName, {required this.values}); + InsertManyQuery( + super.tableName, { + required this.values, + required this.primaryKey, + }); @override String get statement => runner.serializer.acceptInsertManyQuery(this); diff --git a/lib/src/reflection.dart b/lib/src/reflection.dart index b79caadb..a3ff9c49 100644 --- a/lib/src/reflection.dart +++ b/lib/src/reflection.dart @@ -98,13 +98,19 @@ final class DBEntityField { } static ReferencedField referenced>( - String columnName, - Symbol dartName, { + Refer from, + Refer to, { bool nullable = false, ForeignKeyAction? onUpdate, ForeignKeyAction? onDelete, }) { - return ReferencedField._(columnName, dartName, nullable: nullable, onUpdate: onUpdate, onDelete: onDelete); + return ReferencedField._( + from, + to, + nullable: nullable, + onUpdate: onUpdate, + onDelete: onDelete, + ); } } @@ -130,20 +136,31 @@ final class UpdatedAtField extends DBEntityField { const UpdatedAtField._(String columnName, Symbol dartName) : super(columnName, DateTime, dartName); } +typedef Refer = (Symbol symbol, String dbname); + final class ReferencedField> implements DBEntityField { final String _columnName; final Symbol _dartName; + final bool _nullable; + // final String _referencedColumnName; + // final Symbol _referencedDartName; + final ForeignKeyAction? onUpdate, onDelete; ReferencedField._( - this._columnName, - this._dartName, { + Refer from, + Refer to, { bool nullable = false, this.onDelete, this.onUpdate, - }) : _nullable = nullable; + }) : _nullable = nullable, + _dartName = from.$1, + _columnName = from.$2 + // ,_referencedDartName = to.$1, + // _referencedColumnName = to.$2 + ; DBEntity get reference => Query.getEntity(); diff --git a/pubspec.lock b/pubspec.lock index 88901f0a..e8d20d8e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.0" file: dependency: transitive description: @@ -613,10 +613,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7d5ecd604e499358c5fe35ee828c0298a320d54455e791e9dcf73486bc8d9f0 + sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 url: "https://pub.dev" source: hosted - version: "14.1.0" + version: "14.0.0" watcher: dependency: transitive description: @@ -637,18 +637,18 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.3" webkit_inspection_protocol: dependency: transitive description: @@ -661,10 +661,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.2.0" yaml: dependency: transitive description: @@ -674,4 +674,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.2.0 <4.0.0"