feat(comm): add source_node field to AuditEventDto + SiteCallOperationalDto proto

- AuditEventDto field 22, SiteCallOperationalDto field 12. Both follow the
  existing empty-string-means-null convention.
- Mappers carry SourceNode end-to-end; round-trip tests cover both populated
  and null cases.
This commit is contained in:
Joseph Doherty
2026-05-23 16:10:03 -04:00
parent 990eb02fe0
commit dfaa416ebe
9 changed files with 221 additions and 40 deletions

View File

@@ -50,6 +50,7 @@ public static class AuditEventDtoMapper
ExecutionId = evt.ExecutionId?.ToString() ?? string.Empty,
ParentExecutionId = evt.ParentExecutionId?.ToString() ?? string.Empty,
SourceSiteId = evt.SourceSiteId ?? string.Empty,
SourceNode = evt.SourceNode ?? string.Empty,
SourceInstanceId = evt.SourceInstanceId ?? string.Empty,
SourceScript = evt.SourceScript ?? string.Empty,
Actor = evt.Actor ?? string.Empty,
@@ -97,6 +98,7 @@ public static class AuditEventDtoMapper
ExecutionId = NullIfEmpty(dto.ExecutionId) is { } eid ? Guid.Parse(eid) : null,
ParentExecutionId = NullIfEmpty(dto.ParentExecutionId) is { } pid ? Guid.Parse(pid) : null,
SourceSiteId = NullIfEmpty(dto.SourceSiteId),
SourceNode = NullIfEmpty(dto.SourceNode),
SourceInstanceId = NullIfEmpty(dto.SourceInstanceId),
SourceScript = NullIfEmpty(dto.SourceScript),
Actor = NullIfEmpty(dto.Actor),

View File

@@ -55,6 +55,7 @@ public static class SiteCallDtoMapper
Channel = dto.Channel,
Target = dto.Target,
SourceSite = dto.SourceSite,
SourceNode = string.IsNullOrEmpty(dto.SourceNode) ? null : dto.SourceNode,
Status = dto.Status,
RetryCount = dto.RetryCount,
LastError = string.IsNullOrEmpty(dto.LastError) ? null : dto.LastError,

View File

@@ -93,6 +93,7 @@ message AuditEventDto {
string extra = 19;
string execution_id = 20; // empty string represents null
string parent_execution_id = 21; // empty string represents null
string source_node = 22; // empty string represents null
}
message AuditEventBatch { repeated AuditEventDto events = 1; }
@@ -114,6 +115,7 @@ message SiteCallOperationalDto {
google.protobuf.Timestamp created_at_utc = 9;
google.protobuf.Timestamp updated_at_utc = 10;
google.protobuf.Timestamp terminal_at_utc = 11; // absent when not terminal
string source_node = 12; // empty string represents null
}
message CachedTelemetryPacket {

View File

@@ -41,7 +41,7 @@ namespace ScadaLink.Communication.Grpc {
"c3RhdGUYAyABKA4yGi5zaXRlc3RyZWFtLkFsYXJtU3RhdGVFbnVtEhAKCHBy",
"aW9yaXR5GAQgASgFEi0KCXRpbWVzdGFtcBgFIAEoCzIaLmdvb2dsZS5wcm90",
"b2J1Zi5UaW1lc3RhbXASKQoFbGV2ZWwYBiABKA4yGi5zaXRlc3RyZWFtLkFs",
"YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAkiqAQKDUF1ZGl0RXZlbnRE",
"YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAkivQQKDUF1ZGl0RXZlbnRE",
"dG8SEAoIZXZlbnRfaWQYASABKAkSMwoPb2NjdXJyZWRfYXRfdXRjGAIgASgL",
"MhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdjaGFubmVsGAMgASgJ",
"EgwKBGtpbmQYBCABKAkSFgoOY29ycmVsYXRpb25faWQYBSABKAkSFgoOc291",
@@ -53,43 +53,44 @@ namespace ScadaLink.Communication.Grpc {
"bWVzc2FnZRgOIAEoCRIUCgxlcnJvcl9kZXRhaWwYDyABKAkSFwoPcmVxdWVz",
"dF9zdW1tYXJ5GBAgASgJEhgKEHJlc3BvbnNlX3N1bW1hcnkYESABKAkSGQoR",
"cGF5bG9hZF90cnVuY2F0ZWQYEiABKAgSDQoFZXh0cmEYEyABKAkSFAoMZXhl",
"Y3V0aW9uX2lkGBQgASgJEhsKE3BhcmVudF9leGVjdXRpb25faWQYFSABKAki",
"PAoPQXVkaXRFdmVudEJhdGNoEikKBmV2ZW50cxgBIAMoCzIZLnNpdGVzdHJl",
"YW0uQXVkaXRFdmVudER0byInCglJbmdlc3RBY2sSGgoSYWNjZXB0ZWRfZXZl",
"bnRfaWRzGAEgAygJIvQCChZTaXRlQ2FsbE9wZXJhdGlvbmFsRHRvEhwKFHRy",
"YWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJEg8KB2NoYW5uZWwYAiABKAkSDgoG",
"dGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9zaXRlGAQgASgJEg4KBnN0YXR1cxgF",
"IAEoCRITCgtyZXRyeV9jb3VudBgGIAEoBRISCgpsYXN0X2Vycm9yGAcgASgJ",
"EjAKC2h0dHBfc3RhdHVzGAggASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMy",
"VmFsdWUSMgoOY3JlYXRlZF9hdF91dGMYCSABKAsyGi5nb29nbGUucHJvdG9i",
"dWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRfYXRfdXRjGAogASgLMhouZ29vZ2xl",
"LnByb3RvYnVmLlRpbWVzdGFtcBIzCg90ZXJtaW5hbF9hdF91dGMYCyABKAsy",
"Gi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoABChVDYWNoZWRUZWxlbWV0",
"cnlQYWNrZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1",
"ZGl0RXZlbnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFt",
"LlNpdGVDYWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0",
"Y2gSMgoHcGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1l",
"dHJ5UGFja2V0IlsKFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2Vf",
"dXRjGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRj",
"aF9zaXplGAIgASgFIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2",
"ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3Jl",
"X2F2YWlsYWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5TUEVD",
"SUZJRUQQABIQCgxRVUFMSVRZX0dPT0QQARIVChFRVUFMSVRZX1VOQ0VSVEFJ",
"ThACEg8KC1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoXQUxB",
"Uk1fU1RBVEVfVU5TUEVDSUZJRUQQABIWChJBTEFSTV9TVEFURV9OT1JNQUwQ",
"ARIWChJBTEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVudW0S",
"FAoQQUxBUk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJNX0xFVkVMX0xPVxABEhcK",
"E0FMQVJNX0xFVkVMX0xPV19MT1cQAhIUChBBTEFSTV9MRVZFTF9ISUdIEAMS",
"GQoVQUxBUk1fTEVWRUxfSElHSF9ISUdIEAQy4QIKEVNpdGVTdHJlYW1TZXJ2",
"aWNlElUKEVN1YnNjcmliZUluc3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0YW5j",
"ZVN0cmVhbVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVudDAB",
"EkcKEUluZ2VzdEF1ZGl0RXZlbnRzEhsuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50",
"QmF0Y2gaFS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJQChVJbmdlc3RDYWNoZWRU",
"ZWxlbWV0cnkSIC5zaXRlc3RyZWFtLkNhY2hlZFRlbGVtZXRyeUJhdGNoGhUu",
"c2l0ZXN0cmVhbS5Jbmdlc3RBY2sSWgoPUHVsbEF1ZGl0RXZlbnRzEiIuc2l0",
"ZXN0cmVhbS5QdWxsQXVkaXRFdmVudHNSZXF1ZXN0GiMuc2l0ZXN0cmVhbS5Q",
"dWxsQXVkaXRFdmVudHNSZXNwb25zZUIfqgIcU2NhZGFMaW5rLkNvbW11bmlj",
"YXRpb24uR3JwY2IGcHJvdG8z"));
"Y3V0aW9uX2lkGBQgASgJEhsKE3BhcmVudF9leGVjdXRpb25faWQYFSABKAkS",
"EwoLc291cmNlX25vZGUYFiABKAkiPAoPQXVkaXRFdmVudEJhdGNoEikKBmV2",
"ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0byInCglJbmdl",
"c3RBY2sSGgoSYWNjZXB0ZWRfZXZlbnRfaWRzGAEgAygJIokDChZTaXRlQ2Fs",
"bE9wZXJhdGlvbmFsRHRvEhwKFHRyYWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJ",
"Eg8KB2NoYW5uZWwYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9z",
"aXRlGAQgASgJEg4KBnN0YXR1cxgFIAEoCRITCgtyZXRyeV9jb3VudBgGIAEo",
"BRISCgpsYXN0X2Vycm9yGAcgASgJEjAKC2h0dHBfc3RhdHVzGAggASgLMhsu",
"Z29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUSMgoOY3JlYXRlZF9hdF91dGMY",
"CSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRf",
"YXRfdXRjGAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCg90",
"ZXJtaW5hbF9hdF91dGMYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0",
"YW1wEhMKC3NvdXJjZV9ub2RlGAwgASgJIoABChVDYWNoZWRUZWxlbWV0cnlQ",
"YWNrZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1ZGl0",
"RXZlbnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFtLlNp",
"dGVDYWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gS",
"MgoHcGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5",
"UGFja2V0IlsKFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2VfdXRj",
"GAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRjaF9z",
"aXplGAIgASgFIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2ZW50",
"cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3JlX2F2",
"YWlsYWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5TUEVDSUZJ",
"RUQQABIQCgxRVUFMSVRZX0dPT0QQARIVChFRVUFMSVRZX1VOQ0VSVEFJThAC",
"Eg8KC1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoXQUxBUk1f",
"U1RBVEVfVU5TUEVDSUZJRUQQABIWChJBTEFSTV9TVEFURV9OT1JNQUwQARIW",
"ChJBTEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVudW0SFAoQ",
"QUxBUk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJNX0xFVkVMX0xPVxABEhcKE0FM",
"QVJNX0xFVkVMX0xPV19MT1cQAhIUChBBTEFSTV9MRVZFTF9ISUdIEAMSGQoV",
"QUxBUk1fTEVWRUxfSElHSF9ISUdIEAQy4QIKEVNpdGVTdHJlYW1TZXJ2aWNl",
"ElUKEVN1YnNjcmliZUluc3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0YW5jZVN0",
"cmVhbVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVudDABEkcK",
"EUluZ2VzdEF1ZGl0RXZlbnRzEhsuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50QmF0",
"Y2gaFS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJQChVJbmdlc3RDYWNoZWRUZWxl",
"bWV0cnkSIC5zaXRlc3RyZWFtLkNhY2hlZFRlbGVtZXRyeUJhdGNoGhUuc2l0",
"ZXN0cmVhbS5Jbmdlc3RBY2sSWgoPUHVsbEF1ZGl0RXZlbnRzEiIuc2l0ZXN0",
"cmVhbS5QdWxsQXVkaXRFdmVudHNSZXF1ZXN0GiMuc2l0ZXN0cmVhbS5QdWxs",
"QXVkaXRFdmVudHNSZXNwb25zZUIfqgIcU2NhZGFMaW5rLkNvbW11bmljYXRp",
"b24uR3JwY2IGcHJvdG8z"));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ScadaLink.Communication.Grpc.Quality), typeof(global::ScadaLink.Communication.Grpc.AlarmStateEnum), typeof(global::ScadaLink.Communication.Grpc.AlarmLevelEnum), }, null, new pbr::GeneratedClrTypeInfo[] {
@@ -97,10 +98,10 @@ namespace ScadaLink.Communication.Grpc {
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.SiteStreamEvent), global::ScadaLink.Communication.Grpc.SiteStreamEvent.Parser, new[]{ "CorrelationId", "AttributeChanged", "AlarmChanged" }, new[]{ "Event" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AttributeValueUpdate), global::ScadaLink.Communication.Grpc.AttributeValueUpdate.Parser, new[]{ "InstanceUniqueName", "AttributePath", "AttributeName", "Value", "Quality", "Timestamp" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AlarmStateUpdate), global::ScadaLink.Communication.Grpc.AlarmStateUpdate.Parser, new[]{ "InstanceUniqueName", "AlarmName", "State", "Priority", "Timestamp", "Level", "Message" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AuditEventDto), global::ScadaLink.Communication.Grpc.AuditEventDto.Parser, new[]{ "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", "ExecutionId", "ParentExecutionId" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AuditEventDto), global::ScadaLink.Communication.Grpc.AuditEventDto.Parser, new[]{ "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", "ExecutionId", "ParentExecutionId", "SourceNode" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AuditEventBatch), global::ScadaLink.Communication.Grpc.AuditEventBatch.Parser, new[]{ "Events" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.IngestAck), global::ScadaLink.Communication.Grpc.IngestAck.Parser, new[]{ "AcceptedEventIds" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.SiteCallOperationalDto), global::ScadaLink.Communication.Grpc.SiteCallOperationalDto.Parser, new[]{ "TrackedOperationId", "Channel", "Target", "SourceSite", "Status", "RetryCount", "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.SiteCallOperationalDto), global::ScadaLink.Communication.Grpc.SiteCallOperationalDto.Parser, new[]{ "TrackedOperationId", "Channel", "Target", "SourceSite", "Status", "RetryCount", "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc", "SourceNode" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.CachedTelemetryPacket), global::ScadaLink.Communication.Grpc.CachedTelemetryPacket.Parser, new[]{ "AuditEvent", "Operational" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.CachedTelemetryBatch), global::ScadaLink.Communication.Grpc.CachedTelemetryBatch.Parser, new[]{ "Packets" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.PullAuditEventsRequest), global::ScadaLink.Communication.Grpc.PullAuditEventsRequest.Parser, new[]{ "SinceUtc", "BatchSize" }, null, null, null, null),
@@ -1594,6 +1595,7 @@ namespace ScadaLink.Communication.Grpc {
extra_ = other.extra_;
executionId_ = other.executionId_;
parentExecutionId_ = other.parentExecutionId_;
sourceNode_ = other.sourceNode_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -1871,6 +1873,21 @@ namespace ScadaLink.Communication.Grpc {
}
}
/// <summary>Field number for the "source_node" field.</summary>
public const int SourceNodeFieldNumber = 22;
private string sourceNode_ = "";
/// <summary>
/// empty string represents null
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string SourceNode {
get { return sourceNode_; }
set {
sourceNode_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
@@ -1907,6 +1924,7 @@ namespace ScadaLink.Communication.Grpc {
if (Extra != other.Extra) return false;
if (ExecutionId != other.ExecutionId) return false;
if (ParentExecutionId != other.ParentExecutionId) return false;
if (SourceNode != other.SourceNode) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -1935,6 +1953,7 @@ namespace ScadaLink.Communication.Grpc {
if (Extra.Length != 0) hash ^= Extra.GetHashCode();
if (ExecutionId.Length != 0) hash ^= ExecutionId.GetHashCode();
if (ParentExecutionId.Length != 0) hash ^= ParentExecutionId.GetHashCode();
if (SourceNode.Length != 0) hash ^= SourceNode.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -2035,6 +2054,10 @@ namespace ScadaLink.Communication.Grpc {
output.WriteRawTag(170, 1);
output.WriteString(ParentExecutionId);
}
if (SourceNode.Length != 0) {
output.WriteRawTag(178, 1);
output.WriteString(SourceNode);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -2127,6 +2150,10 @@ namespace ScadaLink.Communication.Grpc {
output.WriteRawTag(170, 1);
output.WriteString(ParentExecutionId);
}
if (SourceNode.Length != 0) {
output.WriteRawTag(178, 1);
output.WriteString(SourceNode);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -2200,6 +2227,9 @@ namespace ScadaLink.Communication.Grpc {
if (ParentExecutionId.Length != 0) {
size += 2 + pb::CodedOutputStream.ComputeStringSize(ParentExecutionId);
}
if (SourceNode.Length != 0) {
size += 2 + pb::CodedOutputStream.ComputeStringSize(SourceNode);
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -2282,6 +2312,9 @@ namespace ScadaLink.Communication.Grpc {
if (other.ParentExecutionId.Length != 0) {
ParentExecutionId = other.ParentExecutionId;
}
if (other.SourceNode.Length != 0) {
SourceNode = other.SourceNode;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -2394,6 +2427,10 @@ namespace ScadaLink.Communication.Grpc {
ParentExecutionId = input.ReadString();
break;
}
case 178: {
SourceNode = input.ReadString();
break;
}
}
}
#endif
@@ -2506,6 +2543,10 @@ namespace ScadaLink.Communication.Grpc {
ParentExecutionId = input.ReadString();
break;
}
case 178: {
SourceNode = input.ReadString();
break;
}
}
}
}
@@ -2939,6 +2980,7 @@ namespace ScadaLink.Communication.Grpc {
createdAtUtc_ = other.createdAtUtc_ != null ? other.createdAtUtc_.Clone() : null;
updatedAtUtc_ = other.updatedAtUtc_ != null ? other.updatedAtUtc_.Clone() : null;
terminalAtUtc_ = other.terminalAtUtc_ != null ? other.terminalAtUtc_.Clone() : null;
sourceNode_ = other.sourceNode_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -3097,6 +3139,21 @@ namespace ScadaLink.Communication.Grpc {
}
}
/// <summary>Field number for the "source_node" field.</summary>
public const int SourceNodeFieldNumber = 12;
private string sourceNode_ = "";
/// <summary>
/// empty string represents null
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string SourceNode {
get { return sourceNode_; }
set {
sourceNode_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
@@ -3123,6 +3180,7 @@ namespace ScadaLink.Communication.Grpc {
if (!object.Equals(CreatedAtUtc, other.CreatedAtUtc)) return false;
if (!object.Equals(UpdatedAtUtc, other.UpdatedAtUtc)) return false;
if (!object.Equals(TerminalAtUtc, other.TerminalAtUtc)) return false;
if (SourceNode != other.SourceNode) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -3141,6 +3199,7 @@ namespace ScadaLink.Communication.Grpc {
if (createdAtUtc_ != null) hash ^= CreatedAtUtc.GetHashCode();
if (updatedAtUtc_ != null) hash ^= UpdatedAtUtc.GetHashCode();
if (terminalAtUtc_ != null) hash ^= TerminalAtUtc.GetHashCode();
if (SourceNode.Length != 0) hash ^= SourceNode.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -3202,6 +3261,10 @@ namespace ScadaLink.Communication.Grpc {
output.WriteRawTag(90);
output.WriteMessage(TerminalAtUtc);
}
if (SourceNode.Length != 0) {
output.WriteRawTag(98);
output.WriteString(SourceNode);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -3255,6 +3318,10 @@ namespace ScadaLink.Communication.Grpc {
output.WriteRawTag(90);
output.WriteMessage(TerminalAtUtc);
}
if (SourceNode.Length != 0) {
output.WriteRawTag(98);
output.WriteString(SourceNode);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -3298,6 +3365,9 @@ namespace ScadaLink.Communication.Grpc {
if (terminalAtUtc_ != null) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(TerminalAtUtc);
}
if (SourceNode.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(SourceNode);
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -3354,6 +3424,9 @@ namespace ScadaLink.Communication.Grpc {
}
TerminalAtUtc.MergeFrom(other.TerminalAtUtc);
}
if (other.SourceNode.Length != 0) {
SourceNode = other.SourceNode;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -3429,6 +3502,10 @@ namespace ScadaLink.Communication.Grpc {
input.ReadMessage(TerminalAtUtc);
break;
}
case 98: {
SourceNode = input.ReadString();
break;
}
}
}
#endif
@@ -3504,6 +3581,10 @@ namespace ScadaLink.Communication.Grpc {
input.ReadMessage(TerminalAtUtc);
break;
}
case 98: {
SourceNode = input.ReadString();
break;
}
}
}
}

View File

@@ -100,6 +100,7 @@ public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder
Channel = op.Channel,
Target = op.Target,
SourceSite = op.SourceSite,
SourceNode = op.SourceNode ?? string.Empty,
Status = op.Status,
RetryCount = op.RetryCount,
LastError = op.LastError ?? string.Empty,

View File

@@ -34,6 +34,7 @@ public class AuditEventDtoMapperTests
ExecutionId = executionId,
ParentExecutionId = parentExecutionId,
SourceSiteId = "site-1",
SourceNode = "node-a",
SourceInstanceId = "Pump01",
SourceScript = "OnDemand",
Actor = "design-key",
@@ -61,6 +62,7 @@ public class AuditEventDtoMapperTests
Assert.Equal(original.ExecutionId, roundTripped.ExecutionId);
Assert.Equal(original.ParentExecutionId, roundTripped.ParentExecutionId);
Assert.Equal(original.SourceSiteId, roundTripped.SourceSiteId);
Assert.Equal(original.SourceNode, roundTripped.SourceNode);
Assert.Equal(original.SourceInstanceId, roundTripped.SourceInstanceId);
Assert.Equal(original.SourceScript, roundTripped.SourceScript);
Assert.Equal(original.Actor, roundTripped.Actor);
@@ -99,6 +101,7 @@ public class AuditEventDtoMapperTests
Assert.Equal(string.Empty, dto.ExecutionId);
Assert.Equal(string.Empty, dto.ParentExecutionId);
Assert.Equal(string.Empty, dto.SourceSiteId);
Assert.Equal(string.Empty, dto.SourceNode);
Assert.Equal(string.Empty, dto.SourceInstanceId);
Assert.Equal(string.Empty, dto.SourceScript);
Assert.Equal(string.Empty, dto.Actor);
@@ -124,6 +127,7 @@ public class AuditEventDtoMapperTests
ExecutionId = string.Empty,
ParentExecutionId = string.Empty,
SourceSiteId = string.Empty,
SourceNode = string.Empty,
SourceInstanceId = string.Empty,
SourceScript = string.Empty,
Actor = string.Empty,
@@ -141,6 +145,7 @@ public class AuditEventDtoMapperTests
Assert.Null(evt.ExecutionId);
Assert.Null(evt.ParentExecutionId);
Assert.Null(evt.SourceSiteId);
Assert.Null(evt.SourceNode);
Assert.Null(evt.SourceInstanceId);
Assert.Null(evt.SourceScript);
Assert.Null(evt.Actor);
@@ -232,4 +237,52 @@ public class AuditEventDtoMapperTests
Assert.Equal("ApiCallCached", dto.Kind);
Assert.Equal("Parked", dto.Status);
}
[Fact]
public void AuditEventDto_round_trip_preserves_SourceNode()
{
var evt = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceNode = "node-a"
};
var dto = AuditEventDtoMapper.ToDto(evt);
// Wire form: empty-string-means-null convention; populated value
// travels verbatim.
Assert.Equal("node-a", dto.SourceNode);
var roundTripped = AuditEventDtoMapper.FromDto(dto);
Assert.Equal("node-a", roundTripped.SourceNode);
}
[Fact]
public void AuditEventDto_round_trip_preserves_null_SourceNode()
{
var evt = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
SourceNode = null
};
var dto = AuditEventDtoMapper.ToDto(evt);
// ToDto collapses null → empty on the wire…
Assert.Equal(string.Empty, dto.SourceNode);
var roundTripped = AuditEventDtoMapper.FromDto(dto);
// …and FromDto rehydrates empty → null.
Assert.Null(roundTripped.SourceNode);
}
}

View File

@@ -25,6 +25,7 @@ public class AuditEventProtoTests
Kind = "ApiCall",
CorrelationId = Guid.NewGuid().ToString(),
SourceSiteId = "site-1",
SourceNode = "node-a",
SourceInstanceId = "Pump01",
SourceScript = "OnDemand",
Actor = "design-key",
@@ -49,6 +50,7 @@ public class AuditEventProtoTests
Assert.Equal(original.Kind, deserialized.Kind);
Assert.Equal(original.CorrelationId, deserialized.CorrelationId);
Assert.Equal(original.SourceSiteId, deserialized.SourceSiteId);
Assert.Equal(original.SourceNode, deserialized.SourceNode);
Assert.Equal(original.SourceInstanceId, deserialized.SourceInstanceId);
Assert.Equal(original.SourceScript, deserialized.SourceScript);
Assert.Equal(original.Actor, deserialized.Actor);

View File

@@ -39,6 +39,7 @@ public class CachedTelemetryProtoTests
Channel = "ApiOutbound",
Target = "ERP.GetOrder",
SourceSite = "site-melbourne",
SourceNode = "node-a",
Status = "Delivered",
RetryCount = 3,
LastError = "transient 503",
@@ -55,6 +56,7 @@ public class CachedTelemetryProtoTests
Assert.Equal(original.Channel, deserialized.Channel);
Assert.Equal(original.Target, deserialized.Target);
Assert.Equal(original.SourceSite, deserialized.SourceSite);
Assert.Equal(original.SourceNode, deserialized.SourceNode);
Assert.Equal(original.Status, deserialized.Status);
Assert.Equal(original.RetryCount, deserialized.RetryCount);
Assert.Equal(original.LastError, deserialized.LastError);

View File

@@ -1,3 +1,4 @@
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using ScadaLink.Communication.Grpc;
@@ -28,6 +29,7 @@ public class SiteCallDtoMapperTests
Channel = "ApiOutbound",
Target = "ERP.GetOrder",
SourceSite = "site-melbourne",
SourceNode = "node-a",
Status = "Delivered",
RetryCount = 3,
LastError = "transient 503",
@@ -43,6 +45,7 @@ public class SiteCallDtoMapperTests
Assert.Equal("ApiOutbound", entity.Channel);
Assert.Equal("ERP.GetOrder", entity.Target);
Assert.Equal("site-melbourne", entity.SourceSite);
Assert.Equal("node-a", entity.SourceNode);
Assert.Equal("Delivered", entity.Status);
Assert.Equal(3, entity.RetryCount);
Assert.Equal("transient 503", entity.LastError);
@@ -121,6 +124,40 @@ public class SiteCallDtoMapperTests
Assert.Throws<ArgumentNullException>(() => SiteCallDtoMapper.FromDto(null!));
}
[Fact]
public void SiteCallOperationalDto_round_trip_preserves_SourceNode()
{
// Populated SourceNode travels verbatim across the wire and through
// the DTO→entity mapper.
var dto = NewMinimalDto();
dto.SourceNode = "node-a";
var bytes = dto.ToByteArray();
var onWire = SiteCallOperationalDto.Parser.ParseFrom(bytes);
Assert.Equal("node-a", onWire.SourceNode);
var entity = SiteCallDtoMapper.FromDto(onWire);
Assert.Equal("node-a", entity.SourceNode);
}
[Fact]
public void SiteCallOperationalDto_round_trip_preserves_null_SourceNode()
{
// The DTO uses an empty-string-means-null convention on the wire;
// FromDto rehydrates that back to a true null on the entity.
var dto = NewMinimalDto();
// SourceNode left at proto default (empty string) — semantically null.
var bytes = dto.ToByteArray();
var onWire = SiteCallOperationalDto.Parser.ParseFrom(bytes);
Assert.Equal(string.Empty, onWire.SourceNode);
var entity = SiteCallDtoMapper.FromDto(onWire);
Assert.Null(entity.SourceNode);
}
private static SiteCallOperationalDto NewMinimalDto() => new()
{
TrackedOperationId = Guid.NewGuid().ToString(),