using System.CommandLine; using Femto.Database; using Femto.Database.Seed; using Npgsql; var nameArg = new Argument("name", "the name of the migration"); var migrationsDirectoryOption = new Option( ["--migrations-directory"], () => System.Environment.GetEnvironmentVariable("MigrationsDirectory") ?? "./Migrations", "the directory where the migrations are stored" ); var newCommand = new Command("new", "creates a new migrations") { nameArg, migrationsDirectoryOption, }; newCommand.SetHandler(MakeNewMigration, nameArg, migrationsDirectoryOption); var connectionStringArg = new Argument( "connection-string", "the connection string to the database" ); var connectionStringOption = new Option( ["-c", "--connection-string"], // we default this to the same variable used by the server application () => System.Environment.GetEnvironmentVariable("ConnectionStrings__Database") ?? null, "the connection string to the database" ); var upCommand = new Command("up", "update the database to the most current migration") { migrationsDirectoryOption, connectionStringOption, }; upCommand.SetHandler( async (directory, connectionString) => { if (connectionString is null) { throw new ArgumentException("Connection string is required."); } await MigrateUp(directory, connectionString); }, migrationsDirectoryOption, connectionStringOption ); var seedCommand = new Command("seed", "seed the database with test data") { connectionStringArg }; seedCommand.SetHandler(Seed, connectionStringArg); // Add these near the top with other command definitions var yesOption = new Option(["-y", "--yes"], "Skip confirmation prompt"); var resetCommand = new Command( "reset", "drops the existing database, runs migrations, and seeds test data" ) { connectionStringArg, migrationsDirectoryOption, yesOption, }; resetCommand.SetHandler( CreateTestDatabase, connectionStringArg, migrationsDirectoryOption, yesOption ); var rootCommand = new RootCommand("migrator") { newCommand, upCommand, seedCommand, resetCommand }; return await rootCommand.InvokeAsync(args); static async Task MakeNewMigration(string name, string migrationsDirectory) { Directory.CreateDirectory(migrationsDirectory); var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); var fileName = $"{timestamp}_{name}.sql"; var filePath = Path.Combine(migrationsDirectory, fileName); // Write an initial comment in the file File.WriteAllText(filePath, $"-- Migration: {name}\n-- Created at: {DateTime.UtcNow}"); // Notify the user Console.WriteLine($"Migration created successfully: {filePath}"); } static async Task MigrateUp(string migrationsDirectory, string connectionString) { if (!Directory.Exists(migrationsDirectory)) { Console.WriteLine("Migrations directory does not exist."); return; } await using var dataSource = NpgsqlDataSource.Create(connectionString); await using var migrator = new Migrator(migrationsDirectory, dataSource); await migrator.Migrate(); } static async Task Seed(string connectionString) { var dataSource = NpgsqlDataSource.Create(connectionString); await TestDataSeeder.Seed(dataSource); } static async Task CreateTestDatabase( string connectionString, string migrationsDirectory, bool skipConfirmation ) { var builder = new NpgsqlConnectionStringBuilder(connectionString); if (!skipConfirmation) { builder.Database = "postgres"; try { await using var dataSource = NpgsqlDataSource.Create(builder.ConnectionString); await using var conn = await dataSource.OpenConnectionAsync(); await using var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT 1 FROM pg_database WHERE datname = '{builder.Database}'"; var exists = await cmd.ExecuteScalarAsync(); if (exists is true) { Console.WriteLine("WARNING: This will drop the existing database and recreate it."); Console.Write("Are you sure you want to continue? (y/N): "); var response = Console.ReadLine()?.ToLower(); if (response != "y") { Console.WriteLine("Operation cancelled."); return; } } } catch (Exception ex) { Console.WriteLine($"Error checking database existence: {ex.Message}"); throw; } } var databaseName = builder.Database; builder.Database = "postgres"; // Connect to default database to drop the target try { // Connect to postgres database to drop/create the target database await using var dataSource = NpgsqlDataSource.Create(builder.ConnectionString); await using var conn = await dataSource.OpenConnectionAsync(); await using var cmd = conn.CreateCommand(); // Drop database if it exists cmd.CommandText = $"DROP DATABASE IF EXISTS {databaseName} WITH (FORCE)"; await cmd.ExecuteNonQueryAsync(); // Create fresh database cmd.CommandText = $"CREATE DATABASE {databaseName}"; await cmd.ExecuteNonQueryAsync(); // Now run migrations and seed with the original connection string Console.WriteLine("Running migrations..."); await MigrateUp(migrationsDirectory, connectionString); Console.WriteLine("Seeding database..."); await Seed(connectionString); Console.WriteLine("Database reset completed successfully."); } catch (Exception ex) { Console.WriteLine($"Error resetting database: {ex.Message}"); throw; } }