fix(adminui): alias update pins invariants + LoadAliasTagAsync + null-driver guard (review)
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
/// <summary>The editable state of a Galaxy alias tag, loaded for the alias edit modal.
|
||||
/// <paramref name="FullName"/> is parsed out of the tag's TagConfig {"FullName":…}.</summary>
|
||||
/// <param name="TagId">The tag's stable id (read-only on edit).</param>
|
||||
/// <param name="Name">The tag name.</param>
|
||||
/// <param name="DriverInstanceId">The bound Galaxy gateway driver id.</param>
|
||||
/// <param name="DataType">The OPC UA built-in type name.</param>
|
||||
/// <param name="AccessLevel">The tag-level access baseline.</param>
|
||||
/// <param name="FullName">The Galaxy object reference parsed from TagConfig; empty string when absent.</param>
|
||||
/// <param name="RowVersion">The optimistic-concurrency token last read.</param>
|
||||
public sealed record AliasTagEditDto(
|
||||
string TagId, string Name, string DriverInstanceId, string DataType,
|
||||
TagAccessLevel AccessLevel, string FullName, byte[] RowVersion);
|
||||
@@ -175,6 +175,12 @@ public interface IUnsTreeService
|
||||
/// <returns>The tag's edit projection, or <c>null</c> when missing.</returns>
|
||||
Task<TagEditDto?> LoadTagAsync(string tagId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Load a Galaxy alias tag for editing (FullName parsed from TagConfig). Null if not found.</summary>
|
||||
/// <param name="tagId">The alias tag to load.</param>
|
||||
/// <param name="ct">A token to cancel the load.</param>
|
||||
/// <returns>The alias tag's edit projection, or <c>null</c> when missing.</returns>
|
||||
Task<AliasTagEditDto?> LoadAliasTagAsync(string tagId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a single equipment-bound virtual tag projected for editing, or <c>null</c> if it no longer
|
||||
/// exists. Reads untracked and captures the current concurrency token for last-write-wins saves.
|
||||
|
||||
@@ -204,6 +204,16 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AliasTagEditDto?> LoadAliasTagAsync(string tagId, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var t = await db.Tags.AsNoTracking().FirstOrDefaultAsync(x => x.TagId == tagId, ct);
|
||||
if (t is null) return null;
|
||||
return new AliasTagEditDto(t.TagId, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel,
|
||||
ExtractTagConfigFullName(t.TagConfig) ?? string.Empty, t.RowVersion);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VirtualTagEditDto?> LoadVirtualTagAsync(string virtualTagId, CancellationToken ct = default)
|
||||
{
|
||||
@@ -975,6 +985,8 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
entity.DataType = input.DataType;
|
||||
entity.AccessLevel = input.AccessLevel;
|
||||
entity.FolderPath = null;
|
||||
entity.WriteIdempotent = false;
|
||||
entity.PollGroupId = null;
|
||||
entity.TagConfig = BuildAliasTagConfig(input.FullName);
|
||||
|
||||
try
|
||||
@@ -1397,6 +1409,9 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
string? equipmentCluster,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(driverInstanceId))
|
||||
return new UnsMutationResult(false, "An alias must be bound to a Galaxy gateway.");
|
||||
|
||||
var driver = await db.DriverInstances.FirstOrDefaultAsync(d => d.DriverInstanceId == driverInstanceId, ct);
|
||||
if (driver is null)
|
||||
{
|
||||
|
||||
@@ -288,6 +288,8 @@ public sealed class UnsTreeServiceAliasTagTests
|
||||
doc.RootElement.GetProperty("FullName").GetString().ShouldBe("TestMachine_020.Speed");
|
||||
tag.TagConfig.ShouldContain("FullName");
|
||||
tag.TagConfig.ShouldContain("TestMachine_020.Speed");
|
||||
tag.WriteIdempotent.ShouldBeFalse();
|
||||
tag.PollGroupId.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>An empty/whitespace Galaxy reference is rejected before anything is written.</summary>
|
||||
@@ -364,6 +366,44 @@ public sealed class UnsTreeServiceAliasTagTests
|
||||
result.Error!.ShouldContain("speed-alias");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="UnsTreeService.LoadAliasTagAsync"/> returns a DTO whose <c>FullName</c> matches the
|
||||
/// TagConfig and whose <c>RowVersion</c> is non-empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task LoadAliasTag_returns_dto_with_FullName()
|
||||
{
|
||||
var dbName = SeedCluster();
|
||||
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-LOAD-ALIAS",
|
||||
DriverInstanceId = GatewayDriverId,
|
||||
EquipmentId = EquipmentId,
|
||||
Name = "loaded-speed",
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.ReadWrite,
|
||||
TagConfig = "{\"FullName\":\"TestMachine_020.LoadedSpeed\"}",
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||
|
||||
var dto = await service.LoadAliasTagAsync("TAG-LOAD-ALIAS");
|
||||
|
||||
dto.ShouldNotBeNull();
|
||||
dto!.TagId.ShouldBe("TAG-LOAD-ALIAS");
|
||||
dto.Name.ShouldBe("loaded-speed");
|
||||
dto.DriverInstanceId.ShouldBe(GatewayDriverId);
|
||||
dto.DataType.ShouldBe("Float");
|
||||
dto.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
|
||||
dto.FullName.ShouldBe("TestMachine_020.LoadedSpeed");
|
||||
dto.RowVersion.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update changes the alias's name, data type, access level, and Galaxy reference, returning Ok and
|
||||
/// persisting the new values and the refreshed <c>{"FullName":…}</c> TagConfig.
|
||||
|
||||
Reference in New Issue
Block a user