feat(uns): equipment-bound virtual-tag CRUD
This commit is contained in:
@@ -643,6 +643,174 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<(string ScriptId, string Display)>> LoadScriptsAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var scripts = await db.Scripts
|
||||
.AsNoTracking()
|
||||
.OrderBy(s => s.Name)
|
||||
.Select(s => new { s.ScriptId, s.Name, s.Language })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return scripts
|
||||
.Select(s => (s.ScriptId, Display: $"{s.Name} ({s.Language})"))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> CreateVirtualTagAsync(
|
||||
string equipmentId,
|
||||
VirtualTagInput input,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
if (!await db.Equipment.AnyAsync(e => e.EquipmentId == equipmentId, ct))
|
||||
{
|
||||
return new UnsMutationResult(false, $"Equipment '{equipmentId}' not found.");
|
||||
}
|
||||
|
||||
var guard = CheckVirtualTagRules(input);
|
||||
if (guard is not null)
|
||||
{
|
||||
return guard.Value;
|
||||
}
|
||||
|
||||
if (await db.VirtualTags.AnyAsync(v => v.VirtualTagId == input.VirtualTagId, ct))
|
||||
{
|
||||
return new UnsMutationResult(false, $"VirtualTag '{input.VirtualTagId}' already exists.");
|
||||
}
|
||||
|
||||
if (await db.VirtualTags.AnyAsync(v => v.EquipmentId == equipmentId && v.Name == input.Name, ct))
|
||||
{
|
||||
return new UnsMutationResult(false, $"A virtual tag named '{input.Name}' already exists on this equipment.");
|
||||
}
|
||||
|
||||
db.VirtualTags.Add(new VirtualTag
|
||||
{
|
||||
VirtualTagId = input.VirtualTagId,
|
||||
EquipmentId = equipmentId,
|
||||
Name = input.Name,
|
||||
DataType = input.DataType,
|
||||
ScriptId = input.ScriptId,
|
||||
ChangeTriggered = input.ChangeTriggered,
|
||||
TimerIntervalMs = input.TimerIntervalMs,
|
||||
Historize = input.Historize,
|
||||
Enabled = input.Enabled,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> UpdateVirtualTagAsync(
|
||||
string virtualTagId,
|
||||
VirtualTagInput input,
|
||||
byte[] rowVersion,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var entity = await db.VirtualTags.FirstOrDefaultAsync(v => v.VirtualTagId == virtualTagId, ct);
|
||||
if (entity is null)
|
||||
{
|
||||
return new UnsMutationResult(false, "Row no longer exists.");
|
||||
}
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
|
||||
var guard = CheckVirtualTagRules(input);
|
||||
if (guard is not null)
|
||||
{
|
||||
return guard.Value;
|
||||
}
|
||||
|
||||
if (await db.VirtualTags.AnyAsync(
|
||||
v => v.EquipmentId == entity.EquipmentId && v.Name == input.Name && v.VirtualTagId != virtualTagId,
|
||||
ct))
|
||||
{
|
||||
return new UnsMutationResult(false, $"A virtual tag named '{input.Name}' already exists on this equipment.");
|
||||
}
|
||||
|
||||
// EquipmentId is preserved — virtual tags are always equipment-bound (plan decision #2).
|
||||
entity.Name = input.Name;
|
||||
entity.DataType = input.DataType;
|
||||
entity.ScriptId = input.ScriptId;
|
||||
entity.ChangeTriggered = input.ChangeTriggered;
|
||||
entity.TimerIntervalMs = input.TimerIntervalMs;
|
||||
entity.Historize = input.Historize;
|
||||
entity.Enabled = input.Enabled;
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return new UnsMutationResult(false, "Another user changed this virtual tag while you were editing.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> DeleteVirtualTagAsync(
|
||||
string virtualTagId,
|
||||
byte[] rowVersion,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var entity = await db.VirtualTags.FirstOrDefaultAsync(v => v.VirtualTagId == virtualTagId, ct);
|
||||
if (entity is null)
|
||||
{
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
db.VirtualTags.Remove(entity);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new UnsMutationResult(true, null);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return new UnsMutationResult(false, "Another user changed this virtual tag while you were viewing it.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new UnsMutationResult(false, $"Delete failed: {ex.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a virtual tag's script binding and trigger configuration (mirrors the DbContext CHECK
|
||||
/// constraints): a script must be chosen, at least one of change-trigger / timer must be set, and a
|
||||
/// set timer must be at least 50 ms. Returns <c>null</c> when the input is valid.
|
||||
/// </summary>
|
||||
private static UnsMutationResult? CheckVirtualTagRules(VirtualTagInput input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input.ScriptId))
|
||||
{
|
||||
return new UnsMutationResult(false, "Pick a script.");
|
||||
}
|
||||
|
||||
if (!input.ChangeTriggered && input.TimerIntervalMs is null)
|
||||
{
|
||||
return new UnsMutationResult(false, "Pick at least one trigger — change or timer.");
|
||||
}
|
||||
|
||||
if (input.TimerIntervalMs is not null && input.TimerIntervalMs < 50)
|
||||
{
|
||||
return new UnsMutationResult(false, "TimerIntervalMs must be at least 50 ms.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Returns <c>true</c> if <paramref name="json"/> parses as a well-formed JSON document.</summary>
|
||||
private static bool IsValidJson(string json)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user