refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming bundle DTO against the existing entity in the target
|
||||
/// database and produces a single <see cref="ImportPreviewItem"/> that
|
||||
/// classifies the conflict and (for <c>Modified</c>) carries a coarse field
|
||||
/// diff in JSON.
|
||||
/// <para>
|
||||
/// "Coarse" means: each persistent field is compared as a value; differing
|
||||
/// fields appear in <c>changes</c> with old/new values (or hashes for large
|
||||
/// blobs like script code). Per-line / Myers-style diff is explicitly out of
|
||||
/// scope for v1 — the design plan defers it to a follow-up task. Script
|
||||
/// bodies record only a line-count delta to give the operator a sense of the
|
||||
/// change without paying the diff cost up front.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Entity versions are not yet tracked on the POCOs, so the
|
||||
/// <see cref="ImportPreviewItem.ExistingVersion"/> / <see cref="ImportPreviewItem.IncomingVersion"/>
|
||||
/// fields are always <c>null</c> here. They are reserved for a future
|
||||
/// optimistic-concurrency feature.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ArtifactDiff
|
||||
{
|
||||
private static readonly JsonSerializerOptions DiffJsonOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
// ---- Templates ----
|
||||
/// <summary>
|
||||
/// Compares an incoming template against the existing template in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming template from the bundle.</param>
|
||||
/// <param name="existing">The existing template in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareTemplate(TemplateDto incoming, Template? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null)
|
||||
{
|
||||
return New("Template", incoming.Name);
|
||||
}
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "Description", existing.Description, incoming.Description);
|
||||
AddIfDifferent(changes, "FolderName", FolderNameOf(existing), incoming.FolderName);
|
||||
AddIfDifferent(changes, "BaseTemplateName", BaseTemplateNameOf(existing), incoming.BaseTemplateName);
|
||||
|
||||
// Children: compare each child collection by name. We track which names
|
||||
// were added, which were removed, and which existed on both sides but
|
||||
// diverged in body. We use coarse value equality / line counts for
|
||||
// scripts so the diff JSON stays under a few KB per item.
|
||||
DiffChildren(
|
||||
existing.Attributes,
|
||||
incoming.Attributes,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
AttributesEqual,
|
||||
"Attributes",
|
||||
changes);
|
||||
|
||||
DiffChildren(
|
||||
existing.Alarms,
|
||||
incoming.Alarms,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
AlarmsEqual,
|
||||
"Alarms",
|
||||
changes);
|
||||
|
||||
DiffScriptChildren(existing.Scripts, incoming.Scripts, changes);
|
||||
|
||||
// Compositions diff by InstanceName since ComposedTemplateId vs
|
||||
// ComposedTemplateName aren't directly comparable. The bundle side
|
||||
// already serializes to the name form, so the comparison reduces to
|
||||
// (InstanceName, ComposedTemplateName) tuple equality.
|
||||
DiffChildren(
|
||||
existing.Compositions,
|
||||
incoming.Compositions,
|
||||
e => e.InstanceName,
|
||||
i => i.InstanceName,
|
||||
(e, i) => CompositionTargetNameOf(e) == i.ComposedTemplateName,
|
||||
"Compositions",
|
||||
changes);
|
||||
|
||||
return BuildItem("Template", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming shared script against the existing shared script in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming shared script from the bundle.</param>
|
||||
/// <param name="existing">The existing shared script in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareSharedScript(SharedScriptDto incoming, SharedScript? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("SharedScript", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "ParameterDefinitions", existing.ParameterDefinitions, incoming.ParameterDefinitions);
|
||||
AddIfDifferent(changes, "ReturnDefinition", existing.ReturnDefinition, incoming.ReturnDefinition);
|
||||
AddCodeChangeIfDifferent(changes, "Code", existing.Code, incoming.Code);
|
||||
|
||||
return BuildItem("SharedScript", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming external system against the existing external system in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming external system from the bundle.</param>
|
||||
/// <param name="existing">The existing external system in the database, or null if new.</param>
|
||||
/// <param name="existingMethods">The existing external system methods, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareExternalSystem(ExternalSystemDto incoming, ExternalSystemDefinition? existing, IReadOnlyList<ExternalSystemMethod>? existingMethods)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("ExternalSystem", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "BaseUrl", existing.EndpointUrl, incoming.BaseUrl);
|
||||
AddIfDifferent(changes, "AuthType", existing.AuthType, incoming.AuthType);
|
||||
|
||||
// Secrets: presence-only comparison (we never echo the value in the diff).
|
||||
var existingHasSecret = !string.IsNullOrEmpty(existing.AuthConfiguration);
|
||||
var incomingHasSecret = incoming.Secrets is not null && incoming.Secrets.Values.ContainsKey("AuthConfiguration");
|
||||
if (existingHasSecret != incomingHasSecret)
|
||||
{
|
||||
changes.Add(new FieldChange("Secrets.AuthConfiguration",
|
||||
existingHasSecret ? "<present>" : null,
|
||||
incomingHasSecret ? "<present>" : null));
|
||||
}
|
||||
|
||||
// Methods are name-keyed children.
|
||||
var existingForCompare = existingMethods ?? Array.Empty<ExternalSystemMethod>();
|
||||
DiffChildren(
|
||||
existingForCompare,
|
||||
incoming.Methods,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
ExternalSystemMethodsEqual,
|
||||
"Methods",
|
||||
changes);
|
||||
|
||||
return BuildItem("ExternalSystem", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming database connection against the existing database connection in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming database connection from the bundle.</param>
|
||||
/// <param name="existing">The existing database connection in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareDatabaseConnection(DatabaseConnectionDto incoming, DatabaseConnectionDefinition? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("DatabaseConnection", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "MaxRetries", existing.MaxRetries, incoming.MaxRetries);
|
||||
AddIfDifferent(changes, "RetryDelay", existing.RetryDelay.ToString(), incoming.RetryDelay.ToString());
|
||||
|
||||
// ConnectionString lives in Secrets only — presence-only comparison.
|
||||
var existingHasSecret = !string.IsNullOrEmpty(existing.ConnectionString);
|
||||
var incomingHasSecret = incoming.Secrets is not null && incoming.Secrets.Values.ContainsKey("ConnectionString");
|
||||
if (existingHasSecret != incomingHasSecret)
|
||||
{
|
||||
changes.Add(new FieldChange("Secrets.ConnectionString",
|
||||
existingHasSecret ? "<present>" : null,
|
||||
incomingHasSecret ? "<present>" : null));
|
||||
}
|
||||
|
||||
return BuildItem("DatabaseConnection", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming notification list against the existing notification list in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming notification list from the bundle.</param>
|
||||
/// <param name="existing">The existing notification list in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareNotificationList(NotificationListDto incoming, NotificationList? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("NotificationList", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "Type", existing.Type.ToString(), incoming.Type.ToString());
|
||||
|
||||
DiffChildren(
|
||||
existing.Recipients,
|
||||
incoming.Recipients,
|
||||
e => e.Name,
|
||||
i => i.Name,
|
||||
(e, i) => e.EmailAddress == i.EmailAddress,
|
||||
"Recipients",
|
||||
changes);
|
||||
|
||||
return BuildItem("NotificationList", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming SMTP configuration against the existing SMTP configuration in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming SMTP configuration from the bundle.</param>
|
||||
/// <param name="existing">The existing SMTP configuration in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareSmtpConfiguration(SmtpConfigDto incoming, SmtpConfiguration? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("SmtpConfiguration", incoming.Host);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "Port", existing.Port, incoming.Port);
|
||||
AddIfDifferent(changes, "AuthType", existing.AuthType, incoming.AuthType);
|
||||
AddIfDifferent(changes, "FromAddress", existing.FromAddress, incoming.FromAddress);
|
||||
AddIfDifferent(changes, "TlsMode", existing.TlsMode, incoming.TlsMode);
|
||||
AddIfDifferent(changes, "ConnectionTimeoutSeconds", existing.ConnectionTimeoutSeconds, incoming.ConnectionTimeoutSeconds);
|
||||
AddIfDifferent(changes, "MaxConcurrentConnections", existing.MaxConcurrentConnections, incoming.MaxConcurrentConnections);
|
||||
AddIfDifferent(changes, "MaxRetries", existing.MaxRetries, incoming.MaxRetries);
|
||||
AddIfDifferent(changes, "RetryDelay", existing.RetryDelay.ToString(), incoming.RetryDelay.ToString());
|
||||
|
||||
var existingHasSecret = !string.IsNullOrEmpty(existing.Credentials);
|
||||
var incomingHasSecret = incoming.Secrets is not null && incoming.Secrets.Values.ContainsKey("Credentials");
|
||||
if (existingHasSecret != incomingHasSecret)
|
||||
{
|
||||
changes.Add(new FieldChange("Secrets.Credentials",
|
||||
existingHasSecret ? "<present>" : null,
|
||||
incomingHasSecret ? "<present>" : null));
|
||||
}
|
||||
|
||||
return BuildItem("SmtpConfiguration", incoming.Host, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming API key against the existing API key in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming API key from the bundle.</param>
|
||||
/// <param name="existing">The existing API key in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareApiKey(ApiKeyDto incoming, ApiKey? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("ApiKey", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "IsEnabled", existing.IsEnabled, incoming.IsEnabled);
|
||||
// KeyHash is opaque — record only changed/unchanged, not the value.
|
||||
if (!string.Equals(existing.KeyHash, incoming.KeyHash, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add(new FieldChange("KeyHash", "<changed>", "<changed>"));
|
||||
}
|
||||
return BuildItem("ApiKey", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming API method against the existing API method in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming API method from the bundle.</param>
|
||||
/// <param name="existing">The existing API method in the database, or null if new.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareApiMethod(ApiMethodDto incoming, ApiMethod? existing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("ApiMethod", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "ApprovedApiKeyIds", existing.ApprovedApiKeyIds, incoming.ApprovedApiKeyIds);
|
||||
AddIfDifferent(changes, "ParameterDefinitions", existing.ParameterDefinitions, incoming.ParameterDefinitions);
|
||||
AddIfDifferent(changes, "ReturnDefinition", existing.ReturnDefinition, incoming.ReturnDefinition);
|
||||
AddIfDifferent(changes, "TimeoutSeconds", existing.TimeoutSeconds, incoming.TimeoutSeconds);
|
||||
AddCodeChangeIfDifferent(changes, "Script", existing.Script, incoming.Script);
|
||||
|
||||
return BuildItem("ApiMethod", incoming.Name, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares an incoming template folder against the existing template folder in the database.
|
||||
/// </summary>
|
||||
/// <param name="incoming">The incoming template folder from the bundle.</param>
|
||||
/// <param name="existing">The existing template folder in the database, or null if new.</param>
|
||||
/// <param name="folderNameById">A mapping of folder IDs to names for resolving parent folder references.</param>
|
||||
/// <returns>An import preview item describing the conflict type and differences.</returns>
|
||||
public ImportPreviewItem CompareTemplateFolder(TemplateFolderDto incoming, TemplateFolder? existing, IReadOnlyDictionary<int, string> folderNameById)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(incoming);
|
||||
if (existing is null) return New("TemplateFolder", incoming.Name);
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
AddIfDifferent(changes, "SortOrder", existing.SortOrder, incoming.SortOrder);
|
||||
var existingParentName = existing.ParentFolderId is { } pid && folderNameById.TryGetValue(pid, out var n) ? n : null;
|
||||
AddIfDifferent(changes, "ParentName", existingParentName, incoming.ParentName);
|
||||
return BuildItem("TemplateFolder", incoming.Name, changes);
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
private static ImportPreviewItem New(string entityType, string name) =>
|
||||
new(entityType, name, ExistingVersion: null, IncomingVersion: null, ConflictKind.New, FieldDiffJson: null, BlockerReason: null);
|
||||
|
||||
private static ImportPreviewItem BuildItem(string entityType, string name, List<FieldChange> changes)
|
||||
{
|
||||
if (changes.Count == 0)
|
||||
{
|
||||
return new ImportPreviewItem(entityType, name, null, null, ConflictKind.Identical, FieldDiffJson: null, BlockerReason: null);
|
||||
}
|
||||
var diff = new FieldDiff(
|
||||
Adds: Array.Empty<string>(),
|
||||
Removes: Array.Empty<string>(),
|
||||
Changes: changes);
|
||||
return new ImportPreviewItem(entityType, name, null, null, ConflictKind.Modified, FieldDiffJson: JsonSerializer.Serialize(diff, DiffJsonOptions), BlockerReason: null);
|
||||
}
|
||||
|
||||
private static void AddIfDifferent<T>(List<FieldChange> changes, string field, T existing, T incoming)
|
||||
{
|
||||
if (!Equals(existing, incoming))
|
||||
{
|
||||
changes.Add(new FieldChange(field, existing?.ToString(), incoming?.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCodeChangeIfDifferent(List<FieldChange> changes, string field, string? existing, string? incoming)
|
||||
{
|
||||
// Script bodies can be large — record a line-count delta + change marker
|
||||
// instead of inlining the full text so the diff JSON stays compact.
|
||||
var sameNullness = existing is null == incoming is null;
|
||||
var bothPresentAndEqual = sameNullness && (existing is null || string.Equals(existing, incoming, StringComparison.Ordinal));
|
||||
if (bothPresentAndEqual) return;
|
||||
|
||||
var oldLines = existing?.Split('\n').Length ?? 0;
|
||||
var newLines = incoming?.Split('\n').Length ?? 0;
|
||||
changes.Add(new FieldChange(field, $"<{oldLines} lines>", $"<{newLines} lines>"));
|
||||
}
|
||||
|
||||
private static void DiffChildren<TExisting, TIncoming>(
|
||||
IEnumerable<TExisting> existing,
|
||||
IEnumerable<TIncoming> incoming,
|
||||
Func<TExisting, string> existingKey,
|
||||
Func<TIncoming, string> incomingKey,
|
||||
Func<TExisting, TIncoming, bool> equal,
|
||||
string childCategory,
|
||||
List<FieldChange> changes)
|
||||
{
|
||||
var existingByName = existing.GroupBy(existingKey, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
var incomingByName = incoming.GroupBy(incomingKey, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
|
||||
var added = incomingByName.Keys.Where(k => !existingByName.ContainsKey(k)).OrderBy(k => k, StringComparer.Ordinal).ToList();
|
||||
var removed = existingByName.Keys.Where(k => !incomingByName.ContainsKey(k)).OrderBy(k => k, StringComparer.Ordinal).ToList();
|
||||
|
||||
foreach (var name in added)
|
||||
{
|
||||
changes.Add(new FieldChange($"{childCategory}.{name}", null, "<added>"));
|
||||
}
|
||||
foreach (var name in removed)
|
||||
{
|
||||
changes.Add(new FieldChange($"{childCategory}.{name}", "<present>", null));
|
||||
}
|
||||
foreach (var (name, e) in existingByName)
|
||||
{
|
||||
if (!incomingByName.TryGetValue(name, out var i)) continue;
|
||||
if (!equal(e, i))
|
||||
{
|
||||
changes.Add(new FieldChange($"{childCategory}.{name}", "<modified>", "<modified>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scripts get a finer-grained per-row line-count delta so the preview UI
|
||||
/// can show the operator which scripts changed and roughly how much.
|
||||
/// </summary>
|
||||
private static void DiffScriptChildren(
|
||||
IEnumerable<TemplateScript> existing,
|
||||
IEnumerable<TemplateScriptDto> incoming,
|
||||
List<FieldChange> changes)
|
||||
{
|
||||
var existingByName = existing.GroupBy(s => s.Name, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
var incomingByName = incoming.GroupBy(s => s.Name, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
|
||||
foreach (var name in incomingByName.Keys.Where(k => !existingByName.ContainsKey(k)).OrderBy(k => k, StringComparer.Ordinal))
|
||||
{
|
||||
var inc = incomingByName[name];
|
||||
var newLines = inc.Code.Split('\n').Length;
|
||||
changes.Add(new FieldChange($"Scripts.{name}", null, $"<added, {newLines} lines>"));
|
||||
}
|
||||
foreach (var name in existingByName.Keys.Where(k => !incomingByName.ContainsKey(k)).OrderBy(k => k, StringComparer.Ordinal))
|
||||
{
|
||||
var ex = existingByName[name];
|
||||
var oldLines = ex.Code.Split('\n').Length;
|
||||
changes.Add(new FieldChange($"Scripts.{name}", $"<present, {oldLines} lines>", null));
|
||||
}
|
||||
foreach (var (name, ex) in existingByName)
|
||||
{
|
||||
if (!incomingByName.TryGetValue(name, out var inc)) continue;
|
||||
if (!ScriptsEqual(ex, inc))
|
||||
{
|
||||
var oldLines = ex.Code.Split('\n').Length;
|
||||
var newLines = inc.Code.Split('\n').Length;
|
||||
changes.Add(new FieldChange($"Scripts.{name}", $"<{oldLines} lines>", $"<{newLines} lines>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AttributesEqual(TemplateAttribute e, TemplateAttributeDto i) =>
|
||||
e.Value == i.Value
|
||||
&& e.DataType == i.DataType
|
||||
&& e.IsLocked == i.IsLocked
|
||||
&& e.Description == i.Description
|
||||
&& e.DataSourceReference == i.DataSourceReference;
|
||||
|
||||
private static bool AlarmsEqual(TemplateAlarm e, TemplateAlarmDto i) =>
|
||||
e.Description == i.Description
|
||||
&& e.PriorityLevel == i.PriorityLevel
|
||||
&& e.TriggerType == i.TriggerType
|
||||
&& e.TriggerConfiguration == i.TriggerConfiguration
|
||||
&& e.IsLocked == i.IsLocked;
|
||||
|
||||
private static bool ScriptsEqual(TemplateScript e, TemplateScriptDto i) =>
|
||||
string.Equals(e.Code, i.Code, StringComparison.Ordinal)
|
||||
&& e.TriggerType == i.TriggerType
|
||||
&& e.TriggerConfiguration == i.TriggerConfiguration
|
||||
&& e.ParameterDefinitions == i.ParameterDefinitions
|
||||
&& e.ReturnDefinition == i.ReturnDefinition
|
||||
&& e.IsLocked == i.IsLocked;
|
||||
|
||||
private static bool ExternalSystemMethodsEqual(ExternalSystemMethod e, ExternalSystemMethodDto i) =>
|
||||
e.HttpMethod == i.HttpMethod
|
||||
&& e.Path == i.Path
|
||||
&& e.ParameterDefinitions == i.ParameterDefinitions
|
||||
&& e.ReturnDefinition == i.ReturnDefinition;
|
||||
|
||||
private static string? FolderNameOf(Template t)
|
||||
{
|
||||
// Templates carry only a FK to the folder; the EntitySerializer projects
|
||||
// it to a name. The diff doesn't have access to a folder lookup at
|
||||
// CompareTemplate scope, so fall back to "<id:N>" when only the id is
|
||||
// known. The PreviewAsync caller can pass a hydrated Template (via
|
||||
// GetTemplateWithChildrenAsync) and the folder name typically isn't on
|
||||
// it — this branch is a deliberate best-effort.
|
||||
return t.FolderId is null ? null : $"<id:{t.FolderId}>";
|
||||
}
|
||||
|
||||
private static string? BaseTemplateNameOf(Template t)
|
||||
{
|
||||
return t.ParentTemplateId is null ? null : $"<id:{t.ParentTemplateId}>";
|
||||
}
|
||||
|
||||
private static string CompositionTargetNameOf(TemplateComposition comp)
|
||||
{
|
||||
return $"<id:{comp.ComposedTemplateId}>";
|
||||
}
|
||||
|
||||
private sealed record FieldChange(
|
||||
[property: JsonPropertyName("field")] string Field,
|
||||
[property: JsonPropertyName("oldValue")] string? OldValue,
|
||||
[property: JsonPropertyName("newValue")] string? NewValue);
|
||||
|
||||
private sealed record FieldDiff(
|
||||
[property: JsonPropertyName("adds")] IReadOnlyList<string> Adds,
|
||||
[property: JsonPropertyName("removes")] IReadOnlyList<string> Removes,
|
||||
[property: JsonPropertyName("changes")] IReadOnlyList<FieldChange> Changes);
|
||||
}
|
||||
Reference in New Issue
Block a user