Recover integrations startup migrations and enum persistence
This commit is contained in:
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user