fix(transport): wire TemplateFolder FK on imported templates

ApplyTemplatesAsync built the Template entity via BuildTemplate() but
never read the DTO's FolderName -- so every imported template landed
at the root regardless of which folder it lived in on the source
cluster. ApplyTemplateFoldersAsync had already flushed the folder
rows by that point; the FK just wasn't being set.

Resolve folder-name -> persisted FolderId from the same name table
(via _templateRepo.GetAllFoldersAsync after the folder pass), honour
TemplateFolder Rename resolutions, and set FolderId on Add /
Overwrite / Rename paths alike. The audit-row 'after' state now
includes FolderId so the action's effect is visible in the
configuration audit log.
This commit is contained in:
Joseph Doherty
2026-05-24 08:54:06 -04:00
parent 1361a39770
commit f6f7cb8b36

View File

@@ -822,9 +822,30 @@ public sealed class BundleImporter : IBundleImporter
var stubs = await _templateRepo.GetAllTemplatesAsync(ct).ConfigureAwait(false); var stubs = await _templateRepo.GetAllTemplatesAsync(ct).ConfigureAwait(false);
var byName = stubs.ToDictionary(t => t.Name, t => t, StringComparer.Ordinal); var byName = stubs.ToDictionary(t => t.Name, t => t, StringComparer.Ordinal);
// ApplyTemplateFoldersAsync has already flushed every imported folder
// (and target folders pre-exist), so name-keyed lookup resolves to the
// persisted FolderId every imported template should reference.
var folders = await _templateRepo.GetAllFoldersAsync(ct).ConfigureAwait(false);
var folderIdByName = folders.ToDictionary(f => f.Name, f => f.Id, StringComparer.Ordinal);
// Honour folder renames -- a TemplateFolder resolved as Rename was
// written under its new name; templates that reference the old name in
// the bundle DTO must map to the renamed folder, not be orphaned.
int? ResolveFolderId(string? folderName)
{
if (folderName is null) return null;
var folderResolution = ResolveOrDefault(map, "TemplateFolder", folderName);
var lookupName = (folderResolution.Action == ResolutionAction.Rename
&& !string.IsNullOrEmpty(folderResolution.RenameTo))
? folderResolution.RenameTo
: folderName;
return folderIdByName.TryGetValue(lookupName, out var fid) ? fid : (int?)null;
}
foreach (var dto in dtos) foreach (var dto in dtos)
{ {
var resolution = ResolveOrDefault(map, "Template", dto.Name); var resolution = ResolveOrDefault(map, "Template", dto.Name);
var folderId = ResolveFolderId(dto.FolderName);
switch (resolution.Action) switch (resolution.Action)
{ {
case ResolutionAction.Skip: case ResolutionAction.Skip:
@@ -834,17 +855,19 @@ public sealed class BundleImporter : IBundleImporter
{ {
var name = resolution.RenameTo ?? dto.Name; var name = resolution.RenameTo ?? dto.Name;
var t = BuildTemplate(dto, overrideName: name); var t = BuildTemplate(dto, overrideName: name);
t.FolderId = folderId;
await _templateRepo.AddTemplateAsync(t, ct).ConfigureAwait(false); await _templateRepo.AddTemplateAsync(t, ct).ConfigureAwait(false);
await _auditService.LogAsync(user, "Create", "Template", "0", name, await _auditService.LogAsync(user, "Create", "Template", "0", name,
new { Name = name, dto.Description, RenamedFrom = dto.Name }, ct).ConfigureAwait(false); new { Name = name, dto.Description, FolderId = folderId, RenamedFrom = dto.Name }, ct).ConfigureAwait(false);
summary.Renamed++; summary.Renamed++;
break; break;
} }
case ResolutionAction.Overwrite when byName.TryGetValue(dto.Name, out var ex): case ResolutionAction.Overwrite when byName.TryGetValue(dto.Name, out var ex):
ex.Description = dto.Description; ex.Description = dto.Description;
ex.FolderId = folderId;
await _templateRepo.UpdateTemplateAsync(ex, ct).ConfigureAwait(false); await _templateRepo.UpdateTemplateAsync(ex, ct).ConfigureAwait(false);
await _auditService.LogAsync(user, "Update", "Template", ex.Id.ToString(), ex.Name, await _auditService.LogAsync(user, "Update", "Template", ex.Id.ToString(), ex.Name,
new { ex.Name, ex.Description }, ct).ConfigureAwait(false); new { ex.Name, ex.Description, ex.FolderId }, ct).ConfigureAwait(false);
summary.Overwritten++; summary.Overwritten++;
break; break;
case ResolutionAction.Add: case ResolutionAction.Add:
@@ -852,9 +875,10 @@ public sealed class BundleImporter : IBundleImporter
default: default:
{ {
var t = BuildTemplate(dto, overrideName: null); var t = BuildTemplate(dto, overrideName: null);
t.FolderId = folderId;
await _templateRepo.AddTemplateAsync(t, ct).ConfigureAwait(false); await _templateRepo.AddTemplateAsync(t, ct).ConfigureAwait(false);
await _auditService.LogAsync(user, "Create", "Template", "0", t.Name, await _auditService.LogAsync(user, "Create", "Template", "0", t.Name,
new { t.Name, t.Description }, ct).ConfigureAwait(false); new { t.Name, t.Description, t.FolderId }, ct).ConfigureAwait(false);
summary.Added++; summary.Added++;
break; break;
} }