feat(configdb): map SiteCall to SiteCalls table (#22, #23 M3)

Bundle B1 of Audit Log #23 M3: introduces the SiteCall entity + EF mapping
for the central SiteCalls operational-state table. One row per
TrackedOperationId, mirrored from sites via best-effort telemetry then
periodic reconciliation; eventually-consistent mirror, not a dispatcher.

- src/ScadaLink.Commons/Entities/Audit/SiteCall.cs: append-once record
  with required TrackedOperationId/Channel/Target/SourceSite/Status,
  monotonic status update at the repo layer.
- src/ScadaLink.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs:
  table SiteCalls, PK on TrackedOperationId (stored as varchar(36) via
  value conversion through the canonical 'D'-format GUID string —
  matches the wire shape used by gRPC + SQLite columns), two named
  indexes (IX_SiteCalls_Source_Created, IX_SiteCalls_Status_Updated).
- ScadaLinkDbContext: DbSet<SiteCall> SiteCalls in the existing Audit
  section, after AuditLogs.
- Tests in tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/:
  table name, PK, value-conversion shape, index presence + ordering.
This commit is contained in:
Joseph Doherty
2026-05-20 14:04:17 -04:00
parent e416b21dad
commit 3162286ade
4 changed files with 245 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types;
namespace ScadaLink.ConfigurationDatabase.Configurations;
/// <summary>
/// Maps the <see cref="SiteCall"/> record to the central <c>SiteCalls</c> table
/// (Site Call Audit #22, Audit Log #23 M3 Bundle B). Operational state — NOT audit —
/// so the table is non-partitioned, standard <c>[PRIMARY]</c> filegroup, no DB-role
/// restriction. Two named indexes back the Central UI's "from this site" and
/// "in this status" queries.
/// </summary>
public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration<SiteCall>
{
public void Configure(EntityTypeBuilder<SiteCall> builder)
{
builder.ToTable("SiteCalls");
// PK is the strong-typed TrackedOperationId. Stored as varchar(36) by converting
// through the canonical "D"-format GUID string. Going through the string surface
// (rather than uniqueidentifier) keeps the column shape identical to how the id
// is serialised on the wire (gRPC strings, SQLite TEXT on the site) — one
// consistent format everywhere makes operational debugging far easier than
// mixing a uniqueidentifier central column with TEXT site columns.
builder.HasKey(s => s.TrackedOperationId);
builder.Property(s => s.TrackedOperationId)
.HasConversion(
id => id.Value.ToString("D"),
s => new TrackedOperationId(Guid.Parse(s)))
.HasMaxLength(36)
.IsUnicode(false)
.IsRequired();
// Enum-as-string columns: bounded varchar, ASCII.
builder.Property(s => s.Channel)
.HasMaxLength(32)
.IsUnicode(false)
.IsRequired();
builder.Property(s => s.Status)
.HasMaxLength(32)
.IsUnicode(false)
.IsRequired();
builder.Property(s => s.SourceSite)
.HasMaxLength(64)
.IsUnicode(false)
.IsRequired();
builder.Property(s => s.Target)
.HasMaxLength(256)
.IsUnicode(false)
.IsRequired();
// Bounded unicode message column.
builder.Property(s => s.LastError)
.HasMaxLength(1024);
// Indexes — names locked for reconciliation/migration discoverability.
// Source_Created backs "calls from this site" (Central UI Site Calls page,
// filter by SourceSite, newest first).
builder.HasIndex(s => new { s.SourceSite, s.CreatedAtUtc })
.IsDescending(false, true)
.HasDatabaseName("IX_SiteCalls_Source_Created");
// Status_Updated backs "calls in this status" (e.g. parked rows awaiting
// operator action, newest UpdatedAtUtc first).
builder.HasIndex(s => new { s.Status, s.UpdatedAtUtc })
.IsDescending(false, true)
.HasDatabaseName("IX_SiteCalls_Status_Updated");
}
}

View File

@@ -85,6 +85,7 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
// Audit
public DbSet<AuditLogEntry> AuditLogEntries => Set<AuditLogEntry>();
public DbSet<AuditEvent> AuditLogs => Set<AuditEvent>();
public DbSet<SiteCall> SiteCalls => Set<SiteCall>();
// Data Protection Keys (for shared ASP.NET Data Protection across nodes)
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();