Recover integrations startup migrations and enum persistence

This commit is contained in:
master
2026-03-07 02:45:40 +02:00
parent 803940bd36
commit b7cfdbd553
7 changed files with 176 additions and 28 deletions

View File

@@ -11,6 +11,8 @@ using StellaOps.Integrations.WebService.Infrastructure;
using StellaOps.Integrations.WebService.Security;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Infrastructure.Postgres.Migrations;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Localization;
using StellaOps.Router.AspNet;
var builder = WebApplication.CreateBuilder(args);
@@ -27,9 +29,20 @@ var connectionString = builder.Configuration.GetConnectionString("IntegrationsDb
?? builder.Configuration.GetConnectionString("Default")
?? "Host=localhost;Database=stellaops_integrations;Username=postgres;Password=postgres";
builder.Services.Configure<PostgresOptions>(options =>
{
options.ConnectionString = connectionString;
options.SchemaName = IntegrationDbContext.DefaultSchemaName;
});
builder.Services.AddDbContext<IntegrationDbContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddStartupMigrations(
IntegrationDbContext.DefaultSchemaName,
"Integrations.Persistence",
typeof(IntegrationDbContext).Assembly);
// Repository
builder.Services.AddScoped<IIntegrationRepository, PostgresIntegrationRepository>();
@@ -96,21 +109,6 @@ builder.TryAddStellaOpsLocalBinding("integrations");
var app = builder.Build();
app.LogStellaOpsLocalHostname("integrations");
// Auto-migrate: ensure integrations schema and tables exist on startup
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<IntegrationDbContext>();
try
{
await db.Database.EnsureCreatedAsync();
app.Logger.LogInformation("Integrations database schema ensured");
}
catch (Exception ex)
{
app.Logger.LogWarning(ex, "Integrations database EnsureCreated failed (may already exist)");
}
}
// Configure pipeline
if (app.Environment.IsDevelopment())
{

View File

@@ -18,6 +18,7 @@
<ProjectReference Include="..\__Plugins\StellaOps.Integrations.Plugin.Harbor\StellaOps.Integrations.Plugin.Harbor.csproj" />
<ProjectReference Include="..\__Plugins\StellaOps.Integrations.Plugin.InMemory\StellaOps.Integrations.Plugin.InMemory.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\..\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />

View File

@@ -12,7 +12,7 @@ namespace StellaOps.Integrations.Persistence.EfCore.CompiledModels
public partial class IntegrationDbContextModel
{
private IntegrationDbContextModel()
: base(skipDetectChanges: false, modelId: new Guid("a127631f-ea21-49e7-ba49-1169d76b0980"), entityTypeCount: 1)
: base(skipDetectChanges: false, modelId: new Guid("2bdd4ca7-26c5-4648-ad34-9d64426b7528"), entityTypeCount: 1)
{
}

View File

@@ -115,10 +115,12 @@ namespace StellaOps.Integrations.Persistence.EfCore.CompiledModels
"LastHealthStatus",
typeof(HealthStatus),
propertyInfo: typeof(IntegrationEntity).GetProperty("LastHealthStatus", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(IntegrationEntity).GetField("<LastHealthStatus>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
lastHealthStatus.SetSentinelFromProviderValue(0);
fieldInfo: typeof(IntegrationEntity).GetField("<LastHealthStatus>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
providerPropertyType: typeof(string));
lastHealthStatus.SetSentinelFromProviderValue("Unknown");
lastHealthStatus.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
lastHealthStatus.AddAnnotation("Relational:ColumnName", "last_health_status");
lastHealthStatus.AddAnnotation("Relational:ColumnType", "text");
var name = runtimeEntityType.AddProperty(
"Name",
@@ -143,19 +145,23 @@ namespace StellaOps.Integrations.Persistence.EfCore.CompiledModels
"Provider",
typeof(IntegrationProvider),
propertyInfo: typeof(IntegrationEntity).GetProperty("Provider", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(IntegrationEntity).GetField("<Provider>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
provider.SetSentinelFromProviderValue(0);
fieldInfo: typeof(IntegrationEntity).GetField("<Provider>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
providerPropertyType: typeof(string));
provider.SetSentinelFromProviderValue("0");
provider.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
provider.AddAnnotation("Relational:ColumnName", "provider");
provider.AddAnnotation("Relational:ColumnType", "text");
var status = runtimeEntityType.AddProperty(
"Status",
typeof(IntegrationStatus),
propertyInfo: typeof(IntegrationEntity).GetProperty("Status", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(IntegrationEntity).GetField("<Status>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
status.SetSentinelFromProviderValue(0);
fieldInfo: typeof(IntegrationEntity).GetField("<Status>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
providerPropertyType: typeof(string));
status.SetSentinelFromProviderValue("Pending");
status.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
status.AddAnnotation("Relational:ColumnName", "status");
status.AddAnnotation("Relational:ColumnType", "text");
var tagsJson = runtimeEntityType.AddProperty(
"TagsJson",
@@ -181,10 +187,12 @@ namespace StellaOps.Integrations.Persistence.EfCore.CompiledModels
"Type",
typeof(IntegrationType),
propertyInfo: typeof(IntegrationEntity).GetProperty("Type", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(IntegrationEntity).GetField("<Type>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
type.SetSentinelFromProviderValue(0);
fieldInfo: typeof(IntegrationEntity).GetField("<Type>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
providerPropertyType: typeof(string));
type.SetSentinelFromProviderValue("0");
type.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
type.AddAnnotation("Relational:ColumnName", "type");
type.AddAnnotation("Relational:ColumnType", "text");
var updatedAt = runtimeEntityType.AddProperty(
"UpdatedAt",

View File

@@ -37,16 +37,16 @@ public partial class IntegrationDbContext : DbContext
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(256).IsRequired();
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(1024);
entity.Property(e => e.Type).HasColumnName("type").IsRequired();
entity.Property(e => e.Provider).HasColumnName("provider").IsRequired();
entity.Property(e => e.Status).HasColumnName("status").IsRequired();
entity.Property(e => e.Type).HasConversion<string>().HasColumnType("text").HasColumnName("type").IsRequired();
entity.Property(e => e.Provider).HasConversion<string>().HasColumnType("text").HasColumnName("provider").IsRequired();
entity.Property(e => e.Status).HasConversion<string>().HasColumnType("text").HasColumnName("status").IsRequired();
entity.Property(e => e.Endpoint).HasColumnName("endpoint").HasMaxLength(2048).IsRequired();
entity.Property(e => e.AuthRefUri).HasColumnName("auth_ref_uri").HasMaxLength(1024);
entity.Property(e => e.OrganizationId).HasColumnName("organization_id").HasMaxLength(256);
entity.Property(e => e.ConfigJson).HasColumnName("config_json").HasColumnType("jsonb");
entity.Property(e => e.LastHealthStatus).HasColumnName("last_health_status");
entity.Property(e => e.LastHealthStatus).HasConversion<string>().HasColumnType("text").HasColumnName("last_health_status");
entity.Property(e => e.LastHealthCheckAt).HasColumnName("last_health_check_at");
entity.Property(e => e.CreatedAt).HasColumnName("created_at").IsRequired();

View File

@@ -0,0 +1,61 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using StellaOps.Integrations.Persistence;
using StellaOps.Integrations.Persistence.EfCore.CompiledModels;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Integrations.Tests;
public sealed class IntegrationPersistenceModelTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RuntimeModel_StoresEnumColumnsAsText()
{
var options = new DbContextOptionsBuilder<IntegrationDbContext>()
.UseNpgsql("Host=localhost;Database=stellaops_test;Username=postgres;Password=postgres")
.Options;
using var context = new IntegrationDbContext(options);
var entityType = context.Model.FindEntityType(typeof(IntegrationEntity));
entityType.Should().NotBeNull();
AssertTextBackedEnum(entityType!, nameof(IntegrationEntity.Type));
AssertTextBackedEnum(entityType!, nameof(IntegrationEntity.Provider));
AssertTextBackedEnum(entityType!, nameof(IntegrationEntity.Status));
AssertTextBackedEnum(entityType!, nameof(IntegrationEntity.LastHealthStatus));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_StoresEnumColumnsAsText()
{
var entityType = IntegrationDbContextModel.Instance.FindEntityType(typeof(IntegrationEntity));
entityType.Should().NotBeNull();
AssertTextColumn(entityType!, nameof(IntegrationEntity.Type));
AssertTextColumn(entityType!, nameof(IntegrationEntity.Provider));
AssertTextColumn(entityType!, nameof(IntegrationEntity.Status));
AssertTextColumn(entityType!, nameof(IntegrationEntity.LastHealthStatus));
}
private static void AssertTextBackedEnum(IReadOnlyEntityType entityType, string propertyName)
{
var property = entityType.FindProperty(propertyName);
property.Should().NotBeNull($"{propertyName} must be mapped in the integration persistence model");
property!.GetColumnType().Should().Be("text");
property.GetTypeMapping().Converter.Should().NotBeNull($"{propertyName} must serialize enum values as text");
property.GetTypeMapping().Converter!.ProviderClrType.Should().Be(typeof(string));
}
private static void AssertTextColumn(IReadOnlyEntityType entityType, string propertyName)
{
var property = entityType.FindProperty(propertyName);
property.Should().NotBeNull($"{propertyName} must be present in the compiled model");
property!.FindAnnotation("Relational:ColumnType")?.Value.Should().Be("text");
}
}