using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; using StellaOps.TaskRunner.Core.Configuration; using StellaOps.TaskRunner.Core.Execution; namespace StellaOps.TaskRunner.Infrastructure.Execution; public sealed class MongoPackRunApprovalStore : IPackRunApprovalStore { private readonly IMongoCollection collection; public MongoPackRunApprovalStore(IMongoDatabase database, TaskRunnerMongoOptions options) { ArgumentNullException.ThrowIfNull(database); ArgumentNullException.ThrowIfNull(options); collection = database.GetCollection(options.ApprovalsCollection); EnsureIndexes(collection); } public async Task SaveAsync(string runId, IReadOnlyList approvals, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(runId); ArgumentNullException.ThrowIfNull(approvals); var filter = Builders.Filter.Eq(document => document.RunId, runId); await collection.DeleteManyAsync(filter, cancellationToken).ConfigureAwait(false); if (approvals.Count == 0) { return; } var documents = approvals .Select(approval => PackRunApprovalDocument.FromDomain(runId, approval)) .ToList(); await collection.InsertManyAsync(documents, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetAsync(string runId, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(runId); var filter = Builders.Filter.Eq(document => document.RunId, runId); var documents = await collection .Find(filter) .SortBy(document => document.ApprovalId) .ToListAsync(cancellationToken) .ConfigureAwait(false); return documents .Select(document => document.ToDomain()) .ToList(); } public async Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(runId); ArgumentNullException.ThrowIfNull(approval); var filter = Builders.Filter.And( Builders.Filter.Eq(document => document.RunId, runId), Builders.Filter.Eq(document => document.ApprovalId, approval.ApprovalId)); var existingDocument = await collection .Find(filter) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (existingDocument is null) { throw new InvalidOperationException($"Approval '{approval.ApprovalId}' not found for run '{runId}'."); } var document = PackRunApprovalDocument.FromDomain(runId, approval, existingDocument.Id); await collection .ReplaceOneAsync(filter, document, cancellationToken: cancellationToken) .ConfigureAwait(false); } public static IEnumerable> GetIndexModels() { yield return new CreateIndexModel( Builders.IndexKeys .Ascending(document => document.RunId) .Ascending(document => document.ApprovalId), new CreateIndexOptions { Unique = true, Name = "pack_run_approvals_run_approval" }); yield return new CreateIndexModel( Builders.IndexKeys .Ascending(document => document.RunId) .Ascending(document => document.Status), new CreateIndexOptions { Name = "pack_run_approvals_run_status" }); } private static void EnsureIndexes(IMongoCollection target) => target.Indexes.CreateMany(GetIndexModels()); public sealed class PackRunApprovalDocument { [BsonId] public ObjectId Id { get; init; } public string RunId { get; init; } = default!; public string ApprovalId { get; init; } = default!; public IReadOnlyList RequiredGrants { get; init; } = Array.Empty(); public IReadOnlyList StepIds { get; init; } = Array.Empty(); public IReadOnlyList Messages { get; init; } = Array.Empty(); public string? ReasonTemplate { get; init; } public DateTime RequestedAt { get; init; } public string Status { get; init; } = default!; public string? ActorId { get; init; } public DateTime? CompletedAt { get; init; } public string? Summary { get; init; } public static PackRunApprovalDocument FromDomain(string runId, PackRunApprovalState approval, ObjectId? id = null) => new() { Id = id ?? ObjectId.GenerateNewId(), RunId = runId, ApprovalId = approval.ApprovalId, RequiredGrants = approval.RequiredGrants ?? Array.Empty(), StepIds = approval.StepIds ?? Array.Empty(), Messages = approval.Messages ?? Array.Empty(), ReasonTemplate = approval.ReasonTemplate, RequestedAt = approval.RequestedAt.UtcDateTime, Status = approval.Status.ToString(), ActorId = approval.ActorId, CompletedAt = approval.CompletedAt?.UtcDateTime, Summary = approval.Summary }; public PackRunApprovalState ToDomain() { var status = Enum.Parse(Status, ignoreCase: true); return new PackRunApprovalState( ApprovalId, RequiredGrants?.ToList() ?? new List(), StepIds?.ToList() ?? new List(), Messages?.ToList() ?? new List(), ReasonTemplate, new DateTimeOffset(RequestedAt, TimeSpan.Zero), status, ActorId, CompletedAt is null ? null : new DateTimeOffset(CompletedAt.Value, TimeSpan.Zero), Summary); } } }