fix(templates): cascade child compositions when composing a composite
When the user composes a template that already has compositions of its own (e.g. \$Sensor → Probe1 slot), only the outer derived was created — the source's children weren't replicated. AddCompositionAsync now walks the source's composition graph and creates a parallel derived for every slot it encounters, each linked back through ParentTemplateId so override chains stay intact (\$Probe → \$Sensor.Probe1 → \$Pump.TempSensor.Probe1). The cascade pre-flights every name it would create — a deep collision aborts before any rows mutate. Internal helper CreateCascadedCompositionAsync skips the "base templates only" check since it operates on the source side which may legitimately reference derived rows.
This commit is contained in:
@@ -321,6 +321,49 @@ public class TemplateServiceTests
|
||||
_repoMock.Verify(r => r.AddTemplateCompositionAsync(It.IsAny<TemplateComposition>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComposition_CascadesChildCompositions()
|
||||
{
|
||||
// $Probe (base) → $Sensor.Probe1 (derived) ← $Sensor composes "Probe1"
|
||||
// Composing $Sensor into $Pump as "TempSensor" should produce:
|
||||
// $Pump.TempSensor (derived from $Sensor)
|
||||
// $Pump.TempSensor.Probe1 (derived from $Sensor.Probe1)
|
||||
// plus a composition row on $Pump.TempSensor pointing at the new inner derived.
|
||||
var probe = new Template("Probe") { Id = 10 };
|
||||
var sensorProbe1 = new Template("Sensor.Probe1") { Id = 11, IsDerived = true, ParentTemplateId = 10, OwnerCompositionId = 1 };
|
||||
var sensor = new Template("Sensor") { Id = 2 };
|
||||
sensor.Compositions.Add(new TemplateComposition("Probe1") { Id = 1, TemplateId = 2, ComposedTemplateId = 11 });
|
||||
var pump = new Template("Pump") { Id = 1 };
|
||||
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(pump);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(sensor);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(11, It.IsAny<CancellationToken>())).ReturnsAsync(sensorProbe1);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { pump, sensor, probe, sensorProbe1 });
|
||||
|
||||
var captured = new List<Template>();
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<Template, CancellationToken>((t, _) => captured.Add(t))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var capturedCompositions = new List<TemplateComposition>();
|
||||
_repoMock.Setup(r => r.AddTemplateCompositionAsync(It.IsAny<TemplateComposition>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<TemplateComposition, CancellationToken>((c, _) => capturedCompositions.Add(c))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var result = await _service.AddCompositionAsync(1, 2, "TempSensor", "admin");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(2, captured.Count);
|
||||
Assert.Equal("Pump.TempSensor", captured[0].Name);
|
||||
Assert.Equal(2, captured[0].ParentTemplateId);
|
||||
Assert.Equal("Pump.TempSensor.Probe1", captured[1].Name);
|
||||
Assert.Equal(11, captured[1].ParentTemplateId);
|
||||
Assert.Equal(2, capturedCompositions.Count);
|
||||
Assert.Equal("TempSensor", capturedCompositions[0].InstanceName);
|
||||
Assert.Equal("Probe1", capturedCompositions[1].InstanceName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComposition_BaseIsDerived_Fails()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user