88 lines
3.8 KiB
C#
88 lines
3.8 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace ItWorx.KnowledgeHub.Domain.Sources;
|
|
|
|
public enum DataSourceKind { Inventory, Gitea, KnowledgeClient, ManualIntake, LeafWiki, CuratedPackage }
|
|
|
|
/// <summary>Operator registration and observed evidence; credentials never enter this aggregate.</summary>
|
|
public sealed class DataSource
|
|
{
|
|
private DataSource() { }
|
|
public Guid Id { get; private set; }
|
|
public Guid WorkspaceId { get; private set; }
|
|
public string SourceKey { get; private set; } = "";
|
|
public string DisplayName { get; private set; } = "";
|
|
public DataSourceKind Kind { get; private set; }
|
|
public bool Enabled { get; private set; } = true;
|
|
public int ExpectedIntervalHours { get; private set; }
|
|
public int Version { get; private set; } = 1;
|
|
public DateTimeOffset CreatedAt { get; private set; }
|
|
public DateTimeOffset? LastAttemptAt { get; private set; }
|
|
public DateTimeOffset? LastSuccessAt { get; private set; }
|
|
public DateTimeOffset? LastFailureAt { get; private set; }
|
|
public DateTimeOffset? DataObservedAt { get; private set; }
|
|
public long? RecordCount { get; private set; }
|
|
public string? LastErrorCode { get; private set; }
|
|
public string? LastCorrelationId { get; private set; }
|
|
|
|
public static DataSource Create(Guid workspaceId, string sourceKey, string displayName, DataSourceKind kind, int expectedIntervalHours, DateTimeOffset now)
|
|
{
|
|
if (workspaceId == Guid.Empty || !Enum.IsDefined(kind) || !Regex.IsMatch(sourceKey, "^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,199}$", RegexOptions.CultureInvariant))
|
|
{
|
|
throw new ArgumentException("Invalid source identity.");
|
|
}
|
|
var source = new DataSource { Id = Guid.NewGuid(), WorkspaceId = workspaceId, SourceKey = sourceKey, Kind = kind, CreatedAt = now, Version = 0 };
|
|
source.Configure(displayName, true, expectedIntervalHours);
|
|
return source;
|
|
}
|
|
|
|
public void Configure(string displayName, bool enabled, int expectedIntervalHours)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(displayName) || displayName.Length > 120 || displayName.Any(char.IsControl) || expectedIntervalHours is < 1 or > 8760)
|
|
{
|
|
throw new ArgumentException("Invalid source policy.");
|
|
}
|
|
DisplayName = displayName.Trim();
|
|
Enabled = enabled;
|
|
ExpectedIntervalHours = expectedIntervalHours;
|
|
Version++;
|
|
}
|
|
|
|
public void Observe(bool success, long? recordCount, string? errorCode, string? correlationId, DateTimeOffset observedAt, DateTimeOffset? dataObservedAt = null)
|
|
{
|
|
if (recordCount < 0 || (!success && errorCode is not ("authentication" or "connectivity" or "validation" or "unexpected"))
|
|
|| (LastAttemptAt is { } previous && observedAt < previous)
|
|
|| dataObservedAt > observedAt || correlationId?.Length > 100 || correlationId?.Any(char.IsControl) == true)
|
|
{
|
|
throw new ArgumentException("Invalid source observation.");
|
|
}
|
|
LastAttemptAt = observedAt;
|
|
LastCorrelationId = correlationId;
|
|
if (success)
|
|
{
|
|
LastSuccessAt = observedAt;
|
|
var captured = dataObservedAt ?? observedAt;
|
|
if (DataObservedAt is null || captured >= DataObservedAt)
|
|
{
|
|
DataObservedAt = captured;
|
|
RecordCount = recordCount;
|
|
}
|
|
LastErrorCode = null;
|
|
}
|
|
else
|
|
{
|
|
LastFailureAt = observedAt;
|
|
LastErrorCode = errorCode;
|
|
}
|
|
Version++;
|
|
}
|
|
|
|
public string GetState(DateTimeOffset now)
|
|
{
|
|
if (!Enabled) return "Disabled";
|
|
if (LastErrorCode is not null) return "Failed";
|
|
if (DataObservedAt is null || DataObservedAt > now) return "Unknown";
|
|
return now - DataObservedAt.Value > TimeSpan.FromHours(ExpectedIntervalHours) ? "Stale" : "Current";
|
|
}
|
|
}
|