using System.Diagnostics.CodeAnalysis;
namespace DodoSSH.Client.Domain;
/// What was done to an item.
public enum ActivityOperation
{
/// It was created.
Created = 0,
/// It was changed.
Updated = 1,
/// It was deleted.
Deleted = 2,
}
///
/// One create, edit or delete of a keychain item, decrypted.
///
///
///
/// holds names and never values. That is the rule the whole type is built
/// around, and it is the same one ADR 0006 imposes on the server's own detail column: an audit log
/// that recorded what a password used to be would be a plaintext credential store with a vault drawn around
/// it. "Password" is what somebody needs to see; the old password is what nobody does.
///
///
/// is a copy, taken at the time. Deleting the item is one of the three things
/// this records, so a lookup would resolve to nothing in exactly the case the entry matters most. It is also
/// what makes a rename readable — an entry saying "renamed 'old-db'" is useful, and one saying "renamed
/// 'prod-db'" because that is what it is called now is not.
///
///
public sealed record ActivityLogSecret : IVaultSecret
{
/// What kind of item this was about, as the sync contract names it.
///
/// Stored as the wire enum's name rather than its number, so an entry written by a build that knows a
/// kind this one does not still reads as something — an unknown name is shown as itself, where an
/// unknown number would have to be shown as a number.
///
public required string ItemKind { get; init; }
/// The item, so an entry can be traced to what it was about.
public required Guid ItemId { get; init; }
/// What the item was called at the time.
public required string ItemLabel { get; init; }
/// What was done.
public ActivityOperation Operation { get; init; }
///
/// The names of the fields that changed, separated by ", ". Never their values.
///
///
///
/// One string rather than a collection, and the choice is about equality. A plain
/// on a record gets reference equality from the compiler-generated
/// Equals, which is the trap exists to avoid — and a second type of that
/// shape is a lot of machinery for a value that is written once and only ever displayed. The separator is
/// unambiguous because these are C# property names, which cannot contain one.
///
///
/// Empty for a create and for a delete, where "which fields" has no meaning — every field arrived, or all
/// of them went. Empty is also the honest answer when an update's before and after could not be compared,
/// which is why nothing reading this may take empty to mean "nothing changed".
///
///
public string ChangedFields { get; init; } = string.Empty;
/// When it happened.
public required DateTimeOffset At { get; init; }
/// Which machine it was done from, as that machine calls itself.
public required string DeviceName { get; init; }
/// Which account in this organisation did it.
public Guid ActorUserId { get; init; }
/// What this entry is called, derived from what it records.
///
public string Label => $"{Operation} {ItemLabel}";
/// Whether this is storable, and why not if it is not.
public bool TryValidate([NotNullWhen(false)] out string? reason)
{
if (string.IsNullOrWhiteSpace(ItemKind))
{
reason = "An activity log entry needs the kind of item it was about.";
return false;
}
if (ItemId == Guid.Empty)
{
reason = "An activity log entry needs the item it was about.";
return false;
}
if (string.IsNullOrWhiteSpace(DeviceName))
{
reason = "An activity log entry needs the machine it was done from.";
return false;
}
// The label is deliberately not checked. An item somebody created and never named has an empty one,
// and refusing to record that would mean the log's completeness depended on the user's tidiness.
reason = null;
return true;
}
}