fix(transport): carry TemplateAlarm.OnTriggerScript by name in bundle DTO
This commit is contained in:
@@ -78,7 +78,8 @@ public sealed record TemplateAlarmDto(
|
|||||||
int PriorityLevel,
|
int PriorityLevel,
|
||||||
AlarmTriggerType TriggerType,
|
AlarmTriggerType TriggerType,
|
||||||
string? TriggerConfiguration,
|
string? TriggerConfiguration,
|
||||||
bool IsLocked);
|
bool IsLocked,
|
||||||
|
string? OnTriggerScriptName);
|
||||||
|
|
||||||
public sealed record TemplateScriptDto(
|
public sealed record TemplateScriptDto(
|
||||||
string Name,
|
string Name,
|
||||||
|
|||||||
@@ -29,7 +29,15 @@ public sealed class EntitySerializer
|
|||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
ParentName: f.ParentFolderId is { } pid && folderNameById.TryGetValue(pid, out var pname) ? pname : null,
|
ParentName: f.ParentFolderId is { } pid && folderNameById.TryGetValue(pid, out var pname) ? pname : null,
|
||||||
SortOrder: f.SortOrder)).ToList(),
|
SortOrder: f.SortOrder)).ToList(),
|
||||||
Templates: aggregate.Templates.Select(t => new TemplateDto(
|
Templates: aggregate.Templates.Select(t =>
|
||||||
|
{
|
||||||
|
// Build per-template script-id → name lookup once so the alarm
|
||||||
|
// projection below resolves OnTriggerScriptId by name in O(1).
|
||||||
|
// Scripts can only target sibling scripts on the same template
|
||||||
|
// (TemplateAlarm.OnTriggerScriptId FK is scoped to TemplateId),
|
||||||
|
// so we don't need a global script index.
|
||||||
|
var scriptNameById = t.Scripts.ToDictionary(s => s.Id, s => s.Name);
|
||||||
|
return new TemplateDto(
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
FolderName: t.FolderId is { } fid && folderNameById.TryGetValue(fid, out var fname) ? fname : null,
|
FolderName: t.FolderId is { } fid && folderNameById.TryGetValue(fid, out var fname) ? fname : null,
|
||||||
BaseTemplateName: t.ParentTemplateId is { } btid && templateNameById.TryGetValue(btid, out var bname) ? bname : null,
|
BaseTemplateName: t.ParentTemplateId is { } btid && templateNameById.TryGetValue(btid, out var bname) ? bname : null,
|
||||||
@@ -47,7 +55,14 @@ public sealed class EntitySerializer
|
|||||||
PriorityLevel: a.PriorityLevel,
|
PriorityLevel: a.PriorityLevel,
|
||||||
TriggerType: a.TriggerType,
|
TriggerType: a.TriggerType,
|
||||||
TriggerConfiguration: a.TriggerConfiguration,
|
TriggerConfiguration: a.TriggerConfiguration,
|
||||||
IsLocked: a.IsLocked)).ToList(),
|
IsLocked: a.IsLocked,
|
||||||
|
// Carry the on-trigger script by NAME — the importer resolves
|
||||||
|
// this back to a script id once the parent template's scripts
|
||||||
|
// have been persisted and assigned ids. If the FK doesn't
|
||||||
|
// resolve in this aggregate (e.g. corrupt/orphan row), the
|
||||||
|
// name comes through as null and the importer leaves the
|
||||||
|
// FK null on the imported alarm.
|
||||||
|
OnTriggerScriptName: a.OnTriggerScriptId is { } sid && scriptNameById.TryGetValue(sid, out var sn) ? sn : null)).ToList(),
|
||||||
Scripts: t.Scripts.Select(s => new TemplateScriptDto(
|
Scripts: t.Scripts.Select(s => new TemplateScriptDto(
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Code: s.Code,
|
Code: s.Code,
|
||||||
@@ -59,7 +74,8 @@ public sealed class EntitySerializer
|
|||||||
MinTimeBetweenRuns: s.MinTimeBetweenRuns)).ToList(),
|
MinTimeBetweenRuns: s.MinTimeBetweenRuns)).ToList(),
|
||||||
Compositions: t.Compositions.Select(c => new TemplateCompositionDto(
|
Compositions: t.Compositions.Select(c => new TemplateCompositionDto(
|
||||||
InstanceName: c.InstanceName,
|
InstanceName: c.InstanceName,
|
||||||
ComposedTemplateName: templateNameById.TryGetValue(c.ComposedTemplateId, out var cn) ? cn : string.Empty)).ToList())).ToList(),
|
ComposedTemplateName: templateNameById.TryGetValue(c.ComposedTemplateId, out var cn) ? cn : string.Empty)).ToList());
|
||||||
|
}).ToList(),
|
||||||
SharedScripts: aggregate.SharedScripts.Select(s => new SharedScriptDto(
|
SharedScripts: aggregate.SharedScripts.Select(s => new SharedScriptDto(
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Code: s.Code,
|
Code: s.Code,
|
||||||
|
|||||||
@@ -79,15 +79,6 @@ public sealed class EntitySerializerTests
|
|||||||
IsLocked = true,
|
IsLocked = true,
|
||||||
Description = "PSI",
|
Description = "PSI",
|
||||||
});
|
});
|
||||||
basic.Alarms.Add(new TemplateAlarm("High")
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
TemplateId = 1,
|
|
||||||
PriorityLevel = 2,
|
|
||||||
TriggerType = AlarmTriggerType.RangeViolation,
|
|
||||||
TriggerConfiguration = "{\"threshold\":100}",
|
|
||||||
IsLocked = false,
|
|
||||||
});
|
|
||||||
basic.Scripts.Add(new TemplateScript("OnUpdate", "return 1;")
|
basic.Scripts.Add(new TemplateScript("OnUpdate", "return 1;")
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
@@ -98,6 +89,19 @@ public sealed class EntitySerializerTests
|
|||||||
IsLocked = false,
|
IsLocked = false,
|
||||||
MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
|
MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
|
||||||
});
|
});
|
||||||
|
basic.Alarms.Add(new TemplateAlarm("High")
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
TemplateId = 1,
|
||||||
|
PriorityLevel = 2,
|
||||||
|
TriggerType = AlarmTriggerType.RangeViolation,
|
||||||
|
TriggerConfiguration = "{\"threshold\":100}",
|
||||||
|
IsLocked = false,
|
||||||
|
// FU-B / #37 — alarm fires "OnUpdate" script when triggered. The FK is
|
||||||
|
// an in-aggregate script id; the DTO carries the name and the
|
||||||
|
// importer resolves the FK on the way back in.
|
||||||
|
OnTriggerScriptId = 1,
|
||||||
|
});
|
||||||
|
|
||||||
var assembly = new Template("Assembly") { Id = 2, FolderId = 1 };
|
var assembly = new Template("Assembly") { Id = 2, FolderId = 1 };
|
||||||
assembly.Compositions.Add(new TemplateComposition("MotorA")
|
assembly.Compositions.Add(new TemplateComposition("MotorA")
|
||||||
@@ -115,6 +119,15 @@ public sealed class EntitySerializerTests
|
|||||||
|
|
||||||
var sut = new EntitySerializer();
|
var sut = new EntitySerializer();
|
||||||
var dto = sut.ToBundleContent(aggregate);
|
var dto = sut.ToBundleContent(aggregate);
|
||||||
|
|
||||||
|
// FU-B / #37 — verify the DTO carries OnTriggerScriptName by NAME (not id).
|
||||||
|
// The bundle is portable across environments so a script-id FK can't
|
||||||
|
// survive a round-trip; resolution back to a script id is the importer's
|
||||||
|
// job once the parent template's scripts have been re-persisted.
|
||||||
|
var dtoBasic = Assert.Single(dto.Templates, t => t.Name == "Basic");
|
||||||
|
var dtoAlarm = Assert.Single(dtoBasic.Alarms);
|
||||||
|
Assert.Equal("OnUpdate", dtoAlarm.OnTriggerScriptName);
|
||||||
|
|
||||||
var roundTripped = sut.FromBundleContent(dto);
|
var roundTripped = sut.FromBundleContent(dto);
|
||||||
|
|
||||||
var rtBasic = Assert.Single(roundTripped.Templates, t => t.Name == "Basic");
|
var rtBasic = Assert.Single(roundTripped.Templates, t => t.Name == "Basic");
|
||||||
@@ -129,6 +142,9 @@ public sealed class EntitySerializerTests
|
|||||||
Assert.Equal(AlarmTriggerType.RangeViolation, rtAlarm.TriggerType);
|
Assert.Equal(AlarmTriggerType.RangeViolation, rtAlarm.TriggerType);
|
||||||
Assert.Equal("{\"threshold\":100}", rtAlarm.TriggerConfiguration);
|
Assert.Equal("{\"threshold\":100}", rtAlarm.TriggerConfiguration);
|
||||||
Assert.Equal(2, rtAlarm.PriorityLevel);
|
Assert.Equal(2, rtAlarm.PriorityLevel);
|
||||||
|
// FromBundleContent leaves OnTriggerScriptId null — the importer
|
||||||
|
// resolves it post-persist when target-DB script ids are known.
|
||||||
|
Assert.Null(rtAlarm.OnTriggerScriptId);
|
||||||
|
|
||||||
var rtScript = Assert.Single(rtBasic.Scripts);
|
var rtScript = Assert.Single(rtBasic.Scripts);
|
||||||
Assert.Equal("OnUpdate", rtScript.Name);
|
Assert.Equal("OnUpdate", rtScript.Name);
|
||||||
|
|||||||
Reference in New Issue
Block a user