femto-backend/Femto.Database/Program.cs
2025-05-18 17:18:34 +02:00

182 lines
5.7 KiB
C#

using System.CommandLine;
using Femto.Database;
using Femto.Database.Seed;
using Npgsql;
var nameArg = new Argument<string>("name", "the name of the migration");
var migrationsDirectoryOption = new Option<string>(
["--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<string>(
"--connection-string",
"the connection string to the database"
);
var connectionStringOption = new Option<string?>(
["-c", "--connection-string"],
() => 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,
connectionStringArg,
};
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<bool>(["-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;
}
}