diff --git a/_tests_/.gitignore b/_tests_/.gitignore new file mode 100644 index 00000000..4c1d83cc --- /dev/null +++ b/_tests_/.gitignore @@ -0,0 +1 @@ +database/database.dart diff --git a/_tests_/database/migrations/2024_04_21_003612_create_users_table.dart b/_tests_/database/migrations/2024_04_21_003612_create_users_table.dart new file mode 100644 index 00000000..db35abda --- /dev/null +++ b/_tests_/database/migrations/2024_04_21_003612_create_users_table.dart @@ -0,0 +1,14 @@ +import 'package:yaroorm/yaroorm.dart'; +import 'package:yaroorm_tests/src/models.dart'; + +class AddUsersTable extends Migration { + @override + void up(List schemas) { + schemas.add(UserSchema); + } + + @override + void down(List schemas) { + schemas.add(Schema.dropIfExists(UserSchema)); + } +} diff --git a/_tests_/test/integration/fixtures/migrations.dart b/_tests_/database/migrations/2024_04_21_003650_create_posts_table.dart similarity index 58% rename from _tests_/test/integration/fixtures/migrations.dart rename to _tests_/database/migrations/2024_04_21_003650_create_posts_table.dart index c57130cc..f754f9f2 100644 --- a/_tests_/test/integration/fixtures/migrations.dart +++ b/_tests_/database/migrations/2024_04_21_003650_create_posts_table.dart @@ -1,18 +1,5 @@ import 'package:yaroorm/yaroorm.dart'; - -import 'models.dart'; - -class AddUsersTable extends Migration { - @override - void up(List schemas) { - schemas.add(UserSchema); - } - - @override - void down(List schemas) { - schemas.add(Schema.dropIfExists(UserSchema)); - } -} +import 'package:yaroorm_tests/src/models.dart'; class AddPostsTable extends Migration { @override diff --git a/_tests_/test/integration/fixtures/database.dart b/_tests_/lib/db_config.dart similarity index 84% rename from _tests_/test/integration/fixtures/database.dart rename to _tests_/lib/db_config.dart index 77d68f7b..b97159ff 100644 --- a/_tests_/test/integration/fixtures/database.dart +++ b/_tests_/lib/db_config.dart @@ -1,10 +1,6 @@ import 'package:path/path.dart' as path; import 'package:yaroorm/yaroorm.dart'; -import 'migrations.dart'; - -part 'database.g.dart'; - @DB.useConfig final config = YaroormConfig( 'foo_sqlite', @@ -12,7 +8,7 @@ final config = YaroormConfig( DatabaseConnection( 'foo_sqlite', DatabaseDriverType.sqlite, - database: path.absolute('test/integration', 'db.sqlite'), + database: path.absolute('database', 'db.sqlite'), dbForeignKeys: true, ), DatabaseConnection( @@ -44,5 +40,4 @@ final config = YaroormConfig( port: 3002, ), ], - migrations: [AddUsersTable(), AddPostsTable()], ); diff --git a/_tests_/test/integration/fixtures/models.dart b/_tests_/lib/src/models.dart similarity index 100% rename from _tests_/test/integration/fixtures/models.dart rename to _tests_/lib/src/models.dart diff --git a/_tests_/test/integration/fixtures/test_data.dart b/_tests_/lib/test_data.dart similarity index 100% rename from _tests_/test/integration/fixtures/test_data.dart rename to _tests_/lib/test_data.dart diff --git a/_tests_/test/integration/e2e_basic.dart b/_tests_/test/integration/e2e_basic.dart index de74d6d8..314c48bc 100644 --- a/_tests_/test/integration/e2e_basic.dart +++ b/_tests_/test/integration/e2e_basic.dart @@ -1,13 +1,14 @@ import 'package:test/test.dart'; import 'package:collection/collection.dart'; import 'package:yaroorm/yaroorm.dart'; +import 'package:yaroorm_tests/src/models.dart'; -import 'fixtures/models.dart'; +import '../../database/database.dart'; +import '../../lib/test_data.dart'; import 'fixtures/migrator.dart'; -import 'fixtures/test_data.dart'; void runBasicE2ETest(String connectionName) { - Query.addTypeDef(userTypeData); + initializeORM(); final driver = DB.driver(connectionName); diff --git a/_tests_/test/integration/fixtures/migrator.dart b/_tests_/test/integration/fixtures/migrator.dart index 4b9cfad2..d7a76467 100644 --- a/_tests_/test/integration/fixtures/migrator.dart +++ b/_tests_/test/integration/fixtures/migrator.dart @@ -1,17 +1,9 @@ import 'dart:io'; import 'package:test/test.dart'; -import 'database.dart'; - -import 'package:yaroorm/src/cli/orm.dart'; - -void main(List args) async { - initializeORM(); - await OrmCLIRunner.start(args); -} Future runMigrator(String connectionName, String command) async { - final commands = ['run', '_tests_/test/integration/fixtures/migrator.dart', command, '--connection=$connectionName']; + final commands = ['run', 'yaroorm', command, '--connection=$connectionName']; print('> dart ${commands.join(' ')}\n'); final result = await Process.run('dart', commands); diff --git a/_tests_/test/integration/mariadb.e2e.dart b/_tests_/test/integration/mariadb.e2e.dart index c684a192..90282195 100644 --- a/_tests_/test/integration/mariadb.e2e.dart +++ b/_tests_/test/integration/mariadb.e2e.dart @@ -1,10 +1,10 @@ import 'package:test/test.dart'; -import 'fixtures/database.dart' as db; +import '../../database/database.dart'; import 'e2e_basic.dart'; void main() async { - db.initializeORM(); + initializeORM(); group('MariaDB', () { group('Basic E2E Test', () => runBasicE2ETest('bar_mariadb')); diff --git a/_tests_/test/integration/mysql.e2e.dart b/_tests_/test/integration/mysql.e2e.dart index 084d33fa..0f93ba84 100644 --- a/_tests_/test/integration/mysql.e2e.dart +++ b/_tests_/test/integration/mysql.e2e.dart @@ -1,10 +1,10 @@ import 'package:test/test.dart'; -import 'fixtures/database.dart' as db; +import '../../database/database.dart'; import 'e2e_basic.dart'; void main() async { - db.initializeORM(); + initializeORM(); group('MySQL', () { group('Basic E2E Test', () => runBasicE2ETest('moo_mysql')); diff --git a/_tests_/test/integration/pgsql.e2e.dart b/_tests_/test/integration/pgsql.e2e.dart index 31ad5bed..d01d4b12 100644 --- a/_tests_/test/integration/pgsql.e2e.dart +++ b/_tests_/test/integration/pgsql.e2e.dart @@ -1,9 +1,9 @@ import 'package:test/test.dart'; -import 'fixtures/database.dart' as db; +import '../../database/database.dart'; import 'e2e_basic.dart'; void main() async { - db.initializeORM(); + initializeORM(); group('Postgres', () { group('Basic E2E Test', () => runBasicE2ETest('foo_pgsql')); diff --git a/_tests_/test/integration/sqlite.e2e.dart b/_tests_/test/integration/sqlite.e2e.dart index 89a5ef48..b9f78269 100644 --- a/_tests_/test/integration/sqlite.e2e.dart +++ b/_tests_/test/integration/sqlite.e2e.dart @@ -1,10 +1,10 @@ import 'package:test/test.dart'; -import 'fixtures/database.dart' as db; +import '../../database/database.dart'; import 'e2e_basic.dart'; void main() async { - db.initializeORM(); + initializeORM(); group('SQLite', () { group('Basic E2E Test', () => runBasicE2ETest('foo_sqlite')); diff --git a/_tests_/test/yaroorm_test.dart b/_tests_/test/yaroorm_test.dart index 812c4f33..32b255ea 100644 --- a/_tests_/test/yaroorm_test.dart +++ b/_tests_/test/yaroorm_test.dart @@ -3,15 +3,15 @@ import 'package:yaroorm/src/database/driver/mysql_driver.dart'; import 'package:yaroorm/src/database/driver/pgsql_driver.dart'; import 'package:yaroorm/src/database/driver/sqlite_driver.dart'; import 'package:yaroorm/yaroorm.dart'; +import 'package:yaroorm_tests/src/models.dart'; -import 'integration/fixtures/database.dart' as db; -import 'integration/fixtures/models.dart'; +import '../database/database.dart'; Matcher throwsArgumentErrorWithMessage(String message) => throwsA(isA().having((p0) => p0.message, '', message)); void main() { - setUpAll(db.initializeORM); + setUpAll(initializeORM); group('DatabaseDriver.init', () { group('when sqlite connection', () { diff --git a/bin/yaroorm.dart b/bin/yaroorm.dart index 0cede4d9..533d6d3b 100644 --- a/bin/yaroorm.dart +++ b/bin/yaroorm.dart @@ -1,116 +1,53 @@ import 'dart:io'; -import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; -import 'package:analyzer/dart/analysis/results.dart'; -import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/file_system/physical_file_system.dart'; import 'package:path/path.dart' as path; -import 'package:source_gen/source_gen.dart'; -import 'package:yaroorm/src/cli/logger.dart'; +import 'package:yaroorm/src/cli/commands/init_orm_command.dart'; -import 'package:yaroorm/src/migration.dart' show Migration; -import 'package:yaroorm/src/database/entity/entity.dart' show Entity; +const _migratorFileContent = ''' +import 'package:yaroorm/src/cli/orm.dart'; +import 'package:yaroorm/yaroorm.dart'; + +import '../../database/database.dart'; void main(List args) async { - final migratorDirectoryPath = migratorPath; + initializeORM(); + + await OrmCLIRunner.start(args); +} +'''; + +void main(List args) async { + final migratorDirectoryPath = path.join(Directory.current.path, '.dart_tool', 'yaroorm'); + final dir = Directory(migratorDirectoryPath); + if (!dir.existsSync()) dir.createSync(); + final dartFile = path.join(migratorDirectoryPath, 'migrator.dart'); - final aotFilePath = path.join(migratorDirectoryPath, 'migrator'); + final kernelFilePath = path.join(migratorDirectoryPath, 'migrator_kernel'); - if (args.isNotEmpty && args[0] == 'init') { - await _initOrmInProject(Directory.current); - exit(0); - } + final migratorFile = File(dartFile); + final kernelFile = File(kernelFilePath); - if (!File(dartFile).existsSync()) { - logger.err('🗙 Migrator file does not exist'); - exit(0); + final isInitCommand = args.isNotEmpty && args[0] == InitializeOrmCommand.commandName; + if (isInitCommand && kernelFile.existsSync()) { + kernelFile.delete(); } - final aotFile = File(aotFilePath); - if (!aotFile.existsSync()) { - /// TODO(codekeyz): add checksum check for invalidating aot - Process.start('dart', ['compile', 'exe', dartFile, '-o', aotFilePath], mode: ProcessStartMode.detached); + if (!migratorFile.existsSync()) { + await migratorFile.writeAsString(_migratorFileContent); } late Process process; - if (aotFile.existsSync()) { - process = await Process.start(aotFilePath, args); + if (kernelFile.existsSync()) { + process = await Process.start('dart', ['run', kernelFilePath, ...args]); } else { process = await Process.start('dart', ['run', dartFile, ...args]); } stdout.addStream(process.stdout); stderr.addStream(process.stderr); -} - -String get migratorPath { - return path.join(Directory.current.path, '.dart_tool', 'yaroorm/bin'); -} - -Future _initOrmInProject(Directory workingDir) async { - // final progress = logger.progress('Initializing ORM in project'); - - final collection = AnalysisContextCollection( - includedPaths: [workingDir.absolute.path], - resourceProvider: PhysicalResourceProvider.INSTANCE, - ); - - List migrations = []; - List entities = []; - - await for (final (library, _, _) in _libraries(collection)) { - final result = _validateLibrary(library, library.element.identifier); - if (result == null) continue; - - if (result.migrations != null) { - migrations.add(result.migrations!); - } - - if (result.entityClasses != null) { - entities.add(result.entityClasses!); - } - } - - print(migrations.map((e) => (e.elements.map((e) => e.name), e.path))); - print(entities.map((e) => (e.elements.map((e) => e.name), e.path))); -} - -TypeChecker _typeChecker(Type type) => TypeChecker.fromRuntime(type); - -class Item { - final Iterable elements; - final String path; - - const Item(this.elements, this.path); -} - -({Item? migrations, Item? entityClasses})? _validateLibrary(ResolvedLibraryResult library, String identifier) { - final classElements = library.element.topLevelElements - .where((e) => !e.isPrivate && e is ClassElement && e.supertype != null && !e.isAbstract) - .toList() - .cast(); - - if (classElements.isEmpty) return null; - - final migrationClasses = classElements.where((element) => _typeChecker(Migration).isExactlyType(element.supertype!)); - final entityClasses = classElements.where((element) => _typeChecker(Entity).isExactlyType(element.supertype!)); - - return ( - migrations: migrationClasses.isEmpty ? null : Item(migrationClasses, identifier), - entityClasses: entityClasses.isEmpty ? null : Item(entityClasses, identifier), - ); -} -Stream<(ResolvedLibraryResult, String, String)> _libraries(AnalysisContextCollection collection) async* { - for (var context in collection.contexts) { - var analyzedFiles = context.contextRoot.analyzedFiles().toList(); - analyzedFiles.sort(); - final analyzedDartFiles = analyzedFiles.where((path) => path.endsWith('.dart') && !path.endsWith('_test.dart')); - for (final filePath in analyzedDartFiles) { - final library = await context.currentSession.getResolvedLibrary(filePath); - if (library is ResolvedLibraryResult) { - yield (library, filePath, context.contextRoot.root.path); - } - } + if (!isInitCommand && !kernelFile.existsSync()) { + /// TODO(codekeyz): add checksum check for invalidating kernel snapshot + Process.start('dart', ['compile', 'kernel', dartFile, '-o', kernelFilePath], mode: ProcessStartMode.detached); } } diff --git a/lib/builder.dart b/lib/builder.dart index 74d3bba4..e01492e7 100644 --- a/lib/builder.dart +++ b/lib/builder.dart @@ -1,6 +1,6 @@ import 'package:build/build.dart'; -import 'src/generator.dart'; +import 'src/builder/generator.dart'; /// Builds generators for `build_runner` to run Builder yaroormBuilder(BuilderOptions options) => generatorFactoryBuilder(options); diff --git a/lib/src/generator.dart b/lib/src/builder/generator.dart similarity index 79% rename from lib/src/generator.dart rename to lib/src/builder/generator.dart index 73ba25c8..cf8076f6 100644 --- a/lib/src/generator.dart +++ b/lib/src/builder/generator.dart @@ -1,56 +1,19 @@ -import 'dart:async'; - import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/nullability_suffix.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'; import 'package:dart_style/dart_style.dart'; import 'package:recase/recase.dart'; import 'package:source_gen/source_gen.dart'; -import 'package:yaroorm/yaroorm.dart'; // ignore: implementation_imports -import 'database/entity/entity.dart' as entity; -import 'database/database.dart' as db; +import '../database/entity/entity.dart' as entity; +import 'utils.dart'; final _emitter = DartEmitter(useNullSafetySyntax: true, orderDirectives: true); -Builder generatorFactoryBuilder(BuilderOptions options) => SharedPartBuilder([ - EntityGenerator(), - DBInitGenerator(), - ], 'yaroorm'); - -typedef FieldData = ({FieldElement field, ConstantReader reader}); - -extension DartTypeExt on DartType { - bool get isNullable => nullabilitySuffix == NullabilitySuffix.question; -} - -String _getFieldDbName(FieldElement element) { - final elementName = element.name; - final meta = _typeChecker(entity.TableColumn).firstAnnotationOf(element, throwOnUnresolved: false); - if (meta != null) { - return ConstantReader(meta).peek('name')?.stringValue ?? elementName; - } - return elementName; -} - -String _getTypeDefName(String className) { - return '${className.snakeCase}TypeData'; -} - -extension IterableExtension on Iterable { - T? firstWhereOrNull(bool Function(T element) test) { - for (final element in this) { - if (test(element)) return element; - } - return null; - } -} - -TypeChecker _typeChecker(Type type) => TypeChecker.fromRuntime(type); +Builder generatorFactoryBuilder(BuilderOptions options) => SharedPartBuilder([EntityGenerator()], 'yaroorm'); class EntityGenerator extends GeneratorForAnnotation { @override @@ -75,7 +38,7 @@ class EntityGenerator extends GeneratorForAnnotation { Type type, ) { for (final field in fields) { - final result = _typeChecker(type).firstAnnotationOf(field, throwOnUnresolved: false); + final result = typeChecker(type).firstAnnotationOf(field, throwOnUnresolved: false); if (result != null) { return (field: field, reader: ConstantReader(result)); } @@ -85,11 +48,10 @@ class EntityGenerator extends GeneratorForAnnotation { 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)); + final hasManyGetters = getterFields.where((getter) => typeChecker(entity.HasMany).isExactlyType(getter.type)); if (hasManyGetters.isNotEmpty) { - final hasManyClass = hasManyGetters.first.type.element; - print(hasManyClass?.name); + // final hasManyClass = hasManyGetters.first.type.element; } final fields = classElement.fields.where(allowedTypes).toList(); @@ -129,9 +91,9 @@ class EntityGenerator extends GeneratorForAnnotation { String generateCodeForField(FieldElement e) { final symbol = '#${e.name}'; - final columnName = _getFieldDbName(e); + final columnName = getFieldDbName(e); - final meta = _typeChecker(entity.TableColumn).firstAnnotationOf(e, throwOnUnresolved: false); + final meta = typeChecker(entity.TableColumn).firstAnnotationOf(e, throwOnUnresolved: false); final requiredOpts = ''' "$columnName", @@ -141,14 +103,14 @@ class EntityGenerator extends GeneratorForAnnotation { if (meta != null) { final metaReader = ConstantReader(meta); - final isReferenceField = _typeChecker(entity.reference).isExactly(meta.type!.element!); + final isReferenceField = typeChecker(entity.reference).isExactly(meta.type!.element!); if (isReferenceField) { final referencedType = metaReader.peek('type')!.typeValue; final element = referencedType.element as ClassElement; final superType = element.supertype?.element; - if (superType == null || !_typeChecker(entity.Entity).isExactly(superType)) { + if (superType == null || !typeChecker(entity.Entity).isExactly(superType)) { throw InvalidGenerationSourceError( 'Generator cannot target field `${e.name}` on `$className` class.', todo: 'Type passed to [reference] annotation must be a subtype of `Entity`.', @@ -187,7 +149,7 @@ class EntityGenerator extends GeneratorForAnnotation { } final queryName = '${className}Query'; - final typeDataName = _getTypeDefName(className); + final typeDataName = getTypeDefName(className); final library = Library((b) => b ..body.addAll([ @@ -263,7 +225,7 @@ return switch(field) { ..name = field.name ..constant = true ..lambda = true - ..initializers.add(Code('super("${_getFieldDbName(field)}", direction)')) + ..initializers.add(Code('super("${getFieldDbName(field)}", direction)')) ..requiredParameters.add(Parameter((p) => p ..type = refer('OrderDirection') ..name = 'direction')))))), @@ -367,7 +329,7 @@ return switch(field) { /// This generates WHERE-EQUAL Clauses for a field Method _generateFieldWhereClause(FieldElement field, String className) { - final dbColumnName = _getFieldDbName(field); + final dbColumnName = getFieldDbName(field); final fieldType = field.type.getDisplayString(withNullability: true); return Method( @@ -420,46 +382,3 @@ return switch(field) { throw UnsupportedError('Parameters for TypeConverters not yet supported'); } } - -class DBInitGenerator extends GeneratorForAnnotation { - final List _entities = []; - - @override - FutureOr generate(LibraryReader library, BuildStep buildStep) async { - final entityChecker = TypeChecker.fromRuntime(Table); - - for (final annotatedElement in library.annotatedWithExact(entityChecker, throwOnUnresolved: false)) { - final element = annotatedElement.element; - if (element is! ClassElement) continue; - if (element.isAbstract || element.isPrivate) continue; - - _entities.add(element); - } - - return super.generate(library, buildStep); - } - - @override - generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { - if (_entities.isEmpty) return null; - - if (element is! TopLevelVariableElement) { - throw InvalidGenerationSourceError('Generator cannot target `${element.name}`.', - todo: 'Must be a top-level variable.'); - } - - final field = Method.returnsVoid((m) => m - ..name = 'initializeORM' - ..body = Code(''' -// Add Type Definitions to Query Runner -${_entities.map((entity) => 'Query.addTypeDef<${entity.name}>(${_getTypeDefName(entity.name)});').join('\n')} - -DB.init(${element.name}); - -''')); - - return DartFormatter().format([ - field.accept(_emitter), - ].join('\n\n')); - } -} diff --git a/lib/src/builder/utils.dart b/lib/src/builder/utils.dart new file mode 100644 index 00000000..ad59b533 --- /dev/null +++ b/lib/src/builder/utils.dart @@ -0,0 +1,154 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/element/element.dart'; +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:mason_logger/mason_logger.dart'; +import 'package:recase/recase.dart'; +import 'package:source_gen/source_gen.dart'; + +import '../database/database.dart' show UseORMConfig; +import '../database/entity/entity.dart' as entity; +import '../migration.dart' show Migration; + +class YaroormCliException implements Exception { + final String message; + YaroormCliException(this.message) : super(); + + @override + String toString() { + return 'ORM CLI Error: $message'; + } +} + +typedef FieldData = ({FieldElement field, ConstantReader reader}); + +TypeChecker typeChecker(Type type) => TypeChecker.fromRuntime(type); + +extension DartTypeExt on DartType { + bool get isNullable => nullabilitySuffix == NullabilitySuffix.question; +} + +String getFieldDbName(FieldElement element) { + final elementName = element.name; + final meta = typeChecker(entity.TableColumn).firstAnnotationOf(element, throwOnUnresolved: false); + if (meta != null) { + return ConstantReader(meta).peek('name')?.stringValue ?? elementName; + } + return elementName; +} + +String getTypeDefName(String className) { + return '${className.snakeCase}TypeData'; +} + +final _numberRegex = RegExp(r'\d+'); +DateTime parseMigrationFileDate(String fileName) { + final matches = _numberRegex.allMatches(fileName).map((e) => e.group(0)).toList(); + final time = matches.last; + if (matches.isEmpty || matches.length != 4 || time!.length != 6) { + throw YaroormCliException('Invalid migration name: -> $fileName'); + } + return DateTime.parse( + '${matches[0]}-${matches[1]}-${matches[2]} ${time.substring(0, 2)}:${time.substring(2, 4)}:${time.substring(4)}'); +} + +Future< + ({ + List migrations, + List entities, + TopLevelVariableElement dbConfig, + })> resolveMigrationAndEntitiesInDir(Directory workingDir) async { + final collection = AnalysisContextCollection( + includedPaths: [workingDir.absolute.path], + resourceProvider: PhysicalResourceProvider.INSTANCE, + ); + + final List migrations = []; + final List entities = []; + + TopLevelVariableElement? dbConfig; + + await for (final (library, _, _) in _libraries(collection)) { + /// Resolve ORM config + final configIsInitiallyNull = dbConfig == null; + final config = library.element.topLevelElements + .firstWhereOrNull((element) => typeChecker(UseORMConfig).hasAnnotationOfExact(element)); + if (config != null) { + if (configIsInitiallyNull) { + if (config is! TopLevelVariableElement || config.isPrivate) { + throw YaroormCliException('ORM config has to be a top level public variable'); + // progress.fail('🗙 ORM initialize step failed'); + // logger.err(); + // exit(ExitCode.software.code); + } + + dbConfig = config; + } else { + throw YaroormCliException('Found more than one ORM Config'); + + // progress.fail('🗙 ORM initialize step failed'); + // logger.err('Found more than one ORM Config'); + // exit(ExitCode.software.code); + } + } + + final result = _validateLibrary(library, library.element.identifier); + if (result == null) continue; + + if (result.migrations != null) { + migrations.add(result.migrations!); + } + + if (result.entityClasses != null) { + entities.add(result.entityClasses!); + } + } + + if (dbConfig == null) { + throw YaroormCliException('Did you forget to annotate ORM Config with ${cyan.wrap('@DB.useConfig')} ?'); + } + + return (migrations: migrations, entities: entities, dbConfig: dbConfig); +} + +class Item { + final Iterable elements; + final String path; + + const Item(this.elements, this.path); +} + +({Item? migrations, Item? entityClasses})? _validateLibrary(ResolvedLibraryResult library, String identifier) { + final classElements = library.element.topLevelElements + .where((e) => !e.isPrivate && e is ClassElement && e.supertype != null && !e.isAbstract) + .toList() + .cast(); + + if (classElements.isEmpty) return null; + + final migrationClasses = classElements.where((element) => typeChecker(Migration).isExactlyType(element.supertype!)); + final entityClasses = classElements.where((element) => typeChecker(entity.Entity).isExactlyType(element.supertype!)); + + return ( + migrations: migrationClasses.isEmpty ? null : Item(migrationClasses, identifier), + entityClasses: entityClasses.isEmpty ? null : Item(entityClasses, identifier), + ); +} + +Stream<(ResolvedLibraryResult, String, String)> _libraries(AnalysisContextCollection collection) async* { + for (var context in collection.contexts) { + final analyzedFiles = context.contextRoot.analyzedFiles().toList(); + final analyzedDartFiles = analyzedFiles.where((path) => path.endsWith('.dart') && !path.endsWith('_test.dart')); + for (final filePath in analyzedDartFiles) { + final library = await context.currentSession.getResolvedLibrary(filePath); + if (library is ResolvedLibraryResult) { + yield (library, filePath, context.contextRoot.root.path); + } + } + } +} diff --git a/lib/src/cli/commands/command.dart b/lib/src/cli/commands/command.dart index 06dfbee4..909b4de0 100644 --- a/lib/src/cli/commands/command.dart +++ b/lib/src/cli/commands/command.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:args/command_runner.dart'; +import 'package:mason_logger/mason_logger.dart'; import '../../../yaroorm.dart'; import '../model/migration.dart'; @@ -40,7 +41,7 @@ abstract class OrmCommand extends Command { } List get migrationDefinitions { - return (ormConfig.migrations) + return (DB.migrations) .where((e) => e.connection == null || e.connection == dbConnection) .map(MigrationDefn.new) .toList(); @@ -56,7 +57,7 @@ abstract class OrmCommand extends Command { await execute(driver); await driver.disconnect(); - return 0; + return ExitCode.success.code; } Future execute(DatabaseDriver driver); diff --git a/lib/src/cli/commands/init_orm_command.dart b/lib/src/cli/commands/init_orm_command.dart new file mode 100644 index 00000000..9e8a64b4 --- /dev/null +++ b/lib/src/cli/commands/init_orm_command.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:args/command_runner.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:collection/collection.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:mason_logger/mason_logger.dart'; + +import '../../builder/utils.dart'; +import '../logger.dart'; + +import 'package:path/path.dart' as path; + +class InitializeOrmCommand extends Command { + static const String commandName = 'init'; + + @override + String get description => 'Initialize ORM in project'; + + @override + String get name => commandName; + + @override + FutureOr? run() async { + final workingDir = Directory.current; + final progress = logger.progress('Initializing ORM in project'); + + try { + final result = await resolveMigrationAndEntitiesInDir(workingDir); + + await _initOrmInProject(workingDir, result.migrations, result.entities, result.dbConfig); + + progress.complete('ORM initialized 🚀'); + + return ExitCode.success.code; + } on YaroormCliException catch (e) { + progress.fail('🗙 ORM initialize step failed'); + logger.err(e.toString()); + exit(ExitCode.software.code); + } + } +} + +Future _initOrmInProject( + Directory workingDir, + List migrations, + List entities, + TopLevelVariableElement dbConfig, +) async { + final entityNames = entities.map((e) => e.elements.map((e) => e.name)).fold({}, (preV, e) => preV..addAll(e)); + final databaseFile = File(path.join(workingDir.path, 'database/database.dart')); + + // Resolve ORM Config file import path + const filePrefix = 'file://'; + var configPath = dbConfig.library.identifier; + if (configPath.startsWith(filePrefix)) { + configPath = configPath.replaceFirst(filePrefix, '').trim(); + } + configPath = configPath.replaceFirst(workingDir.path, '').trim().replaceFirst('/database', '.'); + + final migrationFileNameDateMap = migrations + .map((e) => path.basename(e.path)) + .fold({}, (preV, filename) => preV..[filename] = parseMigrationFileDate(filename)); + + final sortedMigrationsList = (migrations + ..sort((a, b) { + return migrationFileNameDateMap[path.basename(a.path)]! + .compareTo(migrationFileNameDateMap[path.basename(b.path)]!); + })) + .mapIndexed((index, element) => (index: index, element: element)); + + final library = Library((p0) => p0 + ..comments.add('GENERATED CODE - DO NOT MODIFY BY HAND') + ..directives.addAll([ + Directive.import('package:yaroorm/yaroorm.dart'), + ...entities.map((e) => e.path).toSet().map((e) => Directive.import(e)), + Directive.import(configPath, as: 'config'), + ...sortedMigrationsList + .map((e) => Directive.import('migrations/${path.basename(e.element.path)}', as: '_m${e.index}')) + ]) + ..body.add(Method.returnsVoid((m) => m + ..name = 'initializeORM' + ..body = Code(''' +/// Add Type Definitions to Query Runner +${entityNames.map((name) => 'Query.addTypeDef<$name>(${getTypeDefName(name)});').join('\n')} + +/// Configure Migrations Order +DB.migrations.addAll([ + ${sortedMigrationsList.map((mig) => mig.element.elements.map((classElement) => '_m${mig.index}.${classElement.name}()').join(', ')).join(', ')}, +]); + +DB.init(config.${dbConfig.name}); +''')))); + final emitter = DartEmitter.scoped(orderDirectives: true, useNullSafetySyntax: true); + final code = DartFormatter().format([library.accept(emitter)].join('\n\n')); + + await databaseFile.writeAsString(code); +} diff --git a/lib/src/cli/orm.dart b/lib/src/cli/orm.dart index 60f20d2d..893798a6 100644 --- a/lib/src/cli/orm.dart +++ b/lib/src/cli/orm.dart @@ -1,6 +1,7 @@ import 'package:cli_completion/cli_completion.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:args/command_runner.dart'; +import 'package:yaroorm/src/cli/commands/init_orm_command.dart'; import 'commands/migrate_rollback_command.dart'; import 'commands/migrate_fresh_command.dart'; @@ -11,8 +12,7 @@ import 'commands/command.dart'; import '_misc.dart'; import 'logger.dart'; -const executableName = 'yaroo orm'; -const packageName = 'yaroo_cli'; +const executableName = 'yaroorm'; const description = 'yaroorm command-line tool'; class OrmCLIRunner extends CompletionCommandRunner { @@ -36,6 +36,7 @@ class OrmCLIRunner extends CompletionCommandRunner { help: 'specify database connection', ); + addCommand(InitializeOrmCommand()); addCommand(MigrateCommand()); addCommand(MigrateFreshCommand()); addCommand(MigrationRollbackCommand()); diff --git a/lib/src/config.dart b/lib/src/config.dart index dbfc72e6..0b02c370 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -1,12 +1,10 @@ import 'database/driver/driver.dart'; -import 'migration.dart'; class YaroormConfig { final String defaultConnName; final List connections; final String migrationsTable; - final List migrations; DatabaseConnection get defaultDBConn => connections.firstWhere((e) => e.name == defaultConnName); @@ -14,7 +12,6 @@ class YaroormConfig { this.defaultConnName, { required this.connections, this.migrationsTable = 'migrations', - this.migrations = const [], }) { final hasDefault = connections.any((e) => e.name == defaultConnName); if (!hasDefault) { diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 79ee9c74..cd211094 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta_meta.dart'; import '../config.dart'; +import '../migration.dart'; import '../query/query.dart'; import 'entity/entity.dart'; import 'driver/driver.dart'; @@ -28,6 +29,8 @@ class UseORMConfig { class DB { static const useConfig = UseORMConfig._(); + static List migrations = []; + static YaroormConfig config = throw StateError(pleaseInitializeMessage); static final Map _driverInstances = {}; diff --git a/lib/src/migration.dart b/lib/src/migration.dart index 4c91e8be..cf92d1e1 100644 --- a/lib/src/migration.dart +++ b/lib/src/migration.dart @@ -178,7 +178,7 @@ abstract class Schema { }; } - return CreateSchema._( + return CreateSchema._( entity.tableName, (table) { table.id( @@ -207,11 +207,7 @@ abstract class Schema { ); } - static Schema create(String name, TableBluePrintFunc func) => CreateSchema._(name, func); - - static Schema dropIfExists(CreateSchema value) { - return _DropSchema(value.tableName); - } + static Schema dropIfExists(CreateSchema value) => _DropSchema(value.tableName); static Schema rename(String from, String to) => _RenameSchema(from, to); } @@ -228,7 +224,7 @@ abstract class Migration { void down(List schemas); } -final class CreateSchema extends Schema { +final class CreateSchema> extends Schema { CreateSchema._(super.name, super.func) : super._(); @override diff --git a/lib/src/reflection.dart b/lib/src/reflection.dart index 55aa1d86..b79caadb 100644 --- a/lib/src/reflection.dart +++ b/lib/src/reflection.dart @@ -131,7 +131,6 @@ final class UpdatedAtField extends DBEntityField { } final class ReferencedField> implements DBEntityField { - final DBEntity reference; final String _columnName; final Symbol _dartName; final bool _nullable; @@ -144,8 +143,9 @@ final class ReferencedField> implements DBEntityField { bool nullable = false, this.onDelete, this.onUpdate, - }) : _nullable = nullable, - reference = Query.getEntity(); + }) : _nullable = nullable; + + DBEntity get reference => Query.getEntity(); @override String get columnName => _columnName;