diff --git a/src/ScadaLink.TemplateEngine/TemplateService.cs b/src/ScadaLink.TemplateEngine/TemplateService.cs index 46e5ef3..17cc646 100644 --- a/src/ScadaLink.TemplateEngine/TemplateService.cs +++ b/src/ScadaLink.TemplateEngine/TemplateService.cs @@ -767,8 +767,7 @@ public class TemplateService if (composedTemplate != null && composedTemplate.IsDerived && composedTemplate.OwnerCompositionId == compositionId) { - await _repository.DeleteTemplateAsync(composedTemplate.Id, cancellationToken); - await _auditService.LogAsync(user, "Delete", "Template", composedTemplate.Id.ToString(), composedTemplate.Name, null, cancellationToken); + await CascadeDeleteDerivedAsync(composedTemplate, user, cancellationToken); } await _repository.SaveChangesAsync(cancellationToken); @@ -776,6 +775,27 @@ public class TemplateService return Result.Success(true); } + /// + /// Recursively deletes a derived template along with the cascade of inner + /// derived templates the compose flow created. Each composition row on the + /// derived has its slot-owned child template removed first, then the row, + /// then the derived itself. + /// + private async Task CascadeDeleteDerivedAsync(Template derived, string user, CancellationToken cancellationToken) + { + foreach (var child in derived.Compositions.ToList()) + { + var childDerived = await _repository.GetTemplateByIdAsync(child.ComposedTemplateId, cancellationToken); + await _repository.DeleteTemplateCompositionAsync(child.Id, cancellationToken); + await _auditService.LogAsync(user, "Delete", "TemplateComposition", child.Id.ToString(), child.InstanceName, null, cancellationToken); + if (childDerived != null && childDerived.IsDerived && childDerived.OwnerCompositionId == child.Id) + await CascadeDeleteDerivedAsync(childDerived, user, cancellationToken); + } + + await _repository.DeleteTemplateAsync(derived.Id, cancellationToken); + await _auditService.LogAsync(user, "Delete", "Template", derived.Id.ToString(), derived.Name, null, cancellationToken); + } + private static Template BuildDerivedTemplate(Template baseTemplate, string derivedName) { var derived = new Template(derivedName) diff --git a/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs b/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs index 87ca49e..e3cff5d 100644 --- a/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs +++ b/tests/ScadaLink.TemplateEngine.Tests/TemplateServiceTests.cs @@ -436,6 +436,33 @@ public class TemplateServiceTests Assert.Contains("already exists", result.Error); } + [Fact] + public async Task DeleteComposition_CascadesNestedDerivedTemplates() + { + // Pump.TempSensor is a cascaded derived under Pump (outer derived) that + // is owned by composition 50. Deleting composition 50 must drop: + // - the outer derived (Pump) + // - its nested composition (TempSensor on Pump) + // - the nested derived (Pump.TempSensor) + var nestedComp = new TemplateComposition("TempSensor") { Id = 51, TemplateId = 77, ComposedTemplateId = 78 }; + var nestedDerived = new Template("Tank.Pump.TempSensor") { Id = 78, IsDerived = true, OwnerCompositionId = 51, ParentTemplateId = 3 }; + var outerComposition = new TemplateComposition("Pump") { Id = 50, TemplateId = 1, ComposedTemplateId = 77 }; + var outerDerived = new Template("Tank.Pump") { Id = 77, IsDerived = true, OwnerCompositionId = 50, ParentTemplateId = 2 }; + outerDerived.Compositions.Add(nestedComp); + + _repoMock.Setup(r => r.GetTemplateCompositionByIdAsync(50, It.IsAny())).ReturnsAsync(outerComposition); + _repoMock.Setup(r => r.GetTemplateByIdAsync(77, It.IsAny())).ReturnsAsync(outerDerived); + _repoMock.Setup(r => r.GetTemplateByIdAsync(78, It.IsAny())).ReturnsAsync(nestedDerived); + + var result = await _service.DeleteCompositionAsync(50, "admin"); + + Assert.True(result.IsSuccess); + _repoMock.Verify(r => r.DeleteTemplateCompositionAsync(50, It.IsAny()), Times.Once); + _repoMock.Verify(r => r.DeleteTemplateCompositionAsync(51, It.IsAny()), Times.Once); + _repoMock.Verify(r => r.DeleteTemplateAsync(77, It.IsAny()), Times.Once); + _repoMock.Verify(r => r.DeleteTemplateAsync(78, It.IsAny()), Times.Once); + } + [Fact] public async Task DeleteComposition_CascadesDerivedTemplate() {