fix(transport): wire template inheritance edges on bundle import (C3) — derived templates no longer land as roots
This commit is contained in:
@@ -1042,6 +1042,7 @@ public sealed class BundleImporter : IBundleImporter
|
||||
await _dbContext.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
await ResolveAlarmScriptLinksAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
|
||||
await ResolveCompositionEdgesAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
|
||||
await ResolveInheritanceEdgesAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
|
||||
|
||||
// ---- Site/instance-scoped apply: instances LAST ----
|
||||
// The instance pass needs every FK target materialised first:
|
||||
@@ -2092,6 +2093,91 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second-pass rewire for template inheritance edges (<c>ParentTemplateId</c>),
|
||||
/// mirroring <see cref="ResolveCompositionEdgesAsync"/>. The bundle carries the
|
||||
/// base by name (<c>TemplateDto.BaseTemplateName</c>); the create/overwrite pass
|
||||
/// never sets the FK, so without this pass a derived template lands as a root and
|
||||
/// its inheritance is silently lost (arch-review C3). Runs after templates are
|
||||
/// flushed so both the derived template and its base are resolvable by name.
|
||||
/// Never throws — an unresolvable base leaves the edge null and records an audit
|
||||
/// row, keeping the import all-or-nothing on real faults only.
|
||||
/// </summary>
|
||||
private async Task ResolveInheritanceEdgesAsync(
|
||||
IReadOnlyList<TemplateDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> resolutionMap,
|
||||
string user,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (dtos.Count == 0) return;
|
||||
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
var resolution = ResolveOrDefault(resolutionMap, "Template", dto.Name);
|
||||
if (resolution.Action == ResolutionAction.Skip) continue;
|
||||
|
||||
var importedName = resolution.Action == ResolutionAction.Rename
|
||||
? (resolution.RenameTo ?? dto.Name)
|
||||
: dto.Name;
|
||||
|
||||
// The just-imported/updated template is tracked in the Local set.
|
||||
var template = _dbContext.Templates.Local.FirstOrDefault(t =>
|
||||
string.Equals(t.Name, importedName, StringComparison.Ordinal));
|
||||
if (template is null) continue;
|
||||
|
||||
// A root-template bundle — or an Overwrite that dropped the base —
|
||||
// must clear any stale parent edge on the target row.
|
||||
if (string.IsNullOrEmpty(dto.BaseTemplateName))
|
||||
{
|
||||
template.ParentTemplateId = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve the base template's PERSISTED name through the resolution
|
||||
// map (it may have been renamed on import), then look it up by name —
|
||||
// first in the tracked Local set (anything imported this run), then in
|
||||
// the wider target DB (pre-existing rows not staged in this transaction).
|
||||
var baseResolution = ResolveOrDefault(resolutionMap, "Template", dto.BaseTemplateName);
|
||||
var baseName = baseResolution.Action == ResolutionAction.Rename
|
||||
? (baseResolution.RenameTo ?? dto.BaseTemplateName)
|
||||
: dto.BaseTemplateName;
|
||||
|
||||
var baseStub = _dbContext.Templates.Local.FirstOrDefault(t =>
|
||||
string.Equals(t.Name, baseName, StringComparison.Ordinal));
|
||||
int? baseId = baseStub?.Id;
|
||||
if (baseId is null)
|
||||
{
|
||||
var allTargets = await _templateRepo.GetAllTemplatesAsync(ct).ConfigureAwait(false);
|
||||
baseId = allTargets
|
||||
.FirstOrDefault(t => string.Equals(t.Name, baseName, StringComparison.Ordinal))?.Id;
|
||||
}
|
||||
|
||||
if (baseId is null)
|
||||
{
|
||||
// Unresolvable base (e.g. it was Skip-resolved and isn't in the
|
||||
// target). Leave the edge null and record it; never throw.
|
||||
template.ParentTemplateId = null;
|
||||
await _auditService.LogAsync(
|
||||
user,
|
||||
"BundleImportBaseTemplateUnresolved",
|
||||
"Template",
|
||||
"0",
|
||||
importedName,
|
||||
new
|
||||
{
|
||||
DerivedTemplateName = importedName,
|
||||
UnresolvedBaseTemplateName = dto.BaseTemplateName,
|
||||
ResolvedBaseName = baseName,
|
||||
Reason = "Base template name not present in bundle or target.",
|
||||
},
|
||||
ct).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
template.ParentTemplateId = baseId.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplySharedScriptsAsync(
|
||||
IReadOnlyList<SharedScriptDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> map,
|
||||
|
||||
Reference in New Issue
Block a user