wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10

This commit is contained in:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -0,0 +1,9 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using StellaOps.AirGap.Persistence.EfCore.CompiledModels;
using StellaOps.AirGap.Persistence.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
[assembly: DbContextModel(typeof(AirGapDbContext), typeof(AirGapDbContextModel))]

View File

@@ -0,0 +1,48 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using StellaOps.AirGap.Persistence.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.AirGap.Persistence.EfCore.CompiledModels
{
[DbContext(typeof(AirGapDbContext))]
public partial class AirGapDbContextModel : RuntimeModel
{
private static readonly bool _useOldBehavior31751 =
System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
static AirGapDbContextModel()
{
var model = new AirGapDbContextModel();
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 = (AirGapDbContextModel)model.FinalizeModel();
}
private static AirGapDbContextModel _instance;
public static IModel Instance => _instance;
partial void Initialize();
partial void Customize();
}
}

View File

@@ -0,0 +1,34 @@
// <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.AirGap.Persistence.EfCore.CompiledModels
{
public partial class AirGapDbContextModel
{
private AirGapDbContextModel()
: base(skipDetectChanges: false, modelId: new Guid("fd07ee8a-66dd-4965-a96c-9898cb1ec690"), entityTypeCount: 3)
{
}
partial void Initialize()
{
var bundleVersion = BundleVersionEntityType.Create(this);
var bundleVersionHistory = BundleVersionHistoryEntityType.Create(this);
var state = StateEntityType.Create(this);
BundleVersionEntityType.CreateAnnotations(bundleVersion);
BundleVersionHistoryEntityType.CreateAnnotations(bundleVersionHistory);
StateEntityType.CreateAnnotations(state);
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
AddAnnotation("ProductVersion", "10.0.0");
AddAnnotation("Relational:MaxIdentifierLength", 63);
}
}
}

View File

@@ -0,0 +1,181 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.AirGap.Persistence.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.AirGap.Persistence.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class BundleVersionEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.AirGap.Persistence.EfCore.Models.BundleVersion",
typeof(BundleVersion),
baseEntityType,
propertyCount: 14,
namedIndexCount: 1,
keyCount: 1);
var tenantId = runtimeEntityType.AddProperty(
"TenantId",
typeof(string),
propertyInfo: typeof(BundleVersion).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
tenantId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var bundleType = runtimeEntityType.AddProperty(
"BundleType",
typeof(string),
propertyInfo: typeof(BundleVersion).GetProperty("BundleType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<BundleType>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
bundleType.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleType.AddAnnotation("Relational:ColumnName", "bundle_type");
var activatedAt = runtimeEntityType.AddProperty(
"ActivatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersion).GetProperty("ActivatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<ActivatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
activatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
activatedAt.AddAnnotation("Relational:ColumnName", "activated_at");
var bundleCreatedAt = runtimeEntityType.AddProperty(
"BundleCreatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersion).GetProperty("BundleCreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<BundleCreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
bundleCreatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleCreatedAt.AddAnnotation("Relational:ColumnName", "bundle_created_at");
var bundleDigest = runtimeEntityType.AddProperty(
"BundleDigest",
typeof(string),
propertyInfo: typeof(BundleVersion).GetProperty("BundleDigest", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<BundleDigest>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
bundleDigest.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleDigest.AddAnnotation("Relational:ColumnName", "bundle_digest");
var createdAt = runtimeEntityType.AddProperty(
"CreatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersion).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var forceActivateReason = runtimeEntityType.AddProperty(
"ForceActivateReason",
typeof(string),
propertyInfo: typeof(BundleVersion).GetProperty("ForceActivateReason", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<ForceActivateReason>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
forceActivateReason.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
forceActivateReason.AddAnnotation("Relational:ColumnName", "force_activate_reason");
var major = runtimeEntityType.AddProperty(
"Major",
typeof(int),
propertyInfo: typeof(BundleVersion).GetProperty("Major", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<Major>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
major.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
major.AddAnnotation("Relational:ColumnName", "major");
var minor = runtimeEntityType.AddProperty(
"Minor",
typeof(int),
propertyInfo: typeof(BundleVersion).GetProperty("Minor", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<Minor>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
minor.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
minor.AddAnnotation("Relational:ColumnName", "minor");
var patch = runtimeEntityType.AddProperty(
"Patch",
typeof(int),
propertyInfo: typeof(BundleVersion).GetProperty("Patch", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<Patch>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
patch.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
patch.AddAnnotation("Relational:ColumnName", "patch");
var prerelease = runtimeEntityType.AddProperty(
"Prerelease",
typeof(string),
propertyInfo: typeof(BundleVersion).GetProperty("Prerelease", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<Prerelease>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
prerelease.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
prerelease.AddAnnotation("Relational:ColumnName", "prerelease");
var updatedAt = runtimeEntityType.AddProperty(
"UpdatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersion).GetProperty("UpdatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<UpdatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
updatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var versionString = runtimeEntityType.AddProperty(
"VersionString",
typeof(string),
propertyInfo: typeof(BundleVersion).GetProperty("VersionString", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<VersionString>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
versionString.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
versionString.AddAnnotation("Relational:ColumnName", "version_string");
var wasForceActivated = runtimeEntityType.AddProperty(
"WasForceActivated",
typeof(bool),
propertyInfo: typeof(BundleVersion).GetProperty("WasForceActivated", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersion).GetField("<WasForceActivated>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: false);
wasForceActivated.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
wasForceActivated.AddAnnotation("Relational:ColumnName", "was_force_activated");
var key = runtimeEntityType.AddKey(
new[] { tenantId, bundleType });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "bundle_versions_pkey");
var idx_airgap_bundle_versions_tenant = runtimeEntityType.AddIndex(
new[] { tenantId },
name: "idx_airgap_bundle_versions_tenant");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", "airgap");
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "bundle_versions");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,189 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.AirGap.Persistence.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.AirGap.Persistence.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class BundleVersionHistoryEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.AirGap.Persistence.EfCore.Models.BundleVersionHistory",
typeof(BundleVersionHistory),
baseEntityType,
propertyCount: 15,
namedIndexCount: 1,
keyCount: 1);
var id = runtimeEntityType.AddProperty(
"Id",
typeof(long),
propertyInfo: typeof(BundleVersionHistory).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
afterSaveBehavior: PropertySaveBehavior.Throw,
sentinel: 0L);
id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
id.AddAnnotation("Relational:ColumnName", "id");
id.AddAnnotation("Relational:DefaultValueSql", "nextval('bundle_version_history_id_seq'::regclass)");
var activatedAt = runtimeEntityType.AddProperty(
"ActivatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersionHistory).GetProperty("ActivatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<ActivatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
activatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
activatedAt.AddAnnotation("Relational:ColumnName", "activated_at");
var bundleCreatedAt = runtimeEntityType.AddProperty(
"BundleCreatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersionHistory).GetProperty("BundleCreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<BundleCreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
bundleCreatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleCreatedAt.AddAnnotation("Relational:ColumnName", "bundle_created_at");
var bundleDigest = runtimeEntityType.AddProperty(
"BundleDigest",
typeof(string),
propertyInfo: typeof(BundleVersionHistory).GetProperty("BundleDigest", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<BundleDigest>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
bundleDigest.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleDigest.AddAnnotation("Relational:ColumnName", "bundle_digest");
var bundleType = runtimeEntityType.AddProperty(
"BundleType",
typeof(string),
propertyInfo: typeof(BundleVersionHistory).GetProperty("BundleType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<BundleType>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
bundleType.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
bundleType.AddAnnotation("Relational:ColumnName", "bundle_type");
var createdAt = runtimeEntityType.AddProperty(
"CreatedAt",
typeof(DateTime),
propertyInfo: typeof(BundleVersionHistory).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var deactivatedAt = runtimeEntityType.AddProperty(
"DeactivatedAt",
typeof(DateTime?),
propertyInfo: typeof(BundleVersionHistory).GetProperty("DeactivatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<DeactivatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
deactivatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
deactivatedAt.AddAnnotation("Relational:ColumnName", "deactivated_at");
var forceActivateReason = runtimeEntityType.AddProperty(
"ForceActivateReason",
typeof(string),
propertyInfo: typeof(BundleVersionHistory).GetProperty("ForceActivateReason", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<ForceActivateReason>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
forceActivateReason.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
forceActivateReason.AddAnnotation("Relational:ColumnName", "force_activate_reason");
var major = runtimeEntityType.AddProperty(
"Major",
typeof(int),
propertyInfo: typeof(BundleVersionHistory).GetProperty("Major", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<Major>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
major.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
major.AddAnnotation("Relational:ColumnName", "major");
var minor = runtimeEntityType.AddProperty(
"Minor",
typeof(int),
propertyInfo: typeof(BundleVersionHistory).GetProperty("Minor", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<Minor>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
minor.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
minor.AddAnnotation("Relational:ColumnName", "minor");
var patch = runtimeEntityType.AddProperty(
"Patch",
typeof(int),
propertyInfo: typeof(BundleVersionHistory).GetProperty("Patch", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<Patch>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0);
patch.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
patch.AddAnnotation("Relational:ColumnName", "patch");
var prerelease = runtimeEntityType.AddProperty(
"Prerelease",
typeof(string),
propertyInfo: typeof(BundleVersionHistory).GetProperty("Prerelease", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<Prerelease>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
prerelease.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
prerelease.AddAnnotation("Relational:ColumnName", "prerelease");
var tenantId = runtimeEntityType.AddProperty(
"TenantId",
typeof(string),
propertyInfo: typeof(BundleVersionHistory).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
tenantId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var versionString = runtimeEntityType.AddProperty(
"VersionString",
typeof(string),
propertyInfo: typeof(BundleVersionHistory).GetProperty("VersionString", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<VersionString>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
versionString.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
versionString.AddAnnotation("Relational:ColumnName", "version_string");
var wasForceActivated = runtimeEntityType.AddProperty(
"WasForceActivated",
typeof(bool),
propertyInfo: typeof(BundleVersionHistory).GetProperty("WasForceActivated", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(BundleVersionHistory).GetField("<WasForceActivated>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: false);
wasForceActivated.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
wasForceActivated.AddAnnotation("Relational:ColumnName", "was_force_activated");
var key = runtimeEntityType.AddKey(
new[] { id });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "bundle_version_history_pkey");
var idx_airgap_bundle_version_history_tenant = runtimeEntityType.AddIndex(
new[] { tenantId, bundleType, activatedAt },
name: "idx_airgap_bundle_version_history_tenant");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", "airgap");
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "bundle_version_history");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -0,0 +1,169 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.AirGap.Persistence.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.AirGap.Persistence.EfCore.CompiledModels
{
[EntityFrameworkInternal]
public partial class StateEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.AirGap.Persistence.EfCore.Models.State",
typeof(State),
baseEntityType,
propertyCount: 11,
namedIndexCount: 2,
keyCount: 1);
var tenantId = runtimeEntityType.AddProperty(
"TenantId",
typeof(string),
propertyInfo: typeof(State).GetProperty("TenantId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<TenantId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
afterSaveBehavior: PropertySaveBehavior.Throw);
tenantId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var contentBudgets = runtimeEntityType.AddProperty(
"ContentBudgets",
typeof(string),
propertyInfo: typeof(State).GetProperty("ContentBudgets", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<ContentBudgets>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd);
contentBudgets.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
contentBudgets.AddAnnotation("Relational:ColumnName", "content_budgets");
contentBudgets.AddAnnotation("Relational:ColumnType", "jsonb");
contentBudgets.AddAnnotation("Relational:DefaultValueSql", "'{}'::jsonb");
var createdAt = runtimeEntityType.AddProperty(
"CreatedAt",
typeof(DateTime),
propertyInfo: typeof(State).GetProperty("CreatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<CreatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
createdAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
createdAt.AddAnnotation("Relational:ColumnName", "created_at");
createdAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var driftBaselineSeconds = runtimeEntityType.AddProperty(
"DriftBaselineSeconds",
typeof(long),
propertyInfo: typeof(State).GetProperty("DriftBaselineSeconds", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<DriftBaselineSeconds>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: 0L);
driftBaselineSeconds.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
driftBaselineSeconds.AddAnnotation("Relational:ColumnName", "drift_baseline_seconds");
var id = runtimeEntityType.AddProperty(
"Id",
typeof(string),
propertyInfo: typeof(State).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
id.AddAnnotation("Relational:ColumnName", "id");
var lastTransitionAt = runtimeEntityType.AddProperty(
"LastTransitionAt",
typeof(DateTime),
propertyInfo: typeof(State).GetProperty("LastTransitionAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<LastTransitionAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
lastTransitionAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
lastTransitionAt.AddAnnotation("Relational:ColumnName", "last_transition_at");
lastTransitionAt.AddAnnotation("Relational:DefaultValueSql", "'0001-01-01 00:00:00+00'::timestamp with time zone");
var policyHash = runtimeEntityType.AddProperty(
"PolicyHash",
typeof(string),
propertyInfo: typeof(State).GetProperty("PolicyHash", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<PolicyHash>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
nullable: true);
policyHash.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
policyHash.AddAnnotation("Relational:ColumnName", "policy_hash");
var @sealed = runtimeEntityType.AddProperty(
"Sealed",
typeof(bool),
propertyInfo: typeof(State).GetProperty("Sealed", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<Sealed>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
sentinel: false);
@sealed.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
@sealed.AddAnnotation("Relational:ColumnName", "sealed");
var stalenessBudget = runtimeEntityType.AddProperty(
"StalenessBudget",
typeof(string),
propertyInfo: typeof(State).GetProperty("StalenessBudget", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<StalenessBudget>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd);
stalenessBudget.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
stalenessBudget.AddAnnotation("Relational:ColumnName", "staleness_budget");
stalenessBudget.AddAnnotation("Relational:ColumnType", "jsonb");
stalenessBudget.AddAnnotation("Relational:DefaultValueSql", "'{\"breachSeconds\": 7200, \"warningSeconds\": 3600}'::jsonb");
var timeAnchor = runtimeEntityType.AddProperty(
"TimeAnchor",
typeof(string),
propertyInfo: typeof(State).GetProperty("TimeAnchor", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<TimeAnchor>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd);
timeAnchor.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
timeAnchor.AddAnnotation("Relational:ColumnName", "time_anchor");
timeAnchor.AddAnnotation("Relational:ColumnType", "jsonb");
timeAnchor.AddAnnotation("Relational:DefaultValueSql", "'{}'::jsonb");
var updatedAt = runtimeEntityType.AddProperty(
"UpdatedAt",
typeof(DateTime),
propertyInfo: typeof(State).GetProperty("UpdatedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: typeof(State).GetField("<UpdatedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
valueGenerated: ValueGenerated.OnAdd,
sentinel: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
updatedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None);
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var key = runtimeEntityType.AddKey(
new[] { tenantId });
runtimeEntityType.SetPrimaryKey(key);
key.AddAnnotation("Relational:Name", "state_pkey");
var idx_airgap_state_sealed = runtimeEntityType.AddIndex(
new[] { @sealed },
name: "idx_airgap_state_sealed");
idx_airgap_state_sealed.AddAnnotation("Relational:Filter", "(sealed = true)");
var idx_airgap_state_tenant = runtimeEntityType.AddIndex(
new[] { tenantId },
name: "idx_airgap_state_tenant");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
runtimeEntityType.AddAnnotation("Relational:Schema", "airgap");
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
runtimeEntityType.AddAnnotation("Relational:TableName", "state");
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
Customize(runtimeEntityType);
}
static partial void Customize(RuntimeEntityType runtimeEntityType);
}
}

View File

@@ -1,35 +1,129 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Persistence.Postgres;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.AirGap.Persistence.EfCore.Models;
namespace StellaOps.AirGap.Persistence.EfCore.Context;
/// <summary>
/// EF Core DbContext for AirGap module.
/// This is a stub that will be scaffolded from the PostgreSQL database.
/// </summary>
public class AirGapDbContext : DbContext
public partial class AirGapDbContext : DbContext
{
private readonly string _schemaName;
public AirGapDbContext(DbContextOptions<AirGapDbContext> options)
: this(options, null)
{
}
public AirGapDbContext(DbContextOptions<AirGapDbContext> options, IOptions<PostgresOptions>? postgresOptions)
public AirGapDbContext(DbContextOptions<AirGapDbContext> options, string? schemaName = null)
: base(options)
{
var schema = postgresOptions?.Value.SchemaName;
_schemaName = string.IsNullOrWhiteSpace(schema)
? AirGapDataSource.DefaultSchemaName
: schema;
_schemaName = string.IsNullOrWhiteSpace(schemaName)
? "airgap"
: schemaName.Trim();
}
public virtual DbSet<BundleVersion> BundleVersions { get; set; }
public virtual DbSet<BundleVersionHistory> BundleVersionHistories { get; set; }
public virtual DbSet<State> States { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(_schemaName);
base.OnModelCreating(modelBuilder);
var schemaName = _schemaName;
modelBuilder.Entity<BundleVersion>(entity =>
{
entity.HasKey(e => new { e.TenantId, e.BundleType }).HasName("bundle_versions_pkey");
entity.ToTable("bundle_versions", schemaName);
entity.HasIndex(e => e.TenantId, "idx_airgap_bundle_versions_tenant");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.BundleType).HasColumnName("bundle_type");
entity.Property(e => e.ActivatedAt).HasColumnName("activated_at");
entity.Property(e => e.BundleCreatedAt).HasColumnName("bundle_created_at");
entity.Property(e => e.BundleDigest).HasColumnName("bundle_digest");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("created_at");
entity.Property(e => e.ForceActivateReason).HasColumnName("force_activate_reason");
entity.Property(e => e.Major).HasColumnName("major");
entity.Property(e => e.Minor).HasColumnName("minor");
entity.Property(e => e.Patch).HasColumnName("patch");
entity.Property(e => e.Prerelease).HasColumnName("prerelease");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("updated_at");
entity.Property(e => e.VersionString).HasColumnName("version_string");
entity.Property(e => e.WasForceActivated).HasColumnName("was_force_activated");
});
modelBuilder.Entity<BundleVersionHistory>(entity =>
{
entity.HasKey(e => e.Id).HasName("bundle_version_history_pkey");
entity.ToTable("bundle_version_history", schemaName);
entity.HasIndex(e => new { e.TenantId, e.BundleType, e.ActivatedAt }, "idx_airgap_bundle_version_history_tenant").IsDescending(false, false, true);
entity.Property(e => e.Id)
.HasDefaultValueSql("nextval('bundle_version_history_id_seq'::regclass)")
.HasColumnName("id");
entity.Property(e => e.ActivatedAt).HasColumnName("activated_at");
entity.Property(e => e.BundleCreatedAt).HasColumnName("bundle_created_at");
entity.Property(e => e.BundleDigest).HasColumnName("bundle_digest");
entity.Property(e => e.BundleType).HasColumnName("bundle_type");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("created_at");
entity.Property(e => e.DeactivatedAt).HasColumnName("deactivated_at");
entity.Property(e => e.ForceActivateReason).HasColumnName("force_activate_reason");
entity.Property(e => e.Major).HasColumnName("major");
entity.Property(e => e.Minor).HasColumnName("minor");
entity.Property(e => e.Patch).HasColumnName("patch");
entity.Property(e => e.Prerelease).HasColumnName("prerelease");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.VersionString).HasColumnName("version_string");
entity.Property(e => e.WasForceActivated).HasColumnName("was_force_activated");
});
modelBuilder.Entity<State>(entity =>
{
entity.HasKey(e => e.TenantId).HasName("state_pkey");
entity.ToTable("state", schemaName);
entity.HasIndex(e => e.Sealed, "idx_airgap_state_sealed").HasFilter("(sealed = true)");
entity.HasIndex(e => e.TenantId, "idx_airgap_state_tenant");
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.ContentBudgets)
.HasDefaultValueSql("'{}'::jsonb")
.HasColumnType("jsonb")
.HasColumnName("content_budgets");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("created_at");
entity.Property(e => e.DriftBaselineSeconds).HasColumnName("drift_baseline_seconds");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.LastTransitionAt)
.HasDefaultValueSql("'0001-01-01 00:00:00+00'::timestamp with time zone")
.HasColumnName("last_transition_at");
entity.Property(e => e.PolicyHash).HasColumnName("policy_hash");
entity.Property(e => e.Sealed).HasColumnName("sealed");
entity.Property(e => e.StalenessBudget)
.HasDefaultValueSql("'{\"breachSeconds\": 7200, \"warningSeconds\": 3600}'::jsonb")
.HasColumnType("jsonb")
.HasColumnName("staleness_budget");
entity.Property(e => e.TimeAnchor)
.HasDefaultValueSql("'{}'::jsonb")
.HasColumnType("jsonb")
.HasColumnName("time_anchor");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("updated_at");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace StellaOps.AirGap.Persistence.EfCore.Context;
public sealed class AirGapDesignTimeDbContextFactory : IDesignTimeDbContextFactory<AirGapDbContext>
{
private const string DefaultConnectionString = "Host=localhost;Port=55434;Database=postgres;Username=postgres;Password=postgres;Search Path=airgap,public";
private const string ConnectionStringEnvironmentVariable = "STELLAOPS_AIRGAP_EF_CONNECTION";
public AirGapDbContext CreateDbContext(string[] args)
{
var connectionString = ResolveConnectionString();
var options = new DbContextOptionsBuilder<AirGapDbContext>()
.UseNpgsql(connectionString)
.Options;
return new AirGapDbContext(options);
}
private static string ResolveConnectionString()
{
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
return string.IsNullOrWhiteSpace(fromEnvironment) ? DefaultConnectionString : fromEnvironment;
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
namespace StellaOps.AirGap.Persistence.EfCore.Models;
public partial class BundleVersion
{
public string TenantId { get; set; } = null!;
public string BundleType { get; set; } = null!;
public string VersionString { get; set; } = null!;
public int Major { get; set; }
public int Minor { get; set; }
public int Patch { get; set; }
public string? Prerelease { get; set; }
public DateTime BundleCreatedAt { get; set; }
public string BundleDigest { get; set; } = null!;
public DateTime ActivatedAt { get; set; }
public bool WasForceActivated { get; set; }
public string? ForceActivateReason { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace StellaOps.AirGap.Persistence.EfCore.Models;
public partial class BundleVersionHistory
{
public long Id { get; set; }
public string TenantId { get; set; } = null!;
public string BundleType { get; set; } = null!;
public string VersionString { get; set; } = null!;
public int Major { get; set; }
public int Minor { get; set; }
public int Patch { get; set; }
public string? Prerelease { get; set; }
public DateTime BundleCreatedAt { get; set; }
public string BundleDigest { get; set; } = null!;
public DateTime ActivatedAt { get; set; }
public DateTime? DeactivatedAt { get; set; }
public bool WasForceActivated { get; set; }
public string? ForceActivateReason { get; set; }
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace StellaOps.AirGap.Persistence.EfCore.Models;
public partial class State
{
public string Id { get; set; } = null!;
public string TenantId { get; set; } = null!;
public bool Sealed { get; set; }
public string? PolicyHash { get; set; }
public string TimeAnchor { get; set; } = null!;
public DateTime LastTransitionAt { get; set; }
public string StalenessBudget { get; set; } = null!;
public long DriftBaselineSeconds { get; set; }
public string ContentBudgets { get; set; } = null!;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,28 @@
using System;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using StellaOps.AirGap.Persistence.EfCore.CompiledModels;
using StellaOps.AirGap.Persistence.EfCore.Context;
namespace StellaOps.AirGap.Persistence.Postgres;
internal static class AirGapDbContextFactory
{
public static AirGapDbContext Create(NpgsqlConnection connection, int commandTimeoutSeconds, string schemaName)
{
var normalizedSchema = string.IsNullOrWhiteSpace(schemaName)
? AirGapDataSource.DefaultSchemaName
: schemaName.Trim();
var optionsBuilder = new DbContextOptionsBuilder<AirGapDbContext>()
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
if (string.Equals(normalizedSchema, AirGapDataSource.DefaultSchemaName, StringComparison.Ordinal))
{
// Use the static compiled model module when schema mapping matches the default model.
optionsBuilder.UseModel(AirGapDbContextModel.Instance);
}
return new AirGapDbContext(optionsBuilder.Options, normalizedSchema);
}
}

View File

@@ -1,37 +1,42 @@
using Npgsql;
using StellaOps.AirGap.Controller.Domain;
using StellaOps.AirGap.Persistence.EfCore.Models;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
public sealed partial class PostgresAirGapStateStore
{
private AirGapState Map(NpgsqlDataReader reader)
private AirGapState Map(State row)
{
var id = reader.GetString(0);
var tenantId = reader.GetString(1);
var sealed_ = reader.GetBoolean(2);
var policyHash = reader.IsDBNull(3) ? null : reader.GetString(3);
var timeAnchorJson = reader.GetFieldValue<string>(4);
var lastTransitionAt = reader.GetFieldValue<DateTimeOffset>(5);
var stalenessBudgetJson = reader.GetFieldValue<string>(6);
var driftBaselineSeconds = reader.GetInt64(7);
var contentBudgetsJson = reader.IsDBNull(8) ? null : reader.GetFieldValue<string>(8);
var timeAnchor = DeserializeTimeAnchor(timeAnchorJson);
var stalenessBudget = DeserializeStalenessBudget(stalenessBudgetJson);
var contentBudgets = DeserializeContentBudgets(contentBudgetsJson);
var timeAnchor = DeserializeTimeAnchor(row.TimeAnchor);
var stalenessBudget = DeserializeStalenessBudget(row.StalenessBudget);
var contentBudgets = DeserializeContentBudgets(row.ContentBudgets);
return new AirGapState
{
Id = id,
TenantId = tenantId,
Sealed = sealed_,
PolicyHash = policyHash,
Id = row.Id,
TenantId = row.TenantId,
Sealed = row.Sealed,
PolicyHash = row.PolicyHash,
TimeAnchor = timeAnchor,
LastTransitionAt = lastTransitionAt,
LastTransitionAt = ToUtcOffset(row.LastTransitionAt),
StalenessBudget = stalenessBudget,
DriftBaselineSeconds = driftBaselineSeconds,
DriftBaselineSeconds = row.DriftBaselineSeconds,
ContentBudgets = contentBudgets
};
}
private static DateTimeOffset ToUtcOffset(DateTime value)
{
if (value.Kind == DateTimeKind.Utc)
{
return new DateTimeOffset(value, TimeSpan.Zero);
}
if (value.Kind == DateTimeKind.Local)
{
return new DateTimeOffset(value.ToUniversalTime(), TimeSpan.Zero);
}
return new DateTimeOffset(DateTime.SpecifyKind(value, DateTimeKind.Utc), TimeSpan.Zero);
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Threading;
using Npgsql;
using Microsoft.EntityFrameworkCore;
using StellaOps.AirGap.Controller.Domain;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
@@ -13,44 +11,33 @@ public sealed partial class PostgresAirGapStateStore
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var tenantKey = NormalizeTenantId(tenantId);
var stateTable = GetQualifiedTableName("state");
await using var connection = await DataSource.OpenConnectionAsync(tenantKey, "reader", cancellationToken)
.ConfigureAwait(false);
var sql = $$"""
SELECT id, tenant_id, sealed, policy_hash, time_anchor, last_transition_at,
staleness_budget, drift_baseline_seconds, content_budgets
FROM {{stateTable}}
WHERE tenant_id = @tenant_id;
""";
await using var dbContext = AirGapDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
await using var command = CreateCommand(sql, connection);
AddParameter(command, "tenant_id", tenantKey);
var current = await dbContext.States
.AsNoTracking()
.FirstOrDefaultAsync(s => s.TenantId == tenantKey, cancellationToken)
.ConfigureAwait(false);
await using (var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false))
if (current is not null)
{
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return Map(reader);
}
return Map(current);
}
// Fallback for legacy rows stored without normalization.
await using var fallbackCommand = CreateCommand($$"""
SELECT id, tenant_id, sealed, policy_hash, time_anchor, last_transition_at,
staleness_budget, drift_baseline_seconds, content_budgets
FROM {{stateTable}}
WHERE LOWER(tenant_id) = LOWER(@tenant_id)
ORDER BY updated_at DESC, id DESC
LIMIT 1;
""", connection);
AddParameter(fallbackCommand, "tenant_id", tenantId);
await using var fallbackReader = await fallbackCommand.ExecuteReaderAsync(cancellationToken)
var lowerTenant = tenantId.Trim().ToLowerInvariant();
var fallback = await dbContext.States
.AsNoTracking()
.Where(s => s.TenantId.ToLower() == lowerTenant)
.OrderByDescending(s => s.UpdatedAt)
.ThenByDescending(s => s.Id)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (await fallbackReader.ReadAsync(cancellationToken).ConfigureAwait(false))
if (fallback is not null)
{
return Map(fallbackReader);
return Map(fallback);
}
return new AirGapState { TenantId = tenantId };

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using StellaOps.AirGap.Controller.Domain;
using StellaOps.AirGap.Persistence.EfCore.Models;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
@@ -12,42 +13,89 @@ public sealed partial class PostgresAirGapStateStore
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var tenantKey = NormalizeTenantId(state.TenantId);
var stateTable = GetQualifiedTableName("state");
await using var connection = await DataSource.OpenConnectionAsync(tenantKey, "writer", cancellationToken)
.ConfigureAwait(false);
var sql = $$"""
INSERT INTO {{stateTable}} (
id, tenant_id, sealed, policy_hash, time_anchor, last_transition_at,
staleness_budget, drift_baseline_seconds, content_budgets
)
VALUES (
@id, @tenant_id, @sealed, @policy_hash, @time_anchor, @last_transition_at,
@staleness_budget, @drift_baseline_seconds, @content_budgets
)
ON CONFLICT (tenant_id) DO UPDATE SET
id = EXCLUDED.id,
sealed = EXCLUDED.sealed,
policy_hash = EXCLUDED.policy_hash,
time_anchor = EXCLUDED.time_anchor,
last_transition_at = EXCLUDED.last_transition_at,
staleness_budget = EXCLUDED.staleness_budget,
drift_baseline_seconds = EXCLUDED.drift_baseline_seconds,
content_budgets = EXCLUDED.content_budgets,
updated_at = NOW();
""";
await using var dbContext = AirGapDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
await using var command = CreateCommand(sql, connection);
AddParameter(command, "id", state.Id);
AddParameter(command, "tenant_id", tenantKey);
AddParameter(command, "sealed", state.Sealed);
AddParameter(command, "policy_hash", (object?)state.PolicyHash ?? DBNull.Value);
AddJsonbParameter(command, "time_anchor", SerializeTimeAnchor(state.TimeAnchor));
AddParameter(command, "last_transition_at", state.LastTransitionAt);
AddJsonbParameter(command, "staleness_budget", SerializeStalenessBudget(state.StalenessBudget));
AddParameter(command, "drift_baseline_seconds", state.DriftBaselineSeconds);
AddJsonbParameter(command, "content_budgets", SerializeContentBudgets(state.ContentBudgets));
var existing = await dbContext.States
.FirstOrDefaultAsync(s => s.TenantId == tenantKey, cancellationToken)
.ConfigureAwait(false);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
if (existing is null)
{
dbContext.States.Add(ToEntity(state, tenantKey));
}
else
{
Apply(existing, state, tenantKey);
existing.UpdatedAt = DateTime.UtcNow;
}
try
{
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
dbContext.ChangeTracker.Clear();
var conflict = await dbContext.States
.FirstOrDefaultAsync(s => s.TenantId == tenantKey, cancellationToken)
.ConfigureAwait(false);
if (conflict is null)
{
throw;
}
Apply(conflict, state, tenantKey);
conflict.UpdatedAt = DateTime.UtcNow;
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
}
private State ToEntity(AirGapState state, string tenantKey)
{
return new State
{
Id = state.Id,
TenantId = tenantKey,
Sealed = state.Sealed,
PolicyHash = state.PolicyHash,
TimeAnchor = SerializeTimeAnchor(state.TimeAnchor),
LastTransitionAt = state.LastTransitionAt.UtcDateTime,
StalenessBudget = SerializeStalenessBudget(state.StalenessBudget),
DriftBaselineSeconds = state.DriftBaselineSeconds,
ContentBudgets = SerializeContentBudgets(state.ContentBudgets),
UpdatedAt = DateTime.UtcNow
};
}
private void Apply(State entity, AirGapState state, string tenantKey)
{
entity.Id = state.Id;
entity.TenantId = tenantKey;
entity.Sealed = state.Sealed;
entity.PolicyHash = state.PolicyHash;
entity.TimeAnchor = SerializeTimeAnchor(state.TimeAnchor);
entity.LastTransitionAt = state.LastTransitionAt.UtcDateTime;
entity.StalenessBudget = SerializeStalenessBudget(state.StalenessBudget);
entity.DriftBaselineSeconds = state.DriftBaselineSeconds;
entity.ContentBudgets = SerializeContentBudgets(state.ContentBudgets);
}
private static bool IsUniqueViolation(DbUpdateException exception)
{
Exception? current = exception;
while (current is not null)
{
if (current is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation })
{
return true;
}
current = current.InnerException;
}
return false;
}
}

View File

@@ -1,63 +1,60 @@
using System.Threading;
using Npgsql;
using StellaOps.AirGap.Importer.Versioning;
using StellaOps.AirGap.Persistence.EfCore.Models;
using BundleVersionEntity = StellaOps.AirGap.Persistence.EfCore.Models.BundleVersion;
using BundleVersionHistoryEntity = StellaOps.AirGap.Persistence.EfCore.Models.BundleVersionHistory;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
public sealed partial class PostgresBundleVersionStore
{
private static BundleVersionRecord Map(NpgsqlDataReader reader)
private static BundleVersionRecord Map(BundleVersionEntity row)
{
var tenantId = reader.GetString(0);
var bundleType = reader.GetString(1);
var versionString = reader.GetString(2);
var major = reader.GetInt32(3);
var minor = reader.GetInt32(4);
var patch = reader.GetInt32(5);
var prerelease = reader.IsDBNull(6) ? null : reader.GetString(6);
var bundleCreatedAt = reader.GetFieldValue<DateTimeOffset>(7);
var bundleDigest = reader.GetString(8);
var activatedAt = reader.GetFieldValue<DateTimeOffset>(9);
var wasForceActivated = reader.GetBoolean(10);
var forceActivateReason = reader.IsDBNull(11) ? null : reader.GetString(11);
return new BundleVersionRecord(
TenantId: tenantId,
BundleType: bundleType,
VersionString: versionString,
Major: major,
Minor: minor,
Patch: patch,
Prerelease: prerelease,
BundleCreatedAt: bundleCreatedAt,
BundleDigest: bundleDigest,
ActivatedAt: activatedAt,
WasForceActivated: wasForceActivated,
ForceActivateReason: forceActivateReason);
TenantId: row.TenantId,
BundleType: row.BundleType,
VersionString: row.VersionString,
Major: row.Major,
Minor: row.Minor,
Patch: row.Patch,
Prerelease: row.Prerelease,
BundleCreatedAt: ToUtcOffset(row.BundleCreatedAt),
BundleDigest: row.BundleDigest,
ActivatedAt: ToUtcOffset(row.ActivatedAt),
WasForceActivated: row.WasForceActivated,
ForceActivateReason: row.ForceActivateReason);
}
private async Task<BundleVersionRecord?> GetCurrentForUpdateAsync(
NpgsqlConnection connection,
NpgsqlTransaction transaction,
string versionTable,
string tenantKey,
string bundleTypeKey,
CancellationToken ct)
private static BundleVersionRecord Map(BundleVersionHistoryEntity row)
{
var sql = $$"""
SELECT tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
FROM {{versionTable}}
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type
FOR UPDATE;
""";
await using var command = CreateCommand(sql, connection);
command.Transaction = transaction;
AddParameter(command, "tenant_id", tenantKey);
AddParameter(command, "bundle_type", bundleTypeKey);
await using var reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false);
return await reader.ReadAsync(ct).ConfigureAwait(false) ? Map(reader) : null;
return new BundleVersionRecord(
TenantId: row.TenantId,
BundleType: row.BundleType,
VersionString: row.VersionString,
Major: row.Major,
Minor: row.Minor,
Patch: row.Patch,
Prerelease: row.Prerelease,
BundleCreatedAt: ToUtcOffset(row.BundleCreatedAt),
BundleDigest: row.BundleDigest,
ActivatedAt: ToUtcOffset(row.ActivatedAt),
WasForceActivated: row.WasForceActivated,
ForceActivateReason: row.ForceActivateReason);
}
private static DateTimeOffset ToUtcOffset(DateTime value)
{
if (value.Kind == DateTimeKind.Utc)
{
return new DateTimeOffset(value, TimeSpan.Zero);
}
if (value.Kind == DateTimeKind.Local)
{
return new DateTimeOffset(value.ToUniversalTime(), TimeSpan.Zero);
}
return new DateTimeOffset(DateTime.SpecifyKind(value, DateTimeKind.Utc), TimeSpan.Zero);
}
private static DateTime ToUtcDateTime(DateTimeOffset value) => value.UtcDateTime;
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Npgsql;
using Microsoft.EntityFrameworkCore;
using StellaOps.AirGap.Importer.Versioning;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
@@ -21,21 +18,17 @@ public sealed partial class PostgresBundleVersionStore
var tenantKey = NormalizeKey(tenantId);
var bundleTypeKey = NormalizeKey(bundleType);
var versionTable = GetQualifiedTableName("bundle_versions");
await using var connection = await DataSource.OpenConnectionAsync(tenantKey, "reader", ct).ConfigureAwait(false);
var sql = $$"""
SELECT tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
FROM {{versionTable}}
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type;
""";
await using var dbContext = AirGapDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
await using var command = CreateCommand(sql, connection);
AddParameter(command, "tenant_id", tenantKey);
AddParameter(command, "bundle_type", bundleTypeKey);
var row = await dbContext.BundleVersions
.AsNoTracking()
.FirstOrDefaultAsync(
b => b.TenantId == tenantKey && b.BundleType == bundleTypeKey,
ct)
.ConfigureAwait(false);
await using var reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false);
return await reader.ReadAsync(ct).ConfigureAwait(false) ? Map(reader) : null;
return row is null ? null : Map(row);
}
public async Task<IReadOnlyList<BundleVersionRecord>> GetHistoryAsync(
@@ -57,29 +50,18 @@ public sealed partial class PostgresBundleVersionStore
var tenantKey = NormalizeKey(tenantId);
var bundleTypeKey = NormalizeKey(bundleType);
var historyTable = GetQualifiedTableName("bundle_version_history");
await using var connection = await DataSource.OpenConnectionAsync(tenantKey, "reader", ct).ConfigureAwait(false);
var sql = $$"""
SELECT tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
FROM {{historyTable}}
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type
ORDER BY activated_at DESC, id DESC
LIMIT @limit;
""";
await using var dbContext = AirGapDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
await using var command = CreateCommand(sql, connection);
AddParameter(command, "tenant_id", tenantKey);
AddParameter(command, "bundle_type", bundleTypeKey);
AddParameter(command, "limit", limit);
var rows = await dbContext.BundleVersionHistories
.AsNoTracking()
.Where(b => b.TenantId == tenantKey && b.BundleType == bundleTypeKey)
.OrderByDescending(b => b.ActivatedAt)
.ThenByDescending(b => b.Id)
.Take(limit)
.ToListAsync(ct)
.ConfigureAwait(false);
await using var reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false);
var results = new List<BundleVersionRecord>();
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
results.Add(Map(reader));
}
return results;
return rows.Select(Map).ToList();
}
}

View File

@@ -1,58 +1,49 @@
using System;
using System.Threading;
using Npgsql;
using StellaOps.AirGap.Importer.Versioning;
using StellaOps.AirGap.Persistence.EfCore.Context;
using StellaOps.AirGap.Persistence.EfCore.Models;
using BundleVersionEntity = StellaOps.AirGap.Persistence.EfCore.Models.BundleVersion;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
public sealed partial class PostgresBundleVersionStore
{
private async Task UpsertCurrentAsync(
NpgsqlConnection connection,
NpgsqlTransaction tx,
string versionTable,
private static void UpsertCurrent(
AirGapDbContext dbContext,
BundleVersionEntity? currentEntity,
BundleVersionRecord record,
string tenantKey,
string bundleTypeKey,
CancellationToken ct)
string bundleTypeKey)
{
var upsertSql = $$"""
INSERT INTO {{versionTable}} (
tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, was_force_activated, force_activate_reason
)
VALUES (
@tenant_id, @bundle_type, @version_string, @major, @minor, @patch, @prerelease,
@bundle_created_at, @bundle_digest, @activated_at, @was_force_activated, @force_activate_reason
)
ON CONFLICT (tenant_id, bundle_type) DO UPDATE SET
version_string = EXCLUDED.version_string,
major = EXCLUDED.major,
minor = EXCLUDED.minor,
patch = EXCLUDED.patch,
prerelease = EXCLUDED.prerelease,
bundle_created_at = EXCLUDED.bundle_created_at,
bundle_digest = EXCLUDED.bundle_digest,
activated_at = EXCLUDED.activated_at,
was_force_activated = EXCLUDED.was_force_activated,
force_activate_reason = EXCLUDED.force_activate_reason,
updated_at = NOW();
""";
if (currentEntity is null)
{
dbContext.BundleVersions.Add(new BundleVersionEntity
{
TenantId = tenantKey,
BundleType = bundleTypeKey,
VersionString = record.VersionString,
Major = record.Major,
Minor = record.Minor,
Patch = record.Patch,
Prerelease = record.Prerelease,
BundleCreatedAt = ToUtcDateTime(record.BundleCreatedAt),
BundleDigest = record.BundleDigest,
ActivatedAt = ToUtcDateTime(record.ActivatedAt),
WasForceActivated = record.WasForceActivated,
ForceActivateReason = record.ForceActivateReason
});
return;
}
await using var upsertCmd = CreateCommand(upsertSql, connection);
upsertCmd.Transaction = tx;
AddParameter(upsertCmd, "tenant_id", tenantKey);
AddParameter(upsertCmd, "bundle_type", bundleTypeKey);
AddParameter(upsertCmd, "version_string", record.VersionString);
AddParameter(upsertCmd, "major", record.Major);
AddParameter(upsertCmd, "minor", record.Minor);
AddParameter(upsertCmd, "patch", record.Patch);
AddParameter(upsertCmd, "prerelease", (object?)record.Prerelease ?? DBNull.Value);
AddParameter(upsertCmd, "bundle_created_at", record.BundleCreatedAt);
AddParameter(upsertCmd, "bundle_digest", record.BundleDigest);
AddParameter(upsertCmd, "activated_at", record.ActivatedAt);
AddParameter(upsertCmd, "was_force_activated", record.WasForceActivated);
AddParameter(upsertCmd, "force_activate_reason", (object?)record.ForceActivateReason ?? DBNull.Value);
await upsertCmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
currentEntity.VersionString = record.VersionString;
currentEntity.Major = record.Major;
currentEntity.Minor = record.Minor;
currentEntity.Patch = record.Patch;
currentEntity.Prerelease = record.Prerelease;
currentEntity.BundleCreatedAt = ToUtcDateTime(record.BundleCreatedAt);
currentEntity.BundleDigest = record.BundleDigest;
currentEntity.ActivatedAt = ToUtcDateTime(record.ActivatedAt);
currentEntity.WasForceActivated = record.WasForceActivated;
currentEntity.ForceActivateReason = record.ForceActivateReason;
currentEntity.UpdatedAt = DateTime.UtcNow;
}
}

View File

@@ -1,71 +1,56 @@
using System;
using System.Threading;
using Npgsql;
using Microsoft.EntityFrameworkCore;
using StellaOps.AirGap.Importer.Versioning;
using StellaOps.AirGap.Persistence.EfCore.Context;
using StellaOps.AirGap.Persistence.EfCore.Models;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
public sealed partial class PostgresBundleVersionStore
{
private async Task CloseHistoryAsync(
NpgsqlConnection connection,
NpgsqlTransaction tx,
string historyTable,
private static async Task CloseHistoryAsync(
AirGapDbContext dbContext,
BundleVersionRecord record,
string tenantKey,
string bundleTypeKey,
CancellationToken ct)
{
var closeHistorySql = $$"""
UPDATE {{historyTable}}
SET deactivated_at = @activated_at
WHERE tenant_id = @tenant_id AND bundle_type = @bundle_type AND deactivated_at IS NULL;
""";
var activeRows = await dbContext.BundleVersionHistories
.Where(h => h.TenantId == tenantKey && h.BundleType == bundleTypeKey && h.DeactivatedAt == null)
.ToListAsync(ct)
.ConfigureAwait(false);
await using var closeCmd = CreateCommand(closeHistorySql, connection);
closeCmd.Transaction = tx;
AddParameter(closeCmd, "tenant_id", tenantKey);
AddParameter(closeCmd, "bundle_type", bundleTypeKey);
AddParameter(closeCmd, "activated_at", record.ActivatedAt);
await closeCmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
if (activeRows.Count == 0)
{
return;
}
var deactivatedAt = ToUtcDateTime(record.ActivatedAt);
foreach (var row in activeRows)
{
row.DeactivatedAt = deactivatedAt;
}
}
private async Task InsertHistoryAsync(
NpgsqlConnection connection,
NpgsqlTransaction tx,
string historyTable,
private static void InsertHistory(
AirGapDbContext dbContext,
BundleVersionRecord record,
string tenantKey,
string bundleTypeKey,
CancellationToken ct)
string bundleTypeKey)
{
var historySql = $$"""
INSERT INTO {{historyTable}} (
tenant_id, bundle_type, version_string, major, minor, patch, prerelease,
bundle_created_at, bundle_digest, activated_at, deactivated_at,
was_force_activated, force_activate_reason
)
VALUES (
@tenant_id, @bundle_type, @version_string, @major, @minor, @patch, @prerelease,
@bundle_created_at, @bundle_digest, @activated_at, NULL,
@was_force_activated, @force_activate_reason
);
""";
await using var historyCmd = CreateCommand(historySql, connection);
historyCmd.Transaction = tx;
AddParameter(historyCmd, "tenant_id", tenantKey);
AddParameter(historyCmd, "bundle_type", bundleTypeKey);
AddParameter(historyCmd, "version_string", record.VersionString);
AddParameter(historyCmd, "major", record.Major);
AddParameter(historyCmd, "minor", record.Minor);
AddParameter(historyCmd, "patch", record.Patch);
AddParameter(historyCmd, "prerelease", (object?)record.Prerelease ?? DBNull.Value);
AddParameter(historyCmd, "bundle_created_at", record.BundleCreatedAt);
AddParameter(historyCmd, "bundle_digest", record.BundleDigest);
AddParameter(historyCmd, "activated_at", record.ActivatedAt);
AddParameter(historyCmd, "was_force_activated", record.WasForceActivated);
AddParameter(historyCmd, "force_activate_reason", (object?)record.ForceActivateReason ?? DBNull.Value);
await historyCmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
dbContext.BundleVersionHistories.Add(new BundleVersionHistory
{
TenantId = tenantKey,
BundleType = bundleTypeKey,
VersionString = record.VersionString,
Major = record.Major,
Minor = record.Minor,
Patch = record.Patch,
Prerelease = record.Prerelease,
BundleCreatedAt = ToUtcDateTime(record.BundleCreatedAt),
BundleDigest = record.BundleDigest,
ActivatedAt = ToUtcDateTime(record.ActivatedAt),
WasForceActivated = record.WasForceActivated,
ForceActivateReason = record.ForceActivateReason
});
}
}

View File

@@ -1,5 +1,5 @@
using System;
using System.Threading;
using System.Data;
using Microsoft.EntityFrameworkCore;
using StellaOps.AirGap.Importer.Versioning;
namespace StellaOps.AirGap.Persistence.Postgres.Repositories;
@@ -14,30 +14,25 @@ public sealed partial class PostgresBundleVersionStore
var tenantKey = NormalizeKey(record.TenantId);
var bundleTypeKey = NormalizeKey(record.BundleType);
var versionTable = GetQualifiedTableName("bundle_versions");
var historyTable = GetQualifiedTableName("bundle_version_history");
await using var connection = await DataSource.OpenConnectionAsync(tenantKey, "writer", ct).ConfigureAwait(false);
await using var tx = await connection.BeginTransactionAsync(ct).ConfigureAwait(false);
await using var dbContext = AirGapDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
await using var tx = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct)
.ConfigureAwait(false);
var current = await GetCurrentForUpdateAsync(
connection,
tx,
versionTable,
tenantKey,
bundleTypeKey,
var currentEntity = await dbContext.BundleVersions
.FirstOrDefaultAsync(
b => b.TenantId == tenantKey && b.BundleType == bundleTypeKey,
ct)
.ConfigureAwait(false);
var current = currentEntity is null ? null : Map(currentEntity);
EnsureMonotonicVersion(record, current);
await CloseHistoryAsync(connection, tx, historyTable, record, tenantKey, bundleTypeKey, ct)
.ConfigureAwait(false);
await InsertHistoryAsync(connection, tx, historyTable, record, tenantKey, bundleTypeKey, ct)
.ConfigureAwait(false);
await UpsertCurrentAsync(connection, tx, versionTable, record, tenantKey, bundleTypeKey, ct)
.ConfigureAwait(false);
await CloseHistoryAsync(dbContext, record, tenantKey, bundleTypeKey, ct).ConfigureAwait(false);
InsertHistory(dbContext, record, tenantKey, bundleTypeKey);
UpsertCurrent(dbContext, currentEntity, record, tenantKey, bundleTypeKey);
await dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
await tx.CommitAsync(ct).ConfigureAwait(false);
}
}

View File

@@ -14,6 +14,11 @@
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<!-- Prevent automatic compiled-model binding so non-default schemas can build runtime models. -->
<Compile Remove="EfCore\CompiledModels\AirGapDbContextAssemblyAttributes.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />

View File

@@ -1,8 +1,14 @@
# StellaOps.AirGap.Persistence Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
Source of truth:
- `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`
- `docs/implplan/SPRINT_20260222_064_AirGap_next_smallest_module_dal_to_efcore.md`
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | DONE | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/AirGap/__Libraries/StellaOps.AirGap.Persistence/StellaOps.AirGap.Persistence.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| AIRGAP-EF-01 | DONE | Scaffolded EF models/context for AirGap schema (`state`, `bundle_versions`, `bundle_version_history`). |
| AIRGAP-EF-02 | DONE | Converted `PostgresAirGapStateStore` and `PostgresBundleVersionStore` DAL flows to EF Core with preserved contracts. |
| AIRGAP-EF-03 | DONE | Added compiled model generation and static model runtime wiring for default `airgap` schema. |
| AIRGAP-EF-04 | DONE | Completed sequential build/test + docs updates for AirGap EF migration workflow. |