docs(comments): strip internal task/milestone/bundle bookkeeping from code comments
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
@@ -73,7 +73,7 @@ public sealed class BundleExporter : IBundleExporter
|
||||
var resolved = await _resolver.ResolveAsync(selection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 2. Convert to the wire-shaped DTO (strips EF identity, refs-by-name).
|
||||
// M8 (B4): the resolver's site/dataConnection/instance closure is wired
|
||||
// The resolver's site/dataConnection/instance closure is wired
|
||||
// through via object-initializer — without this the aggregate would
|
||||
// carry the empty defaults and the bundle would ship empty site/
|
||||
// instance arrays even when those were selected (review item I3).
|
||||
@@ -86,16 +86,16 @@ public sealed class BundleExporter : IBundleExporter
|
||||
DatabaseConnections: resolved.DatabaseConnections,
|
||||
NotificationLists: resolved.NotificationLists,
|
||||
SmtpConfigurations: resolved.SmtpConfigs,
|
||||
// Inbound API keys are not transported between environments (re-arch C4).
|
||||
// Inbound API keys are not transported between environments.
|
||||
ApiMethods: resolved.ApiMethods)
|
||||
{
|
||||
Sites = resolved.Sites,
|
||||
DataConnections = resolved.DataConnections,
|
||||
Instances = resolved.Instances,
|
||||
// SMS (S10b): wire the resolver's SMS config closure through the
|
||||
// Wire the resolver's SMS config closure through the
|
||||
// aggregate. Without this the aggregate carries the empty default and the
|
||||
// bundle ships an empty smsConfigs array even when SMS configs were
|
||||
// selected — mirrors the M8 site/instance fix (review item I3).
|
||||
// selected — mirrors the site/instance fix (review item I3).
|
||||
SmsConfigurations = resolved.SmsConfigs,
|
||||
};
|
||||
var contentDto = _entitySerializer.ToBundleContent(aggregate);
|
||||
@@ -110,7 +110,7 @@ public sealed class BundleExporter : IBundleExporter
|
||||
NotificationLists: resolved.NotificationLists.Count,
|
||||
SmtpConfigs: resolved.SmtpConfigs.Count,
|
||||
ApiMethods: resolved.ApiMethods.Count,
|
||||
// M8 (B3): additive site/dataConnection/instance counts. Sourced from
|
||||
// Additive site/dataConnection/instance counts. Sourced from
|
||||
// the serialized content DTO — the same arrays the importer reads —
|
||||
// so the manifest summary always matches the packed payload. These
|
||||
// arrays default to empty until B1/B2 populate them, so the counts
|
||||
@@ -118,7 +118,7 @@ public sealed class BundleExporter : IBundleExporter
|
||||
Sites: contentDto.Sites.Count,
|
||||
DataConnections: contentDto.DataConnections.Count,
|
||||
Instances: contentDto.Instances.Count,
|
||||
// SMS (S10b): additive count sourced from the serialized content DTO — the
|
||||
// Additive count sourced from the serialized content DTO — the
|
||||
// same array the importer reads — so the manifest summary always matches
|
||||
// the packed payload.
|
||||
SmsConfigs: contentDto.SmsConfigs.Count);
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Export;
|
||||
/// Templates are returned topologically sorted (base-before-derived) via Kahn's
|
||||
/// algorithm so importers can apply them in order.
|
||||
/// <para>
|
||||
/// M8 adds site/instance-scoped expansion: selecting a site pulls its
|
||||
/// Site/instance-scoped expansion: selecting a site pulls its
|
||||
/// <c>DataConnection</c>s and <c>Instance</c>s; selecting an instance (with
|
||||
/// <see cref="ExportSelection.IncludeDependencies"/>) pulls its owning site, the
|
||||
/// data connections it binds, and feeds its template into the existing template
|
||||
@@ -111,7 +111,7 @@ public sealed class DependencyResolver
|
||||
if (sm is not null) smtpConfigs[sm.Id] = sm;
|
||||
}
|
||||
|
||||
// SMS provider configs (S10b): mirror the SMTP seed exactly — resolve each
|
||||
// SMS provider configs: mirror the SMTP seed exactly — resolve each
|
||||
// selected id via the by-id repository accessor; missing ids are skipped.
|
||||
var smsConfigs = new Dictionary<int, SmsConfiguration>();
|
||||
foreach (var id in selection.SmsConfigurationIds.Distinct())
|
||||
@@ -121,7 +121,7 @@ public sealed class DependencyResolver
|
||||
}
|
||||
|
||||
// Inbound API keys are intentionally NOT resolved into the bundle: per the
|
||||
// inbound-API-key re-architecture (C4) keys are not transported between
|
||||
// inbound-API-key re-architecture, keys are not transported between
|
||||
// environments. Only API methods travel.
|
||||
var apiMethods = new Dictionary<int, ApiMethod>();
|
||||
foreach (var id in selection.ApiMethodIds.Distinct())
|
||||
@@ -130,7 +130,7 @@ public sealed class DependencyResolver
|
||||
if (m is not null) apiMethods[m.Id] = m;
|
||||
}
|
||||
|
||||
// ---- Seed: site/instance-scoped selection (M8) ----
|
||||
// ---- Seed: site/instance-scoped selection ----
|
||||
// sites/dataConnections/instances are keyed by surrogate id for dedup.
|
||||
var sites = new Dictionary<int, Site>();
|
||||
var dataConnections = new Dictionary<int, DataConnection>();
|
||||
@@ -200,7 +200,7 @@ public sealed class DependencyResolver
|
||||
// ---- Topological sort of templates (base-before-derived) ----
|
||||
var orderedTemplates = TopologicallySortTemplates(templates.Values);
|
||||
|
||||
// ---- Deterministic site/instance ordering (M8) ----
|
||||
// ---- Deterministic site/instance ordering ----
|
||||
// siteId → SiteIdentifier map: every connection's/instance's owning site lands
|
||||
// in `sites` after closure expansion, so ordering + dependsOn edges resolve names.
|
||||
var siteIdentifierById = sites.Values.ToDictionary(s => s.Id, s => s.SiteIdentifier);
|
||||
@@ -266,13 +266,13 @@ public sealed class DependencyResolver
|
||||
Sites = orderedSites,
|
||||
DataConnections = orderedDataConnections,
|
||||
Instances = orderedInstances,
|
||||
// SMS (S10b): ordered by AccountSid — the natural key the importer matches
|
||||
// SMS: ordered by AccountSid — the natural key the importer matches
|
||||
// on, mirroring how SMTP is ordered by Host.
|
||||
SmsConfigs = smsConfigs.Values.OrderBy(s => s.AccountSid, StringComparer.Ordinal).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Instance child loading (M8) ----
|
||||
// ---- Instance child loading ----
|
||||
// Loads an instance and attaches its overrides/bindings onto the entity's own
|
||||
// navigation collections, so the serializer reads children off the entity (the
|
||||
// same shape central-config entities use). Returns null if the instance is gone.
|
||||
@@ -293,7 +293,7 @@ public sealed class DependencyResolver
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ---- Site/instance closure (M8) ----
|
||||
// ---- Site/instance closure ----
|
||||
// For every gathered instance: ensure its owning Site is in the bundle, include
|
||||
// the DataConnections it binds (via ConnectionBinding.DataConnectionId resolved
|
||||
// within the instance's site, plus NativeAlarmSourceOverride.ConnectionNameOverride
|
||||
@@ -618,19 +618,19 @@ public sealed class DependencyResolver
|
||||
{
|
||||
entries.Add(new ManifestContentEntry("SmtpConfiguration", s.Host, 1, Array.Empty<string>()));
|
||||
}
|
||||
// SMS (S10b): mirror SMTP — one manifest row per config, keyed by AccountSid
|
||||
// SMS: mirror SMTP — one manifest row per config, keyed by AccountSid
|
||||
// (the natural key the importer matches on).
|
||||
foreach (var s in smsConfigs.OrderBy(x => x.AccountSid, StringComparer.Ordinal))
|
||||
{
|
||||
entries.Add(new ManifestContentEntry("SmsConfiguration", s.AccountSid, 1, Array.Empty<string>()));
|
||||
}
|
||||
// Inbound API keys are not transported (re-arch C4) — no ApiKey manifest entries.
|
||||
// Inbound API keys are not transported — no ApiKey manifest entries.
|
||||
foreach (var m in apiMethods.OrderBy(x => x.Name, StringComparer.Ordinal))
|
||||
{
|
||||
entries.Add(new ManifestContentEntry("ApiMethod", m.Name, 1, Array.Empty<string>()));
|
||||
}
|
||||
|
||||
// ---- M8: site/instance-scoped manifest rows ----
|
||||
// ---- Site/instance-scoped manifest rows ----
|
||||
// Sites are roots (no dependsOn). DataConnections depend on their site.
|
||||
// Instances depend on their template, their site, and each data connection
|
||||
// they bind. Inputs arrive pre-ordered, so iterate as-is for determinism.
|
||||
|
||||
@@ -25,11 +25,11 @@ public sealed record ResolvedExport(
|
||||
IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
|
||||
IReadOnlyList<NotificationList> NotificationLists,
|
||||
IReadOnlyList<SmtpConfiguration> SmtpConfigs,
|
||||
// Inbound API keys are not transported between environments (re-arch C4); only methods.
|
||||
// Inbound API keys are not transported between environments; only methods.
|
||||
IReadOnlyList<ApiMethod> ApiMethods,
|
||||
IReadOnlyList<ManifestContentEntry> ContentManifest)
|
||||
{
|
||||
// M8: site/instance-scoped closure. Additive init-only collections with empty
|
||||
// Site/instance-scoped closure. Additive init-only collections with empty
|
||||
// defaults so the positional ctor stays source-compatible for callers/tests
|
||||
// that only resolve central-config selections (sites/instances stay empty).
|
||||
// The resolver opts in via object-initializer. Each <see cref="Instance"/>
|
||||
@@ -59,7 +59,7 @@ public sealed record ResolvedExport(
|
||||
|
||||
/// <summary>
|
||||
/// SMS provider configurations in the closure, ordered by
|
||||
/// <see cref="SmsConfiguration.AccountSid"/> (S10b). Mirrors
|
||||
/// <see cref="SmsConfiguration.AccountSid"/>. Mirrors
|
||||
/// <see cref="SmtpConfigs"/>; init-only with an empty default so callers that
|
||||
/// only resolve other selections keep compiling.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||
/// "Coarse" means: each persistent field is compared as a value; differing
|
||||
/// non-code fields appear in <c>changes</c> with old/new values. Code / large-
|
||||
/// text fields (script bodies, API-method scripts) instead carry a structured
|
||||
/// per-line Myers diff (T20): the <see cref="FieldChange.OldValue"/> /
|
||||
/// per-line Myers diff: the <see cref="FieldChange.OldValue"/> /
|
||||
/// <see cref="FieldChange.NewValue"/> keep a compact <c><N lines></c>
|
||||
/// summary for fallback rendering, and a <see cref="FieldChange.LineDiff"/>
|
||||
/// payload carries the hunk lines (+/-/context) plus add/remove totals and a
|
||||
@@ -210,7 +210,7 @@ public sealed class ArtifactDiff
|
||||
incoming.Recipients,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
// S10: a recipient is unchanged only when BOTH contacts match — a phone
|
||||
// A recipient is unchanged only when BOTH contacts match — a phone
|
||||
// number added/changed on an existing recipient is a real diff.
|
||||
(e, i) => e.EmailAddress == i.EmailAddress && e.PhoneNumber == i.PhoneNumber,
|
||||
"Recipients",
|
||||
@@ -254,7 +254,7 @@ public sealed class ArtifactDiff
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming SMS provider configuration against the existing one in
|
||||
/// the database (S10b). Mirrors <see cref="CompareSmtpConfiguration"/>: keyed by
|
||||
/// the database. Mirrors <see cref="CompareSmtpConfiguration"/>: keyed by
|
||||
/// the natural key <c>AccountSid</c>, with the auth token diffed presence-only
|
||||
/// (it lives in <see cref="SecretsBlock"/>, never compared by value).
|
||||
/// </summary>
|
||||
@@ -286,7 +286,7 @@ public sealed class ArtifactDiff
|
||||
return BuildItem("SmsConfiguration", incoming.AccountSid, changes);
|
||||
}
|
||||
|
||||
// CompareApiKey was removed in re-arch C4: inbound API keys are not transported
|
||||
// CompareApiKey was removed: inbound API keys are not transported
|
||||
// between environments, so the import preview never diffs keys.
|
||||
|
||||
/// <summary>
|
||||
@@ -301,7 +301,7 @@ public sealed class ArtifactDiff
|
||||
if (existing is null) return New("ApiMethod", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
// ApprovedApiKeyIds is not transported (re-arch C4) and is excluded from the diff.
|
||||
// ApprovedApiKeyIds is not transported and is excluded from the diff.
|
||||
AddIfDifferent(changes, "ParameterDefinitions", existing.ParameterDefinitions, incoming.ParameterDefinitions);
|
||||
AddIfDifferent(changes, "ReturnDefinition", existing.ReturnDefinition, incoming.ReturnDefinition);
|
||||
AddIfDifferent(changes, "TimeoutSeconds", existing.TimeoutSeconds, incoming.TimeoutSeconds);
|
||||
@@ -329,7 +329,7 @@ public sealed class ArtifactDiff
|
||||
return BuildItem("TemplateFolder", incoming.Name, changes);
|
||||
}
|
||||
|
||||
// ---- Site / Connection / Instance (M8) ----
|
||||
// ---- Site / Connection / Instance ----
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming site against the existing site in the database.
|
||||
@@ -504,8 +504,8 @@ public sealed class ArtifactDiff
|
||||
|
||||
private static void AddCodeChangeIfDifferent(List<FieldChange> changes, string field, string? existing, string? incoming)
|
||||
{
|
||||
// Code/large-text fields: when they differ, emit a real per-line Myers diff
|
||||
// (T20). The OldValue/NewValue keep a compact "<N lines>" summary as a
|
||||
// Code/large-text fields: when they differ, emit a real per-line Myers diff.
|
||||
// The OldValue/NewValue keep a compact "<N lines>" summary as a
|
||||
// fallback for renderers that don't consume the structured payload; the
|
||||
// LineDiff carries the +/- hunk lines. Identical code emits no change.
|
||||
var sameNullness = existing is null == incoming is null;
|
||||
@@ -635,7 +635,7 @@ public sealed class ArtifactDiff
|
||||
if (!ScriptsEqual(ex, inc))
|
||||
{
|
||||
// A script row can diverge in Code and/or its trigger/param metadata.
|
||||
// The structured line diff covers the Code body (T20); when only the
|
||||
// The structured line diff covers the Code body; when only the
|
||||
// metadata changed the hunk list is all-context (no +/-), which still
|
||||
// tells the operator the row changed.
|
||||
changes.Add(BuildCodeFieldChange($"Scripts.{name}", ex.Code, inc.Code));
|
||||
@@ -696,7 +696,7 @@ public sealed class ArtifactDiff
|
||||
[property: JsonPropertyName("field")] string Field,
|
||||
[property: JsonPropertyName("oldValue")] string? OldValue,
|
||||
[property: JsonPropertyName("newValue")] string? NewValue,
|
||||
// Present only for code / large-text fields (T20). Null (and omitted from
|
||||
// Present only for code / large-text fields. Null (and omitted from
|
||||
// the JSON by the WhenWritingNull policy) for ordinary coarse fields.
|
||||
[property: JsonPropertyName("lineDiff")] LineDiffPayload? LineDiff = null);
|
||||
|
||||
|
||||
@@ -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): optional. Implemented in DeploymentManager (where the flattening
|
||||
// Optional. 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: previously this loop iterated GetAllTemplatesAsync()
|
||||
// Previously 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 site/instance-scoped types ----
|
||||
// ---- Site/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: connection 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: instance template + referenced-connection blockers ----
|
||||
// ---- Instance 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: site-qualify the blocker Name so two sites' same-named-but-missing
|
||||
// Site-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: the site-qualified identity (<c>{siteIdentifier}/{connectionName}</c>) used
|
||||
/// The 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: validate every site / connection / template reference the
|
||||
// Validate 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 site/instance-scoped apply: sites + connections FIRST ----
|
||||
// ---- Site/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 — second-pass rewire of name-keyed
|
||||
// Second-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 site/instance-scoped apply: instances LAST ----
|
||||
// ---- Site/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 pre-commit point (#16): stale-instance computation ----
|
||||
// ---- Pre-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: legacy inbound API keys present in the bundle that
|
||||
// Legacy 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): enumerates the deployed instances stale-ed by this import.
|
||||
/// Enumerates 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: reconstruct a recipient carrying whichever contact(s) the bundle holds.
|
||||
// Reconstruct 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 — site/instance-scoped apply.
|
||||
// Site/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: the resolution is keyed by the SITE-QUALIFIED name
|
||||
// The 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: every distinct <c>(sourceSiteIdentifier, connectionName)</c> pair an instance
|
||||
/// Every 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: validates every cross-environment reference the site/instance payload
|
||||
/// Validates 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Transport-004: thrown by <see cref="BundleImporter.LoadAsync"/> when the caller
|
||||
/// Thrown by <see cref="BundleImporter.LoadAsync"/> when the caller
|
||||
/// has exceeded the configured per-IP-per-hour unlock attempt cap
|
||||
/// (<see cref="TransportOptions.MaxUnlockAttemptsPerIpPerHour"/>). The 429-equivalent
|
||||
/// signal: the caller must wait for the trailing-hour window to roll forward before
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Concurrent;
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Transport-004: in-memory sliding-window rate limiter for bundle-unlock passphrase
|
||||
/// In-memory sliding-window rate limiter for bundle-unlock passphrase
|
||||
/// attempts, keyed by client IP. The design doc (§11) declares a per-IP-per-hour cap
|
||||
/// (default 10) as a brute-force defence against a stolen bundle; this class is the
|
||||
/// minimal server-side implementation.
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
/// <b>Important — unknown-property tolerance:</b>
|
||||
/// <see cref="JsonUnmappedMemberHandling"/> is left at its default
|
||||
/// (<c>Skip</c>); setting it to <c>Disallow</c> would break backward-
|
||||
/// compatible deserialization of pre-C4 bundles, which may carry a top-level
|
||||
/// compatible deserialization of older bundles, which may carry a top-level
|
||||
/// <c>apiKeys</c> array and/or <c>ApprovedApiKeyIds</c> fields inside
|
||||
/// <c>apiMethods[]</c> entries. That tolerance is load-bearing and must be
|
||||
/// preserved.
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
/// on the application side of the bundle boundary.
|
||||
/// </summary>
|
||||
// ApiKeys is intentionally absent: inbound API keys are not transported between
|
||||
// environments (re-arch C4). The aggregate only carries API methods.
|
||||
// environments. The aggregate only carries API methods.
|
||||
public sealed record EntityAggregate(
|
||||
IReadOnlyList<TemplateFolder> TemplateFolders,
|
||||
IReadOnlyList<Template> Templates,
|
||||
@@ -28,15 +28,15 @@ public sealed record EntityAggregate(
|
||||
IReadOnlyList<SmtpConfiguration> SmtpConfigurations,
|
||||
IReadOnlyList<ApiMethod> ApiMethods)
|
||||
{
|
||||
// M8: site/instance-scoped entities. Init-only with empty-array defaults so
|
||||
// Site/instance-scoped entities. Init-only with empty-array defaults so
|
||||
// existing positional `new EntityAggregate(...)` callers keep compiling and
|
||||
// never see a null collection; new callers opt in via object-initializer.
|
||||
public IReadOnlyList<Site> Sites { get; init; } = Array.Empty<Site>();
|
||||
public IReadOnlyList<DataConnection> DataConnections { get; init; } = Array.Empty<DataConnection>();
|
||||
public IReadOnlyList<Instance> Instances { get; init; } = Array.Empty<Instance>();
|
||||
|
||||
// SMS (S10): carried alongside SmtpConfigurations. Init-only with an empty
|
||||
// default for the same source-compat reason as the M8 collections above —
|
||||
// SMS: carried alongside SmtpConfigurations. Init-only with an empty
|
||||
// default for the same source-compat reason as the site/instance collections above —
|
||||
// every existing positional `new EntityAggregate(...)` caller keeps compiling
|
||||
// and never sees null; producers that resolve SMS config opt in via
|
||||
// object-initializer.
|
||||
@@ -52,7 +52,7 @@ public sealed record EntityAggregate(
|
||||
/// <para>
|
||||
/// <see cref="ApiKeys"/> is a <b>legacy, read-only</b> field retained purely for
|
||||
/// backward-compatible deserialization of bundles produced before the
|
||||
/// inbound-API-key re-architecture (C4). New exports never populate it (it stays
|
||||
/// inbound-API-key re-architecture. New exports never populate it (it stays
|
||||
/// <c>null</c> and is dropped from the JSON by the serializer's
|
||||
/// <c>WhenWritingNull</c> policy); the importer counts any keys present in an old
|
||||
/// bundle, ignores them, and surfaces a note — keys are re-created per environment.
|
||||
@@ -69,7 +69,7 @@ public sealed record BundleContentDto(
|
||||
IReadOnlyList<ApiMethodDto> ApiMethods,
|
||||
IReadOnlyList<ApiKeyDto>? ApiKeys = null)
|
||||
{
|
||||
// M8: site/instance-scoped payloads. Modeled as init-only properties with
|
||||
// Site/instance-scoped payloads. Modeled as init-only properties with
|
||||
// empty-array defaults (rather than additional positional ctor params) for
|
||||
// two reasons:
|
||||
// 1. Forward-compat: deserializing an OLDER bundle whose content blob has
|
||||
@@ -85,9 +85,9 @@ public sealed record BundleContentDto(
|
||||
public IReadOnlyList<DataConnectionDto> DataConnections { get; init; } = Array.Empty<DataConnectionDto>();
|
||||
public IReadOnlyList<InstanceDto> Instances { get; init; } = Array.Empty<InstanceDto>();
|
||||
|
||||
// SMS (S10): central-only SMS provider configs, carried alongside SmtpConfigs.
|
||||
// SMS: central-only SMS provider configs, carried alongside SmtpConfigs.
|
||||
// Modeled as an init-only property with an empty default (NOT a positional ctor
|
||||
// param) for the same two reasons as the M8 site/instance arrays above:
|
||||
// param) for the same two reasons as the site/instance arrays above:
|
||||
// 1. Forward-compat: deserializing a bundle written before SMS support leaves
|
||||
// this at Array.Empty<SmsConfigDto>() — consumers always see an empty list,
|
||||
// never null.
|
||||
@@ -146,7 +146,7 @@ public sealed record TemplateScriptDto(
|
||||
string? ReturnDefinition,
|
||||
bool IsLocked,
|
||||
TimeSpan? MinTimeBetweenRuns,
|
||||
// M2.5 (#9): per-script execution timeout (seconds). Additive trailing field;
|
||||
// Per-script execution timeout (seconds). Additive trailing field;
|
||||
// null on bundles written before this field existed.
|
||||
int? ExecutionTimeoutSeconds = null);
|
||||
|
||||
@@ -189,12 +189,12 @@ public sealed record NotificationListDto(
|
||||
|
||||
public sealed record NotificationRecipientDto(
|
||||
string Name,
|
||||
// S10: nullable so an SMS-only recipient (no email) round-trips faithfully
|
||||
// Nullable so an SMS-only recipient (no email) round-trips faithfully
|
||||
// instead of being coerced to an empty string. Email-only recipients (the
|
||||
// historical case) still carry a non-null address; the entity enforces that at
|
||||
// least one contact is present.
|
||||
string? EmailAddress,
|
||||
// S10: SMS recipients carry an E.164 phone number instead of (or in addition
|
||||
// SMS recipients carry an E.164 phone number instead of (or in addition
|
||||
// to) an email. Trailing + nullable so a bundle written before SMS support
|
||||
// deserializes this as null (backward-compatible); the serializer's
|
||||
// WhenWritingNull policy keeps it out of email-only bundles' JSON.
|
||||
@@ -228,8 +228,9 @@ public sealed record SmsConfigDto(
|
||||
TimeSpan RetryDelay,
|
||||
SecretsBlock? Secrets);
|
||||
|
||||
// Legacy DTO: only deserialized from pre-C4 bundles so the importer can count and
|
||||
// ignore the keys they contain. New exports never emit an ApiKeys array.
|
||||
// Legacy DTO: only deserialized from bundles produced before the inbound-API-key
|
||||
// re-architecture so the importer can count and ignore the keys they contain.
|
||||
// New exports never emit an ApiKeys array.
|
||||
public sealed record ApiKeyDto(
|
||||
string Name,
|
||||
string KeyHash,
|
||||
@@ -237,7 +238,7 @@ public sealed record ApiKeyDto(
|
||||
SecretsBlock? Secrets);
|
||||
|
||||
// ApprovedApiKeyIds is intentionally absent: it linked methods to keys that are no
|
||||
// longer transported (re-arch C4). Scopes are re-granted per environment. The field
|
||||
// longer transported. Scopes are re-granted per environment. The field
|
||||
// in any old bundle is simply ignored on read.
|
||||
public sealed record ApiMethodDto(
|
||||
string Name,
|
||||
@@ -246,7 +247,7 @@ public sealed record ApiMethodDto(
|
||||
string? ReturnDefinition,
|
||||
int TimeoutSeconds);
|
||||
|
||||
// --- M8: site/instance-scoped transport DTOs --------------------------------
|
||||
// --- Site/instance-scoped transport DTOs --------------------------------
|
||||
// These travel alongside the original central-config DTOs above. Sites and
|
||||
// instances are referenced by their stable string identities (SiteIdentifier,
|
||||
// UniqueName, TemplateName) rather than database ids so a bundle resolves
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class EntitySerializer
|
||||
var folderNameById = aggregate.TemplateFolders.ToDictionary(f => f.Id, f => f.Name);
|
||||
var templateNameById = aggregate.Templates.ToDictionary(t => t.Id, t => t.Name);
|
||||
|
||||
// M8 name-link lookups. Instances/data-connections reference their owning
|
||||
// Name-link lookups. Instances/data-connections reference their owning
|
||||
// site by the portable SiteIdentifier (not the EF surrogate SiteId); a
|
||||
// connection binding references its DataConnection by Name. Build both
|
||||
// id→name maps once so the projections below resolve in O(1).
|
||||
@@ -138,7 +138,7 @@ public sealed class EntitySerializer
|
||||
NotificationLists: aggregate.NotificationLists.Select(nl => new NotificationListDto(
|
||||
Name: nl.Name,
|
||||
Type: nl.Type,
|
||||
// S10: carry BOTH contacts. Keep any recipient that has at least
|
||||
// Carry BOTH contacts. Keep any recipient that has at least
|
||||
// one contact (email OR phone) — SMS-only recipients have a null
|
||||
// EmailAddress and would have been dropped by the old email-only
|
||||
// filter. EmailAddress/PhoneNumber are nullable on the DTO so a
|
||||
@@ -172,7 +172,7 @@ public sealed class EntitySerializer
|
||||
RetryDelay: smtp.RetryDelay,
|
||||
Secrets: secrets);
|
||||
}).ToList(),
|
||||
// Inbound API keys are not transported between environments (re-arch C4):
|
||||
// Inbound API keys are not transported between environments:
|
||||
// the bundle carries API methods only. ApiMethod.ApprovedApiKeyIds is also
|
||||
// excluded — it references keys that aren't in the bundle; method→key scopes
|
||||
// are re-granted per environment via the admin UI/CLI. The legacy ApiKeys
|
||||
@@ -184,7 +184,7 @@ public sealed class EntitySerializer
|
||||
ReturnDefinition: m.ReturnDefinition,
|
||||
TimeoutSeconds: m.TimeoutSeconds)).ToList())
|
||||
{
|
||||
// M8 site/instance-scoped payloads. These are init-only (non-positional)
|
||||
// Site/instance-scoped payloads. These are init-only (non-positional)
|
||||
// on BundleContentDto, so they're set via object-initializer here.
|
||||
Sites = aggregate.Sites.Select(s => new SiteDto(
|
||||
SiteIdentifier: s.SiteIdentifier,
|
||||
@@ -218,7 +218,7 @@ public sealed class EntitySerializer
|
||||
FailoverRetryCount: c.FailoverRetryCount,
|
||||
Secrets: secretValues.Count > 0 ? new SecretsBlock(secretValues) : null);
|
||||
}).ToList(),
|
||||
// SMS (S10): mirror the SMTP secret handling exactly — the provider
|
||||
// SMS: mirror the SMTP secret handling exactly — the provider
|
||||
// auth token (never a public field) rides inside the SecretsBlock, so a
|
||||
// "share without secrets" export can drop it as a unit. Omit the secret
|
||||
// entirely when the token is null/empty so the importer's TryGetValue
|
||||
@@ -248,7 +248,7 @@ public sealed class EntitySerializer
|
||||
UniqueName: inst.UniqueName,
|
||||
TemplateName: templateNameById.TryGetValue(inst.TemplateId, out var tn) ? tn : string.Empty,
|
||||
SiteIdentifier: siteIdentifierBySiteId.TryGetValue(inst.SiteId, out var isid) ? isid : string.Empty,
|
||||
// TODO(M8): Areas don't travel in the bundle yet — EntityAggregate
|
||||
// TODO: Areas don't travel in the bundle yet — EntityAggregate
|
||||
// carries no Areas collection, so there's no id→name lookup. Emit
|
||||
// null until area transport is added; the importer leaves AreaId null.
|
||||
AreaName: null,
|
||||
@@ -268,7 +268,7 @@ public sealed class EntitySerializer
|
||||
ConditionFilterOverride: o.ConditionFilterOverride)).ToList(),
|
||||
// Connection bindings reference their DataConnection by NAME — the
|
||||
// numeric DataConnectionId FK can't survive a cross-environment
|
||||
// bundle, so the importer (D1) resolves ConnectionName→target FK at
|
||||
// bundle, so the importer resolves ConnectionName→target FK at
|
||||
// apply time. If the FK doesn't resolve in this aggregate (orphan
|
||||
// row), the name comes through empty.
|
||||
ConnectionBindings: inst.ConnectionBindings.Select(b => new InstanceConnectionBindingDto(
|
||||
@@ -424,7 +424,7 @@ public sealed class EntitySerializer
|
||||
var list = new NotificationList(dto.Name) { Id = ix + 1, Type = dto.Type };
|
||||
foreach (var r in dto.Recipients)
|
||||
{
|
||||
// S10: reconstruct whichever contact(s) the bundle carries.
|
||||
// Reconstruct whichever contact(s) the bundle carries.
|
||||
// Email-present recipients use the email ctor (unchanged
|
||||
// historical path) and additively restore the phone number;
|
||||
// phone-only recipients (null email) take the SMS factory so we
|
||||
@@ -453,7 +453,7 @@ public sealed class EntitySerializer
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// SMS (S10): mirror the SMTP path — restore AccountSid/FromNumber and decrypt
|
||||
// SMS: mirror the SMTP path — restore AccountSid/FromNumber and decrypt
|
||||
// the auth token back out of the SecretsBlock (null when the key was omitted
|
||||
// because the source token was null/empty).
|
||||
var smsConfigurations = content.SmsConfigs
|
||||
@@ -469,7 +469,7 @@ public sealed class EntitySerializer
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Inbound API keys are not transported (re-arch C4) — content.ApiKeys is a
|
||||
// Inbound API keys are not transported — content.ApiKeys is a
|
||||
// legacy field only present on pre-C4 bundles; it is ignored here. The
|
||||
// BundleImporter is responsible for counting and reporting any such keys.
|
||||
// ApiMethod.ApprovedApiKeyIds is likewise not reconstructed: scopes are
|
||||
@@ -484,7 +484,7 @@ public sealed class EntitySerializer
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// M8: sites first — assign synthetic ids by ordinal. Addresses are carried
|
||||
// Sites first — assign synthetic ids by ordinal. Addresses are carried
|
||||
// verbatim; the importer decides whether to keep or rewrite them.
|
||||
var sites = content.Sites
|
||||
.Select((dto, ix) => new Site(dto.Name, dto.SiteIdentifier)
|
||||
@@ -516,7 +516,7 @@ public sealed class EntitySerializer
|
||||
|
||||
// Instances: resolve template + site by name/identifier. Areas don't travel
|
||||
// (see export TODO), so AreaId stays null. Connection bindings keep their
|
||||
// resolved DataConnection NAME on the wire DTO — the importer (D1) reads
|
||||
// resolved DataConnection NAME on the wire DTO — the importer reads
|
||||
// bindings from BundleContentDto.Instances[].ConnectionBindings directly and
|
||||
// resolves ConnectionName→the target environment's DataConnectionId at apply
|
||||
// time. FromBundleContent is NOT on the importer's path (BundleImporter walks
|
||||
|
||||
@@ -13,7 +13,7 @@ public sealed class ManifestBuilder
|
||||
{
|
||||
public const int CurrentBundleFormatVersion = 1;
|
||||
|
||||
// Schema minor version. Bumped 1.0 → 1.1 in M8 to carry the additive
|
||||
// Schema minor version. Bumped 1.0 → 1.1 to carry the additive
|
||||
// site/dataConnection/instance summary counts (and their content arrays).
|
||||
// Minor bumps are additive-only: ManifestValidator gates solely on
|
||||
// BundleFormatVersion, so an older "1.0" bundle still validates Ok.
|
||||
|
||||
Reference in New Issue
Block a user