feat(adminui): reject {{equip}} virtual tags whose equipment has no derivable base
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
|
||||
/// <summary>Extracts the driver-side <c>FullName</c> from a <c>Tag.TagConfig</c> JSON blob.
|
||||
/// Mirrors <c>ScriptTagCatalog.ExtractFullNameFromTagConfig</c> (kept as a small local copy so
|
||||
/// VirtualTag validation does not depend on the ScriptAnalysis catalog). Falls back to the raw
|
||||
/// blob when it is not a JSON object with a string <c>FullName</c>.</summary>
|
||||
internal static class TagConfigFullName
|
||||
{
|
||||
/// <summary>Extract the FullName, or the raw blob when absent.</summary>
|
||||
/// <param name="tagConfig">The Tag.TagConfig JSON.</param>
|
||||
/// <returns>The FullName string, or the raw blob.</returns>
|
||||
public static string Extract(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig ?? string.Empty;
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(tagConfig);
|
||||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("FullName", out var fullName)
|
||||
&& fullName.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
return fullName.GetString() ?? tagConfig;
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException) { /* fall through */ }
|
||||
return tagConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
@@ -889,6 +890,34 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the bound script uses the reserved <c>{{equip}}</c> token, the owning equipment must have
|
||||
/// a derivable tag base (≥1 driver tag, all sharing one object prefix). Returns a rejection result
|
||||
/// when the token is present but no base can be derived; <c>null</c> otherwise (token absent, or a
|
||||
/// base exists). The script source is fetched from the supplied (in-scope) context.
|
||||
/// </summary>
|
||||
private static async Task<UnsMutationResult?> ValidateEquipTokenAsync(
|
||||
OtOpcUaConfigDbContext db, string equipmentId, string scriptId, CancellationToken ct)
|
||||
{
|
||||
var src = await db.Scripts.Where(s => s.ScriptId == scriptId)
|
||||
.Select(s => s.SourceCode).FirstOrDefaultAsync(ct);
|
||||
if (!EquipmentScriptPaths.ContainsEquipToken(src)) return null;
|
||||
|
||||
var configs = await db.Tags.Where(t => t.EquipmentId == equipmentId)
|
||||
.Select(t => t.TagConfig).ToListAsync(ct);
|
||||
var fullNames = configs.Select(TagConfigFullName.Extract);
|
||||
if (EquipmentScriptPaths.DeriveEquipmentBase(fullNames) is null)
|
||||
{
|
||||
// NOTE: literal {{equip}} must survive — in a C# interpolated string `{{`/`}}`
|
||||
// collapse to single braces, so keep the token text in a NON-interpolated segment.
|
||||
return new UnsMutationResult(false,
|
||||
$"Equipment '{equipmentId}' has no single tag base, so the "
|
||||
+ "{{equip}} token can't be resolved. Add at least one driver tag under this "
|
||||
+ "equipment (all sharing one object prefix), or remove {{equip}} from the script.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UnsMutationResult> CreateVirtualTagAsync(
|
||||
string equipmentId,
|
||||
@@ -918,6 +947,9 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
return new UnsMutationResult(false, $"A virtual tag named '{input.Name}' already exists on this equipment.");
|
||||
}
|
||||
|
||||
var equipGuard = await ValidateEquipTokenAsync(db, equipmentId, input.ScriptId, ct);
|
||||
if (equipGuard is not null) return equipGuard.Value;
|
||||
|
||||
db.VirtualTags.Add(new VirtualTag
|
||||
{
|
||||
VirtualTagId = input.VirtualTagId,
|
||||
@@ -964,6 +996,9 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
return new UnsMutationResult(false, $"A virtual tag named '{input.Name}' already exists on this equipment.");
|
||||
}
|
||||
|
||||
var equipGuard = await ValidateEquipTokenAsync(db, entity.EquipmentId, input.ScriptId, ct);
|
||||
if (equipGuard is not null) return equipGuard.Value;
|
||||
|
||||
// EquipmentId is preserved — virtual tags are always equipment-bound (plan decision #2).
|
||||
entity.Name = input.Name;
|
||||
entity.DataType = input.DataType;
|
||||
|
||||
Reference in New Issue
Block a user