feat(templates): lock ParentTemplateId after creation

Template inheritance is set once at create time and immutable on update.
UpdateTemplateAsync now returns "Parent template cannot be changed after
creation." when the caller sends a parent that differs from the stored
value — server-side enforcement covers UI, ManagementService, and CLI.
TemplateEdit renders the parent as static plaintext rather than an
editable dropdown; TemplateCreate's parent picker is unchanged.
This commit is contained in:
Joseph Doherty
2026-05-11 21:29:21 -04:00
parent 8e388a89c5
commit b4cb7e6f5f
3 changed files with 43 additions and 42 deletions

View File

@@ -83,28 +83,16 @@ public class TemplateService
if (template == null)
return Result<Template>.Failure($"Template with ID {templateId} not found.");
// Validate parent change
if (parentTemplateId.HasValue && parentTemplateId.Value != (template.ParentTemplateId ?? 0))
// ParentTemplateId is immutable after creation — set once at create time.
// Reject any attempt to change it (null→value, value→null, or value→other).
if (parentTemplateId != template.ParentTemplateId)
{
var parent = await _repository.GetTemplateByIdAsync(parentTemplateId.Value, cancellationToken);
if (parent == null)
return Result<Template>.Failure($"Parent template with ID {parentTemplateId.Value} not found.");
// Check inheritance acyclicity
var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
var cycleError = CycleDetector.DetectInheritanceCycle(templateId, parentTemplateId.Value, allTemplates);
if (cycleError != null)
return Result<Template>.Failure(cycleError);
// Check cross-graph cycle
var crossCycleError = CycleDetector.DetectCrossGraphCycle(templateId, parentTemplateId, null, allTemplates);
if (crossCycleError != null)
return Result<Template>.Failure(crossCycleError);
return Result<Template>.Failure(
"Parent template cannot be changed after creation.");
}
template.Name = name;
template.Description = description;
template.ParentTemplateId = parentTemplateId;
// Check for naming collisions after the change
var collisionResult = await ValidateCollisionsAsync(template, cancellationToken);