feat(transport): import name-map plumbing via CLI + ManagementActor (M8 D3)
This commit is contained in:
@@ -2421,7 +2421,11 @@ public class ManagementActor : ReceiveActor
|
||||
var mods = preview.Items.Count(i => i.Kind == ConflictKind.Modified);
|
||||
var ids = preview.Items.Count(i => i.Kind == ConflictKind.Identical);
|
||||
var blocks = preview.Items.Count(i => i.Kind == ConflictKind.Blocker);
|
||||
return new PreviewBundleResult(preview.Items, adds, mods, ids, blocks);
|
||||
// M8 (D3): surface the required site/connection mappings so an operator
|
||||
// (CLI or UI) sees which references need resolving before applying.
|
||||
return new PreviewBundleResult(
|
||||
preview.Items, adds, mods, ids, blocks,
|
||||
preview.RequiredSiteMappings, preview.RequiredConnectionMappings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -2462,10 +2466,19 @@ public class ManagementActor : ReceiveActor
|
||||
$"Bundle has {blockers.Count} blocker(s); import aborted. {details}");
|
||||
}
|
||||
|
||||
// M8 (D3): resolve every required site/connection reference into a
|
||||
// BundleNameMap. Precedence: an explicit operator spec wins; otherwise
|
||||
// fall back to the preview's auto-match; otherwise (no match) create-new
|
||||
// ONLY if the create-missing flag is set, else fail with a clear message.
|
||||
var nameMap = BuildNameMap(cmd, preview);
|
||||
|
||||
// Dedupe by (EntityType, Name) -- the preview can emit multiple rows per
|
||||
// entity (e.g. one row per modified member of a template), but ApplyAsync
|
||||
// requires a unique resolution per key. Last-write-wins matches the
|
||||
// Central UI's TransportImport.BuildDefaultResolutions behavior.
|
||||
// Central UI's TransportImport.BuildDefaultResolutions behavior. For
|
||||
// DataConnection rows the preview item Name is already site-qualified
|
||||
// ({site}/{name}, D1-FIX), so keying generically by (EntityType, Name) is
|
||||
// automatically correct — no bare-connection-name special case needed.
|
||||
var renameStamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss");
|
||||
var resolutionsMap = new Dictionary<(string, string), ImportResolution>();
|
||||
foreach (var item in preview.Items)
|
||||
@@ -2484,7 +2497,96 @@ public class ManagementActor : ReceiveActor
|
||||
item.EntityType, item.Name, action, renameTo);
|
||||
}
|
||||
|
||||
return await importer.ApplyAsync(session.SessionId, resolutionsMap.Values.ToList(), username);
|
||||
return await importer.ApplyAsync(
|
||||
session.SessionId, resolutionsMap.Values.ToList(), username, nameMap: nameMap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the operator-supplied <see cref="SiteMappingSpec"/> /
|
||||
/// <see cref="ConnectionMappingSpec"/> lists with the preview's auto-matches
|
||||
/// and the create-missing flags into a <see cref="BundleNameMap"/>.
|
||||
/// <para>
|
||||
/// Per required site reference: an explicit spec wins (target present →
|
||||
/// <see cref="MappingAction.MapToExisting"/>; null/blank target →
|
||||
/// <see cref="MappingAction.CreateNew"/>); otherwise the preview's
|
||||
/// <see cref="RequiredSiteMapping.AutoMatchTargetIdentifier"/> is used when
|
||||
/// present (MapToExisting); otherwise CreateNew only when
|
||||
/// <see cref="ImportBundleCommand.CreateMissingSites"/> is set, else the
|
||||
/// reference is unresolved and the import fails. Connections mirror this with
|
||||
/// <see cref="ImportBundleCommand.CreateMissingConnections"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static BundleNameMap BuildNameMap(ImportBundleCommand cmd, ImportPreview preview)
|
||||
{
|
||||
var siteSpecs = (cmd.SiteMappings ?? Array.Empty<SiteMappingSpec>())
|
||||
.ToDictionary(s => s.SourceSiteIdentifier, StringComparer.Ordinal);
|
||||
var connSpecs = (cmd.ConnectionMappings ?? Array.Empty<ConnectionMappingSpec>())
|
||||
.ToDictionary(
|
||||
c => (c.SourceSiteIdentifier, c.SourceConnectionName),
|
||||
c => c);
|
||||
|
||||
var siteMappings = new List<SiteMapping>();
|
||||
var unresolved = new List<string>();
|
||||
foreach (var required in preview.RequiredSiteMappings)
|
||||
{
|
||||
if (siteSpecs.TryGetValue(required.SourceSiteIdentifier, out var spec))
|
||||
{
|
||||
siteMappings.Add(string.IsNullOrWhiteSpace(spec.TargetSiteIdentifier)
|
||||
? new SiteMapping(required.SourceSiteIdentifier, MappingAction.CreateNew, null)
|
||||
: new SiteMapping(required.SourceSiteIdentifier, MappingAction.MapToExisting, spec.TargetSiteIdentifier));
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(required.AutoMatchTargetIdentifier))
|
||||
{
|
||||
siteMappings.Add(new SiteMapping(
|
||||
required.SourceSiteIdentifier, MappingAction.MapToExisting, required.AutoMatchTargetIdentifier));
|
||||
}
|
||||
else if (cmd.CreateMissingSites)
|
||||
{
|
||||
siteMappings.Add(new SiteMapping(
|
||||
required.SourceSiteIdentifier, MappingAction.CreateNew, null));
|
||||
}
|
||||
else
|
||||
{
|
||||
unresolved.Add($"site '{required.SourceSiteIdentifier}'");
|
||||
}
|
||||
}
|
||||
|
||||
var connMappings = new List<ConnectionMapping>();
|
||||
foreach (var required in preview.RequiredConnectionMappings)
|
||||
{
|
||||
var key = (required.SourceSiteIdentifier, required.SourceConnectionName);
|
||||
if (connSpecs.TryGetValue(key, out var spec))
|
||||
{
|
||||
connMappings.Add(string.IsNullOrWhiteSpace(spec.TargetConnectionName)
|
||||
? new ConnectionMapping(required.SourceSiteIdentifier, required.SourceConnectionName, MappingAction.CreateNew, null)
|
||||
: new ConnectionMapping(required.SourceSiteIdentifier, required.SourceConnectionName, MappingAction.MapToExisting, spec.TargetConnectionName));
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(required.AutoMatchTargetName))
|
||||
{
|
||||
connMappings.Add(new ConnectionMapping(
|
||||
required.SourceSiteIdentifier, required.SourceConnectionName, MappingAction.MapToExisting, required.AutoMatchTargetName));
|
||||
}
|
||||
else if (cmd.CreateMissingConnections)
|
||||
{
|
||||
connMappings.Add(new ConnectionMapping(
|
||||
required.SourceSiteIdentifier, required.SourceConnectionName, MappingAction.CreateNew, null));
|
||||
}
|
||||
else
|
||||
{
|
||||
unresolved.Add($"connection '{required.SourceSiteIdentifier}/{required.SourceConnectionName}'");
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.Count > 0)
|
||||
{
|
||||
throw new ManagementCommandException(
|
||||
$"Import has {unresolved.Count} unresolved mapping(s): " +
|
||||
$"{string.Join(", ", unresolved.OrderBy(u => u, StringComparer.Ordinal))}. " +
|
||||
"Supply --map-site/--map-connection for each, or pass " +
|
||||
"--create-missing-sites/--create-missing-connections to create them from the bundle.");
|
||||
}
|
||||
|
||||
return new BundleNameMap(siteMappings, connMappings);
|
||||
}
|
||||
|
||||
private static byte[] DecodeBundle(string base64)
|
||||
|
||||
Reference in New Issue
Block a user