fix(configuration): resolve Medium code-review findings (Configuration-002, -003, -006, -009)

Configuration-002: sp_PublishGeneration is transaction-nesting aware
(BEGIN TRANSACTION vs SAVE TRANSACTION on @@TRANCOUNT) so a caller's outer
transaction survives a publish failure; sp_ValidateDraft wrapped in TRY/CATCH.
Configuration-003: ValidatePathLength uses the cluster's actual Enterprise/Site
lengths when available, falling back to the conservative approximation.
Configuration-006: ResilientConfigReader treats a command-timeout
TaskCanceledException as a fault (not caller cancellation) and falls back.
Configuration-009: removed the checked-in plaintext sa connection string;
CreateDbContext now requires OTOPCUA_CONFIG_CONNECTION.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 08:13:27 -04:00
parent 7e54e1e4a0
commit c126fc7a7d
9 changed files with 274 additions and 31 deletions
@@ -11,6 +11,18 @@ public sealed class DraftSnapshot
public required long GenerationId { get; init; }
public required string ClusterId { get; init; }
/// <summary>
/// Cluster's Enterprise segment (UNS level 1). When set, <see cref="DraftValidator"/> uses
/// the actual length for path-length checks instead of a conservative 32-char upper bound.
/// </summary>
public string? Enterprise { get; init; }
/// <summary>
/// Cluster's Site segment (UNS level 2). When set, <see cref="DraftValidator"/> uses the
/// actual length for path-length checks instead of a conservative 32-char upper bound.
/// </summary>
public string? Site { get; init; }
public IReadOnlyList<Namespace> Namespaces { get; init; } = [];
public IReadOnlyList<DriverInstance> DriverInstances { get; init; } = [];
public IReadOnlyList<Device> Devices { get; init; } = [];
@@ -59,8 +59,13 @@ public static class DraftValidator
/// <summary>Cluster.Enterprise + Site + area + line + equipment + 4 slashes ≤ 200 chars.</summary>
private static void ValidatePathLength(DraftSnapshot draft, List<ValidationError> errors)
{
// The cluster row isn't in the snapshot — we assume caller pre-validated Enterprise+Site
// length and bound them as constants <= 64 chars each. Here we validate the dynamic portion.
// Use actual Enterprise/Site lengths when the snapshot carries them (populated by
// DraftValidationService from the ServerCluster row). Fall back to a conservative
// 32-char upper bound per segment when not supplied — over-penalises short values
// but never under-penalises long ones, which is acceptable for the fallback case.
var enterpriseLen = draft.Enterprise?.Length ?? 32;
var siteLen = draft.Site?.Length ?? 32;
var areaById = draft.UnsAreas.ToDictionary(a => a.UnsAreaId);
var lineById = draft.UnsLines.ToDictionary(l => l.UnsLineId);
@@ -69,8 +74,7 @@ public static class DraftValidator
if (!lineById.TryGetValue(eq.UnsLineId!, out var line)) continue;
if (!areaById.TryGetValue(line.UnsAreaId, out var area)) continue;
// rough upper bound: Enterprise+Site at most 32+32; add dynamic segments + 4 slashes
var len = 32 + 32 + area.Name.Length + line.Name.Length + eq.Name.Length + 4;
var len = enterpriseLen + siteLen + area.Name.Length + line.Name.Length + eq.Name.Length + 4;
if (len > MaxPathLength)
errors.Add(new("PathTooLong",
$"Equipment path exceeds {MaxPathLength} chars (approx {len})",