using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using StellaOps.Infrastructure.Postgres.Migrations; namespace StellaOps.Cli.Services; /// /// Helper for running, verifying, and querying PostgreSQL migrations from the CLI. /// internal sealed class MigrationCommandService { private readonly IConfiguration _configuration; private readonly ILoggerFactory _loggerFactory; public MigrationCommandService(IConfiguration configuration, ILoggerFactory loggerFactory) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } public Task RunAsync( MigrationModuleInfo module, string? connectionOverride, MigrationCategory? category, bool dryRun, int? timeoutSeconds, CancellationToken cancellationToken) { var connectionString = ResolveConnectionString(module, connectionOverride); var runner = CreateRunner(module, connectionString); var options = new MigrationRunOptions { CategoryFilter = category, DryRun = dryRun, TimeoutSeconds = timeoutSeconds.GetValueOrDefault(300), ValidateChecksums = true, FailOnChecksumMismatch = true }; return runner.RunFromAssemblyAsync(module.MigrationsAssembly, module.ResourcePrefix, options, cancellationToken); } public async Task GetStatusAsync( MigrationModuleInfo module, string? connectionOverride, CancellationToken cancellationToken) { var connectionString = ResolveConnectionString(module, connectionOverride); var logger = _loggerFactory.CreateLogger($"migrationstatus.{module.Name}"); var statusService = new MigrationStatusService( connectionString, module.SchemaName, module.Name, module.MigrationsAssembly, logger); return await statusService.GetStatusAsync(cancellationToken).ConfigureAwait(false); } public Task> VerifyAsync( MigrationModuleInfo module, string? connectionOverride, CancellationToken cancellationToken) { var connectionString = ResolveConnectionString(module, connectionOverride); var runner = CreateRunner(module, connectionString); return runner.ValidateChecksumsAsync(module.MigrationsAssembly, module.ResourcePrefix, cancellationToken); } private MigrationRunner CreateRunner(MigrationModuleInfo module, string connectionString) => new(connectionString, module.SchemaName, module.Name, _loggerFactory.CreateLogger($"migration.{module.Name}")); private string ResolveConnectionString(MigrationModuleInfo module, string? connectionOverride) { if (!string.IsNullOrWhiteSpace(connectionOverride)) { return connectionOverride; } var envCandidates = new[] { $"STELLAOPS_POSTGRES_{module.Name.ToUpperInvariant()}_CONNECTION", $"STELLAOPS_POSTGRES_{module.SchemaName.ToUpperInvariant()}_CONNECTION", "STELLAOPS_POSTGRES_CONNECTION", "STELLAOPS_DB_CONNECTION" }; foreach (var key in envCandidates) { var value = Environment.GetEnvironmentVariable(key); if (!string.IsNullOrWhiteSpace(value)) { return value; } } var configCandidates = new[] { $"StellaOps:Database:{module.Name}:ConnectionString", $"Database:{module.Name}:ConnectionString", $"StellaOps:Postgres:ConnectionString", $"Postgres:ConnectionString", "Database:ConnectionString" }; foreach (var key in configCandidates) { var value = _configuration[key]; if (!string.IsNullOrWhiteSpace(value)) { return value; } } throw new InvalidOperationException( $"No PostgreSQL connection string found for module '{module.Name}'. " + "Provide --connection or set STELLAOPS_POSTGRES_CONNECTION."); } }