using Bit.Seeder.Factories; using Bit.Seeder.Models; using Bit.Seeder.Pipeline; namespace Bit.Seeder.Steps; /// /// Resolves preset favorite assignments, setting Cipher.Favorites JSON for each /// (cipher, user) tuple declared in the preset. /// internal sealed class CreateCipherFavoritesStep(List assignments) : IStep { public void Execute(SeederContext context) { var cipherNames = context.Registry.FixtureCipherNameToId; var emailToUserId = context.Registry.UserEmailPrefixToUserId; // Phase 1: Validate all references before any mutations foreach (var a in assignments) { if (!cipherNames.ContainsKey(a.Cipher)) { var available = string.Join(", ", cipherNames.Keys.OrderBy(k => k)); throw new InvalidOperationException( $"Favorite assignment references unknown cipher '{a.Cipher}'. Available ciphers: {available}"); } if (!emailToUserId.ContainsKey(a.User)) { var available = string.Join(", ", emailToUserId.Keys.OrderBy(k => k)); throw new InvalidOperationException( $"Favorite assignment references unknown user '{a.User}'. Available users: {available}"); } } // Phase 2: Accumulate (cipherId → [userId]) mappings var cipherFavoriteMap = new Dictionary>(); var seen = new HashSet<(Guid CipherId, Guid UserId)>(); foreach (var a in assignments) { var cipherId = cipherNames[a.Cipher]; var userId = emailToUserId[a.User]; if (!seen.Add((cipherId, userId))) { throw new InvalidOperationException( $"Duplicate favorite assignment: cipher '{a.Cipher}' + user '{a.User}' appears more than once."); } if (!cipherFavoriteMap.TryGetValue(cipherId, out var userIds)) { userIds = []; cipherFavoriteMap[cipherId] = userIds; } userIds.Add(userId); } // Phase 3: Set Cipher.Favorites JSON var cipherLookup = context.Ciphers.ToDictionary(c => c.Id); foreach (var (cipherId, userIds) in cipherFavoriteMap) { cipherLookup[cipherId].Favorites = CipherComposer.BuildFavoritesJson(userIds); } } }