diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs index d34dd51a..875d3758 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs @@ -8,8 +8,8 @@ public record DeleteTemplateCommand(int TemplateId); public record ValidateTemplateCommand(int TemplateId); // Template member operations -public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked); -public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked); +public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null); +public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null); public record DeleteTemplateAttributeCommand(int AttributeId); public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked); public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked); diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index 8456c69e..a99a0729 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -1442,9 +1442,13 @@ public class ManagementActor : ReceiveActor private static async Task HandleAddAttribute(IServiceProvider sp, AddTemplateAttributeCommand cmd, string user) { var svc = sp.GetRequiredService(); + var dataType = Enum.Parse(cmd.DataType, ignoreCase: true); + var elementType = ParseElementDataType(cmd.ElementDataType); + ValidateAttributeTypes(cmd.Name, dataType, elementType, cmd.Value); var attr = new TemplateAttribute(cmd.Name) { - DataType = Enum.Parse(cmd.DataType, ignoreCase: true), + DataType = dataType, + ElementDataType = elementType, Value = cmd.Value, Description = cmd.Description, DataSourceReference = cmd.DataSourceReference, @@ -1457,9 +1461,13 @@ public class ManagementActor : ReceiveActor private static async Task HandleUpdateAttribute(IServiceProvider sp, UpdateTemplateAttributeCommand cmd, string user) { var svc = sp.GetRequiredService(); + var dataType = Enum.Parse(cmd.DataType, ignoreCase: true); + var elementType = ParseElementDataType(cmd.ElementDataType); + ValidateAttributeTypes(cmd.Name, dataType, elementType, cmd.Value); var attr = new TemplateAttribute(cmd.Name) { - DataType = Enum.Parse(cmd.DataType, ignoreCase: true), + DataType = dataType, + ElementDataType = elementType, Value = cmd.Value, Description = cmd.Description, DataSourceReference = cmd.DataSourceReference, @@ -1469,6 +1477,57 @@ public class ManagementActor : ReceiveActor return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error); } + /// + /// Parses an optional element data type token. Returns null when the token is + /// empty/whitespace; throws on an + /// unrecognised type name. + /// + private static Commons.Types.Enums.DataType? ParseElementDataType(string? elementDataType) + { + if (string.IsNullOrWhiteSpace(elementDataType)) return null; + if (!Enum.TryParse(elementDataType, ignoreCase: true, out var parsed)) + throw new ManagementCommandException($"Unrecognised element type '{elementDataType}'."); + return parsed; + } + + /// + /// Validates the (DataType, ElementDataType, Value) triple shared by the add + /// and update attribute handlers. Throws + /// on any violation: + /// + /// List attributes require a valid scalar element type. + /// Scalar attributes may not carry an element type. + /// A List default value must decode against the declared element type. + /// + /// + private static void ValidateAttributeTypes( + string name, Commons.Types.Enums.DataType dataType, Commons.Types.Enums.DataType? elementType, string? value) + { + if (dataType == Commons.Types.Enums.DataType.List) + { + if (elementType is null || !Commons.Types.AttributeValueCodec.IsValidElementType(elementType.Value)) + throw new ManagementCommandException( + $"List attribute '{name}' requires a valid element type (String, Int32, Float, Double, Boolean, DateTime)."); + + if (!string.IsNullOrWhiteSpace(value)) + { + try + { + Commons.Types.AttributeValueCodec.Decode(value, Commons.Types.Enums.DataType.List, elementType); + } + catch (FormatException ex) + { + throw new ManagementCommandException( + $"List attribute '{name}' has an invalid list value: {ex.Message}"); + } + } + } + else if (elementType is not null) + { + throw new ManagementCommandException("Element type is only valid on List attributes."); + } + } + private static async Task HandleDeleteAttribute(IServiceProvider sp, DeleteTemplateAttributeCommand cmd, string user) { var svc = sp.GetRequiredService(); diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs index 770fa3dc..da5eea7b 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs @@ -387,6 +387,140 @@ public class ManagementActorTests : TestKit, IDisposable Assert.Contains("Designer", response.Message); } + // ======================================================================== + // MV-10: ElementDataType accept + validate on attribute add/update + // ======================================================================== + + [Fact] + public void AddListAttribute_WithStringElementType_PersistsBothColumns() + { + // A template exists with no attributes; AddAttributeAsync will save the + // entity built by the handler. Capture it to assert the persisted shape. + var template = new Template("T1") { Id = 1 }; + _templateRepo.GetTemplateByIdAsync(1, Arg.Any()).Returns(template); + _templateRepo.GetAllTemplatesAsync(Arg.Any()) + .Returns(new List