using System.Collections.Generic; using System.Linq; using MongoDB.Bson; namespace StellaOps.Concelier.Connector.Ghsa.Internal; internal sealed record GhsaCursor( DateTimeOffset? LastUpdatedExclusive, DateTimeOffset? CurrentWindowStart, DateTimeOffset? CurrentWindowEnd, int NextPage, IReadOnlyCollection PendingDocuments, IReadOnlyCollection PendingMappings) { private static readonly IReadOnlyCollection EmptyGuidList = Array.Empty(); public static GhsaCursor Empty { get; } = new( null, null, null, 1, EmptyGuidList, EmptyGuidList); public BsonDocument ToBsonDocument() { var document = new BsonDocument { ["nextPage"] = NextPage, ["pendingDocuments"] = new BsonArray(PendingDocuments.Select(id => id.ToString())), ["pendingMappings"] = new BsonArray(PendingMappings.Select(id => id.ToString())), }; if (LastUpdatedExclusive.HasValue) { document["lastUpdatedExclusive"] = LastUpdatedExclusive.Value.UtcDateTime; } if (CurrentWindowStart.HasValue) { document["currentWindowStart"] = CurrentWindowStart.Value.UtcDateTime; } if (CurrentWindowEnd.HasValue) { document["currentWindowEnd"] = CurrentWindowEnd.Value.UtcDateTime; } return document; } public static GhsaCursor FromBson(BsonDocument? document) { if (document is null || document.ElementCount == 0) { return Empty; } var lastUpdatedExclusive = document.TryGetValue("lastUpdatedExclusive", out var lastUpdated) ? ParseDate(lastUpdated) : null; var windowStart = document.TryGetValue("currentWindowStart", out var windowStartValue) ? ParseDate(windowStartValue) : null; var windowEnd = document.TryGetValue("currentWindowEnd", out var windowEndValue) ? ParseDate(windowEndValue) : null; var nextPage = document.TryGetValue("nextPage", out var nextPageValue) && nextPageValue.IsInt32 ? Math.Max(1, nextPageValue.AsInt32) : 1; var pendingDocuments = ReadGuidArray(document, "pendingDocuments"); var pendingMappings = ReadGuidArray(document, "pendingMappings"); return new GhsaCursor( lastUpdatedExclusive, windowStart, windowEnd, nextPage, pendingDocuments, pendingMappings); } public GhsaCursor WithPendingDocuments(IEnumerable ids) => this with { PendingDocuments = ids?.Distinct().ToArray() ?? EmptyGuidList }; public GhsaCursor WithPendingMappings(IEnumerable ids) => this with { PendingMappings = ids?.Distinct().ToArray() ?? EmptyGuidList }; public GhsaCursor WithLastUpdatedExclusive(DateTimeOffset? timestamp) => this with { LastUpdatedExclusive = timestamp }; public GhsaCursor WithCurrentWindowStart(DateTimeOffset? timestamp) => this with { CurrentWindowStart = timestamp }; public GhsaCursor WithCurrentWindowEnd(DateTimeOffset? timestamp) => this with { CurrentWindowEnd = timestamp }; public GhsaCursor WithNextPage(int page) => this with { NextPage = page < 1 ? 1 : page }; private static DateTimeOffset? ParseDate(BsonValue value) { return value.BsonType switch { BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc), BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(), _ => null, }; } private static IReadOnlyCollection ReadGuidArray(BsonDocument document, string field) { if (!document.TryGetValue(field, out var value) || value is not BsonArray array) { return EmptyGuidList; } var results = new List(array.Count); foreach (var element in array) { if (element is null) { continue; } if (Guid.TryParse(element.ToString(), out var guid)) { results.Add(guid); } } return results; } }