From 1996b219616badf85137531a125e89283d090c8d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 16 Mar 2026 19:15:50 -0400 Subject: [PATCH] Phase 1 WP-1: EF Core DbContext with Fluent API mappings for all 26 entities ScadaLinkDbContext with 10 configuration classes (Fluent API only), initial migration creating 25 tables, environment-aware migration helper (auto-apply dev, validate-only prod), DesignTimeDbContextFactory, optimistic concurrency on DeploymentRecord. 20 tests verify schema, CRUD, relationships, cascades. --- .../Configurations/AuditConfiguration.cs | 40 + .../Configurations/DeploymentConfiguration.cs | 63 + .../ExternalSystemConfiguration.cs | 81 ++ .../Configurations/InboundApiConfiguration.cs | 50 + .../Configurations/InstanceConfiguration.cs | 113 ++ .../NotificationConfiguration.cs | 66 + .../Configurations/ScriptConfiguration.cs | 28 + .../Configurations/SecurityConfiguration.cs | 44 + .../Configurations/SiteConfiguration.cs | 68 + .../Configurations/TemplateConfiguration.cs | 149 +++ .../DesignTimeDbContextFactory.cs | 29 + .../MigrationHelper.cs | 41 + .../20260316231104_InitialCreate.Designer.cs | 1144 +++++++++++++++++ .../20260316231104_InitialCreate.cs | 883 +++++++++++++ .../ScadaLinkDbContextModelSnapshot.cs | 1141 ++++++++++++++++ .../ScadaLink.ConfigurationDatabase.csproj | 7 + .../ScadaLinkDbContext.cs | 71 + .../ServiceCollectionExtensions.cs | 19 +- src/ScadaLink.Host/Program.cs | 17 +- src/ScadaLink.Host/ScadaLink.Host.csproj | 7 + ...adaLink.ConfigurationDatabase.Tests.csproj | 7 +- .../UnitTest1.cs | 434 ++++++- .../ScadaLink.Host.Tests/HostStartupTests.cs | 1 + 23 files changed, 4494 insertions(+), 9 deletions(-) create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/DeploymentConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/InboundApiConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/InstanceConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/NotificationConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/ScriptConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/SecurityConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/SiteConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Configurations/TemplateConfiguration.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/DesignTimeDbContextFactory.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Migrations/20260316231104_InitialCreate.Designer.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Migrations/20260316231104_InitialCreate.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Migrations/ScadaLinkDbContextModelSnapshot.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs new file mode 100644 index 0000000..708da4d --- /dev/null +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ScadaLink.Commons.Entities.Audit; + +namespace ScadaLink.ConfigurationDatabase.Configurations; + +public class AuditLogEntryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(a => a.Id); + + builder.Property(a => a.User) + .IsRequired() + .HasMaxLength(200); + + builder.Property(a => a.Action) + .IsRequired() + .HasMaxLength(100); + + builder.Property(a => a.EntityType) + .IsRequired() + .HasMaxLength(200); + + builder.Property(a => a.EntityId) + .IsRequired() + .HasMaxLength(200); + + builder.Property(a => a.EntityName) + .IsRequired() + .HasMaxLength(200); + + // Indexes for common query patterns + builder.HasIndex(a => a.Timestamp); + builder.HasIndex(a => a.User); + builder.HasIndex(a => a.EntityType); + builder.HasIndex(a => a.EntityId); + builder.HasIndex(a => a.Action); + } +} diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/DeploymentConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/DeploymentConfiguration.cs new file mode 100644 index 0000000..0747028 --- /dev/null +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/DeploymentConfiguration.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ScadaLink.Commons.Entities.Deployment; +using ScadaLink.Commons.Entities.Instances; + +namespace ScadaLink.ConfigurationDatabase.Configurations; + +public class DeploymentRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(d => d.Id); + + builder.Property(d => d.DeploymentId) + .IsRequired() + .HasMaxLength(100); + + builder.Property(d => d.RevisionHash) + .HasMaxLength(100); + + builder.Property(d => d.DeployedBy) + .IsRequired() + .HasMaxLength(200); + + builder.Property(d => d.Status) + .HasConversion() + .HasMaxLength(50); + + builder.HasOne() + .WithMany() + .HasForeignKey(d => d.InstanceId) + .OnDelete(DeleteBehavior.Restrict); + + // Optimistic concurrency on deployment status records + builder.Property("RowVersion") + .IsRowVersion(); + + builder.HasIndex(d => d.DeploymentId).IsUnique(); + builder.HasIndex(d => d.InstanceId); + builder.HasIndex(d => d.DeployedAt); + } +} + +public class SystemArtifactDeploymentRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(d => d.Id); + + builder.Property(d => d.ArtifactType) + .IsRequired() + .HasMaxLength(100); + + builder.Property(d => d.DeployedBy) + .IsRequired() + .HasMaxLength(200); + + builder.Property(d => d.PerSiteStatus) + .HasMaxLength(4000); + + builder.HasIndex(d => d.DeployedAt); + } +} diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs new file mode 100644 index 0000000..9572bc1 --- /dev/null +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs @@ -0,0 +1,81 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ScadaLink.Commons.Entities.ExternalSystems; + +namespace ScadaLink.ConfigurationDatabase.Configurations; + +public class ExternalSystemDefinitionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Id); + + builder.Property(e => e.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(e => e.EndpointUrl) + .IsRequired() + .HasMaxLength(2000); + + builder.Property(e => e.AuthType) + .IsRequired() + .HasMaxLength(50); + + builder.Property(e => e.AuthConfiguration) + .HasMaxLength(4000); + + builder.HasMany() + .WithOne() + .HasForeignKey(m => m.ExternalSystemDefinitionId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasIndex(e => e.Name).IsUnique(); + } +} + +public class ExternalSystemMethodConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder.Property(m => m.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(m => m.HttpMethod) + .IsRequired() + .HasMaxLength(10); + + builder.Property(m => m.Path) + .IsRequired() + .HasMaxLength(2000); + + builder.Property(m => m.ParameterDefinitions) + .HasMaxLength(4000); + + builder.Property(m => m.ReturnDefinition) + .HasMaxLength(4000); + + builder.HasIndex(m => new { m.ExternalSystemDefinitionId, m.Name }).IsUnique(); + } +} + +public class DatabaseConnectionDefinitionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(d => d.Id); + + builder.Property(d => d.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(d => d.ConnectionString) + .IsRequired() + .HasMaxLength(4000); + + builder.HasIndex(d => d.Name).IsUnique(); + } +} diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/InboundApiConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/InboundApiConfiguration.cs new file mode 100644 index 0000000..ddba8b9 --- /dev/null +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/InboundApiConfiguration.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ScadaLink.Commons.Entities.InboundApi; + +namespace ScadaLink.ConfigurationDatabase.Configurations; + +public class ApiKeyConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(k => k.Id); + + builder.Property(k => k.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(k => k.KeyValue) + .IsRequired() + .HasMaxLength(500); + + builder.HasIndex(k => k.Name).IsUnique(); + builder.HasIndex(k => k.KeyValue).IsUnique(); + } +} + +public class ApiMethodConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(m => m.Id); + + builder.Property(m => m.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(m => m.Script) + .IsRequired(); + + builder.Property(m => m.ApprovedApiKeyIds) + .HasMaxLength(4000); + + builder.Property(m => m.ParameterDefinitions) + .HasMaxLength(4000); + + builder.Property(m => m.ReturnDefinition) + .HasMaxLength(4000); + + builder.HasIndex(m => m.Name).IsUnique(); + } +} diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/InstanceConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/InstanceConfiguration.cs new file mode 100644 index 0000000..55ce1ac --- /dev/null +++ b/src/ScadaLink.ConfigurationDatabase/Configurations/InstanceConfiguration.cs @@ -0,0 +1,113 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ScadaLink.Commons.Entities.Instances; +using ScadaLink.Commons.Entities.Sites; +using ScadaLink.Commons.Entities.Templates; + +namespace ScadaLink.ConfigurationDatabase.Configurations; + +public class InstanceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(i => i.Id); + + builder.Property(i => i.UniqueName) + .IsRequired() + .HasMaxLength(200); + + builder.Property(i => i.State) + .HasConversion() + .HasMaxLength(50); + + builder.HasOne