From d64f60aa77004b5d217ed3f52034a43621d316c8 Mon Sep 17 00:00:00 2001 From: Marcus Dammann Date: Mon, 13 Apr 2026 20:45:31 +0200 Subject: [PATCH] Introduce EF Core migrations with legacy database baseline detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, the schema was created via EnsureCreated(), which only works for brand new databases. Adding new columns (like the OCI registry fields added in v0.61) broke existing installations with "no such column" errors at runtime. Changes: - Add IDesignTimeDbContextFactory for dotnet-ef tooling - Generate InitialCreate migration representing the pre-v0.61 schema (without OCI columns) — this is the baseline for existing installs - Generate AddOciRegistrySource migration (ALTER TABLE ADD COLUMN) - Add explicit column configuration for OCI properties - Replace EnsureDatabaseCreated() with MigrateDatabase() that: 1. Detects legacy databases (tables exist but no __EFMigrationsHistory) 2. Inserts InitialCreate as baseline so existing schema is treated as already-migrated 3. Runs Migrate() to apply any subsequent migrations Test harness: - CustomWebApplicationFactory now uses Migrate() instead of EnsureCreated() so integration tests exercise the same pipeline as production - 4 new MigrationBaselineTests covering: fresh DB, legacy DB, data preservation, and idempotency (2 runs in sequence) All 2870 unit tests and 402 integration tests pass. --- src/ReadyStackGo.Api/Program.cs | 4 +- .../StackSourceConfiguration.cs | 16 + .../DependencyInjection.cs | 102 +- .../DesignTimeDbContextFactory.cs | 21 + .../20260413183518_InitialCreate.Designer.cs | 948 +++++++++++++++++ .../20260413183518_InitialCreate.cs | 564 ++++++++++ ...413183602_AddOciRegistrySource.Designer.cs | 968 ++++++++++++++++++ .../20260413183602_AddOciRegistrySource.cs | 73 ++ .../ReadyStackGoDbContextModelSnapshot.cs | 965 +++++++++++++++++ .../CustomWebApplicationFactory.cs | 6 +- .../DataAccess/MigrationBaselineTests.cs | 255 +++++ 11 files changed, 3915 insertions(+), 7 deletions(-) create mode 100644 src/ReadyStackGo.Infrastructure.DataAccess/DesignTimeDbContextFactory.cs create mode 100644 src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.Designer.cs create mode 100644 src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.cs create mode 100644 src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.Designer.cs create mode 100644 src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.cs create mode 100644 src/ReadyStackGo.Infrastructure.DataAccess/Migrations/ReadyStackGoDbContextModelSnapshot.cs create mode 100644 tests/ReadyStackGo.UnitTests/Infrastructure/DataAccess/MigrationBaselineTests.cs diff --git a/src/ReadyStackGo.Api/Program.cs b/src/ReadyStackGo.Api/Program.cs index d7811ecc..fb08df41 100644 --- a/src/ReadyStackGo.Api/Program.cs +++ b/src/ReadyStackGo.Api/Program.cs @@ -140,8 +140,8 @@ public static async Task Main(string[] args) var app = builder.Build(); - // Initialize SQLite database - app.Services.EnsureDatabaseCreated(); + // Apply EF Core migrations (handles fresh DBs, legacy DBs from EnsureCreated era, and upgrades) + app.Services.MigrateDatabase(); // Run distribution-specific bootstrap (idempotent, safe on every startup) await RunBootstrapperAsync(app); diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/Configurations/StackSourceConfiguration.cs b/src/ReadyStackGo.Infrastructure.DataAccess/Configurations/StackSourceConfiguration.cs index 36c0cab5..29d7b2a0 100644 --- a/src/ReadyStackGo.Infrastructure.DataAccess/Configurations/StackSourceConfiguration.cs +++ b/src/ReadyStackGo.Infrastructure.DataAccess/Configurations/StackSourceConfiguration.cs @@ -61,6 +61,22 @@ public void Configure(EntityTypeBuilder builder) builder.Property(s => s.GitSslVerify) .HasDefaultValue(true); + // OCI Registry fields (v0.61) + builder.Property(s => s.RegistryUrl) + .HasMaxLength(255); + + builder.Property(s => s.Repository) + .HasMaxLength(500); + + builder.Property(s => s.RegistryUsername) + .HasMaxLength(255); + + builder.Property(s => s.RegistryPassword) + .HasMaxLength(1000); // Encrypted value may be longer + + builder.Property(s => s.TagPattern) + .HasMaxLength(200); + // Indexes builder.HasIndex(s => s.Name).IsUnique(); builder.HasIndex(s => s.Type); diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/DependencyInjection.cs b/src/ReadyStackGo.Infrastructure.DataAccess/DependencyInjection.cs index 3cf91068..ad4cc4c4 100644 --- a/src/ReadyStackGo.Infrastructure.DataAccess/DependencyInjection.cs +++ b/src/ReadyStackGo.Infrastructure.DataAccess/DependencyInjection.cs @@ -48,12 +48,108 @@ public static IServiceCollection AddDataAccess(this IServiceCollection services, } /// - /// Ensures the database is created. + /// Applies pending EF Core migrations to the database. + /// Handles three cases: + /// 1. Fresh database → all migrations run, schema created from scratch. + /// 2. Legacy database from before migrations were introduced → InitialCreate + /// is retroactively marked as applied (baseline), subsequent migrations run. + /// 3. Existing migrated database → only new pending migrations run. /// - public static void EnsureDatabaseCreated(this IServiceProvider services) + public static void MigrateDatabase(this IServiceProvider services) { using var scope = services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - context.Database.EnsureCreated(); + + // Case 2: Detect legacy database (tables exist from EnsureCreated but no + // __EFMigrationsHistory). Insert the InitialCreate migration entry so that + // EF Core treats the existing schema as the baseline. + if (IsLegacyDatabase(context)) + { + BaselineLegacyDatabase(context); + } + + // Case 1 + 3: Apply all pending migrations (or create schema if fresh). + context.Database.Migrate(); + } + + /// + /// Checks if this is a legacy database: tables exist (e.g., StackSources) but + /// EF Core's __EFMigrationsHistory table does not. + /// + private static bool IsLegacyDatabase(ReadyStackGoDbContext context) + { + // Cannot connect at all → fresh database, not legacy + if (!context.Database.CanConnect()) + return false; + + var connection = context.Database.GetDbConnection(); + var wasClosed = connection.State != System.Data.ConnectionState.Open; + if (wasClosed) connection.Open(); + + try + { + using var cmd = connection.CreateCommand(); + // SQLite: check sqlite_master for table presence + cmd.CommandText = + "SELECT " + + " (SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory') AS HasHistory, " + + " (SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='StackSources') AS HasStackSources"; + + using var reader = cmd.ExecuteReader(); + if (!reader.Read()) return false; + + var hasHistory = reader.GetInt32(0) > 0; + var hasStackSources = reader.GetInt32(1) > 0; + + // Legacy: tables exist but no migration history + return !hasHistory && hasStackSources; + } + finally + { + if (wasClosed) connection.Close(); + } + } + + /// + /// Creates __EFMigrationsHistory and inserts InitialCreate as already applied, + /// so that Migrate() only runs subsequent migrations (AddOciRegistrySource, etc.) + /// against the existing legacy schema. + /// + private static void BaselineLegacyDatabase(ReadyStackGoDbContext context) + { + var connection = context.Database.GetDbConnection(); + var wasClosed = connection.State != System.Data.ConnectionState.Open; + if (wasClosed) connection.Open(); + + try + { + using var tx = connection.BeginTransaction(); + + using (var create = connection.CreateCommand()) + { + create.Transaction = tx; + create.CommandText = + "CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (" + + " \"MigrationId\" TEXT NOT NULL CONSTRAINT \"PK___EFMigrationsHistory\" PRIMARY KEY, " + + " \"ProductVersion\" TEXT NOT NULL" + + ");"; + create.ExecuteNonQuery(); + } + + using (var insert = connection.CreateCommand()) + { + insert.Transaction = tx; + insert.CommandText = + "INSERT INTO \"__EFMigrationsHistory\" (\"MigrationId\", \"ProductVersion\") " + + "VALUES ('20260413183518_InitialCreate', '9.0.0');"; + insert.ExecuteNonQuery(); + } + + tx.Commit(); + } + finally + { + if (wasClosed) connection.Close(); + } } } diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/DesignTimeDbContextFactory.cs b/src/ReadyStackGo.Infrastructure.DataAccess/DesignTimeDbContextFactory.cs new file mode 100644 index 00000000..bdd51984 --- /dev/null +++ b/src/ReadyStackGo.Infrastructure.DataAccess/DesignTimeDbContextFactory.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace ReadyStackGo.Infrastructure.DataAccess; + +/// +/// Design-time factory for EF Core tooling (dotnet-ef migrations). +/// Uses an in-memory SQLite connection so no real database file is required +/// when generating migrations. +/// +public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +{ + public ReadyStackGoDbContext CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=design-time.db") + .Options; + + return new ReadyStackGoDbContext(options); + } +} diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.Designer.cs b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.Designer.cs new file mode 100644 index 00000000..ef418883 --- /dev/null +++ b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.Designer.cs @@ -0,0 +1,948 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ReadyStackGo.Infrastructure.DataAccess; + +#nullable disable + +namespace ReadyStackGo.Infrastructure.DataAccess.Migrations +{ + [DbContext(typeof(ReadyStackGoDbContext))] + [Migration("20260413183518_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Deployments.Deployment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CancellationReason") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrentPhase") + .HasColumnType("INTEGER"); + + b.Property("DeployedBy") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HealthCheckConfigs") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("HealthCheckConfigsJson"); + + b.Property("InitContainerResults") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("InitContainerResultsJson"); + + b.Property("IsCancellationRequested") + .HasColumnType("INTEGER"); + + b.Property("LastUpgradedAt") + .HasColumnType("TEXT"); + + b.Property("MaintenanceObserverConfig") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceObserverConfigJson"); + + b.Property("MaintenanceTrigger") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceTriggerJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER"); + + b.Property("PreviousVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProgressMessage") + .HasColumnType("TEXT"); + + b.Property("ProgressPercentage") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StackId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("StackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StackVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpgradeCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Variables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("VariablesJson"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("Status"); + + b.ToTable("Deployments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Environments.Environment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConnectionConfig") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ConnectionConfigJson"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("SshCredential") + .HasColumnType("TEXT") + .HasColumnName("SshCredentialJson"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Environments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Environments.EnvironmentVariable", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("IsEncrypted") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("EnvironmentId", "Key") + .IsUnique(); + + b.ToTable("EnvironmentVariables", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Health.HealthSnapshot", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Bus") + .HasColumnType("TEXT") + .HasColumnName("BusHealthJson"); + + b.Property("CapturedAtUtc") + .HasColumnType("TEXT"); + + b.Property("CurrentVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeploymentId") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("Infra") + .HasColumnType("TEXT") + .HasColumnName("InfraHealthJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER") + .HasColumnName("OperationMode"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Overall") + .HasColumnType("INTEGER") + .HasColumnName("OverallStatus"); + + b.Property("Self") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("SelfHealthJson"); + + b.Property("StackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("TargetVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CapturedAtUtc"); + + b.HasIndex("DeploymentId"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("DeploymentId", "CapturedAtUtc"); + + b.ToTable("HealthSnapshots", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductDeployment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("ContinueOnError") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeployedBy") + .HasColumnType("TEXT"); + + b.Property("DeploymentName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("LastUpgradedAt") + .HasColumnType("TEXT"); + + b.Property("MaintenanceObserverConfig") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceObserverConfigJson"); + + b.Property("MaintenanceTrigger") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceTriggerJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER"); + + b.Property("PhaseHistory") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("PhaseHistoryJson"); + + b.Property("PreviousVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProductDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ProductGroupId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SharedVariables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("SharedVariablesJson"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpgradeCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Status"); + + b.ToTable("ProductDeployments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Registries.Registry", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.Property("_imagePatterns") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("ImagePatterns"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "IsDefault") + .HasFilter("[IsDefault] = 1"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Registries", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.ApiKeys.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("IsRevoked") + .HasColumnType("INTEGER"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("KeyPrefix") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.Property("_permissions") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("Permissions"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("ApiKeys", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Organizations.Organization", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("Id"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("OwnerId"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Organizations", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Users.User", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FailedLoginAttempts") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("LockReason") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("MustChangePassword") + .HasColumnType("INTEGER"); + + b.Property("PasswordChangedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.StackManagement.Sources.StackSource", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("FilePattern") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("GitBranch") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("GitPassword") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("GitSslVerify") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("GitUrl") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("GitUsername") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastSyncedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Path") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Enabled"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("StackSources", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Deployments.Deployment", b => + { + b.OwnsMany("ReadyStackGo.Domain.Deployment.Deployments.DeployedService", "Services", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("ContainerId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b1.Property("ContainerName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("Image") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("ServiceName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b1.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.ToTable("DeployedServices", (string)null); + + b1.WithOwner() + .HasForeignKey("DeploymentId"); + }); + + b.OwnsMany("ReadyStackGo.Domain.Deployment.Deployments.DeploymentPhaseRecord", "PhaseHistory", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("Phase") + .HasColumnType("INTEGER"); + + b1.Property("Timestamp") + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.ToTable("DeploymentPhaseHistory", (string)null); + + b1.WithOwner() + .HasForeignKey("DeploymentId"); + }); + + b.Navigation("PhaseHistory"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductDeployment", b => + { + b.OwnsMany("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductStackDeployment", "Stacks", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("CompletedAt") + .HasColumnType("TEXT"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("DeploymentStackName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b1.Property("IsNewInUpgrade") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b1.Property("Order") + .HasColumnType("INTEGER"); + + b1.Property("ProductDeploymentId") + .HasColumnType("TEXT"); + + b1.Property("ServiceCount") + .HasColumnType("INTEGER"); + + b1.Property("StackDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("StackId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("StackName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("StartedAt") + .HasColumnType("TEXT"); + + b1.Property("Status") + .HasColumnType("INTEGER"); + + b1.Property("Variables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("VariablesJson"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.HasIndex("ProductDeploymentId"); + + b1.ToTable("ProductStackDeployments", (string)null); + + b1.WithOwner() + .HasForeignKey("ProductDeploymentId"); + }); + + b.Navigation("Stacks"); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Users.User", b => + { + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.EmailAddress", "Email", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("TEXT") + .HasColumnName("Email"); + + b1.HasKey("UserId"); + + b1.HasIndex("Value") + .IsUnique(); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.Enablement", "Enablement", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("Enabled"); + + b1.Property("EndDate") + .HasColumnType("TEXT") + .HasColumnName("EnablementEndDate"); + + b1.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("EnablementStartDate"); + + b1.HasKey("UserId"); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.HashedPassword", "Password", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Hash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("PasswordHash"); + + b1.HasKey("UserId"); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsMany("ReadyStackGo.Domain.IdentityAccess.Users.RoleAssignment", "RoleAssignments", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b1.Property("AssignedAt") + .HasColumnType("TEXT") + .HasColumnName("AssignedAt"); + + b1.Property("RoleId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("RoleId"); + + b1.Property("ScopeId") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("ScopeId"); + + b1.Property("ScopeType") + .HasColumnType("INTEGER") + .HasColumnName("ScopeType"); + + b1.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("UserId", "RoleId", "ScopeType", "ScopeId") + .IsUnique(); + + b1.ToTable("UserRoles", (string)null); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.Navigation("Email") + .IsRequired(); + + b.Navigation("Enablement") + .IsRequired(); + + b.Navigation("Password") + .IsRequired(); + + b.Navigation("RoleAssignments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.cs b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.cs new file mode 100644 index 00000000..56558aa5 --- /dev/null +++ b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183518_InitialCreate.cs @@ -0,0 +1,564 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ReadyStackGo.Infrastructure.DataAccess.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiKeys", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + KeyHash = table.Column(type: "TEXT", maxLength: 64, nullable: false), + KeyPrefix = table.Column(type: "TEXT", maxLength: 12, nullable: false), + EnvironmentId = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + LastUsedAt = table.Column(type: "TEXT", nullable: true), + ExpiresAt = table.Column(type: "TEXT", nullable: true), + IsRevoked = table.Column(type: "INTEGER", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + RevokedReason = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Permissions = table.Column(type: "TEXT", maxLength: 4000, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Deployments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + EnvironmentId = table.Column(type: "TEXT", nullable: false), + StackId = table.Column(type: "TEXT", maxLength: 200, nullable: false), + StackName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + StackVersion = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ProjectName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + OperationMode = table.Column(type: "INTEGER", nullable: false), + ErrorMessage = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + CompletedAt = table.Column(type: "TEXT", nullable: true), + DeployedBy = table.Column(type: "TEXT", nullable: false), + CurrentPhase = table.Column(type: "INTEGER", nullable: false), + ProgressPercentage = table.Column(type: "INTEGER", nullable: false), + ProgressMessage = table.Column(type: "TEXT", nullable: true), + IsCancellationRequested = table.Column(type: "INTEGER", nullable: false), + CancellationReason = table.Column(type: "TEXT", nullable: true), + VariablesJson = table.Column(type: "TEXT", nullable: false), + MaintenanceObserverConfigJson = table.Column(type: "TEXT", nullable: true), + MaintenanceTriggerJson = table.Column(type: "TEXT", nullable: true), + HealthCheckConfigsJson = table.Column(type: "TEXT", nullable: false), + InitContainerResultsJson = table.Column(type: "TEXT", nullable: false), + LastUpgradedAt = table.Column(type: "TEXT", nullable: true), + PreviousVersion = table.Column(type: "TEXT", maxLength: 50, nullable: true), + UpgradeCount = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Deployments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Environments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + ConnectionConfigJson = table.Column(type: "TEXT", nullable: false), + SshCredentialJson = table.Column(type: "TEXT", nullable: true), + IsDefault = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Environments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "EnvironmentVariables", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + EnvironmentId = table.Column(type: "TEXT", nullable: false), + Key = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Value = table.Column(type: "TEXT", nullable: false), + IsEncrypted = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EnvironmentVariables", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "HealthSnapshots", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + EnvironmentId = table.Column(type: "TEXT", nullable: false), + DeploymentId = table.Column(type: "TEXT", nullable: false), + StackName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + CapturedAtUtc = table.Column(type: "TEXT", nullable: false), + OverallStatus = table.Column(type: "INTEGER", nullable: false), + OperationMode = table.Column(type: "INTEGER", nullable: false), + TargetVersion = table.Column(type: "TEXT", maxLength: 50, nullable: true), + CurrentVersion = table.Column(type: "TEXT", maxLength: 50, nullable: true), + BusHealthJson = table.Column(type: "TEXT", nullable: true), + InfraHealthJson = table.Column(type: "TEXT", nullable: true), + SelfHealthJson = table.Column(type: "TEXT", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HealthSnapshots", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Organizations", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Active = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + OwnerId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Organizations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ProductDeployments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + EnvironmentId = table.Column(type: "TEXT", nullable: false), + ProductGroupId = table.Column(type: "TEXT", maxLength: 500, nullable: false), + ProductId = table.Column(type: "TEXT", maxLength: 500, nullable: false), + ProductName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + ProductDisplayName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + ProductVersion = table.Column(type: "TEXT", maxLength: 50, nullable: false), + DeploymentName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + DeployedBy = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + CompletedAt = table.Column(type: "TEXT", nullable: true), + ErrorMessage = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + ContinueOnError = table.Column(type: "INTEGER", nullable: false), + SharedVariablesJson = table.Column(type: "TEXT", nullable: false), + PreviousVersion = table.Column(type: "TEXT", maxLength: 50, nullable: true), + LastUpgradedAt = table.Column(type: "TEXT", nullable: true), + UpgradeCount = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), + OperationMode = table.Column(type: "INTEGER", nullable: false), + MaintenanceTriggerJson = table.Column(type: "TEXT", nullable: true), + MaintenanceObserverConfigJson = table.Column(type: "TEXT", nullable: true), + PhaseHistoryJson = table.Column(type: "TEXT", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductDeployments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Registries", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Url = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Username = table.Column(type: "TEXT", maxLength: 255, nullable: true), + Password = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + IsDefault = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + ImagePatterns = table.Column(type: "TEXT", maxLength: 4000, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Registries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "StackSources", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Type = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + LastSyncedAt = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + Path = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + FilePattern = table.Column(type: "TEXT", maxLength: 200, nullable: true), + GitUrl = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + GitBranch = table.Column(type: "TEXT", maxLength: 255, nullable: true), + GitUsername = table.Column(type: "TEXT", maxLength: 255, nullable: true), + GitPassword = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + GitSslVerify = table.Column(type: "INTEGER", nullable: false, defaultValue: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StackSources", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Username = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Email = table.Column(type: "TEXT", maxLength: 254, nullable: false), + PasswordHash = table.Column(type: "TEXT", maxLength: 256, nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + EnablementStartDate = table.Column(type: "TEXT", nullable: true), + EnablementEndDate = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + IsLocked = table.Column(type: "INTEGER", nullable: false), + LockReason = table.Column(type: "TEXT", nullable: true), + LockedUntil = table.Column(type: "TEXT", nullable: true), + FailedLoginAttempts = table.Column(type: "INTEGER", nullable: false), + PasswordChangedAt = table.Column(type: "TEXT", nullable: true), + MustChangePassword = table.Column(type: "INTEGER", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "DeployedServices", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ServiceName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ContainerName = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Image = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + DeploymentId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeployedServices", x => x.Id); + table.ForeignKey( + name: "FK_DeployedServices_Deployments_DeploymentId", + column: x => x.DeploymentId, + principalTable: "Deployments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DeploymentPhaseHistory", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Phase = table.Column(type: "INTEGER", nullable: false), + Message = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false), + DeploymentId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeploymentPhaseHistory", x => x.Id); + table.ForeignKey( + name: "FK_DeploymentPhaseHistory_Deployments_DeploymentId", + column: x => x.DeploymentId, + principalTable: "Deployments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProductStackDeployments", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + StackName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + StackDisplayName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + StackId = table.Column(type: "TEXT", maxLength: 500, nullable: false), + DeploymentId = table.Column(type: "TEXT", nullable: true), + DeploymentStackName = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Status = table.Column(type: "INTEGER", nullable: false), + StartedAt = table.Column(type: "TEXT", nullable: true), + CompletedAt = table.Column(type: "TEXT", nullable: true), + ErrorMessage = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + Order = table.Column(type: "INTEGER", nullable: false), + ServiceCount = table.Column(type: "INTEGER", nullable: false), + IsNewInUpgrade = table.Column(type: "INTEGER", nullable: false, defaultValue: false), + VariablesJson = table.Column(type: "TEXT", nullable: false), + ProductDeploymentId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductStackDeployments", x => x.Id); + table.ForeignKey( + name: "FK_ProductStackDeployments_ProductDeployments_ProductDeploymentId", + column: x => x.ProductDeploymentId, + principalTable: "ProductDeployments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ScopeType = table.Column(type: "INTEGER", nullable: false), + ScopeId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + AssignedAt = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => x.Id); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_KeyHash", + table: "ApiKeys", + column: "KeyHash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_OrganizationId", + table: "ApiKeys", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_OrganizationId_Name", + table: "ApiKeys", + columns: new[] { "OrganizationId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeployedServices_DeploymentId", + table: "DeployedServices", + column: "DeploymentId"); + + migrationBuilder.CreateIndex( + name: "IX_DeploymentPhaseHistory_DeploymentId", + table: "DeploymentPhaseHistory", + column: "DeploymentId"); + + migrationBuilder.CreateIndex( + name: "IX_Deployments_EnvironmentId", + table: "Deployments", + column: "EnvironmentId"); + + migrationBuilder.CreateIndex( + name: "IX_Deployments_Status", + table: "Deployments", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_Environments_OrganizationId", + table: "Environments", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Environments_OrganizationId_Name", + table: "Environments", + columns: new[] { "OrganizationId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_EnvironmentVariables_EnvironmentId", + table: "EnvironmentVariables", + column: "EnvironmentId"); + + migrationBuilder.CreateIndex( + name: "IX_EnvironmentVariables_EnvironmentId_Key", + table: "EnvironmentVariables", + columns: new[] { "EnvironmentId", "Key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_HealthSnapshots_CapturedAtUtc", + table: "HealthSnapshots", + column: "CapturedAtUtc"); + + migrationBuilder.CreateIndex( + name: "IX_HealthSnapshots_DeploymentId", + table: "HealthSnapshots", + column: "DeploymentId"); + + migrationBuilder.CreateIndex( + name: "IX_HealthSnapshots_DeploymentId_CapturedAtUtc", + table: "HealthSnapshots", + columns: new[] { "DeploymentId", "CapturedAtUtc" }); + + migrationBuilder.CreateIndex( + name: "IX_HealthSnapshots_EnvironmentId", + table: "HealthSnapshots", + column: "EnvironmentId"); + + migrationBuilder.CreateIndex( + name: "IX_Organizations_Name", + table: "Organizations", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProductDeployments_EnvironmentId", + table: "ProductDeployments", + column: "EnvironmentId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductDeployments_ProductGroupId", + table: "ProductDeployments", + column: "ProductGroupId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductDeployments_Status", + table: "ProductDeployments", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_ProductStackDeployments_DeploymentId", + table: "ProductStackDeployments", + column: "DeploymentId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductStackDeployments_ProductDeploymentId", + table: "ProductStackDeployments", + column: "ProductDeploymentId"); + + migrationBuilder.CreateIndex( + name: "IX_Registries_OrganizationId", + table: "Registries", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Registries_OrganizationId_IsDefault", + table: "Registries", + columns: new[] { "OrganizationId", "IsDefault" }, + filter: "[IsDefault] = 1"); + + migrationBuilder.CreateIndex( + name: "IX_Registries_OrganizationId_Name", + table: "Registries", + columns: new[] { "OrganizationId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StackSources_Enabled", + table: "StackSources", + column: "Enabled"); + + migrationBuilder.CreateIndex( + name: "IX_StackSources_Name", + table: "StackSources", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StackSources_Type", + table: "StackSources", + column: "Type"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_UserId_RoleId_ScopeType_ScopeId", + table: "UserRoles", + columns: new[] { "UserId", "RoleId", "ScopeType", "ScopeId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys"); + + migrationBuilder.DropTable( + name: "DeployedServices"); + + migrationBuilder.DropTable( + name: "DeploymentPhaseHistory"); + + migrationBuilder.DropTable( + name: "Environments"); + + migrationBuilder.DropTable( + name: "EnvironmentVariables"); + + migrationBuilder.DropTable( + name: "HealthSnapshots"); + + migrationBuilder.DropTable( + name: "Organizations"); + + migrationBuilder.DropTable( + name: "ProductStackDeployments"); + + migrationBuilder.DropTable( + name: "Registries"); + + migrationBuilder.DropTable( + name: "StackSources"); + + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.DropTable( + name: "Deployments"); + + migrationBuilder.DropTable( + name: "ProductDeployments"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.Designer.cs b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.Designer.cs new file mode 100644 index 00000000..b2d3bdfc --- /dev/null +++ b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.Designer.cs @@ -0,0 +1,968 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ReadyStackGo.Infrastructure.DataAccess; + +#nullable disable + +namespace ReadyStackGo.Infrastructure.DataAccess.Migrations +{ + [DbContext(typeof(ReadyStackGoDbContext))] + [Migration("20260413183602_AddOciRegistrySource")] + partial class AddOciRegistrySource + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Deployments.Deployment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CancellationReason") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrentPhase") + .HasColumnType("INTEGER"); + + b.Property("DeployedBy") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HealthCheckConfigs") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("HealthCheckConfigsJson"); + + b.Property("InitContainerResults") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("InitContainerResultsJson"); + + b.Property("IsCancellationRequested") + .HasColumnType("INTEGER"); + + b.Property("LastUpgradedAt") + .HasColumnType("TEXT"); + + b.Property("MaintenanceObserverConfig") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceObserverConfigJson"); + + b.Property("MaintenanceTrigger") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceTriggerJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER"); + + b.Property("PreviousVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProgressMessage") + .HasColumnType("TEXT"); + + b.Property("ProgressPercentage") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StackId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("StackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StackVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpgradeCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Variables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("VariablesJson"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("Status"); + + b.ToTable("Deployments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Environments.Environment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConnectionConfig") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ConnectionConfigJson"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("SshCredential") + .HasColumnType("TEXT") + .HasColumnName("SshCredentialJson"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Environments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Environments.EnvironmentVariable", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("IsEncrypted") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("EnvironmentId", "Key") + .IsUnique(); + + b.ToTable("EnvironmentVariables", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Health.HealthSnapshot", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Bus") + .HasColumnType("TEXT") + .HasColumnName("BusHealthJson"); + + b.Property("CapturedAtUtc") + .HasColumnType("TEXT"); + + b.Property("CurrentVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeploymentId") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("Infra") + .HasColumnType("TEXT") + .HasColumnName("InfraHealthJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER") + .HasColumnName("OperationMode"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Overall") + .HasColumnType("INTEGER") + .HasColumnName("OverallStatus"); + + b.Property("Self") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("SelfHealthJson"); + + b.Property("StackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("TargetVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CapturedAtUtc"); + + b.HasIndex("DeploymentId"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("DeploymentId", "CapturedAtUtc"); + + b.ToTable("HealthSnapshots", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductDeployment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("ContinueOnError") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeployedBy") + .HasColumnType("TEXT"); + + b.Property("DeploymentName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("LastUpgradedAt") + .HasColumnType("TEXT"); + + b.Property("MaintenanceObserverConfig") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceObserverConfigJson"); + + b.Property("MaintenanceTrigger") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceTriggerJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER"); + + b.Property("PhaseHistory") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("PhaseHistoryJson"); + + b.Property("PreviousVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProductDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ProductGroupId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SharedVariables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("SharedVariablesJson"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpgradeCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Status"); + + b.ToTable("ProductDeployments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Registries.Registry", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.Property("_imagePatterns") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("ImagePatterns"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "IsDefault") + .HasFilter("[IsDefault] = 1"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Registries", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.ApiKeys.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("IsRevoked") + .HasColumnType("INTEGER"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("KeyPrefix") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.Property("_permissions") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("Permissions"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("ApiKeys", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Organizations.Organization", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("Id"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("OwnerId"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Organizations", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Users.User", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FailedLoginAttempts") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("LockReason") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("MustChangePassword") + .HasColumnType("INTEGER"); + + b.Property("PasswordChangedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.StackManagement.Sources.StackSource", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("FilePattern") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("GitBranch") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("GitPassword") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("GitSslVerify") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("GitUrl") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("GitUsername") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastSyncedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Path") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RegistryPassword") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RegistryUrl") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RegistryUsername") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Repository") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("TagPattern") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Enabled"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("StackSources", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Deployments.Deployment", b => + { + b.OwnsMany("ReadyStackGo.Domain.Deployment.Deployments.DeployedService", "Services", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("ContainerId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b1.Property("ContainerName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("Image") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("ServiceName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b1.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.ToTable("DeployedServices", (string)null); + + b1.WithOwner() + .HasForeignKey("DeploymentId"); + }); + + b.OwnsMany("ReadyStackGo.Domain.Deployment.Deployments.DeploymentPhaseRecord", "PhaseHistory", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("Phase") + .HasColumnType("INTEGER"); + + b1.Property("Timestamp") + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.ToTable("DeploymentPhaseHistory", (string)null); + + b1.WithOwner() + .HasForeignKey("DeploymentId"); + }); + + b.Navigation("PhaseHistory"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductDeployment", b => + { + b.OwnsMany("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductStackDeployment", "Stacks", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("CompletedAt") + .HasColumnType("TEXT"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("DeploymentStackName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b1.Property("IsNewInUpgrade") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b1.Property("Order") + .HasColumnType("INTEGER"); + + b1.Property("ProductDeploymentId") + .HasColumnType("TEXT"); + + b1.Property("ServiceCount") + .HasColumnType("INTEGER"); + + b1.Property("StackDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("StackId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("StackName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("StartedAt") + .HasColumnType("TEXT"); + + b1.Property("Status") + .HasColumnType("INTEGER"); + + b1.Property("Variables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("VariablesJson"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.HasIndex("ProductDeploymentId"); + + b1.ToTable("ProductStackDeployments", (string)null); + + b1.WithOwner() + .HasForeignKey("ProductDeploymentId"); + }); + + b.Navigation("Stacks"); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Users.User", b => + { + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.EmailAddress", "Email", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("TEXT") + .HasColumnName("Email"); + + b1.HasKey("UserId"); + + b1.HasIndex("Value") + .IsUnique(); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.Enablement", "Enablement", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("Enabled"); + + b1.Property("EndDate") + .HasColumnType("TEXT") + .HasColumnName("EnablementEndDate"); + + b1.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("EnablementStartDate"); + + b1.HasKey("UserId"); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.HashedPassword", "Password", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Hash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("PasswordHash"); + + b1.HasKey("UserId"); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsMany("ReadyStackGo.Domain.IdentityAccess.Users.RoleAssignment", "RoleAssignments", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b1.Property("AssignedAt") + .HasColumnType("TEXT") + .HasColumnName("AssignedAt"); + + b1.Property("RoleId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("RoleId"); + + b1.Property("ScopeId") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("ScopeId"); + + b1.Property("ScopeType") + .HasColumnType("INTEGER") + .HasColumnName("ScopeType"); + + b1.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("UserId", "RoleId", "ScopeType", "ScopeId") + .IsUnique(); + + b1.ToTable("UserRoles", (string)null); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.Navigation("Email") + .IsRequired(); + + b.Navigation("Enablement") + .IsRequired(); + + b.Navigation("Password") + .IsRequired(); + + b.Navigation("RoleAssignments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.cs b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.cs new file mode 100644 index 00000000..fbc4cdaa --- /dev/null +++ b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/20260413183602_AddOciRegistrySource.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ReadyStackGo.Infrastructure.DataAccess.Migrations +{ + /// + public partial class AddOciRegistrySource : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RegistryPassword", + table: "StackSources", + type: "TEXT", + maxLength: 1000, + nullable: true); + + migrationBuilder.AddColumn( + name: "RegistryUrl", + table: "StackSources", + type: "TEXT", + maxLength: 255, + nullable: true); + + migrationBuilder.AddColumn( + name: "RegistryUsername", + table: "StackSources", + type: "TEXT", + maxLength: 255, + nullable: true); + + migrationBuilder.AddColumn( + name: "Repository", + table: "StackSources", + type: "TEXT", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "TagPattern", + table: "StackSources", + type: "TEXT", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RegistryPassword", + table: "StackSources"); + + migrationBuilder.DropColumn( + name: "RegistryUrl", + table: "StackSources"); + + migrationBuilder.DropColumn( + name: "RegistryUsername", + table: "StackSources"); + + migrationBuilder.DropColumn( + name: "Repository", + table: "StackSources"); + + migrationBuilder.DropColumn( + name: "TagPattern", + table: "StackSources"); + } + } +} diff --git a/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/ReadyStackGoDbContextModelSnapshot.cs b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/ReadyStackGoDbContextModelSnapshot.cs new file mode 100644 index 00000000..487de398 --- /dev/null +++ b/src/ReadyStackGo.Infrastructure.DataAccess/Migrations/ReadyStackGoDbContextModelSnapshot.cs @@ -0,0 +1,965 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ReadyStackGo.Infrastructure.DataAccess; + +#nullable disable + +namespace ReadyStackGo.Infrastructure.DataAccess.Migrations +{ + [DbContext(typeof(ReadyStackGoDbContext))] + partial class ReadyStackGoDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Deployments.Deployment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CancellationReason") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CurrentPhase") + .HasColumnType("INTEGER"); + + b.Property("DeployedBy") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HealthCheckConfigs") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("HealthCheckConfigsJson"); + + b.Property("InitContainerResults") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("InitContainerResultsJson"); + + b.Property("IsCancellationRequested") + .HasColumnType("INTEGER"); + + b.Property("LastUpgradedAt") + .HasColumnType("TEXT"); + + b.Property("MaintenanceObserverConfig") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceObserverConfigJson"); + + b.Property("MaintenanceTrigger") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceTriggerJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER"); + + b.Property("PreviousVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProgressMessage") + .HasColumnType("TEXT"); + + b.Property("ProgressPercentage") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StackId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("StackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StackVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpgradeCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Variables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("VariablesJson"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("Status"); + + b.ToTable("Deployments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Environments.Environment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConnectionConfig") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ConnectionConfigJson"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("SshCredential") + .HasColumnType("TEXT") + .HasColumnName("SshCredentialJson"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Environments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Environments.EnvironmentVariable", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("IsEncrypted") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("EnvironmentId", "Key") + .IsUnique(); + + b.ToTable("EnvironmentVariables", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Health.HealthSnapshot", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Bus") + .HasColumnType("TEXT") + .HasColumnName("BusHealthJson"); + + b.Property("CapturedAtUtc") + .HasColumnType("TEXT"); + + b.Property("CurrentVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeploymentId") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("Infra") + .HasColumnType("TEXT") + .HasColumnName("InfraHealthJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER") + .HasColumnName("OperationMode"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Overall") + .HasColumnType("INTEGER") + .HasColumnName("OverallStatus"); + + b.Property("Self") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("SelfHealthJson"); + + b.Property("StackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("TargetVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CapturedAtUtc"); + + b.HasIndex("DeploymentId"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("DeploymentId", "CapturedAtUtc"); + + b.ToTable("HealthSnapshots", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductDeployment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("ContinueOnError") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeployedBy") + .HasColumnType("TEXT"); + + b.Property("DeploymentName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("LastUpgradedAt") + .HasColumnType("TEXT"); + + b.Property("MaintenanceObserverConfig") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceObserverConfigJson"); + + b.Property("MaintenanceTrigger") + .HasColumnType("TEXT") + .HasColumnName("MaintenanceTriggerJson"); + + b.Property("OperationMode") + .HasColumnType("INTEGER"); + + b.Property("PhaseHistory") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("PhaseHistoryJson"); + + b.Property("PreviousVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProductDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ProductGroupId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProductId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SharedVariables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("SharedVariablesJson"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("UpgradeCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EnvironmentId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("Status"); + + b.ToTable("ProductDeployments", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Registries.Registry", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.Property("_imagePatterns") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("ImagePatterns"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "IsDefault") + .HasFilter("[IsDefault] = 1"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Registries", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.ApiKeys.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EnvironmentId") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("IsRevoked") + .HasColumnType("INTEGER"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("KeyPrefix") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.Property("_permissions") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("Permissions"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("ApiKeys", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Organizations.Organization", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("Id"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("OwnerId"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Organizations", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Users.User", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("FailedLoginAttempts") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("LockReason") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("MustChangePassword") + .HasColumnType("INTEGER"); + + b.Property("PasswordChangedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.StackManagement.Sources.StackSource", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("FilePattern") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("GitBranch") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("GitPassword") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("GitSslVerify") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("GitUrl") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("GitUsername") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastSyncedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Path") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RegistryPassword") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RegistryUrl") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RegistryUsername") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Repository") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("TagPattern") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Enabled"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("StackSources", (string)null); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.Deployments.Deployment", b => + { + b.OwnsMany("ReadyStackGo.Domain.Deployment.Deployments.DeployedService", "Services", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("ContainerId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b1.Property("ContainerName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("Image") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("ServiceName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b1.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.ToTable("DeployedServices", (string)null); + + b1.WithOwner() + .HasForeignKey("DeploymentId"); + }); + + b.OwnsMany("ReadyStackGo.Domain.Deployment.Deployments.DeploymentPhaseRecord", "PhaseHistory", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("Phase") + .HasColumnType("INTEGER"); + + b1.Property("Timestamp") + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.ToTable("DeploymentPhaseHistory", (string)null); + + b1.WithOwner() + .HasForeignKey("DeploymentId"); + }); + + b.Navigation("PhaseHistory"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductDeployment", b => + { + b.OwnsMany("ReadyStackGo.Domain.Deployment.ProductDeployments.ProductStackDeployment", "Stacks", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("CompletedAt") + .HasColumnType("TEXT"); + + b1.Property("DeploymentId") + .HasColumnType("TEXT"); + + b1.Property("DeploymentStackName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b1.Property("IsNewInUpgrade") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b1.Property("Order") + .HasColumnType("INTEGER"); + + b1.Property("ProductDeploymentId") + .HasColumnType("TEXT"); + + b1.Property("ServiceCount") + .HasColumnType("INTEGER"); + + b1.Property("StackDisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("StackId") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b1.Property("StackName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b1.Property("StartedAt") + .HasColumnType("TEXT"); + + b1.Property("Status") + .HasColumnType("INTEGER"); + + b1.Property("Variables") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("VariablesJson"); + + b1.HasKey("Id"); + + b1.HasIndex("DeploymentId"); + + b1.HasIndex("ProductDeploymentId"); + + b1.ToTable("ProductStackDeployments", (string)null); + + b1.WithOwner() + .HasForeignKey("ProductDeploymentId"); + }); + + b.Navigation("Stacks"); + }); + + modelBuilder.Entity("ReadyStackGo.Domain.IdentityAccess.Users.User", b => + { + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.EmailAddress", "Email", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("TEXT") + .HasColumnName("Email"); + + b1.HasKey("UserId"); + + b1.HasIndex("Value") + .IsUnique(); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.Enablement", "Enablement", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("Enabled"); + + b1.Property("EndDate") + .HasColumnType("TEXT") + .HasColumnName("EnablementEndDate"); + + b1.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("EnablementStartDate"); + + b1.HasKey("UserId"); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsOne("ReadyStackGo.Domain.IdentityAccess.Users.HashedPassword", "Password", b1 => + { + b1.Property("UserId") + .HasColumnType("TEXT"); + + b1.Property("Hash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("PasswordHash"); + + b1.HasKey("UserId"); + + b1.ToTable("Users"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.OwnsMany("ReadyStackGo.Domain.IdentityAccess.Users.RoleAssignment", "RoleAssignments", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b1.Property("AssignedAt") + .HasColumnType("TEXT") + .HasColumnName("AssignedAt"); + + b1.Property("RoleId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("RoleId"); + + b1.Property("ScopeId") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("ScopeId"); + + b1.Property("ScopeType") + .HasColumnType("INTEGER") + .HasColumnName("ScopeType"); + + b1.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("Id"); + + b1.HasIndex("UserId", "RoleId", "ScopeType", "ScopeId") + .IsUnique(); + + b1.ToTable("UserRoles", (string)null); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.Navigation("Email") + .IsRequired(); + + b.Navigation("Enablement") + .IsRequired(); + + b.Navigation("Password") + .IsRequired(); + + b.Navigation("RoleAssignments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/tests/ReadyStackGo.IntegrationTests/Infrastructure/CustomWebApplicationFactory.cs b/tests/ReadyStackGo.IntegrationTests/Infrastructure/CustomWebApplicationFactory.cs index ce9b71fb..c293d96e 100644 --- a/tests/ReadyStackGo.IntegrationTests/Infrastructure/CustomWebApplicationFactory.cs +++ b/tests/ReadyStackGo.IntegrationTests/Infrastructure/CustomWebApplicationFactory.cs @@ -79,11 +79,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.UseSqlite(_connection); }); - // Build service provider and ensure database is created + // Build service provider and apply migrations (consistent with Program.cs startup). + // Using Migrate() instead of EnsureCreated() ensures the test DB goes through the + // same migration pipeline as production, so new migrations are exercised by tests. var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); + db.Database.Migrate(); }); builder.UseEnvironment("Testing"); diff --git a/tests/ReadyStackGo.UnitTests/Infrastructure/DataAccess/MigrationBaselineTests.cs b/tests/ReadyStackGo.UnitTests/Infrastructure/DataAccess/MigrationBaselineTests.cs new file mode 100644 index 00000000..c8377f4a --- /dev/null +++ b/tests/ReadyStackGo.UnitTests/Infrastructure/DataAccess/MigrationBaselineTests.cs @@ -0,0 +1,255 @@ +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using ReadyStackGo.Domain.StackManagement.Sources; +using ReadyStackGo.Infrastructure.DataAccess; + +namespace ReadyStackGo.UnitTests.Infrastructure.DataAccess; + +/// +/// Tests for the EF Core migration startup logic, especially the legacy-database +/// baseline detection that allows upgrading databases created via EnsureCreated() +/// before migrations were introduced. +/// +public class MigrationBaselineTests +{ + [Fact] + public void MigrateDatabase_FreshDatabase_CreatesSchemaAndAllMigrationsApplied() + { + // Fresh in-memory database, no tables exist. + var services = BuildServices(out var connection); + try + { + services.MigrateDatabase(); + + // Verify StackSources table exists with OCI columns (from AddOciRegistrySource) + using var scope = services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + ColumnExists(context, "StackSources", "RegistryUrl").Should().BeTrue(); + ColumnExists(context, "StackSources", "Repository").Should().BeTrue(); + MigrationHistoryCount(context).Should().BeGreaterThanOrEqualTo(2); + } + finally + { + connection.Dispose(); + } + } + + [Fact] + public void MigrateDatabase_LegacyDatabase_AppliesOciMigrationWithoutReCreatingBaselineTables() + { + // Simulate a legacy database: create schema manually matching the + // InitialCreate baseline (without OCI columns) — no __EFMigrationsHistory. + var services = BuildServices(out var connection); + try + { + CreateLegacyStackSourcesTable(connection); + + // Verify pre-conditions: table exists without OCI, no migration history + TableExists(connection, "StackSources").Should().BeTrue(); + TableExists(connection, "__EFMigrationsHistory").Should().BeFalse(); + ColumnExistsRaw(connection, "StackSources", "RegistryUrl").Should().BeFalse(); + + // Act: run migration startup logic + services.MigrateDatabase(); + + // Assert: OCI columns added, history table created with InitialCreate + AddOciRegistrySource + TableExists(connection, "__EFMigrationsHistory").Should().BeTrue(); + ColumnExistsRaw(connection, "StackSources", "RegistryUrl").Should().BeTrue(); + ColumnExistsRaw(connection, "StackSources", "Repository").Should().BeTrue(); + ColumnExistsRaw(connection, "StackSources", "RegistryUsername").Should().BeTrue(); + ColumnExistsRaw(connection, "StackSources", "RegistryPassword").Should().BeTrue(); + ColumnExistsRaw(connection, "StackSources", "TagPattern").Should().BeTrue(); + } + finally + { + connection.Dispose(); + } + } + + [Fact] + public void MigrateDatabase_LegacyDatabase_PreservesExistingData() + { + // Data in legacy DB must survive the baseline + migration run + var services = BuildServices(out var connection); + try + { + CreateLegacyStackSourcesTable(connection); + InsertLegacyStackSource(connection, id: "legacy-source", name: "Legacy Source"); + + services.MigrateDatabase(); + + // Verify the row is still there and readable via EF Core + using var scope = services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var source = context.StackSources.FirstOrDefault(); + + source.Should().NotBeNull(); + source!.Name.Should().Be("Legacy Source"); + source.RegistryUrl.Should().BeNull(); // New column, no value + } + finally + { + connection.Dispose(); + } + } + + [Fact] + public void MigrateDatabase_RunTwice_IsIdempotent() + { + // Running migrations twice must not fail or duplicate anything + var services = BuildServices(out var connection); + try + { + services.MigrateDatabase(); + services.MigrateDatabase(); // Second call should be a no-op + + using var scope = services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + MigrationHistoryCount(context).Should().BeGreaterThanOrEqualTo(2); + } + finally + { + connection.Dispose(); + } + } + + // ---------- helpers ---------- + + private static ServiceProvider BuildServices(out SqliteConnection connection) + { + // Use a shared, open in-memory SQLite connection so the schema persists + // across scopes (DbContext instances). + connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + var services = new ServiceCollection(); + var sharedConnection = connection; + services.AddDbContext(options => options.UseSqlite(sharedConnection)); + return services.BuildServiceProvider(); + } + + private static void CreateLegacyStackSourcesTable(SqliteConnection connection) + { + // Matches the InitialCreate baseline schema — includes the Version column + // inherited from AggregateRoot for optimistic concurrency, but no OCI columns. + WithOpenConnection(connection, () => + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = """ + CREATE TABLE "StackSources" ( + "Id" TEXT NOT NULL PRIMARY KEY, + "Name" TEXT NOT NULL, + "Type" TEXT NOT NULL, + "Enabled" INTEGER NOT NULL, + "LastSyncedAt" TEXT NULL, + "CreatedAt" TEXT NOT NULL, + "Path" TEXT NULL, + "FilePattern" TEXT NULL, + "GitUrl" TEXT NULL, + "GitBranch" TEXT NULL, + "GitUsername" TEXT NULL, + "GitPassword" TEXT NULL, + "GitSslVerify" INTEGER NOT NULL DEFAULT 1, + "Version" INTEGER NOT NULL DEFAULT 0 + ); + """; + cmd.ExecuteNonQuery(); + return 0; + }); + } + + private static void InsertLegacyStackSource(SqliteConnection connection, string id, string name) + { + WithOpenConnection(connection, () => + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO "StackSources" + ("Id", "Name", "Type", "Enabled", "CreatedAt", "Path", "FilePattern", "Version") + VALUES + (@id, @name, 'LocalDirectory', 1, @now, '/stacks', '*.yml;*.yaml', 0); + """; + cmd.Parameters.AddWithValue("@id", id); + cmd.Parameters.AddWithValue("@name", name); + cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("O")); + cmd.ExecuteNonQuery(); + return 0; + }); + } + + private static bool TableExists(SqliteConnection connection, string tableName) + { + return WithOpenConnection(connection, () => + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name"; + cmd.Parameters.AddWithValue("@name", tableName); + return Convert.ToInt32(cmd.ExecuteScalar()) > 0; + }); + } + + private static bool ColumnExistsRaw(SqliteConnection connection, string table, string column) + { + return WithOpenConnection(connection, () => + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = $"PRAGMA table_info(\"{table}\");"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + if (reader.GetString(1).Equals(column, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + }); + } + + private static T WithOpenConnection(SqliteConnection connection, Func action) + { + var wasClosed = connection.State != System.Data.ConnectionState.Open; + if (wasClosed) connection.Open(); + try + { + return action(); + } + finally + { + if (wasClosed) connection.Close(); + } + } + + private static bool ColumnExists(ReadyStackGoDbContext context, string table, string column) + { + var connection = (SqliteConnection)context.Database.GetDbConnection(); + var wasClosed = connection.State != System.Data.ConnectionState.Open; + if (wasClosed) connection.Open(); + try + { + return ColumnExistsRaw(connection, table, column); + } + finally + { + if (wasClosed) connection.Close(); + } + } + + private static int MigrationHistoryCount(ReadyStackGoDbContext context) + { + var connection = context.Database.GetDbConnection(); + var wasClosed = connection.State != System.Data.ConnectionState.Open; + if (wasClosed) connection.Open(); + try + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\""; + return Convert.ToInt32(cmd.ExecuteScalar()); + } + finally + { + if (wasClosed) connection.Close(); + } + } +}