Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: database migrations #33

Merged
merged 9 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion melos_tunder.iml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/tunder/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/tunder/build" />
<excludeFolder url="file://$MODULE_DIR$/tunder/.pub" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>
</module>
1 change: 1 addition & 0 deletions tunder/lib/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export 'src/database/db.dart';
export 'src/database/schema/schema.dart';
export 'src/database/database_service_provider.dart';
export 'src/database/migration.dart';
export '_common.dart';
2 changes: 1 addition & 1 deletion tunder/lib/src/console/command.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:args/command_runner.dart' as args;
import 'package:mason_logger/mason_logger.dart';

abstract class Command<T> extends args.Command<T> {
abstract class Command<int> extends args.Command<int> {
late final String name;
late final String description;
late Logger logger;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import 'package:tunder/console.dart';
import 'package:tunder/src/console/commands/migrations/mixins/manage_migrations.dart';

abstract class MigrationCommand extends Command<int> with ManageMigrations {}
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import 'dart:io';

import 'package:clock/clock.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as path;
import 'package:tunder/console.dart';
import 'package:tunder/src/console/commands/migrations/contracts/migration_command.dart';
import 'package:tunder/tunder.dart';

class MakeMigrationCommand extends Command {
class MakeMigrationCommand extends MigrationCommand {
final name = 'make:migration';
final description = 'Create a migration file';
final String stubsDir;
final String destinationDir;

String? _migrationName;
String get migrationName => _migrationName ??= argResults!.rest.join(' ');
int? _timestamp;
int get timestamp => _timestamp ??= clock.now().millisecondsSinceEpoch;

String? _id;
String get id {
if (_id != null) return _id!;

return _id = DateFormat('yyyy_MM_dd_HHmmss').format(clock.now());
}

MakeMigrationCommand({
this.destinationDir = ConsoleConfig.migrationDestination,
this.stubsDir = ConsoleConfig.stubsDirectory,
});

run() async {
Future<int> run() async {
var file = File('$stubsDir/migrations/migration.stub');

if (!file.existsSync())
Expand All @@ -34,16 +41,18 @@ class MakeMigrationCommand extends Command {

if (logging != null)
logging.complete('Migration created: ${createdFile.path}');

return 0;
}

File generateMigrationFile(File file) {
var contents = file.readAsStringSync();

contents = contents
.replaceAll('{{ version }}', timestamp.toString())
.replaceAll('{{ id }}', id)
.replaceAll('{{ name }}', migrationName);

var fileName = '${timestamp}_${migrationName.snakeCase}.dart';
var fileName = '${id}_${migrationName.snakeCase}.dart';
var createdFile = _generateFile(fileName, contents);

return createdFile;
Expand All @@ -64,16 +73,18 @@ class MakeMigrationCommand extends Command {
void registerMigrationInListFile(File generated) {
var listFile = File(path.join(destinationDir, 'list.dart'));
var defaultListContents = '''
import 'package:tunder/database.dart';

import 'index.dart';

var migrations = [
final List<Migration> migrations = [
];

''';
var listContents = listFile.existsSync()
? listFile.readAsStringSync()
: defaultListContents;
var migrationInstance = " Migration$timestamp(),\n];";
var migrationInstance = " Migration_$id(),\n];";

listContents = listContents.replaceAll('];', migrationInstance);
listFile
Expand Down
38 changes: 38 additions & 0 deletions tunder/lib/src/console/commands/migrations/migrate_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:tunder/database.dart';
import 'package:tunder/src/console/commands/migrations/contracts/migration_command.dart';
import 'package:tunder/src/console/commands/migrations/mixins/manage_migrations.dart';

class MigrateCommand extends MigrationCommand with ManageMigrations {
final name = 'migrate';
final description = 'Runs migrations';
final List<Migration> migrations;

MigrateCommand(this.migrations) {
this.migrations.sort((a, b) => a.id.compareTo(b.id));
}

Future<int> run() async {
if (!await DB.tableExists('migrations')) await createMigrationsTable();
ranMigrations = await getRanMigrations();

late Progress migrating;
late Migration migration;
try {
for (migration in pendingMigrations) {
migrating = progress('Migrating: ${migration.id} ${migration.name}');

await migration.up();
await insertMigration(migration);

migrating.complete('Migrated: ${migration.id} ${migration.name}');
}

return 0;
} catch (err) {
migrating.fail('Failed: ${migration.id} ${migration.name}');
error(err.toString());
return 1;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:tunder/database.dart';
import 'package:tunder/src/console/commands/migrations/contracts/migration_command.dart';
import 'package:tunder/src/console/commands/migrations/mixins/manage_migrations.dart';

class MigrateRollbackCommand extends MigrationCommand with ManageMigrations {
final name = 'migrate:rollback';
final description = 'Rollback the last database migration';
final List<Migration> migrations;

MigrateRollbackCommand(this.migrations) {
this.migrations.sort((a, b) => a.id.compareTo(b.id));
}

Future<int> run() async {
final ranMigrations = await getRanMigrations() as List<MappedRow>;
if (ranMigrations.isEmpty) {
info('No migrations to rollback');
return 0;
}

final migrationId = ranMigrations.last['id'] as String;
final migration = migrations.where((mig) => mig.id == migrationId).first;
final rollingback =
progress('Rolling back: ${migration.id}_${migration.name.snakeCase}');

try {
await migration.down();
await deleteMigration(migration);
rollingback
.complete('Rolled back: ${migration.id}_${migration.name.snakeCase}');

return 0;
} catch (err) {
rollingback.fail(
'Failed to rollback: ${migration.id}_${migration.name.snakeCase}');
error(err.toString());

return 1;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:colorize/colorize.dart';
import 'package:tunder/database.dart';
import 'package:tunder/src/console/commands/migrations/contracts/migration_command.dart';
import 'package:tunder/src/console/commands/migrations/mixins/manage_migrations.dart';

class MigrateStatusCommand extends MigrationCommand with ManageMigrations {
final name = 'migrate:status';
final description = 'Get the status of each migration';
final List<Migration> migrations;

MigrateStatusCommand(this.migrations) {
this.migrations.sort((a, b) => a.id.compareTo(b.id));
}

Future<int> run() async {
if (!await DB.tableExists('migrations')) await createMigrationsTable();
ranMigrations = await getRanMigrations();

for (final migration in migrations) {
dynamic status = isPending(migration) ? 'pending' : '✔';
status = Colorize(status)..bold();
isPending(migration) ? status.lightYellow() : status.lightGreen();
info(' $status ${migration.id}_${migration.name.snakeCase}');
}

return 0;
}

bool isPending(Migration migration) => pendingMigrations.contains(migration);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:clock/clock.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:tunder/console.dart';
import 'package:tunder/database.dart';

mixin ManageMigrations on Command<int> {
List<Migration> migrations = [];
late final List ranMigrations;
List<Migration> get pendingMigrations => migrations
.where((migration) =>
!ranMigrations.any((existing) => existing['id'] == migration.id))
.toList();

Future<void> createMigrationsTable() async {
Progress creation = progress('Creating migrations table');
await Schema.create('migrations', (table) {
table
..string('id').notNullable().primary()
..string('name').notNullable()
..dateTime('executed_at').notNullable();
});
creation.complete('Migrations table created');
}

Future<List> getRanMigrations() => Query('migrations').orderBy('id').all();

Future<int> insertMigration(Migration migration) =>
Query('migrations').insert({
'id': migration.id,
'name': migration.name,
'executed_at': clock.now(),
});

Future<int> deleteMigration(Migration migration) =>
Query('migrations').whereMap({'id': migration.id}).delete();
}
17 changes: 13 additions & 4 deletions tunder/lib/src/console/console_kernel.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'dart:io';

import 'package:tunder/database.dart';
import 'package:tunder/src/console/command.dart';
import 'package:tunder/src/console/commands/migrations/make_migration_command.dart';
import 'package:tunder/src/console/commands/migrations/migrate_command.dart';
import 'package:tunder/src/console/commands/migrations/migrate_rollback_command.dart';
import 'package:tunder/src/console/commands/migrations/migrate_status_command.dart';
import 'package:tunder/src/console/sky_command.dart';
import 'package:tunder/tunder.dart';

Expand All @@ -14,7 +18,7 @@ class ConsoleKernel implements ConsoleKernelContract {

@override
Future<int> handle(List<String> arguments) async {
SkyCommand runner = app.get(SkyCommand);
SkyCommand<int> runner = app.get(SkyCommand<int>);

(baseCommands() + _commands)
.forEach((command) => runner.addTunderCommand(command));
Expand All @@ -24,15 +28,20 @@ class ConsoleKernel implements ConsoleKernelContract {
return exitCode;
}

List<Command> baseCommands() {
List<Command<int>> baseCommands() {
final appMigrations = migrations();
return [
MakeMigrationCommand(),
MigrateCommand(appMigrations),
MigrateStatusCommand(appMigrations),
MigrateRollbackCommand(appMigrations),
];
}

@override
List<Type> commands() => [];
List<Migration> migrations() => [];

List<Command> get _commands =>
commands().map((command) => app.get<Command>(command)).toList();
List<Command<int>> get _commands =>
commands().map((command) => app.get<Command<int>>(command)).toList();
}
4 changes: 2 additions & 2 deletions tunder/lib/src/console/sky_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:tunder/console.dart' as tunder;
import 'package:tunder/tunder.dart';

class SkyCommand<T> extends CommandRunner<T> {
class SkyCommand<int> extends CommandRunner<int> {
late final Logger logger;
bool silent;

SkyCommand(this.logger, {this.silent = false})
: super('sky', 'Tunder Framework $tunderVersion');

void addTunderCommand(tunder.Command<T> command) {
void addTunderCommand(tunder.Command<int> command) {
super.addCommand(
command
..logger = logger
Expand Down
3 changes: 3 additions & 0 deletions tunder/lib/src/contracts/console_kernel_contract.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'package:tunder/database.dart';

abstract class ConsoleKernelContract {
Future<int> handle(List<String> args);
List<Type> commands();
List<Migration> migrations();
}
2 changes: 1 addition & 1 deletion tunder/lib/src/core/container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Container {
}

_resolveType(Type type) {
var mirror = reflectClass(type);
var mirror = reflectType(type) as ClassMirror;

if (mirror.isAbstract)
throw BindingResolutionException('abstract type $type');
Expand Down
2 changes: 1 addition & 1 deletion tunder/lib/src/database/migration.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
abstract class Migration {
abstract final int version;
abstract final String id;
abstract final String name;
Future up();
Future down();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:tunder/extensions.dart';
import 'package:tunder/database.dart';
import 'package:tunder/src/database/operations/postgres/postgres_count_operation.dart';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:tunder/database.dart';
import 'package:tunder/_common.dart';
import 'package:tunder/src/database/operations/postgres/postgres_delete_operation.dart';

abstract class DeleteOperation {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:tunder/database.dart';
import 'package:tunder/_common.dart';
import 'package:tunder/src/database/operations/postgres/postgres_insert_operation.dart';

abstract class InsertOperation {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:tunder/extensions.dart';
import 'package:tunder/database.dart';
import 'package:tunder/src/database/operations/postgres/postgres_query_operation.dart';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:tunder/database.dart';
import 'package:tunder/_common.dart';
import 'package:tunder/src/database/operations/postgres/postgres_update_operation.dart';

abstract class UpdateOperation {
Expand Down
6 changes: 6 additions & 0 deletions tunder/lib/src/database/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ class Query<T> {
return _where;
}

Query<T> whereMap(Map<String, dynamic> map) {
map.forEach((key, value) => where(key).equals(value));

return this;
}

Where orWhere(String column) {
final _where = Where(column);
or(_where);
Expand Down
Loading