up
This commit is contained in:
@@ -30,7 +30,7 @@ public sealed class RawDocumentStorage
|
||||
string uri,
|
||||
byte[] content,
|
||||
string? contentType,
|
||||
DateTimeOffset? expiresAt,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
CancellationToken cancellationToken,
|
||||
Guid? documentId = null)
|
||||
{
|
||||
|
||||
@@ -418,7 +418,7 @@ public sealed class UbuntuConnector : IFeedConnector
|
||||
await _stateRepository.UpdateCursorAsync(SourceName, doc, _timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string ComputeNoticeHash(BsonDocument document)
|
||||
private string ComputeNoticeHash(BsonDocument document)
|
||||
{
|
||||
var bytes = document.ToBson();
|
||||
var hash = _hash.ComputeHash(bytes, HashAlgorithms.Sha256);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Linksets
|
||||
{
|
||||
public static class PolicyAuthSignalFactory
|
||||
{
|
||||
public static PolicyAuthSignal ToPolicyAuthSignal(AdvisoryLinkset linkset)
|
||||
{
|
||||
if (linkset is null) throw new ArgumentNullException(nameof(linkset));
|
||||
|
||||
var subject = linkset.Normalized?.Purls?.FirstOrDefault() ?? linkset.AdvisoryId;
|
||||
var evidenceUri = $"urn:linkset:{linkset.AdvisoryId}";
|
||||
|
||||
return new PolicyAuthSignal(
|
||||
Id: linkset.AdvisoryId,
|
||||
Tenant: linkset.TenantId,
|
||||
Subject: subject ?? string.Empty,
|
||||
Source: linkset.Source,
|
||||
SignalType: "reachability",
|
||||
Evidence: new[]
|
||||
{
|
||||
new PolicyAuthEvidence(evidenceUri)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record PolicyAuthSignal(
|
||||
string Id,
|
||||
string Tenant,
|
||||
string Subject,
|
||||
string Source,
|
||||
string SignalType,
|
||||
IReadOnlyList<PolicyAuthEvidence> Evidence);
|
||||
|
||||
public sealed record PolicyAuthEvidence(string Uri);
|
||||
}
|
||||
@@ -1,248 +1,276 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MongoDB.Bson
|
||||
{
|
||||
public readonly struct ObjectId : IEquatable<ObjectId>
|
||||
public class BsonValue : IEquatable<BsonValue?>
|
||||
{
|
||||
public Guid Value { get; }
|
||||
public ObjectId(Guid value) => Value = value;
|
||||
public ObjectId(string value) => Value = Guid.TryParse(value, out var g) ? g : Guid.Empty;
|
||||
public static ObjectId GenerateNewId() => new(Guid.NewGuid());
|
||||
public static ObjectId Empty => new(Guid.Empty);
|
||||
public bool Equals(ObjectId other) => Value.Equals(other.Value);
|
||||
public override bool Equals(object? obj) => obj is ObjectId other && Equals(other);
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
public override string ToString() => Value.ToString("N");
|
||||
public static bool operator ==(ObjectId left, ObjectId right) => left.Equals(right);
|
||||
public static bool operator !=(ObjectId left, ObjectId right) => !left.Equals(right);
|
||||
}
|
||||
protected object? RawValue;
|
||||
|
||||
public enum BsonType { Document, Array, String, Boolean, Int32, Int64, Double, DateTime, Guid, Null }
|
||||
|
||||
public class BsonValue
|
||||
{
|
||||
protected readonly object? _value;
|
||||
public BsonValue(object? value) => _value = value;
|
||||
internal object? RawValue => _value;
|
||||
public static implicit operator BsonValue(string value) => new BsonString(value ?? string.Empty);
|
||||
public static implicit operator BsonValue(bool value) => new BsonBoolean(value);
|
||||
public static implicit operator BsonValue(int value) => new BsonInt32(value);
|
||||
public static implicit operator BsonValue(long value) => new BsonInt64(value);
|
||||
public static implicit operator BsonValue(double value) => new BsonDouble(value);
|
||||
public static implicit operator BsonValue(DateTime value) => new BsonDateTime(DateTime.SpecifyKind(value, DateTimeKind.Utc));
|
||||
public static implicit operator BsonValue(DateTimeOffset value) => new BsonDateTime(value.UtcDateTime);
|
||||
public static implicit operator BsonValue(Guid value) => new BsonString(value.ToString("D"));
|
||||
public static BsonValue Create(object? value) => BsonDocument.WrapExternal(value);
|
||||
public virtual BsonType BsonType => _value switch
|
||||
public BsonValue(object? value = null)
|
||||
{
|
||||
null => BsonType.Null,
|
||||
BsonDocument => BsonType.Document,
|
||||
BsonArray => BsonType.Array,
|
||||
string => BsonType.String,
|
||||
bool => BsonType.Boolean,
|
||||
int => BsonType.Int32,
|
||||
long => BsonType.Int64,
|
||||
double => BsonType.Double,
|
||||
DateTime => BsonType.DateTime,
|
||||
DateTimeOffset => BsonType.DateTime,
|
||||
Guid => BsonType.Guid,
|
||||
_ => BsonType.Null
|
||||
};
|
||||
public bool IsString => _value is string;
|
||||
public bool IsBsonDocument => _value is BsonDocument;
|
||||
public bool IsBsonArray => _value is BsonArray;
|
||||
public bool IsBsonNull => _value is null;
|
||||
public string AsString => _value?.ToString() ?? string.Empty;
|
||||
public BsonDocument AsBsonDocument => _value as BsonDocument ?? throw new InvalidCastException();
|
||||
public BsonArray AsBsonArray => _value as BsonArray ?? throw new InvalidCastException();
|
||||
public Guid AsGuid => _value is Guid g ? g : Guid.Empty;
|
||||
public DateTime AsDateTime => _value switch
|
||||
{
|
||||
DateTimeOffset dto => dto.UtcDateTime,
|
||||
DateTime dt => dt,
|
||||
_ => DateTime.MinValue
|
||||
};
|
||||
public int AsInt32 => _value is int i ? i : 0;
|
||||
public long AsInt64 => _value is long l ? l : 0;
|
||||
public double AsDouble => _value is double d ? d : 0d;
|
||||
public bool AsBoolean => _value is bool b && b;
|
||||
public bool IsInt32 => _value is int;
|
||||
public DateTime ToUniversalTime() => _value switch
|
||||
{
|
||||
DateTimeOffset dto => dto.UtcDateTime,
|
||||
DateTime dt => dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime(),
|
||||
string s when DateTimeOffset.TryParse(s, out var parsed) => parsed.UtcDateTime,
|
||||
_ => DateTime.MinValue
|
||||
};
|
||||
public override string ToString() => _value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
public class BsonString : BsonValue { public BsonString(string value) : base(value) { } }
|
||||
public class BsonBoolean : BsonValue { public BsonBoolean(bool value) : base(value) { } }
|
||||
public class BsonInt32 : BsonValue { public BsonInt32(int value) : base(value) { } }
|
||||
public class BsonInt64 : BsonValue { public BsonInt64(long value) : base(value) { } }
|
||||
public class BsonDouble : BsonValue { public BsonDouble(double value) : base(value) { } }
|
||||
public class BsonDateTime : BsonValue { public BsonDateTime(DateTime value) : base(value) { } }
|
||||
public class BsonNull : BsonValue
|
||||
{
|
||||
private BsonNull() : base(null) { }
|
||||
public static BsonNull Value { get; } = new();
|
||||
}
|
||||
|
||||
public sealed class BsonElement
|
||||
{
|
||||
public BsonElement(string name, BsonValue value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
RawValue = value;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public BsonValue Value { get; }
|
||||
}
|
||||
public bool IsString => RawValue is string;
|
||||
public bool IsBoolean => RawValue is bool;
|
||||
public bool IsBsonDocument => RawValue is BsonDocument;
|
||||
public bool IsBsonArray => RawValue is BsonArray;
|
||||
|
||||
public class BsonBinaryData : BsonValue
|
||||
{
|
||||
private readonly byte[] _bytes;
|
||||
public BsonBinaryData(byte[] bytes) : base(null) => _bytes = bytes ?? Array.Empty<byte>();
|
||||
public BsonBinaryData(Guid guid) : this(guid.ToByteArray()) { }
|
||||
public byte[] AsByteArray => _bytes;
|
||||
public Guid ToGuid() => new(_bytes);
|
||||
}
|
||||
|
||||
public class BsonArray : BsonValue, IEnumerable<BsonValue>
|
||||
{
|
||||
private readonly List<BsonValue> _items = new();
|
||||
public BsonArray() : base(null) { }
|
||||
public BsonArray(IEnumerable<BsonValue> values) : this() => _items.AddRange(values);
|
||||
public BsonArray(IEnumerable<object?> values) : this()
|
||||
public string AsString => RawValue switch
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
_items.Add(BsonDocument.WrapExternal(value));
|
||||
}
|
||||
}
|
||||
public void Add(BsonValue value) => _items.Add(value);
|
||||
public IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public BsonValue this[int index] { get => _items[index]; set => _items[index] = value; }
|
||||
public int Count => _items.Count;
|
||||
null => string.Empty,
|
||||
string s => s,
|
||||
Guid g => g.ToString(),
|
||||
_ => Convert.ToString(RawValue, CultureInfo.InvariantCulture) ?? string.Empty
|
||||
};
|
||||
|
||||
public bool AsBoolean => RawValue switch
|
||||
{
|
||||
bool b => b,
|
||||
string s when bool.TryParse(s, out var b) => b,
|
||||
int i => i != 0,
|
||||
long l => l != 0,
|
||||
_ => false
|
||||
};
|
||||
|
||||
public int ToInt32() => RawValue switch
|
||||
{
|
||||
int i => i,
|
||||
long l => (int)l,
|
||||
double d => (int)d,
|
||||
string s when int.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var i) => i,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
public Guid AsGuid => RawValue switch
|
||||
{
|
||||
Guid g => g,
|
||||
string s when Guid.TryParse(s, out var g) => g,
|
||||
_ => Guid.Empty
|
||||
};
|
||||
|
||||
public ObjectId AsObjectId => RawValue switch
|
||||
{
|
||||
ObjectId o => o,
|
||||
string s => ObjectId.Parse(s),
|
||||
_ => ObjectId.Empty
|
||||
};
|
||||
|
||||
public BsonDocument AsBsonDocument => RawValue as BsonDocument ?? (this as BsonDocument ?? new BsonDocument());
|
||||
public BsonArray AsBsonArray => RawValue as BsonArray ?? (this as BsonArray ?? new BsonArray());
|
||||
|
||||
public override string ToString() => AsString;
|
||||
|
||||
internal virtual BsonValue Clone() => new BsonValue(RawValue);
|
||||
|
||||
public bool Equals(BsonValue? other) => other is not null && Equals(RawValue, other.RawValue);
|
||||
public override bool Equals(object? obj) => obj is BsonValue other && Equals(other);
|
||||
public override int GetHashCode() => RawValue?.GetHashCode() ?? 0;
|
||||
|
||||
public static implicit operator BsonValue(string value) => new(value);
|
||||
public static implicit operator BsonValue(Guid value) => new(value);
|
||||
public static implicit operator BsonValue(int value) => new(value);
|
||||
public static implicit operator BsonValue(long value) => new(value);
|
||||
public static implicit operator BsonValue(bool value) => new(value);
|
||||
public static implicit operator BsonValue(double value) => new(value);
|
||||
public static implicit operator BsonValue(DateTimeOffset value) => new(value);
|
||||
}
|
||||
|
||||
public class BsonDocument : BsonValue, IEnumerable<KeyValuePair<string, BsonValue>>
|
||||
public sealed class BsonDocument : BsonValue, IDictionary<string, BsonValue>
|
||||
{
|
||||
private readonly Dictionary<string, BsonValue> _values = new(StringComparer.Ordinal);
|
||||
public BsonDocument() : base(null) { }
|
||||
public BsonDocument(string key, object? value) : this() => _values[key] = Wrap(value);
|
||||
public BsonDocument(IEnumerable<KeyValuePair<string, object?>> pairs) : this()
|
||||
|
||||
public BsonDocument()
|
||||
: base(null)
|
||||
{
|
||||
foreach (var kvp in pairs)
|
||||
RawValue = this;
|
||||
}
|
||||
|
||||
public BsonDocument(IDictionary<string, object?> values)
|
||||
: this()
|
||||
{
|
||||
foreach (var kvp in values)
|
||||
{
|
||||
_values[kvp.Key] = Wrap(kvp.Value);
|
||||
_values[kvp.Key] = ToBsonValue(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static BsonValue Wrap(object? value) => value switch
|
||||
{
|
||||
BsonValue v => v,
|
||||
IEnumerable<BsonValue> enumerable => new BsonArray(enumerable),
|
||||
IEnumerable<object?> objEnum => new BsonArray(objEnum.Select(Wrap)),
|
||||
_ => new BsonValue(value)
|
||||
};
|
||||
|
||||
internal static BsonValue WrapExternal(object? value) => Wrap(value);
|
||||
public int ElementCount => _values.Count;
|
||||
|
||||
public BsonValue this[string key]
|
||||
{
|
||||
get => _values[key];
|
||||
set => _values[key] = Wrap(value);
|
||||
set => _values[key] = value ?? new BsonValue();
|
||||
}
|
||||
|
||||
public int ElementCount => _values.Count;
|
||||
public IEnumerable<BsonElement> Elements => _values.Select(kvp => new BsonElement(kvp.Key, kvp.Value));
|
||||
public ICollection<string> Keys => _values.Keys;
|
||||
public ICollection<BsonValue> Values => _values.Values;
|
||||
public int Count => _values.Count;
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public bool Contains(string key) => _values.ContainsKey(key);
|
||||
public void Add(string key, BsonValue value) => _values[key] = value ?? new BsonValue();
|
||||
public void Add(string key, object? value) => _values[key] = ToBsonValue(value);
|
||||
public void Add(KeyValuePair<string, BsonValue> item) => Add(item.Key, item.Value);
|
||||
|
||||
public void Clear() => _values.Clear();
|
||||
public bool Contains(KeyValuePair<string, BsonValue> item) => _values.Contains(item);
|
||||
public bool ContainsKey(string key) => _values.ContainsKey(key);
|
||||
public void CopyTo(KeyValuePair<string, BsonValue>[] array, int arrayIndex) => ((IDictionary<string, BsonValue>)_values).CopyTo(array, arrayIndex);
|
||||
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() => _values.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator();
|
||||
public bool Remove(string key) => _values.Remove(key);
|
||||
public bool Remove(KeyValuePair<string, BsonValue> item) => _values.Remove(item.Key);
|
||||
public bool TryGetValue(string key, out BsonValue value) => _values.TryGetValue(key, out value!);
|
||||
|
||||
public BsonValue GetValue(string key, BsonValue? defaultValue = null)
|
||||
{
|
||||
return _values.TryGetValue(key, out var value)
|
||||
? value
|
||||
: defaultValue ?? new BsonValue(null);
|
||||
}
|
||||
|
||||
public bool Remove(string key) => _values.Remove(key);
|
||||
|
||||
public void Add(string key, BsonValue value) => _values[key] = value;
|
||||
public void Add(string key, object? value) => _values[key] = Wrap(value);
|
||||
|
||||
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() => _values.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public BsonValue GetValue(string key) => _values[key];
|
||||
|
||||
public BsonDocument DeepClone()
|
||||
{
|
||||
var clone = new BsonDocument();
|
||||
var copy = new BsonDocument();
|
||||
foreach (var kvp in _values)
|
||||
{
|
||||
clone[kvp.Key] = kvp.Value;
|
||||
copy._values[kvp.Key] = kvp.Value?.Clone() ?? new BsonValue();
|
||||
}
|
||||
return clone;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static BsonDocument Parse(string json)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return FromElement(doc.RootElement);
|
||||
return FromElement(doc.RootElement).AsBsonDocument;
|
||||
}
|
||||
|
||||
private static BsonDocument FromElement(JsonElement element)
|
||||
private static BsonValue FromElement(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => FromObject(element),
|
||||
JsonValueKind.Array => FromArray(element),
|
||||
JsonValueKind.String => new BsonValue(element.GetString()),
|
||||
JsonValueKind.Number => element.TryGetInt64(out var l) ? new BsonValue(l) : new BsonValue(element.GetDouble()),
|
||||
JsonValueKind.True => new BsonValue(true),
|
||||
JsonValueKind.False => new BsonValue(false),
|
||||
JsonValueKind.Null or JsonValueKind.Undefined => new BsonValue(null),
|
||||
_ => new BsonValue(element.ToString())
|
||||
};
|
||||
}
|
||||
|
||||
private static BsonDocument FromObject(JsonElement element)
|
||||
{
|
||||
var doc = new BsonDocument();
|
||||
foreach (var prop in element.EnumerateObject())
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
doc[prop.Name] = FromJsonValue(prop.Value);
|
||||
doc[property.Name] = FromElement(property.Value);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
private static BsonValue FromJsonValue(JsonElement element) => element.ValueKind switch
|
||||
private static BsonArray FromArray(JsonElement element)
|
||||
{
|
||||
JsonValueKind.Object => FromElement(element),
|
||||
JsonValueKind.Array => new BsonArray(element.EnumerateArray().Select(FromJsonValue)),
|
||||
JsonValueKind.String => new BsonString(element.GetString() ?? string.Empty),
|
||||
JsonValueKind.Number => element.TryGetInt64(out var l) ? new BsonInt64(l) : new BsonDouble(element.GetDouble()),
|
||||
JsonValueKind.True => new BsonBoolean(true),
|
||||
JsonValueKind.False => new BsonBoolean(false),
|
||||
JsonValueKind.Null or JsonValueKind.Undefined => new BsonValue(null),
|
||||
_ => new BsonValue(null)
|
||||
};
|
||||
|
||||
public string ToJson(MongoDB.Bson.IO.JsonWriterSettings? settings = null)
|
||||
{
|
||||
var dict = _values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value));
|
||||
return JsonSerializer.Serialize(dict, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
var array = new BsonArray();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
array.Add(FromElement(item));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
public byte[] ToBson() => Encoding.UTF8.GetBytes(ToJson());
|
||||
|
||||
private static object? Unwrap(BsonValue value) => value switch
|
||||
internal static BsonValue ToBsonValue(object? value)
|
||||
{
|
||||
BsonDocument doc => doc._values.ToDictionary(kvp => kvp.Key, kvp => Unwrap(kvp.Value)),
|
||||
BsonArray array => array.Select(Unwrap).ToArray(),
|
||||
_ => value.RawValue
|
||||
};
|
||||
return value switch
|
||||
{
|
||||
null => new BsonValue(null),
|
||||
BsonValue bson => bson,
|
||||
string s => new BsonValue(s),
|
||||
Guid g => new BsonValue(g),
|
||||
int i => new BsonValue(i),
|
||||
long l => new BsonValue(l),
|
||||
bool b => new BsonValue(b),
|
||||
double d => new BsonValue(d),
|
||||
float f => new BsonValue(f),
|
||||
DateTime dt => new BsonValue(dt),
|
||||
DateTimeOffset dto => new BsonValue(dto),
|
||||
IEnumerable<object?> enumerable => new BsonArray(enumerable.Select(ToBsonValue)),
|
||||
_ => new BsonValue(value)
|
||||
};
|
||||
}
|
||||
|
||||
internal override BsonValue Clone() => DeepClone();
|
||||
}
|
||||
|
||||
public sealed class BsonArray : BsonValue, IList<BsonValue>
|
||||
{
|
||||
private readonly List<BsonValue> _items = new();
|
||||
|
||||
public BsonArray()
|
||||
: base(null)
|
||||
{
|
||||
RawValue = this;
|
||||
}
|
||||
|
||||
public BsonArray(IEnumerable<BsonValue> items)
|
||||
: this()
|
||||
{
|
||||
_items.AddRange(items);
|
||||
}
|
||||
|
||||
public BsonValue this[int index]
|
||||
{
|
||||
get => _items[index];
|
||||
set => _items[index] = value ?? new BsonValue();
|
||||
}
|
||||
|
||||
public int Count => _items.Count;
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(BsonValue item) => _items.Add(item ?? new BsonValue());
|
||||
public void Add(object? item) => _items.Add(BsonDocument.ToBsonValue(item));
|
||||
public void Clear() => _items.Clear();
|
||||
public bool Contains(BsonValue item) => _items.Contains(item);
|
||||
public void CopyTo(BsonValue[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
|
||||
public IEnumerator<BsonValue> GetEnumerator() => _items.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
|
||||
public int IndexOf(BsonValue item) => _items.IndexOf(item);
|
||||
public void Insert(int index, BsonValue item) => _items.Insert(index, item ?? new BsonValue());
|
||||
public bool Remove(BsonValue item) => _items.Remove(item);
|
||||
public void RemoveAt(int index) => _items.RemoveAt(index);
|
||||
|
||||
internal override BsonValue Clone() => new BsonArray(_items.Select(i => i.Clone()));
|
||||
}
|
||||
|
||||
public readonly struct ObjectId : IEquatable<ObjectId>
|
||||
{
|
||||
private readonly string _value;
|
||||
|
||||
public ObjectId(string value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public static ObjectId Empty { get; } = new(string.Empty);
|
||||
|
||||
public override string ToString() => _value;
|
||||
|
||||
public static ObjectId Parse(string value) => new(value ?? string.Empty);
|
||||
|
||||
public bool Equals(ObjectId other) => string.Equals(_value, other._value, StringComparison.Ordinal);
|
||||
public override bool Equals(object? obj) => obj is ObjectId other && Equals(other);
|
||||
public override int GetHashCode() => _value?.GetHashCode(StringComparison.Ordinal) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
namespace MongoDB.Bson.IO
|
||||
namespace MongoDB.Bson.Serialization.Attributes
|
||||
{
|
||||
public enum JsonOutputMode { Strict, RelaxedExtendedJson }
|
||||
public class JsonWriterSettings
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct)]
|
||||
public sealed class BsonElementAttribute : Attribute
|
||||
{
|
||||
public JsonOutputMode OutputMode { get; set; } = JsonOutputMode.Strict;
|
||||
public BsonElementAttribute(string elementName)
|
||||
{
|
||||
ElementName = elementName;
|
||||
}
|
||||
|
||||
public string ElementName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace MongoDB.Driver
|
||||
{
|
||||
@@ -31,6 +32,7 @@ namespace MongoDB.Driver
|
||||
public interface IMongoClient
|
||||
{
|
||||
IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null);
|
||||
Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class MongoClient : IMongoClient
|
||||
@@ -38,20 +40,47 @@ namespace MongoDB.Driver
|
||||
public MongoClient(string connectionString) { }
|
||||
public MongoClient(MongoClientSettings settings) { }
|
||||
public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null) => new MongoDatabase(name);
|
||||
public Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
public class MongoDatabaseSettings { }
|
||||
|
||||
public sealed class DatabaseNamespace
|
||||
{
|
||||
public DatabaseNamespace(string databaseName) => DatabaseName = databaseName;
|
||||
public string DatabaseName { get; }
|
||||
}
|
||||
|
||||
public interface IMongoDatabase
|
||||
{
|
||||
IMongoCollection<TDocument> GetCollection<TDocument>(string name, MongoCollectionSettings? settings = null);
|
||||
DatabaseNamespace DatabaseNamespace { get; }
|
||||
Task DropCollectionAsync(string name, CancellationToken cancellationToken = default);
|
||||
BsonDocument RunCommand(BsonDocument command, CancellationToken cancellationToken = default);
|
||||
T RunCommand<T>(BsonDocument command, CancellationToken cancellationToken = default);
|
||||
Task<T> RunCommandAsync<T>(BsonDocument command, CancellationToken cancellationToken = default);
|
||||
BsonDocument RunCommand(string command, CancellationToken cancellationToken = default);
|
||||
T RunCommand<T>(string command, CancellationToken cancellationToken = default);
|
||||
Task<T> RunCommandAsync<T>(string command, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class MongoDatabase : IMongoDatabase
|
||||
{
|
||||
public MongoDatabase(string name) => Name = name;
|
||||
public MongoDatabase(string name)
|
||||
{
|
||||
Name = name;
|
||||
DatabaseNamespace = new DatabaseNamespace(name);
|
||||
}
|
||||
public string Name { get; }
|
||||
public DatabaseNamespace DatabaseNamespace { get; }
|
||||
public IMongoCollection<TDocument> GetCollection<TDocument>(string name, MongoCollectionSettings? settings = null) => new MongoCollection<TDocument>(name);
|
||||
public Task DropCollectionAsync(string name, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
public BsonDocument RunCommand(BsonDocument command, CancellationToken cancellationToken = default) => new();
|
||||
public T RunCommand<T>(BsonDocument command, CancellationToken cancellationToken = default) => default!;
|
||||
public Task<T> RunCommandAsync<T>(BsonDocument command, CancellationToken cancellationToken = default) => Task.FromResult(default(T)!);
|
||||
public BsonDocument RunCommand(string command, CancellationToken cancellationToken = default) => new();
|
||||
public T RunCommand<T>(string command, CancellationToken cancellationToken = default) => default!;
|
||||
public Task<T> RunCommandAsync<T>(string command, CancellationToken cancellationToken = default) => Task.FromResult(default(T)!);
|
||||
}
|
||||
|
||||
public class MongoCollectionSettings { }
|
||||
@@ -59,8 +88,10 @@ namespace MongoDB.Driver
|
||||
public interface IMongoCollection<TDocument>
|
||||
{
|
||||
Task InsertOneAsync(TDocument document, InsertOneOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task InsertManyAsync(IEnumerable<TDocument> documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<ReplaceOneResult> ReplaceOneAsync(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<DeleteResult> DeleteOneAsync(FilterDefinition<TDocument> filter, CancellationToken cancellationToken = default);
|
||||
Task<DeleteResult> DeleteManyAsync(FilterDefinition<TDocument> filter, CancellationToken cancellationToken = default);
|
||||
Task<IAsyncCursor<TDocument>> FindAsync(FilterDefinition<TDocument> filter, FindOptions<TDocument, TDocument>? options = null, CancellationToken cancellationToken = default);
|
||||
IFindFluent<TDocument, TDocument> Find(FilterDefinition<TDocument> filter, FindOptions<TDocument, TDocument>? options = null);
|
||||
Task<long> CountDocumentsAsync(FilterDefinition<TDocument> filter, CountOptions? options = null, CancellationToken cancellationToken = default);
|
||||
@@ -88,6 +119,12 @@ namespace MongoDB.Driver
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task InsertManyAsync(IEnumerable<TDocument> documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_docs.AddRange(documents);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<ReplaceOneResult> ReplaceOneAsync(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_docs.Clear();
|
||||
@@ -102,6 +139,13 @@ namespace MongoDB.Driver
|
||||
return Task.FromResult(new DeleteResult(removed ? 1 : 0));
|
||||
}
|
||||
|
||||
public Task<DeleteResult> DeleteManyAsync(FilterDefinition<TDocument> filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var removed = _docs.Count;
|
||||
_docs.Clear();
|
||||
return Task.FromResult(new DeleteResult(removed));
|
||||
}
|
||||
|
||||
public Task<IAsyncCursor<TDocument>> FindAsync(FilterDefinition<TDocument> filter, FindOptions<TDocument, TDocument>? options = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IAsyncCursor<TDocument>>(new AsyncCursor<TDocument>(_docs));
|
||||
|
||||
@@ -212,7 +256,10 @@ namespace MongoDB.Driver
|
||||
=> new FindFluentProjected<TDocument, TNewProjection>(Enumerable.Empty<TNewProjection>());
|
||||
}
|
||||
|
||||
public class FilterDefinition<TDocument> { }
|
||||
public class FilterDefinition<TDocument>
|
||||
{
|
||||
public static FilterDefinition<TDocument> Empty { get; } = new();
|
||||
}
|
||||
public class UpdateDefinition<TDocument> { }
|
||||
public class ProjectionDefinition<TDocument, TProjection> { }
|
||||
public class SortDefinition<TDocument> { }
|
||||
@@ -222,6 +269,7 @@ namespace MongoDB.Driver
|
||||
public class FindOneAndReplaceOptions<TDocument, TProjection> { public bool IsUpsert { get; set; } }
|
||||
public class FindOneAndUpdateOptions<TDocument, TProjection> { public bool IsUpsert { get; set; } }
|
||||
public class InsertOneOptions { }
|
||||
public class InsertManyOptions { }
|
||||
public class CreateIndexOptions { }
|
||||
public class IndexKeysDefinition<TDocument> { }
|
||||
|
||||
@@ -284,7 +332,7 @@ namespace Mongo2Go
|
||||
|
||||
private MongoDbRunner(string connectionString) => ConnectionString = connectionString;
|
||||
|
||||
public static MongoDbRunner Start() => new("mongodb://localhost:27017/fake");
|
||||
public static MongoDbRunner Start(bool singleNodeReplSet = false) => new("mongodb://localhost:27017/fake");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo
|
||||
{
|
||||
public static class MongoStorageDefaults
|
||||
{
|
||||
public const string DefaultDatabaseName = "concelier";
|
||||
|
||||
public static class Collections
|
||||
{
|
||||
public const string AdvisoryStatements = "advisory_statements";
|
||||
public const string AdvisoryRaw = "advisory_raw";
|
||||
public const string Advisory = "advisory";
|
||||
public const string AdvisoryObservations = "advisory_observations";
|
||||
public const string AdvisoryLinksets = "advisory_linksets";
|
||||
public const string Alias = "aliases";
|
||||
public const string Dto = "dto";
|
||||
public const string MergeEvent = "merge_events";
|
||||
public const string Document = "documents";
|
||||
public const string PsirtFlags = "psirt_flags";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +72,32 @@ namespace StellaOps.Concelier.Storage.Mongo
|
||||
this.FetchedAt = FetchedAt ?? CreatedAt;
|
||||
}
|
||||
|
||||
public DocumentRecord(
|
||||
Guid Id,
|
||||
string SourceName,
|
||||
string Uri,
|
||||
string Sha256,
|
||||
string Status = "pending_parse",
|
||||
string? ContentType = null,
|
||||
IReadOnlyDictionary<string, string>? Headers = null,
|
||||
IReadOnlyDictionary<string, string>? Metadata = null,
|
||||
string? Etag = null,
|
||||
DateTimeOffset? LastModified = null,
|
||||
Guid? PayloadId = null,
|
||||
DateTimeOffset? ExpiresAt = null,
|
||||
byte[]? Payload = null,
|
||||
DateTimeOffset? FetchedAt = null)
|
||||
: this(Id, SourceName, Uri, DateTimeOffset.UtcNow, Sha256, Status, ContentType, Headers, Metadata, Etag, LastModified, PayloadId, ExpiresAt, Payload, FetchedAt)
|
||||
{
|
||||
}
|
||||
|
||||
public Guid Id { get; init; }
|
||||
public string SourceName { get; init; }
|
||||
public string Uri { get; init; }
|
||||
public string SourceName { get; init; } = string.Empty;
|
||||
public string Uri { get; init; } = string.Empty;
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset FetchedAt { get; init; }
|
||||
public string Sha256 { get; init; }
|
||||
public string Status { get; init; }
|
||||
public string Sha256 { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string? ContentType { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
@@ -81,37 +108,37 @@ namespace StellaOps.Concelier.Storage.Mongo
|
||||
public byte[]? Payload { get; init; }
|
||||
}
|
||||
|
||||
public interface IDocumentStore
|
||||
{
|
||||
Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken);
|
||||
Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken);
|
||||
Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken);
|
||||
}
|
||||
public interface IDocumentStore
|
||||
{
|
||||
Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken);
|
||||
Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken);
|
||||
Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken);
|
||||
Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class InMemoryDocumentStore : IDocumentStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<(string Source, string Uri), DocumentRecord> _records = new();
|
||||
private readonly ConcurrentDictionary<Guid, DocumentRecord> _byId = new();
|
||||
|
||||
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
{
|
||||
_records.TryGetValue((sourceName, uri), out var record);
|
||||
return Task.FromResult<DocumentRecord?>(record);
|
||||
}
|
||||
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
{
|
||||
_records.TryGetValue((sourceName, uri), out var record);
|
||||
return Task.FromResult<DocumentRecord?>(record);
|
||||
}
|
||||
|
||||
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
_byId.TryGetValue(id, out var record);
|
||||
return Task.FromResult<DocumentRecord?>(record);
|
||||
}
|
||||
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
_byId.TryGetValue(id, out var record);
|
||||
return Task.FromResult<DocumentRecord?>(record);
|
||||
}
|
||||
|
||||
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
_records[(record.SourceName, record.Uri)] = record;
|
||||
_byId[record.Id] = record;
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
_records[(record.SourceName, record.Uri)] = record;
|
||||
_byId[record.Id] = record;
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -129,6 +156,22 @@ public interface IDocumentStore
|
||||
{
|
||||
private readonly InMemoryDocumentStore _inner = new();
|
||||
|
||||
public DocumentStore()
|
||||
{
|
||||
}
|
||||
|
||||
public DocumentStore(object? database, MongoStorageOptions? options)
|
||||
{
|
||||
}
|
||||
|
||||
public DocumentStore(object? database, object? logger)
|
||||
{
|
||||
}
|
||||
|
||||
public DocumentStore(object? database, MongoStorageOptions? options, object? logger)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
=> _inner.FindBySourceAndUriAsync(sourceName, uri, cancellationToken);
|
||||
|
||||
@@ -142,47 +185,70 @@ public interface IDocumentStore
|
||||
=> _inner.UpdateStatusAsync(id, status, cancellationToken);
|
||||
}
|
||||
|
||||
public record DtoRecord(
|
||||
Guid Id,
|
||||
Guid DocumentId,
|
||||
string SourceName,
|
||||
string Format,
|
||||
MongoDB.Bson.BsonDocument Payload,
|
||||
DateTimeOffset CreatedAt)
|
||||
public record DtoRecord
|
||||
{
|
||||
public DtoRecord(
|
||||
Guid Id,
|
||||
Guid DocumentId,
|
||||
string SourceName,
|
||||
string Format,
|
||||
MongoDB.Bson.BsonDocument Payload,
|
||||
DateTimeOffset CreatedAt,
|
||||
string? SchemaVersion = null,
|
||||
DateTimeOffset? ValidatedAt = null)
|
||||
{
|
||||
this.Id = Id;
|
||||
this.DocumentId = DocumentId;
|
||||
this.SourceName = SourceName;
|
||||
this.Format = Format;
|
||||
this.Payload = Payload;
|
||||
this.CreatedAt = CreatedAt;
|
||||
this.SchemaVersion = SchemaVersion ?? string.Empty;
|
||||
this.ValidatedAt = ValidatedAt ?? CreatedAt;
|
||||
}
|
||||
|
||||
public Guid Id { get; init; }
|
||||
public Guid DocumentId { get; init; }
|
||||
public string SourceName { get; init; } = string.Empty;
|
||||
public string Format { get; init; } = string.Empty;
|
||||
public MongoDB.Bson.BsonDocument Payload { get; init; } = new();
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public string SchemaVersion { get; init; } = string.Empty;
|
||||
public DateTimeOffset ValidatedAt { get; init; } = CreatedAt;
|
||||
public DateTimeOffset ValidatedAt { get; init; }
|
||||
}
|
||||
|
||||
public interface IDtoStore
|
||||
{
|
||||
Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken);
|
||||
Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class InMemoryDtoStore : IDtoStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, DtoRecord> _records = new();
|
||||
|
||||
public Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken)
|
||||
public interface IDtoStore
|
||||
{
|
||||
_records[record.DocumentId] = record;
|
||||
return Task.FromResult(record);
|
||||
Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken);
|
||||
Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, int limit, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken)
|
||||
public class InMemoryDtoStore : IDtoStore
|
||||
{
|
||||
_records.TryGetValue(documentId, out var record);
|
||||
return Task.FromResult<DtoRecord?>(record);
|
||||
}
|
||||
private readonly ConcurrentDictionary<Guid, DtoRecord> _records = new();
|
||||
|
||||
public Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, CancellationToken cancellationToken)
|
||||
{
|
||||
var matches = _records.Values.Where(r => string.Equals(r.SourceName, sourceName, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
return Task.FromResult<IReadOnlyList<DtoRecord>>(matches);
|
||||
public Task<DtoRecord> UpsertAsync(DtoRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
_records[record.DocumentId] = record;
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
public Task<DtoRecord?> FindByDocumentIdAsync(Guid documentId, CancellationToken cancellationToken)
|
||||
{
|
||||
_records.TryGetValue(documentId, out var record);
|
||||
return Task.FromResult<DtoRecord?>(record);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DtoRecord>> GetBySourceAsync(string sourceName, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var matches = _records.Values
|
||||
.Where(r => string.Equals(r.SourceName, sourceName, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(limit)
|
||||
.ToArray();
|
||||
return Task.FromResult<IReadOnlyList<DtoRecord>>(matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RawDocumentStorage
|
||||
{
|
||||
@@ -251,7 +317,7 @@ public sealed record SourceStateRecord(
|
||||
sourceName,
|
||||
Enabled: current?.Enabled ?? true,
|
||||
Paused: current?.Paused ?? false,
|
||||
Cursor: cursor.DeepClone(),
|
||||
Cursor: cursor.DeepClone().AsBsonDocument,
|
||||
LastSuccess: completedAt,
|
||||
LastFailure: current?.LastFailure,
|
||||
FailCount: current?.FailCount ?? 0,
|
||||
@@ -288,6 +354,18 @@ public sealed record SourceStateRecord(
|
||||
{
|
||||
private readonly InMemorySourceStateRepository _inner = new();
|
||||
|
||||
public MongoSourceStateRepository()
|
||||
{
|
||||
}
|
||||
|
||||
public MongoSourceStateRepository(object? database, MongoStorageOptions? options)
|
||||
{
|
||||
}
|
||||
|
||||
public MongoSourceStateRepository(object? database, object? logger)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<SourceStateRecord?> TryGetAsync(string sourceName, CancellationToken cancellationToken)
|
||||
=> _inner.TryGetAsync(sourceName, cancellationToken);
|
||||
|
||||
@@ -304,6 +382,15 @@ public sealed record SourceStateRecord(
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.Advisories
|
||||
{
|
||||
public sealed class AdvisoryDocument
|
||||
{
|
||||
public string AdvisoryKey { get; set; } = string.Empty;
|
||||
public MongoDB.Bson.BsonDocument Payload { get; set; } = new();
|
||||
public DateTime? Modified { get; set; }
|
||||
public DateTime? Published { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public interface IAdvisoryStore
|
||||
{
|
||||
Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken);
|
||||
@@ -360,18 +447,49 @@ namespace StellaOps.Concelier.Storage.Mongo.Aliases
|
||||
public sealed record AliasEntry(string Scheme, string Value);
|
||||
public sealed record AliasRecord(string AdvisoryKey, string Scheme, string Value, DateTimeOffset? UpdatedAt = null);
|
||||
public sealed record AliasCollision(string Scheme, string Value, IReadOnlyList<string> AdvisoryKeys);
|
||||
public sealed record AliasUpsertResult(string AdvisoryKey, IReadOnlyList<AliasCollision> Collisions);
|
||||
|
||||
public interface IAliasStore
|
||||
{
|
||||
Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken);
|
||||
Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> entries, DateTimeOffset updatedAt, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class InMemoryAliasStore : IAliasStore
|
||||
public sealed class AliasStore : InMemoryAliasStore
|
||||
{
|
||||
public AliasStore()
|
||||
{
|
||||
}
|
||||
|
||||
public AliasStore(object? database, object? options)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class InMemoryAliasStore : IAliasStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, List<AliasRecord>> _byAdvisory = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<(string Scheme, string Value), List<AliasRecord>> _byAlias = new();
|
||||
|
||||
public Task<AliasUpsertResult> ReplaceAsync(string advisoryKey, IEnumerable<AliasEntry> entries, DateTimeOffset updatedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
var records = entries.Select(e => new AliasRecord(advisoryKey, e.Scheme, e.Value, updatedAt)).ToList();
|
||||
_byAdvisory[advisoryKey] = records;
|
||||
foreach (var record in records)
|
||||
{
|
||||
var list = _byAlias.GetOrAdd((record.Scheme, record.Value), _ => new List<AliasRecord>());
|
||||
list.RemoveAll(r => string.Equals(r.AdvisoryKey, advisoryKey, StringComparison.OrdinalIgnoreCase));
|
||||
list.Add(record);
|
||||
}
|
||||
var collisions = _byAlias.Values
|
||||
.Where(list => list.Count > 1)
|
||||
.Select(list => new AliasCollision(list[0].Scheme, list[0].Value, list.Select(r => r.AdvisoryKey).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()))
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(new AliasUpsertResult(advisoryKey, collisions));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
_byAdvisory.TryGetValue(advisoryKey, out var records);
|
||||
@@ -400,11 +518,16 @@ namespace StellaOps.Concelier.Storage.Mongo.ChangeHistory
|
||||
string Snapshot,
|
||||
string PreviousSnapshot,
|
||||
IReadOnlyList<ChangeHistoryFieldChange> Changes,
|
||||
DateTimeOffset CreatedAt);
|
||||
DateTimeOffset CreatedAt)
|
||||
{
|
||||
public string? PreviousHash => PreviousSnapshotHash;
|
||||
public string? CurrentHash => SnapshotHash;
|
||||
}
|
||||
|
||||
public interface IChangeHistoryStore
|
||||
{
|
||||
Task AddAsync(ChangeHistoryRecord record, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<ChangeHistoryRecord>> GetRecentAsync(string sourceName, string advisoryKey, int limit, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class InMemoryChangeHistoryStore : IChangeHistoryStore
|
||||
@@ -415,6 +538,18 @@ namespace StellaOps.Concelier.Storage.Mongo.ChangeHistory
|
||||
_records.Add(record);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ChangeHistoryRecord>> GetRecentAsync(string sourceName, string advisoryKey, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var matches = _records
|
||||
.Where(r =>
|
||||
string.Equals(r.SourceName, sourceName, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(r.AdvisoryKey, advisoryKey, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToArray();
|
||||
return Task.FromResult<IReadOnlyList<ChangeHistoryRecord>>(matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +732,25 @@ namespace StellaOps.Concelier.Storage.Mongo.MergeEvents
|
||||
return Task.FromResult<IReadOnlyList<MergeEventRecord>>(records);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MergeEventStore : IMergeEventStore
|
||||
{
|
||||
private readonly InMemoryMergeEventStore _inner = new();
|
||||
|
||||
public MergeEventStore()
|
||||
{
|
||||
}
|
||||
|
||||
public MergeEventStore(object? database, object? logger)
|
||||
{
|
||||
}
|
||||
|
||||
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
|
||||
=> _inner.AppendAsync(record, cancellationToken);
|
||||
|
||||
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
|
||||
=> _inner.GetRecentAsync(advisoryKey, limit, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.Documents
|
||||
@@ -617,12 +771,16 @@ namespace StellaOps.Concelier.Storage.Mongo.Dtos
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.PsirtFlags
|
||||
{
|
||||
public sealed record PsirtFlagRecord(string AdvisoryId, string Vendor, string SourceName, string? ExternalId, DateTimeOffset RecordedAt);
|
||||
public sealed record PsirtFlagRecord(string AdvisoryId, string Vendor, string SourceName, string? ExternalId, DateTimeOffset RecordedAt)
|
||||
{
|
||||
public string AdvisoryKey => AdvisoryId;
|
||||
}
|
||||
|
||||
public interface IPsirtFlagStore
|
||||
{
|
||||
Task UpsertAsync(PsirtFlagRecord flag, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<PsirtFlagRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken);
|
||||
Task<PsirtFlagRecord?> FindAsync(string advisoryKey, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class InMemoryPsirtFlagStore : IPsirtFlagStore
|
||||
@@ -645,6 +803,94 @@ namespace StellaOps.Concelier.Storage.Mongo.PsirtFlags
|
||||
|
||||
return Task.FromResult<IReadOnlyList<PsirtFlagRecord>>(records);
|
||||
}
|
||||
|
||||
public Task<PsirtFlagRecord?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
_records.TryGetValue(advisoryKey, out var flag);
|
||||
return Task.FromResult<PsirtFlagRecord?>(flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.Observations
|
||||
{
|
||||
public sealed class AdvisoryObservationDocument
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Tenant { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public AdvisoryObservationSourceDocument Source { get; set; } = new();
|
||||
public AdvisoryObservationUpstreamDocument Upstream { get; set; } = new();
|
||||
public AdvisoryObservationContentDocument Content { get; set; } = new();
|
||||
public AdvisoryObservationLinksetDocument Linkset { get; set; } = new();
|
||||
public IDictionary<string, string> Attributes { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class AdvisoryObservationSourceDocument
|
||||
{
|
||||
public string Vendor { get; set; } = string.Empty;
|
||||
public string Stream { get; set; } = string.Empty;
|
||||
public string Api { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class AdvisoryObservationUpstreamDocument
|
||||
{
|
||||
public string UpstreamId { get; set; } = string.Empty;
|
||||
public string? DocumentVersion { get; set; }
|
||||
public DateTime FetchedAt { get; set; }
|
||||
public DateTime ReceivedAt { get; set; }
|
||||
public string ContentHash { get; set; } = string.Empty;
|
||||
public AdvisoryObservationSignatureDocument Signature { get; set; } = new();
|
||||
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class AdvisoryObservationSignatureDocument
|
||||
{
|
||||
public bool Present { get; set; }
|
||||
public string? Format { get; set; }
|
||||
public string? KeyId { get; set; }
|
||||
public string? Signature { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AdvisoryObservationContentDocument
|
||||
{
|
||||
public string Format { get; set; } = string.Empty;
|
||||
public string SpecVersion { get; set; } = string.Empty;
|
||||
public BsonDocument Raw { get; set; } = new();
|
||||
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class AdvisoryObservationLinksetDocument
|
||||
{
|
||||
public List<string>? Aliases { get; set; }
|
||||
public List<string>? Purls { get; set; }
|
||||
public List<string>? Cpes { get; set; }
|
||||
public List<AdvisoryObservationReferenceDocument> References { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class AdvisoryObservationReferenceDocument
|
||||
{
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.Linksets
|
||||
{
|
||||
public sealed class AdvisoryLinksetDocument
|
||||
{
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
public string Source { get; set; } = string.Empty;
|
||||
public string AdvisoryId { get; set; } = string.Empty;
|
||||
public IReadOnlyList<string> Observations { get; set; } = Array.Empty<string>();
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public AdvisoryLinksetNormalizedDocument Normalized { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class AdvisoryLinksetNormalizedDocument
|
||||
{
|
||||
public IReadOnlyList<string> Purls { get; set; } = Array.Empty<string>();
|
||||
public IReadOnlyList<string> Versions { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user