fix(templates): cascade delete through nested derived templates

DeleteCompositionAsync only dropped the top-level derived template — the
cascaded inner derived rows (created when composing a composite source)
were left orphaned with dangling OwnerCompositionId references. Any
subsequent attempt to recompose the same source hit the name-collision
guard ('Motor Controller.Pump.TempSensor' already exists).

New CascadeDeleteDerivedAsync walks each composition on the derived
template, recursively removes the slot-owned child derived first, then
the composition row, then the derived itself. Mirrors the recursive
shape of CreateCascadedCompositionAsync.
This commit is contained in:
Joseph Doherty
2026-05-12 10:34:55 -04:00
parent 85769486df
commit 57f477fd28
2 changed files with 49 additions and 2 deletions

View File

@@ -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<bool>.Success(true);
}
/// <summary>
/// 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.
/// </summary>
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)