@@ -49,7 +49,7 @@ public sealed class BundleImporter : IBundleImporter
{
// All bundle content deserialization goes through BundleJsonOptions.Default.
// IMPORTANT — unknown JSON properties must remain ALLOWED (the default
// JsonUnmappedMemberHandling.Skip). Pre-C4 bundles may carry a top-level
// JsonUnmappedMemberHandling.Skip). Legacy bundles may carry a top-level
// "apiKeys" array and/or "ApprovedApiKeyIds" inside "apiMethods[]" entries.
// Setting JsonUnmappedMemberHandling.Disallow here would cause those bundles
// to fail deserialization, breaking backward-compat. This tolerance is
@@ -74,7 +74,7 @@ public sealed class BundleImporter : IBundleImporter
private readonly IOptions < TransportOptions > _options ;
private readonly TimeProvider _timeProvider ;
private readonly SemanticValidator _semanticValidator ;
// #16 (M8 D2): o ptional. Implemented in DeploymentManager (where the flattening
// O ptional. Implemented in DeploymentManager (where the flattening
// pipeline lives) and surfaced via the Commons IStaleInstanceProbe seam. Null
// in a host that registers Transport without DeploymentManager — staleness is
// then best-effort empty (informational only, never gates the import).
@@ -96,12 +96,12 @@ public sealed class BundleImporter : IBundleImporter
/// <param name="externalRepo">External system repository for diff and apply.</param>
/// <param name="notificationRepo">Notification repository for diff and apply.</param>
/// <param name="inboundApiRepo">Inbound API repository for diff and apply.</param>
/// <param name="siteRepo">Site repository — supplies the target sites and site-scoped data connections that the preview's site/connection auto-match resolves against (M8 C2) .</param>
/// <param name="siteRepo">Site repository — supplies the target sites and site-scoped data connections that the preview's site/connection auto-match resolves against.</param>
/// <param name="auditService">Audit service for writing per-entity import audit rows.</param>
/// <param name="correlationContext">Correlation context that carries the active BundleImportId.</param>
/// <param name="dbContext">EF Core context used to commit the import transaction.</param>
/// <param name="semanticValidator">Validates template references before applying.</param>
/// <param name="staleInstanceProbe">Optional (#16) : recomputes a deployed instance's current revision hash so the importer can enumerate instances stale-ed by a template overwrite. Null when no flattening pipeline is registered (Transport-without-DeploymentManager hosts) — staleness is then skipped.</param>
/// <param name="staleInstanceProbe">Optional: recomputes a deployed instance's current revision hash so the importer can enumerate instances stale-ed by a template overwrite. Null when no flattening pipeline is registered (Transport-without-DeploymentManager hosts) — staleness is then skipped.</param>
/// <param name="logger">Optional logger.</param>
public BundleImporter (
BundleSerializer bundleSerializer ,
@@ -371,7 +371,7 @@ public sealed class BundleImporter : IBundleImporter
}
// ---- Templates ----
// Transport-008: p reviously this loop iterated GetAllTemplatesAsync()
// P reviously this loop iterated GetAllTemplatesAsync()
// and called GetTemplateWithChildrenAsync(stub.Id) once per matching
// name (classic N+1). The bulk variant fetches every matching template
// with children eager-loaded in a single round-trip.
@@ -429,7 +429,7 @@ public sealed class BundleImporter : IBundleImporter
items . Add ( _diff . CompareSmtpConfiguration ( sm , existing ) ) ;
}
// ---- SmsConfigurations (S10b; no by-AccountSid lookup — scan GetAll) ----
// ---- SmsConfigurations (no by-AccountSid lookup — scan GetAll) ----
var allSms = await _notificationRepo . GetAllSmsConfigurationsAsync ( ct ) . ConfigureAwait ( false ) ;
var smsBySid = allSms . ToDictionary ( s = > s . AccountSid , s = > s , StringComparer . Ordinal ) ;
foreach ( var sms in content . SmsConfigs )
@@ -439,8 +439,8 @@ public sealed class BundleImporter : IBundleImporter
}
// ---- ApiKeys ----
// Inbound API keys are not transported between environments (re-arch C4) .
// New bundles never carry a keys section. A pre-C4 bundle may still contain
// Inbound API keys are not transported between environments.
// New bundles never carry a keys section. A legacy bundle may still contain
// one; we do NOT surface those keys as importable preview rows (they would
// offer Add/Overwrite actions for keys that can't be meaningfully re-created
// from a hash). They are counted, ignored, and reported at apply time.
@@ -452,7 +452,7 @@ public sealed class BundleImporter : IBundleImporter
items . Add ( _diff . CompareApiMethod ( m , existing ) ) ;
}
// ---- M8 s ite/instance-scoped types ----
// ---- S ite/instance-scoped types ----
// Sites/DataConnections/Instances are referenced by stable string identity
// (SiteIdentifier / connection Name / UniqueName) and resolved against the
// TARGET environment's own surrogate keys. We auto-match the source site by
@@ -493,7 +493,7 @@ public sealed class BundleImporter : IBundleImporter
}
// ---- DataConnections (site-scoped; matched by name within the auto-matched target site) ----
// C2: c onnection names are unique only WITHIN a site, so the preview item's
// Connection names are unique only WITHIN a site, so the preview item's
// identity is SITE-QUALIFIED (`{SiteIdentifier}/{Name}`). The diff CONTENT is
// unchanged — only the item Name is qualified (after CompareDataConnection
// returns) so two sites' same-named connections resolve to distinct items and
@@ -543,7 +543,7 @@ public sealed class BundleImporter : IBundleImporter
instDto , existing , existingTemplateName , existingSiteIdentifier , existingAreaName ) ) ;
}
// ---- Required site/connection mappings (M8) ----
// ---- Required site/connection mappings ----
var ( requiredSites , requiredConnections ) = await BuildRequiredMappingsAsync (
content , ResolveTargetSiteAsync , ResolveTargetConnectionsAsync ) . ConfigureAwait ( false ) ;
@@ -556,7 +556,7 @@ public sealed class BundleImporter : IBundleImporter
/// <summary>
/// Collects the distinct set of source sites and (site, connection) pairs the
/// bundle references, auto-matching each against the TARGET environment by
/// identity, and returns the operator-facing required-mapping lists (M8 C2) .
/// identity, and returns the operator-facing required-mapping lists.
/// <para>
/// Site references are drawn from every instance, every site, and every
/// data-connection in the bundle. Connection references are drawn from every
@@ -666,7 +666,7 @@ public sealed class BundleImporter : IBundleImporter
{
var blockers = new List < ImportPreviewItem > ( ) ;
// ---- M8: i nstance template + referenced-connection blockers ----
// ---- I nstance template + referenced-connection blockers ----
// The set of template names the import can satisfy = (in-bundle templates)
// ∪ (templates already in the target DB). An instance whose TemplateName is
// in neither is unresolvable.
@@ -735,7 +735,7 @@ public sealed class BundleImporter : IBundleImporter
if ( bundleConnections . Contains ( ( site , name ) ) ) continue ;
var targetConns = await resolveTargetConnections ( site ) . ConfigureAwait ( false ) ;
if ( targetConns . Any ( c = > string . Equals ( c . Name , name , StringComparison . Ordinal ) ) ) continue ;
// C2: s ite-qualify the blocker Name so two sites' same-named-but-missing
// S ite-qualify the blocker Name so two sites' same-named-but-missing
// connections surface as distinct blockers (a bare name would collide).
blockers . Add ( new ImportPreviewItem (
EntityType : "Instance" ,
@@ -888,7 +888,7 @@ public sealed class BundleImporter : IBundleImporter
private static bool IsIdentifierChar ( char c ) = > c = = '_' | | char . IsLetterOrDigit ( c ) ;
/// <summary>
/// C2: t he site-qualified identity (<c>{siteIdentifier}/{connectionName}</c>) used
/// T he site-qualified identity (<c>{siteIdentifier}/{connectionName}</c>) used
/// for every <c>DataConnection</c> preview item Name + blocker Name, and for the
/// matching resolution lookup in <see cref="ApplyDataConnectionsAsync"/>. Connection
/// names are unique only WITHIN a site, so a bare-name key would collapse two sites'
@@ -939,8 +939,8 @@ public sealed class BundleImporter : IBundleImporter
r = > r ) ;
var summary = new ImportSummary ( ) ;
// Inbound API keys are not transported between environments (re-arch C4) .
// A pre-C4 bundle may still contain a keys section; we ignore those keys
// Inbound API keys are not transported between environments.
// A legacy bundle may still contain a keys section; we ignore those keys
// entirely (never re-create them) but count them so the result can tell
// the operator to re-issue keys on this environment.
var apiKeysIgnored = content . ApiKeys ? . Count ? ? 0 ;
@@ -965,7 +965,7 @@ public sealed class BundleImporter : IBundleImporter
// pre-existing target reads, so it has no ordering dependency on
// the Apply* helpers. Failing here means the change tracker is
// still empty, which keeps the rollback contract simple on both
// the in-memory and relational providers (FU-B: intermediate
// the in-memory and relational providers (intermediate
// SaveChangesAsync between Apply* and the second-pass rewire
// would otherwise prevent the in-memory provider from undoing
// already-flushed template rows on validation failure — the
@@ -976,7 +976,7 @@ public sealed class BundleImporter : IBundleImporter
// a Skip on a dependency surfaces as a missing-reference error
// rather than silently passing.
var validationErrors = await RunSemanticValidationAsync ( content , resolutionMap , ct ) . ConfigureAwait ( false ) ;
// M8: v alidate every site / connection / template reference the
// V alidate every site / connection / template reference the
// site-instance payload depends on BEFORE any row is staged. Running
// this in the validation phase (not as an apply-pass guard) preserves
// the rollback contract: a structurally-unresolvable bundle fails with
@@ -992,7 +992,7 @@ public sealed class BundleImporter : IBundleImporter
throw new SemanticValidationException ( validationErrors ) ;
}
// ---- M8 s ite/instance-scoped apply: sites + connections FIRST ----
// ---- S ite/instance-scoped apply: sites + connections FIRST ----
// Sites are the FK target for both data connections (SiteId) and
// instances (SiteId); data connections are the FK target for instance
// connection bindings (DataConnectionId) and the rewrite target for
@@ -1020,11 +1020,11 @@ public sealed class BundleImporter : IBundleImporter
await ApplyNotificationListsAsync ( content . NotificationLists , resolutionMap , user , summary , ct ) . ConfigureAwait ( false ) ;
await ApplySmtpConfigsAsync ( content . SmtpConfigs , resolutionMap , user , summary , ct ) . ConfigureAwait ( false ) ;
await ApplySmsConfigsAsync ( content . SmsConfigs , resolutionMap , user , summary , ct ) . ConfigureAwait ( false ) ;
// Inbound API keys are NOT applied from a bundle (re-arch C4) — any keys
// Inbound API keys are NOT applied from a bundle — any keys
// in a legacy bundle were counted above (apiKeysIgnored) and are skipped.
await ApplyApiMethodsAsync ( content . ApiMethods , resolutionMap , user , summary , ct ) . ConfigureAwait ( false ) ;
// FU-B / #39 + remainder of #37 — s econd-pass rewire of name-keyed
// S econd-pass rewire of name-keyed
// FKs that can only be resolved AFTER every template's scripts and
// child rows have been staged. We flush here so Pass A can look up
// the just-persisted scripts by name and Pass B can look up the
@@ -1043,7 +1043,7 @@ public sealed class BundleImporter : IBundleImporter
await ResolveAlarmScriptLinksAsync ( content . Templates , resolutionMap , user , ct ) . ConfigureAwait ( false ) ;
await ResolveCompositionEdgesAsync ( content . Templates , resolutionMap , user , ct ) . ConfigureAwait ( false ) ;
// ---- M8 s ite/instance-scoped apply: instances LAST ----
// ---- S ite/instance-scoped apply: instances LAST ----
// The instance pass needs every FK target materialised first:
// • template id — resolved by name against the just-flushed central
// config (Templates were flushed above for the alarm/composition
@@ -1058,9 +1058,9 @@ public sealed class BundleImporter : IBundleImporter
content , resolutionMap , siteBySourceIdentifier , connectionMaps ,
user , summary , ct ) . ConfigureAwait ( false ) ;
// ---- D2 p re-commit point (#16) : stale-instance computation ----
// ---- P re-commit point: stale-instance computation ----
// Everything the import will write is staged on the change tracker at
// this point but NOT yet committed. D2 computes StaleInstanceIds here
// this point but NOT yet committed. This computes StaleInstanceIds here
// — target instances whose template was overwritten by this import and
// therefore now carry a stale flattened-config revision — and threads
// the resulting list into the ImportResult below (replacing the
@@ -1088,7 +1088,7 @@ public sealed class BundleImporter : IBundleImporter
summary . Skipped ,
summary . Renamed ,
} ,
// re-arch C4: l egacy inbound API keys present in the bundle that
// L egacy inbound API keys present in the bundle that
// were ignored (not transported / not re-created here).
ApiKeysIgnored = apiKeysIgnored ,
} ,
@@ -1220,7 +1220,7 @@ public sealed class BundleImporter : IBundleImporter
}
/// <summary>
/// #16 (M8 D2): e numerates the deployed instances stale-ed by this import.
/// E numerates the deployed instances stale-ed by this import.
/// Overwriting a template changes the flattened-config hash of every instance
/// of that template, so a deployed instance's freshly-computed revision hash
/// will no longer match the hash captured in its <c>DeployedConfigSnapshot</c>
@@ -1896,7 +1896,7 @@ public sealed class BundleImporter : IBundleImporter
}
/// <summary>
/// FU-B / remainder of #37 — Pass A of the post-template-flush rewire.
/// Pass A of the post-template-flush rewire.
/// For every imported template (Add / Overwrite / Rename) whose bundle DTO
/// carries any alarm with <c>OnTriggerScriptName</c>, look up the
/// persisted alarm + script by name on the same template and set the FK.
@@ -1970,7 +1970,7 @@ public sealed class BundleImporter : IBundleImporter
}
/// <summary>
/// FU-B / #39 — Pass B of the post-template-flush rewire. For every
/// Pass B of the post-template-flush rewire. For every
/// imported template (Add / Overwrite / Rename) whose bundle DTO carries
/// any <c>Compositions</c>, replace the persisted template's existing
/// composition rows with new ones whose <c>ComposedTemplateId</c> is
@@ -2453,7 +2453,7 @@ public sealed class BundleImporter : IBundleImporter
return list ;
}
// S10: r econstruct a recipient carrying whichever contact(s) the bundle holds.
// R econstruct a recipient carrying whichever contact(s) the bundle holds.
// Email-only and SMS-only round-trip exactly; a recipient with both keeps both.
// Build via the email ctor when an address is present (so the historical
// non-null-email path is unchanged) and additively restore the phone number;
@@ -2544,7 +2544,7 @@ public sealed class BundleImporter : IBundleImporter
target . Credentials = dto . Secrets ? . Values . TryGetValue ( "Credentials" , out var cred ) = = true ? cred : null ;
}
// SMS (S10b) : mirrors ApplySmtpConfigsAsync exactly. SmsConfiguration is keyed by
// SMS: mirrors ApplySmtpConfigsAsync exactly. SmsConfiguration is keyed by
// AccountSid (the diff engine's natural key — analogous to SMTP's Host), so a
// Rename targets AccountSid and Overwrite matches an existing config by AccountSid.
// The provider auth token is decrypted out of the SecretsBlock via the same path
@@ -2618,9 +2618,9 @@ public sealed class BundleImporter : IBundleImporter
target . AuthToken = dto . Secrets ? . Values . TryGetValue ( "AuthToken" , out var token ) = = true ? token : null ;
}
// ApplyApiKeysAsync was removed in re-arch C4 : inbound API keys are not
// ApplyApiKeysAsync was removed: inbound API keys are not
// transported between environments, so a bundle never re-creates keys. Any keys
// present in a legacy (pre-C4) bundle are counted and ignored in ApplyAsync.
// present in a legacy bundle are counted and ignored in ApplyAsync.
private async Task ApplyApiMethodsAsync (
IReadOnlyList < ApiMethodDto > dtos ,
@@ -2651,9 +2651,9 @@ public sealed class BundleImporter : IBundleImporter
}
case ResolutionAction . Overwrite when existing is not null :
existing . Script = dto . Script ;
// Method→key scopes are not transported (re-arch C4) and the
// Method→key scopes are not transported and the
// ApprovedApiKeyIds column was dropped with the SQL Server ApiKey
// entity (re-arch C5) : scopes are re-granted per environment in the
// entity: scopes are re-granted per environment in the
// shared ZB.MOM.WW.Auth.ApiKeys store, never via an imported bundle.
existing . ParameterDefinitions = dto . ParameterDefinitions ;
existing . ReturnDefinition = dto . ReturnDefinition ;
@@ -2680,9 +2680,9 @@ public sealed class BundleImporter : IBundleImporter
private static ApiMethod BuildApiMethod ( ApiMethodDto dto , string? overrideName )
{
// Method→key scopes are not transported (re-arch C4) ; the ApprovedApiKeyIds
// column that once carried them was dropped with the SQL Server ApiKey entity
// (re-arch C5). Scopes are re-granted per environment in the shared
// Method→key scopes are not transported; the ApprovedApiKeyIds
// column that once carried them was dropped with the SQL Server ApiKey entity.
// Scopes are re-granted per environment in the shared
// ZB.MOM.WW.Auth.ApiKeys store.
return new ApiMethod ( overrideName ? ? dto . Name , dto . Script )
{
@@ -2693,7 +2693,7 @@ public sealed class BundleImporter : IBundleImporter
}
// ─────────────────────────────────────────────────────────────────────
// M8 D1 — s ite/instance-scoped apply.
// S ite/instance-scoped apply.
//
// Three passes (sites → connections → instances) resolve every cross-
// environment name reference against the TARGET database's own surrogate
@@ -2716,7 +2716,7 @@ public sealed class BundleImporter : IBundleImporter
/// the same identifier resolves to <see cref="MappingAction.MapToExisting"/>,
/// otherwise <see cref="MappingAction.CreateNew"/>. <c>CreateNew</c> inserts a
/// site from the full <see cref="SiteDto"/> payload (display name, description,
/// and the verbatim Node A/B + gRPC Node A/B addresses — D3's "carry full
/// and the verbatim Node A/B + gRPC Node A/B addresses — the "carry full
/// config" decision). <c>MapToExisting</c> honours the site's
/// <see cref="ImportResolution"/>: <see cref="ResolutionAction.Skip"/> leaves
/// the target untouched; <see cref="ResolutionAction.Overwrite"/> applies the
@@ -2964,7 +2964,7 @@ public sealed class BundleImporter : IBundleImporter
}
// MapToExisting — honour the connection's own conflict resolution.
// C2: t he resolution is keyed by the SITE-QUALIFIED name
// T he resolution is keyed by the SITE-QUALIFIED name
// (`{SiteIdentifier}/{Name}`), matching the qualified preview-item Name —
// so a same-named connection under a different site can't pick up the
// wrong Skip/Overwrite.
@@ -2989,7 +2989,7 @@ public sealed class BundleImporter : IBundleImporter
targetNameByRef [ key ] = existing . Name ;
}
// ---- C1 Pass 2 — referenced-but-not-carried connections ----
// ---- Pass 2 — referenced-but-not-carried connections ----
// A binding (or native-alarm-source override) can name a connection that
// exists in the TARGET database but was NOT carried in the bundle's
// DataConnections (e.g. exported without it). Preview correctly does NOT
@@ -3043,7 +3043,7 @@ public sealed class BundleImporter : IBundleImporter
}
/// <summary>
/// C1: e very distinct <c>(sourceSiteIdentifier, connectionName)</c> pair an instance
/// E very distinct <c>(sourceSiteIdentifier, connectionName)</c> pair an instance
/// references — via a <see cref="InstanceConnectionBindingDto.ConnectionName"/> or a
/// non-null <see cref="InstanceNativeAlarmSourceOverrideDto.ConnectionNameOverride"/>.
/// Drives the connection-map Pass 2 that resolves references the bundle didn't carry.
@@ -3348,7 +3348,7 @@ public sealed class BundleImporter : IBundleImporter
{
continue ;
}
// Resolve ConnectionName → target DataConnectionId. After the C1 Pass- 2
// Resolve ConnectionName → target DataConnectionId. After Pass 2
// in ApplyDataConnectionsAsync, the map carries an entry for every
// referenced connection — carried in the bundle OR auto-matched in the
// target. A non-empty name that STILL doesn't resolve is a structural
@@ -3430,7 +3430,7 @@ public sealed class BundleImporter : IBundleImporter
}
/// <summary>
/// M8: v alidates every cross-environment reference the site/instance payload
/// V alidates every cross-environment reference the site/instance payload
/// depends on, BEFORE any row is staged — so a structurally-unresolvable
/// bundle fails with an empty change tracker (preserving the all-or-nothing
/// rollback contract on every EF provider, including the in-memory one whose