Search/AdvisoryAI and DAL conversion to EF finishes up. Preparation for microservices consolidation.
This commit is contained in:
@@ -120,13 +120,8 @@ app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Timestamp = Dat
|
||||
.WithDescription("Returns the liveness status and current UTC timestamp for the Integration Catalog service. Used by the Router gateway and container orchestrator for health polling.")
|
||||
.AllowAnonymous();
|
||||
|
||||
// Ensure database is created (dev only)
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<IntegrationDbContext>();
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
}
|
||||
// Database schema created by SQL migrations (001_initial_schema.sql)
|
||||
// Run via: stella-ops migration run Integrations --category startup
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerEnabled);
|
||||
await app.LoadTranslationsAsync();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using StellaOps.Integrations.Persistence;
|
||||
using StellaOps.Integrations.Persistence.EfCore.CompiledModels;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable disable
|
||||
|
||||
[assembly: DbContextModel(typeof(IntegrationDbContext), typeof(IntegrationDbContextModel))]
|
||||
@@ -0,0 +1,47 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable disable
|
||||
|
||||
namespace StellaOps.Integrations.Persistence.EfCore.CompiledModels
|
||||
{
|
||||
[DbContext(typeof(IntegrationDbContext))]
|
||||
public partial class IntegrationDbContextModel : RuntimeModel
|
||||
{
|
||||
private static readonly bool _useOldBehavior31751 =
|
||||
System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
|
||||
|
||||
static IntegrationDbContextModel()
|
||||
{
|
||||
var model = new IntegrationDbContextModel();
|
||||
|
||||
if (_useOldBehavior31751)
|
||||
{
|
||||
model.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
|
||||
void RunInitialization()
|
||||
{
|
||||
model.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
model.Customize();
|
||||
_instance = (IntegrationDbContextModel)model.FinalizeModel();
|
||||
}
|
||||
|
||||
private static IntegrationDbContextModel _instance;
|
||||
public static IModel Instance => _instance;
|
||||
|
||||
partial void Initialize();
|
||||
|
||||
partial void Customize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable disable
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
partial void Initialize()
|
||||
{
|
||||
var integrationEntity = IntegrationEntityEntityType.Create(this);
|
||||
|
||||
IntegrationEntityEntityType.CreateAnnotations(integrationEntity);
|
||||
|
||||
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
AddAnnotation("ProductVersion", "10.0.0");
|
||||
AddAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using StellaOps.Integrations.Core;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable disable
|
||||
|
||||
namespace StellaOps.Integrations.Persistence.EfCore.CompiledModels
|
||||
{
|
||||
[EntityFrameworkInternal]
|
||||
public partial class IntegrationEntityEntityType
|
||||
{
|
||||
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
|
||||
{
|
||||
var runtimeEntityType = model.AddEntityType(
|
||||
"StellaOps.Integrations.Persistence.IntegrationEntity",
|
||||
typeof(IntegrationEntity),
|
||||
baseEntityType,
|
||||
propertyCount: 19,
|
||||
unnamedIndexCount: 5,
|
||||
keyCount: 1);
|
||||
|
||||
var id = runtimeEntityType.AddProperty(
|
||||
"Id",
|
||||
typeof(Guid),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
valueGenerated: ValueGenerated.OnAdd,
|
||||
afterSaveBehavior: PropertySaveBehavior.Throw,
|
||||
sentinel: new Guid("00000000-0000-0000-0000-000000000000"));
|
||||
id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
id.AddAnnotation("Relational:ColumnName", "id");
|
||||
|
||||
var authRefUri = runtimeEntityType.AddProperty(
|
||||
"AuthRefUri",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("AuthRefUri", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<AuthRefUri>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true,
|
||||
maxLength: 1024);
|
||||
authRefUri.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
authRefUri.AddAnnotation("Relational:ColumnName", "auth_ref_uri");
|
||||
|
||||
var configJson = runtimeEntityType.AddProperty(
|
||||
"ConfigJson",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("ConfigJson", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<ConfigJson>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true);
|
||||
configJson.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
configJson.AddAnnotation("Relational:ColumnName", "config_json");
|
||||
configJson.AddAnnotation("Relational:ColumnType", "jsonb");
|
||||
|
||||
var createdAt = runtimeEntityType.AddProperty(
|
||||
"CreatedAt",
|
||||
typeof(DateTimeOffset),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
sentinel: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
createdAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
|
||||
|
||||
var createdBy = runtimeEntityType.AddProperty(
|
||||
"CreatedBy",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("CreatedBy", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<CreatedBy>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true,
|
||||
maxLength: 256);
|
||||
createdBy.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
createdBy.AddAnnotation("Relational:ColumnName", "created_by");
|
||||
|
||||
var description = runtimeEntityType.AddProperty(
|
||||
"Description",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("Description", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<Description>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true,
|
||||
maxLength: 1024);
|
||||
description.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
description.AddAnnotation("Relational:ColumnName", "description");
|
||||
|
||||
var endpoint = runtimeEntityType.AddProperty(
|
||||
"Endpoint",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("Endpoint", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<Endpoint>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
maxLength: 2048);
|
||||
endpoint.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
endpoint.AddAnnotation("Relational:ColumnName", "endpoint");
|
||||
|
||||
var isDeleted = runtimeEntityType.AddProperty(
|
||||
"IsDeleted",
|
||||
typeof(bool),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("IsDeleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<IsDeleted>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
sentinel: false);
|
||||
isDeleted.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
isDeleted.AddAnnotation("Relational:ColumnName", "is_deleted");
|
||||
|
||||
var lastHealthCheckAt = runtimeEntityType.AddProperty(
|
||||
"LastHealthCheckAt",
|
||||
typeof(DateTimeOffset?),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("LastHealthCheckAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<LastHealthCheckAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true);
|
||||
lastHealthCheckAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
lastHealthCheckAt.AddAnnotation("Relational:ColumnName", "last_health_check_at");
|
||||
|
||||
var lastHealthStatus = runtimeEntityType.AddProperty(
|
||||
"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);
|
||||
lastHealthStatus.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
lastHealthStatus.AddAnnotation("Relational:ColumnName", "last_health_status");
|
||||
|
||||
var name = runtimeEntityType.AddProperty(
|
||||
"Name",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("Name", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<Name>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
maxLength: 256);
|
||||
name.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
name.AddAnnotation("Relational:ColumnName", "name");
|
||||
|
||||
var organizationId = runtimeEntityType.AddProperty(
|
||||
"OrganizationId",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("OrganizationId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<OrganizationId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true,
|
||||
maxLength: 256);
|
||||
organizationId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
organizationId.AddAnnotation("Relational:ColumnName", "organization_id");
|
||||
|
||||
var provider = runtimeEntityType.AddProperty(
|
||||
"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);
|
||||
provider.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
provider.AddAnnotation("Relational:ColumnName", "provider");
|
||||
|
||||
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);
|
||||
status.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
status.AddAnnotation("Relational:ColumnName", "status");
|
||||
|
||||
var tagsJson = runtimeEntityType.AddProperty(
|
||||
"TagsJson",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("TagsJson", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<TagsJson>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true);
|
||||
tagsJson.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
tagsJson.AddAnnotation("Relational:ColumnName", "tags");
|
||||
tagsJson.AddAnnotation("Relational:ColumnType", "jsonb");
|
||||
|
||||
var tenantId = runtimeEntityType.AddProperty(
|
||||
"TenantId",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true,
|
||||
maxLength: 128);
|
||||
tenantId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
|
||||
|
||||
var type = runtimeEntityType.AddProperty(
|
||||
"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);
|
||||
type.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
type.AddAnnotation("Relational:ColumnName", "type");
|
||||
|
||||
var updatedAt = runtimeEntityType.AddProperty(
|
||||
"UpdatedAt",
|
||||
typeof(DateTimeOffset),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("UpdatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<UpdatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
sentinel: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
updatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
|
||||
|
||||
var updatedBy = runtimeEntityType.AddProperty(
|
||||
"UpdatedBy",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(IntegrationEntity).GetProperty("UpdatedBy", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(IntegrationEntity).GetField("<UpdatedBy>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true,
|
||||
maxLength: 256);
|
||||
updatedBy.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
|
||||
updatedBy.AddAnnotation("Relational:ColumnName", "updated_by");
|
||||
|
||||
var key = runtimeEntityType.AddKey(
|
||||
new[] { id });
|
||||
runtimeEntityType.SetPrimaryKey(key);
|
||||
|
||||
var index = runtimeEntityType.AddIndex(
|
||||
new[] { provider });
|
||||
|
||||
var index0 = runtimeEntityType.AddIndex(
|
||||
new[] { status });
|
||||
|
||||
var index1 = runtimeEntityType.AddIndex(
|
||||
new[] { tenantId });
|
||||
|
||||
var index2 = runtimeEntityType.AddIndex(
|
||||
new[] { type });
|
||||
|
||||
var index3 = runtimeEntityType.AddIndex(
|
||||
new[] { tenantId, name },
|
||||
unique: true);
|
||||
index3.AddAnnotation("Relational:Filter", "is_deleted = false");
|
||||
|
||||
return runtimeEntityType;
|
||||
}
|
||||
|
||||
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
|
||||
{
|
||||
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:Schema", "integrations");
|
||||
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:TableName", "integrations");
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
|
||||
|
||||
Customize(runtimeEntityType);
|
||||
}
|
||||
|
||||
static partial void Customize(RuntimeEntityType runtimeEntityType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace StellaOps.Integrations.Persistence.EfCore.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Design-time DbContext factory for dotnet ef CLI tooling.
|
||||
/// Used by scaffold and optimize commands.
|
||||
/// </summary>
|
||||
public sealed class IntegrationDesignTimeDbContextFactory : IDesignTimeDbContextFactory<IntegrationDbContext>
|
||||
{
|
||||
private const string DefaultConnectionString =
|
||||
"Host=localhost;Port=55433;Database=postgres;Username=postgres;Password=postgres;Search Path=integrations,public";
|
||||
|
||||
private const string ConnectionStringEnvironmentVariable = "STELLAOPS_INTEGRATIONS_EF_CONNECTION";
|
||||
|
||||
public IntegrationDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var connectionString = ResolveConnectionString();
|
||||
var options = new DbContextOptionsBuilder<IntegrationDbContext>()
|
||||
.UseNpgsql(connectionString)
|
||||
.Options;
|
||||
|
||||
return new IntegrationDbContext(options);
|
||||
}
|
||||
|
||||
private static string ResolveConnectionString()
|
||||
{
|
||||
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
|
||||
return string.IsNullOrWhiteSpace(fromEnvironment) ? DefaultConnectionString : fromEnvironment;
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,31 @@ namespace StellaOps.Integrations.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core DbContext for Integration persistence.
|
||||
/// SQL migrations remain authoritative; EF models are scaffolded FROM schema.
|
||||
/// </summary>
|
||||
public sealed class IntegrationDbContext : DbContext
|
||||
public partial class IntegrationDbContext : DbContext
|
||||
{
|
||||
public IntegrationDbContext(DbContextOptions<IntegrationDbContext> options)
|
||||
public const string DefaultSchemaName = "integrations";
|
||||
|
||||
private readonly string _schemaName;
|
||||
|
||||
public IntegrationDbContext(DbContextOptions<IntegrationDbContext> options, string? schemaName = null)
|
||||
: base(options)
|
||||
{
|
||||
_schemaName = string.IsNullOrWhiteSpace(schemaName)
|
||||
? DefaultSchemaName
|
||||
: schemaName.Trim();
|
||||
}
|
||||
|
||||
public DbSet<IntegrationEntity> Integrations => Set<IntegrationEntity>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var schemaName = _schemaName;
|
||||
|
||||
modelBuilder.Entity<IntegrationEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("integrations");
|
||||
entity.ToTable("integrations", schemaName);
|
||||
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Id).HasColumnName("id");
|
||||
@@ -53,7 +63,11 @@ public sealed class IntegrationDbContext : DbContext
|
||||
entity.HasIndex(e => e.TenantId);
|
||||
entity.HasIndex(e => new { e.TenantId, e.Name }).IsUnique().HasFilter("is_deleted = false");
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
-- ============================================================================
|
||||
-- Integrations — Initial Schema
|
||||
-- Extracted from IntegrationDbContext OnModelCreating (EF code-first → raw SQL)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS integrations;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integrations.integrations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(256) NOT NULL,
|
||||
description VARCHAR(1024),
|
||||
type TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
endpoint VARCHAR(2048) NOT NULL,
|
||||
auth_ref_uri VARCHAR(1024),
|
||||
organization_id VARCHAR(256),
|
||||
config_json JSONB,
|
||||
last_health_status TEXT,
|
||||
last_health_check_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
created_by VARCHAR(256),
|
||||
updated_by VARCHAR(256),
|
||||
tenant_id VARCHAR(128),
|
||||
tags JSONB,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_type
|
||||
ON integrations.integrations (type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_provider
|
||||
ON integrations.integrations (provider);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_status
|
||||
ON integrations.integrations (status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_tenant_id
|
||||
ON integrations.integrations (tenant_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_integrations_tenant_name
|
||||
ON integrations.integrations (tenant_id, name)
|
||||
WHERE is_deleted = false;
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StellaOps.Integrations.Persistence.EfCore.CompiledModels;
|
||||
|
||||
namespace StellaOps.Integrations.Persistence.Postgres;
|
||||
|
||||
internal static class IntegrationDbContextFactory
|
||||
{
|
||||
public static IntegrationDbContext Create(NpgsqlConnection connection, int commandTimeoutSeconds, string schemaName)
|
||||
{
|
||||
var normalizedSchema = string.IsNullOrWhiteSpace(schemaName)
|
||||
? IntegrationDbContext.DefaultSchemaName
|
||||
: schemaName.Trim();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<IntegrationDbContext>()
|
||||
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
|
||||
|
||||
if (string.Equals(normalizedSchema, IntegrationDbContext.DefaultSchemaName, StringComparison.Ordinal))
|
||||
{
|
||||
optionsBuilder.UseModel(IntegrationDbContextModel.Instance);
|
||||
}
|
||||
|
||||
return new IntegrationDbContext(optionsBuilder.Options, normalizedSchema);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,26 @@
|
||||
<RootNamespace>StellaOps.Integrations.Persistence</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="StellaOps.Integrations.Persistence.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Embed SQL migrations as resources -->
|
||||
<EmbeddedResource Include="Migrations\**\*.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Prevent automatic compiled-model binding so non-default schemas can build runtime models. -->
|
||||
<Compile Remove="EfCore\CompiledModels\IntegrationDbContextAssemblyAttributes.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Integrations.Core\StellaOps.Integrations.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StellaOps.Integrations.Persistence;
|
||||
using StellaOps.Integrations.Persistence.EfCore.CompiledModels;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Integrations.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Guard tests ensuring the EF Core compiled model is real (not a stub)
|
||||
/// and contains all expected entity type registrations.
|
||||
/// </summary>
|
||||
public sealed class CompiledModelGuardTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompiledModel_Instance_IsNotNull()
|
||||
{
|
||||
IntegrationDbContextModel.Instance.Should().NotBeNull(
|
||||
"compiled model must be generated via 'dotnet ef dbcontext optimize', not a stub");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompiledModel_HasExpectedEntityTypeCount()
|
||||
{
|
||||
var entityTypes = IntegrationDbContextModel.Instance.GetEntityTypes().ToList();
|
||||
entityTypes.Should().HaveCount(1,
|
||||
"integration compiled model must contain exactly 1 entity type (regenerate with 'dotnet ef dbcontext optimize' if count differs)");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(typeof(IntegrationEntity))]
|
||||
public void CompiledModel_ContainsEntityType(Type entityType)
|
||||
{
|
||||
var found = IntegrationDbContextModel.Instance.FindEntityType(entityType);
|
||||
found.Should().NotBeNull(
|
||||
$"compiled model must contain entity type '{entityType.Name}' — regenerate if missing");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompiledModel_EntityTypes_HaveTableNames()
|
||||
{
|
||||
var entityTypes = IntegrationDbContextModel.Instance.GetEntityTypes();
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var tableName = entityType.GetTableName();
|
||||
tableName.Should().NotBeNullOrWhiteSpace(
|
||||
$"entity type '{entityType.ClrType.Name}' must have a table name configured");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Integrations.Contracts;
|
||||
using StellaOps.Integrations.Core;
|
||||
using StellaOps.Integrations.Persistence;
|
||||
@@ -91,14 +99,63 @@ public sealed class IntegrationImpactWebApplicationFactory : WebApplicationFacto
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
builder.ConfigureAppConfiguration((_, configBuilder) =>
|
||||
{
|
||||
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Authority:ResourceServer:Authority"] = "https://integrations-test.local",
|
||||
["Authority:ResourceServer:RequireHttpsMetadata"] = "false",
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IIntegrationRepository>();
|
||||
services.AddSingleton<IIntegrationRepository, InMemoryIntegrationRepository>();
|
||||
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IConfigureOptions<AuthenticationOptions>>();
|
||||
services.RemoveAll<IPostConfigureOptions<AuthenticationOptions>>();
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = IntegrationTestAuthHandler.SchemeName;
|
||||
options.DefaultChallengeScheme = IntegrationTestAuthHandler.SchemeName;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, IntegrationTestAuthHandler>(
|
||||
IntegrationTestAuthHandler.SchemeName, _ => { })
|
||||
.AddScheme<AuthenticationSchemeOptions, IntegrationTestAuthHandler>(
|
||||
StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class IntegrationTestAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
internal const string SchemeName = "IntegrationTest";
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(StellaOpsClaimTypes.Subject, "test-user"),
|
||||
new Claim(StellaOpsClaimTypes.Scope, StellaOpsScopes.IntegrationRead),
|
||||
new Claim(StellaOpsClaimTypes.Scope, StellaOpsScopes.IntegrationWrite),
|
||||
new Claim(StellaOpsClaimTypes.Scope, StellaOpsScopes.IntegrationOperate),
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "test-tenant"),
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, StellaOpsAuthenticationDefaults.AuthenticationType);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, SchemeName);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InMemoryIntegrationRepository : IIntegrationRepository
|
||||
{
|
||||
private readonly Dictionary<Guid, Integration> _items = new();
|
||||
|
||||
Reference in New Issue
Block a user