feat(m9/T24a): guarded move-data-connection-between-sites command + handler
This commit is contained in:
@@ -187,7 +187,7 @@ public class ManagementActor : ReceiveActor
|
||||
or DeleteNotificationListCommand
|
||||
or UpdateSmtpConfigCommand
|
||||
or CreateDataConnectionCommand or UpdateDataConnectionCommand
|
||||
or DeleteDataConnectionCommand
|
||||
or DeleteDataConnectionCommand or MoveDataConnectionCommand
|
||||
or AddTemplateAttributeCommand or UpdateTemplateAttributeCommand or DeleteTemplateAttributeCommand
|
||||
or AddTemplateAlarmCommand or UpdateTemplateAlarmCommand or DeleteTemplateAlarmCommand
|
||||
or AddTemplateNativeAlarmSourceCommand or UpdateTemplateNativeAlarmSourceCommand or DeleteTemplateNativeAlarmSourceCommand
|
||||
@@ -306,6 +306,7 @@ public class ManagementActor : ReceiveActor
|
||||
CreateDataConnectionCommand cmd => await HandleCreateDataConnection(sp, cmd, user.Username),
|
||||
UpdateDataConnectionCommand cmd => await HandleUpdateDataConnection(sp, cmd, user.Username),
|
||||
DeleteDataConnectionCommand cmd => await HandleDeleteDataConnection(sp, cmd, user.Username),
|
||||
MoveDataConnectionCommand cmd => await HandleMoveDataConnection(sp, cmd, user.Username),
|
||||
|
||||
// External Systems
|
||||
ListExternalSystemsCommand => await HandleListExternalSystems(sp),
|
||||
@@ -1373,6 +1374,108 @@ public class ManagementActor : ReceiveActor
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a data connection to another site (M9 / T24a). High-risk + data
|
||||
/// integrity: every guard runs server-side BEFORE the write, and when in
|
||||
/// doubt the move is BLOCKED with a clear error rather than risking an
|
||||
/// orphaned binding/reference. No bindings or name references are rewritten.
|
||||
/// </summary>
|
||||
private static async Task<object?> HandleMoveDataConnection(IServiceProvider sp, MoveDataConnectionCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISiteRepository>();
|
||||
|
||||
var conn = await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId)
|
||||
?? throw new ManagementCommandException($"DataConnection with ID {cmd.DataConnectionId} not found.");
|
||||
|
||||
var sourceSiteId = conn.SiteId;
|
||||
if (sourceSiteId == cmd.TargetSiteId)
|
||||
throw new ManagementCommandException(
|
||||
$"DataConnection '{conn.Name}' (ID {conn.Id}) already belongs to site {cmd.TargetSiteId}.");
|
||||
|
||||
// Guard 1: target site must exist.
|
||||
var targetSite = await repo.GetSiteByIdAsync(cmd.TargetSiteId)
|
||||
?? throw new ManagementCommandException($"Target site with ID {cmd.TargetSiteId} not found.");
|
||||
|
||||
// Guard 2: no name collision at the target site.
|
||||
var targetConnections = await repo.GetDataConnectionsBySiteIdAsync(cmd.TargetSiteId);
|
||||
if (targetConnections.Any(c => c.Id != conn.Id
|
||||
&& string.Equals(c.Name, conn.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new ManagementCommandException(
|
||||
$"Target site '{targetSite.Name}' (ID {cmd.TargetSiteId}) already has a data connection named '{conn.Name}'. " +
|
||||
"Rename one of them before moving.");
|
||||
}
|
||||
|
||||
// Guard 3 (primary data-integrity guard): no InstanceConnectionBinding may
|
||||
// reference the connection. Instances are site-scoped, so a bound
|
||||
// connection cannot leave its site without orphaning the binding.
|
||||
var blockingInstances = await repo.GetInstancesReferencingDataConnectionAsync(conn.Id);
|
||||
if (blockingInstances.Count > 0)
|
||||
{
|
||||
var names = string.Join(", ", blockingInstances
|
||||
.OrderBy(i => i.UniqueName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(i => $"'{i.UniqueName}' (ID {i.Id})"));
|
||||
throw new ManagementCommandException(
|
||||
$"Cannot move data connection '{conn.Name}' (ID {conn.Id}): it is referenced by " +
|
||||
$"{blockingInstances.Count} instance binding(s) on its current site. Rebind or delete the " +
|
||||
$"following instance(s) first: {names}.");
|
||||
}
|
||||
|
||||
// Guard 4 (name-based native-alarm-source references): templates reference
|
||||
// a connection by Name (TemplateNativeAlarmSource.ConnectionName) and
|
||||
// instances may override that name
|
||||
// (InstanceNativeAlarmSourceOverride.ConnectionNameOverride). Moving a
|
||||
// connection changes which physical connection that name resolves to on
|
||||
// the source site, so any such reference to the moving name is a blocker.
|
||||
// Detect and report only — never auto-rewrite.
|
||||
var referenceBlockers = new List<string>();
|
||||
|
||||
var templateRepo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var templates = await templateRepo.GetAllTemplatesAsync();
|
||||
foreach (var template in templates)
|
||||
{
|
||||
foreach (var source in template.NativeAlarmSources)
|
||||
{
|
||||
if (string.Equals(source.ConnectionName, conn.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
referenceBlockers.Add(
|
||||
$"template '{template.Name}' native-alarm-source '{source.Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instance-level overrides on the SOURCE site that name this connection
|
||||
// would orphan once the connection leaves the site.
|
||||
var sourceInstances = await repo.GetInstancesBySiteIdAsync(sourceSiteId);
|
||||
foreach (var instance in sourceInstances)
|
||||
{
|
||||
foreach (var ovr in instance.NativeAlarmSourceOverrides)
|
||||
{
|
||||
if (string.Equals(ovr.ConnectionNameOverride, conn.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
referenceBlockers.Add(
|
||||
$"instance '{instance.UniqueName}' (ID {instance.Id}) native-alarm-source override '{ovr.SourceCanonicalName}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (referenceBlockers.Count > 0)
|
||||
{
|
||||
var details = string.Join(", ", referenceBlockers);
|
||||
throw new ManagementCommandException(
|
||||
$"Cannot move data connection '{conn.Name}' (ID {conn.Id}): its name is referenced by " +
|
||||
$"native-alarm-source binding(s) that would be orphaned or made ambiguous by the move. " +
|
||||
$"Resolve the following reference(s) first: {details}.");
|
||||
}
|
||||
|
||||
// All guards passed: persist the site change and audit.
|
||||
conn.SiteId = cmd.TargetSiteId;
|
||||
await repo.UpdateDataConnectionAsync(conn);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Move", "DataConnection", conn.Id.ToString(), conn.Name, conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// External System handlers
|
||||
|
||||
Reference in New Issue
Block a user