feat(uns): equipment CRUD with #122 driver-cluster guard
This commit is contained in:
@@ -336,4 +336,171 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
return new UnsMutationResult(false, $"Delete failed: {ex.Message}. Likely because equipment still references this line — remove or re-home it first.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> CreateEquipmentAsync(EquipmentInput input, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input.UnsLineId))
|
||||
{
|
||||
return new UnsMutationResult(false, "Pick a UNS line.");
|
||||
}
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
if (await db.Equipment.AnyAsync(e => e.MachineCode == input.MachineCode, ct))
|
||||
{
|
||||
return new UnsMutationResult(false, $"MachineCode '{input.MachineCode}' already exists in this fleet.");
|
||||
}
|
||||
|
||||
var guard = await CheckDriverClusterGuardAsync(db, input, ct);
|
||||
if (guard is not null)
|
||||
{
|
||||
return guard.Value;
|
||||
}
|
||||
|
||||
var uuid = Guid.NewGuid();
|
||||
var equipmentId = $"EQ-{uuid.ToString("N")[..12]}";
|
||||
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = equipmentId,
|
||||
EquipmentUuid = uuid,
|
||||
DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId,
|
||||
UnsLineId = input.UnsLineId,
|
||||
Name = input.Name,
|
||||
MachineCode = input.MachineCode,
|
||||
ZTag = string.IsNullOrWhiteSpace(input.ZTag) ? null : input.ZTag,
|
||||
SAPID = string.IsNullOrWhiteSpace(input.SAPID) ? null : input.SAPID,
|
||||
Manufacturer = input.Manufacturer,
|
||||
Model = input.Model,
|
||||
SerialNumber = input.SerialNumber,
|
||||
HardwareRevision = input.HardwareRevision,
|
||||
SoftwareRevision = input.SoftwareRevision,
|
||||
YearOfConstruction = input.YearOfConstruction,
|
||||
AssetLocation = input.AssetLocation,
|
||||
ManufacturerUri = input.ManufacturerUri,
|
||||
DeviceManualUri = input.DeviceManualUri,
|
||||
Enabled = input.Enabled,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> UpdateEquipmentAsync(
|
||||
string equipmentId,
|
||||
EquipmentInput input,
|
||||
byte[] rowVersion,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == equipmentId, ct);
|
||||
if (entity is null)
|
||||
{
|
||||
return new UnsMutationResult(false, "Row no longer exists.");
|
||||
}
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
|
||||
var guard = await CheckDriverClusterGuardAsync(db, input, ct);
|
||||
if (guard is not null)
|
||||
{
|
||||
return guard.Value;
|
||||
}
|
||||
|
||||
entity.DriverInstanceId = string.IsNullOrWhiteSpace(input.DriverInstanceId) ? null : input.DriverInstanceId;
|
||||
entity.UnsLineId = input.UnsLineId;
|
||||
entity.Name = input.Name;
|
||||
entity.MachineCode = input.MachineCode;
|
||||
entity.ZTag = string.IsNullOrWhiteSpace(input.ZTag) ? null : input.ZTag;
|
||||
entity.SAPID = string.IsNullOrWhiteSpace(input.SAPID) ? null : input.SAPID;
|
||||
entity.Manufacturer = input.Manufacturer;
|
||||
entity.Model = input.Model;
|
||||
entity.SerialNumber = input.SerialNumber;
|
||||
entity.HardwareRevision = input.HardwareRevision;
|
||||
entity.SoftwareRevision = input.SoftwareRevision;
|
||||
entity.YearOfConstruction = input.YearOfConstruction;
|
||||
entity.AssetLocation = input.AssetLocation;
|
||||
entity.ManufacturerUri = input.ManufacturerUri;
|
||||
entity.DeviceManualUri = input.DeviceManualUri;
|
||||
entity.Enabled = input.Enabled;
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return new UnsMutationResult(false, "Another user changed this equipment while you were editing.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> DeleteEquipmentAsync(
|
||||
string equipmentId,
|
||||
byte[] rowVersion,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == equipmentId, ct);
|
||||
if (entity is null)
|
||||
{
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
db.Equipment.Remove(entity);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return new UnsMutationResult(false, "Another user changed this equipment while you were viewing it.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new UnsMutationResult(false, $"Delete failed: {ex.Message}. Likely because tags or virtual tags reference this equipment — remove them first.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decision #122: an equipment may only bind to a driver in the same cluster as its line.
|
||||
/// Only runs when a driver is requested. Resolves the line's cluster (line → area → cluster)
|
||||
/// and the driver's cluster; when both are known and differ, returns the guard failure.
|
||||
/// Returns <c>null</c> when the bind is allowed (driver-less, unresolvable cluster, or match).
|
||||
/// </summary>
|
||||
private static async Task<UnsMutationResult?> CheckDriverClusterGuardAsync(
|
||||
OtOpcUaConfigDbContext db,
|
||||
EquipmentInput input,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.DriverInstanceId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var line = await db.UnsLines.FirstOrDefaultAsync(l => l.UnsLineId == input.UnsLineId, ct);
|
||||
var area = line is null ? null : await db.UnsAreas.FirstOrDefaultAsync(a => a.UnsAreaId == line.UnsAreaId, ct);
|
||||
var lineCluster = area?.ClusterId;
|
||||
|
||||
var driverCluster = await db.DriverInstances
|
||||
.Where(d => d.DriverInstanceId == input.DriverInstanceId)
|
||||
.Select(d => d.ClusterId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (driverCluster is not null && lineCluster is not null && driverCluster != lineCluster)
|
||||
{
|
||||
return new UnsMutationResult(
|
||||
false,
|
||||
$"Driver '{input.DriverInstanceId}' is in cluster '{driverCluster}' but the line is in cluster '{lineCluster}' (decision #122).");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user