using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Types.Enums; using ScadaLink.Communication.Grpc; using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp; namespace ScadaLink.AuditLog.Telemetry; /// /// Bridges Audit Log (#23) rows between the in-process record /// and the wire-format exchanged over the /// IngestAuditEvents RPC. /// /// /// Lossy by design: the proto contract intentionally omits two fields. /// /// — site-local SQLite state, never travels. /// — central-set at ingest time, not at the site. /// /// /// String nullability convention: proto3 scalar strings cannot be absent, so nullable /// .NET strings round-trip as empty strings on the wire. Nullable integers use the /// Int32Value wrapper so they preserve true null semantics. /// /// public static class AuditEventMapper { /// /// Projects an into its wire-format DTO. Null reference /// fields collapse to empty strings; null integer fields leave the wrapper unset. /// public static AuditEventDto ToDto(AuditEvent evt) { ArgumentNullException.ThrowIfNull(evt); var dto = new AuditEventDto { EventId = evt.EventId.ToString(), OccurredAtUtc = Timestamp.FromDateTime(EnsureUtc(evt.OccurredAtUtc)), Channel = evt.Channel.ToString(), Kind = evt.Kind.ToString(), CorrelationId = evt.CorrelationId?.ToString() ?? string.Empty, SourceSiteId = evt.SourceSiteId ?? string.Empty, SourceInstanceId = evt.SourceInstanceId ?? string.Empty, SourceScript = evt.SourceScript ?? string.Empty, Actor = evt.Actor ?? string.Empty, Target = evt.Target ?? string.Empty, Status = evt.Status.ToString(), ErrorMessage = evt.ErrorMessage ?? string.Empty, ErrorDetail = evt.ErrorDetail ?? string.Empty, RequestSummary = evt.RequestSummary ?? string.Empty, ResponseSummary = evt.ResponseSummary ?? string.Empty, PayloadTruncated = evt.PayloadTruncated, Extra = evt.Extra ?? string.Empty }; if (evt.HttpStatus.HasValue) { dto.HttpStatus = evt.HttpStatus.Value; } if (evt.DurationMs.HasValue) { dto.DurationMs = evt.DurationMs.Value; } return dto; } /// /// Reconstructs an from its wire-format DTO. Empty strings /// rehydrate as null reference values; absent integer wrappers stay null. /// and /// are intentionally left null — the central ingest actor sets the latter. /// public static AuditEvent FromDto(AuditEventDto dto) { ArgumentNullException.ThrowIfNull(dto); return new AuditEvent { EventId = Guid.Parse(dto.EventId), OccurredAtUtc = DateTime.SpecifyKind(dto.OccurredAtUtc.ToDateTime(), DateTimeKind.Utc), IngestedAtUtc = null, Channel = Enum.Parse(dto.Channel), Kind = Enum.Parse(dto.Kind), CorrelationId = NullIfEmpty(dto.CorrelationId) is { } cid ? Guid.Parse(cid) : null, SourceSiteId = NullIfEmpty(dto.SourceSiteId), SourceInstanceId = NullIfEmpty(dto.SourceInstanceId), SourceScript = NullIfEmpty(dto.SourceScript), Actor = NullIfEmpty(dto.Actor), Target = NullIfEmpty(dto.Target), Status = Enum.Parse(dto.Status), HttpStatus = dto.HttpStatus, DurationMs = dto.DurationMs, ErrorMessage = NullIfEmpty(dto.ErrorMessage), ErrorDetail = NullIfEmpty(dto.ErrorDetail), RequestSummary = NullIfEmpty(dto.RequestSummary), ResponseSummary = NullIfEmpty(dto.ResponseSummary), PayloadTruncated = dto.PayloadTruncated, Extra = NullIfEmpty(dto.Extra), ForwardState = null }; } private static string? NullIfEmpty(string? value) => string.IsNullOrEmpty(value) ? null : value; private static DateTime EnsureUtc(DateTime value) => value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc); }