using Deal.Modules.Cards.Application.Models; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel.Tenants.Abstractions; using Deal.SharedKernel.Tenants.Models; namespace Deal.Api.Services; /// /// Резолвер ссылок аудита на данные тенанта /// /// Фабрика scope для чтения схемы тенанта. /// Логгер сбоев разрешения ссылок. public sealed class AuditReferenceResolver( IServiceScopeFactory scopeFactory, ILogger logger) : IAuditReferenceResolver { async Task> IAuditReferenceResolver.ResolveAsync( IReadOnlyList records, CancellationToken ct) { var names = new Dictionary(StringComparer.Ordinal); foreach (IGrouping group in GroupByTenant(records)) { await ResolveTenantAsync(group.Key, group, names, ct); } return names; } private static IEnumerable> GroupByTenant(IReadOnlyList records) => records .Where(record => record.TenantId is not null) .GroupBy(record => record.TenantId!.Value); private async Task ResolveTenantAsync( Guid tenantId, IEnumerable records, Dictionary names, CancellationToken ct) { var cardIds = new HashSet(StringComparer.Ordinal); var boardIds = new HashSet(StringComparer.Ordinal); foreach (AuditRecordDto record in records) { foreach (AuditChangeDto change in record.AuditChanges()) { Collect(change.From, cardIds, boardIds); Collect(change.To, cardIds, boardIds); } } if (cardIds.Count == 0 && boardIds.Count == 0) { return; } await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope(); ITenantContext tenantContext = scope.ServiceProvider.GetRequiredService(); try { tenantContext.SetTenant(new TenantId(tenantId.ToString("N"))); ICardStore store = scope.ServiceProvider.GetRequiredService(); await ResolveCardsAsync(store, cardIds, names, ct); await ResolveContainersAsync(store, boardIds, names, ct); } catch (OperationCanceledException) { throw; } catch (Exception exception) { logger.LogWarning(exception, "Резолвер аудита: ссылки тенанта {TenantId} не разрешены", tenantId); } finally { tenantContext.Reset(); } } private static void Collect( string? value, HashSet cardIds, HashSet boardIds) { if (string.IsNullOrEmpty(value)) { return; } if (value.StartsWith(CardIds.CardPrefix, StringComparison.Ordinal)) { cardIds.Add(value); } else if (value.StartsWith(KanbanIdPrefixes.Board, StringComparison.Ordinal)) { boardIds.Add(value); } } private static async Task ResolveCardsAsync( ICardStore store, HashSet cardIds, Dictionary names, CancellationToken ct) { foreach (string cardId in cardIds) { CardDto? card = await store.GetCardAsync(cardId, ct); if (card is not null && !string.IsNullOrWhiteSpace(card.Title)) { names[cardId] = card.Title; } } } private static async Task ResolveContainersAsync( ICardStore store, HashSet boardIds, Dictionary names, CancellationToken ct) { foreach (string boardId in boardIds) { ContainerDto? container = await store.GetContainerAsync(boardId, ct); if (container is not null && !string.IsNullOrWhiteSpace(container.Name)) { names[boardId] = container.Name; } } } }