From eea04789877d1da5996c895121ba2a1841707019 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:16:59 +0000 Subject: [PATCH 01/22] Initial plan From 2ea49c05dd68e5569b0bf25de5240081e0e12278 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:01:11 +0000 Subject: [PATCH 02/22] Implement semantic domain count tracking collection - Add ProjectSemanticDomainCount model to track sense counts per domain per project - Add ISemanticDomainCountRepository interface and SemanticDomainCountRepository implementation - Add ISemanticDomainCountService interface and SemanticDomainCountService implementation - Integrate count updates into WordService for create, update, delete, and restore operations - Register new services in Startup.cs for dependency injection - Add unit tests for SemanticDomainCountService - Update all existing tests to inject the new SemanticDomainCountService dependency - Add migration method to populate counts from existing Frontier data Co-authored-by: imnasnainaec <6411521+imnasnainaec@users.noreply.github.com> --- .../Controllers/AudioControllerTests.cs | 4 +- .../Controllers/LiftControllerTests.cs | 4 +- .../Controllers/MergeControllerTests.cs | 4 +- .../Controllers/WordControllerTests.cs | 4 +- .../SemanticDomainCountRepositoryMock.cs | 61 +++++ Backend.Tests/Services/MergeServiceTests.cs | 4 +- .../SemanticDomainCountServiceTests.cs | 218 ++++++++++++++++++ Backend.Tests/Services/WordServiceTests.cs | 5 +- .../ISemanticDomainCountRepository.cs | 15 ++ .../Interfaces/ISemanticDomainCountService.cs | 14 ++ Backend/Models/ProjectSemanticDomainCount.cs | 45 ++++ .../SemanticDomainCountRepository.cs | 90 ++++++++ .../Services/SemanticDomainCountService.cs | 151 ++++++++++++ Backend/Services/WordService.cs | 54 +++-- Backend/Startup.cs | 2 + 15 files changed, 654 insertions(+), 21 deletions(-) create mode 100644 Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs create mode 100644 Backend.Tests/Services/SemanticDomainCountServiceTests.cs create mode 100644 Backend/Interfaces/ISemanticDomainCountRepository.cs create mode 100644 Backend/Interfaces/ISemanticDomainCountService.cs create mode 100644 Backend/Models/ProjectSemanticDomainCount.cs create mode 100644 Backend/Repositories/SemanticDomainCountRepository.cs create mode 100644 Backend/Services/SemanticDomainCountService.cs diff --git a/Backend.Tests/Controllers/AudioControllerTests.cs b/Backend.Tests/Controllers/AudioControllerTests.cs index 2ceb7f9087..03999507fb 100644 --- a/Backend.Tests/Controllers/AudioControllerTests.cs +++ b/Backend.Tests/Controllers/AudioControllerTests.cs @@ -37,7 +37,9 @@ public void Setup() _projRepo = new ProjectRepositoryMock(); _wordRepo = new WordRepositoryMock(); _permissionService = new PermissionServiceMock(); - _wordService = new WordService(_wordRepo); + var countRepo = new SemanticDomainCountRepositoryMock(); + var countService = new SemanticDomainCountService(countRepo, _wordRepo); + _wordService = new WordService(_wordRepo, countService); _audioController = new AudioController(_wordRepo, _wordService, _permissionService); _projId = _projRepo.Create(new Project { Name = "AudioControllerTests" }).Result!.Id; diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index 7aa5f5597d..45fa5d6a73 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -49,7 +49,9 @@ public void Setup() _speakerRepo = new SpeakerRepositoryMock(); _wordRepo = new WordRepositoryMock(); _liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo); - _wordService = new WordService(_wordRepo); + var countRepo = new SemanticDomainCountRepositoryMock(); + var countService = new SemanticDomainCountService(countRepo, _wordRepo); + _wordService = new WordService(_wordRepo, countService); _liftController = new LiftController(_wordRepo, _projRepo, new PermissionServiceMock(), _liftService, new HubContextMock(), new MockLogger()); diff --git a/Backend.Tests/Controllers/MergeControllerTests.cs b/Backend.Tests/Controllers/MergeControllerTests.cs index c32ef329b3..5f78f57477 100644 --- a/Backend.Tests/Controllers/MergeControllerTests.cs +++ b/Backend.Tests/Controllers/MergeControllerTests.cs @@ -34,7 +34,9 @@ public void Setup() _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); - _wordService = new WordService(_wordRepo); + var countRepo = new SemanticDomainCountRepositoryMock(); + var countService = new SemanticDomainCountService(countRepo, _wordRepo); + _wordService = new WordService(_wordRepo, countService); _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); _mergeController = new MergeController( _mergeService, new HubContextMock(), new PermissionServiceMock()); diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 41b7a990d9..3dead436c6 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -32,7 +32,9 @@ public void Dispose() public void Setup() { _wordRepo = new WordRepositoryMock(); - _wordService = new WordService(_wordRepo); + var countRepo = new SemanticDomainCountRepositoryMock(); + var countService = new SemanticDomainCountService(countRepo, _wordRepo); + _wordService = new WordService(_wordRepo, countService); _permissionService = new PermissionServiceMock(); _wordController = new WordController(_wordRepo, _wordService, _permissionService); } diff --git a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs new file mode 100644 index 0000000000..9163d51494 --- /dev/null +++ b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BackendFramework.Interfaces; +using BackendFramework.Models; + +namespace Backend.Tests.Mocks +{ + public sealed class SemanticDomainCountRepositoryMock : ISemanticDomainCountRepository + { + private readonly List _counts; + + public SemanticDomainCountRepositoryMock() + { + _counts = new List(); + } + + public Task GetCount(string projectId, string domainId) + { + var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId); + return Task.FromResult(count); + } + + public Task> GetAllCounts(string projectId) + { + var counts = _counts.Where(c => c.ProjectId == projectId).ToList(); + return Task.FromResult(counts); + } + + public Task Create(ProjectSemanticDomainCount count) + { + count.Id = Util.RandString(); + _counts.Add(count); + return Task.FromResult(count); + } + + public Task Increment(string projectId, string domainId, int amount = 1) + { + var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId); + if (count is null) + { + count = new ProjectSemanticDomainCount(projectId, domainId, amount) + { + Id = Util.RandString() + }; + _counts.Add(count); + } + else + { + count.Count += amount; + } + return Task.FromResult(true); + } + + public Task DeleteAllCounts(string projectId) + { + var removed = _counts.RemoveAll(c => c.ProjectId == projectId); + return Task.FromResult(removed > 0); + } + } +} diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index 27e002eaea..a63023d70a 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -25,7 +25,9 @@ public void Setup() _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); - _wordService = new WordService(_wordRepo); + var countRepo = new SemanticDomainCountRepositoryMock(); + var countService = new SemanticDomainCountService(countRepo, _wordRepo); + _wordService = new WordService(_wordRepo, countService); _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); } diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs new file mode 100644 index 0000000000..bc6fc02c93 --- /dev/null +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -0,0 +1,218 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Backend.Tests.Mocks; +using BackendFramework.Interfaces; +using BackendFramework.Models; +using BackendFramework.Services; +using NUnit.Framework; + +namespace Backend.Tests.Services +{ + internal sealed class SemanticDomainCountServiceTests + { + private ISemanticDomainCountRepository _countRepo = null!; + private IWordRepository _wordRepo = null!; + private ISemanticDomainCountService _countService = null!; + + private const string ProjId = "CountServiceTestProjId"; + private const string DomainId1 = "1.1"; + private const string DomainId2 = "2.1"; + + [SetUp] + public void Setup() + { + _countRepo = new SemanticDomainCountRepositoryMock(); + _wordRepo = new WordRepositoryMock(); + _countService = new SemanticDomainCountService(_countRepo, _wordRepo); + } + + [Test] + public async Task TestUpdateCountsForWord() + { + var word = new Word + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 }, + new() { Id = DomainId2 } + } + } + } + }; + + await _countService.UpdateCountsForWord(word); + + var count1 = await _countRepo.GetCount(ProjId, DomainId1); + var count2 = await _countRepo.GetCount(ProjId, DomainId2); + + Assert.That(count1, Is.Not.Null); + Assert.That(count1!.Count, Is.EqualTo(1)); + Assert.That(count2, Is.Not.Null); + Assert.That(count2!.Count, Is.EqualTo(1)); + } + + [Test] + public async Task TestUpdateCountsForWords() + { + var words = new List + { + new() + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 } + } + } + } + }, + new() + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 }, + new() { Id = DomainId2 } + } + } + } + } + }; + + await _countService.UpdateCountsForWords(words); + + var count1 = await _countRepo.GetCount(ProjId, DomainId1); + var count2 = await _countRepo.GetCount(ProjId, DomainId2); + + Assert.That(count1, Is.Not.Null); + Assert.That(count1!.Count, Is.EqualTo(2)); + Assert.That(count2, Is.Not.Null); + Assert.That(count2!.Count, Is.EqualTo(1)); + } + + [Test] + public async Task TestUpdateCountsAfterWordUpdate() + { + var oldWord = new Word + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 }, + new() { Id = DomainId2 } + } + } + } + }; + + var newWord = new Word + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 } + } + } + } + }; + + // Start with counts from old word + await _countService.UpdateCountsForWord(oldWord); + + // Update counts + await _countService.UpdateCountsAfterWordUpdate(oldWord, newWord); + + var count1 = await _countRepo.GetCount(ProjId, DomainId1); + var count2 = await _countRepo.GetCount(ProjId, DomainId2); + + Assert.That(count1, Is.Not.Null); + Assert.That(count1!.Count, Is.EqualTo(1)); // Unchanged + Assert.That(count2, Is.Not.Null); + Assert.That(count2!.Count, Is.EqualTo(0)); // Decremented by 1 + } + + [Test] + public async Task TestMigrateCounts() + { + // Create some frontier words + var word1 = new Word + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 } + } + } + } + }; + + var word2 = new Word + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 }, + new() { Id = DomainId2 } + } + } + } + }; + + await _wordRepo.Create(word1); + await _wordRepo.Create(word2); + + // Migrate counts + await _countService.MigrateCounts(ProjId); + + var count1 = await _countRepo.GetCount(ProjId, DomainId1); + var count2 = await _countRepo.GetCount(ProjId, DomainId2); + + Assert.That(count1, Is.Not.Null); + Assert.That(count1!.Count, Is.EqualTo(2)); + Assert.That(count2, Is.Not.Null); + Assert.That(count2!.Count, Is.EqualTo(1)); + } + + [Test] + public async Task TestMigrateClearsOldCounts() + { + // Add an old count + await _countRepo.Create(new ProjectSemanticDomainCount(ProjId, DomainId1, 99)); + + // Migrate with no words + await _countService.MigrateCounts(ProjId); + + var counts = await _countRepo.GetAllCounts(ProjId); + Assert.That(counts, Is.Empty); + } + } +} diff --git a/Backend.Tests/Services/WordServiceTests.cs b/Backend.Tests/Services/WordServiceTests.cs index 1d542e40e8..87f45fade8 100644 --- a/Backend.Tests/Services/WordServiceTests.cs +++ b/Backend.Tests/Services/WordServiceTests.cs @@ -11,6 +11,7 @@ namespace Backend.Tests.Services internal sealed class WordServiceTests { private IWordRepository _wordRepo = null!; + private ISemanticDomainCountService _countService = null!; private IWordService _wordService = null!; private const string ProjId = "WordServiceTestProjId"; @@ -21,7 +22,9 @@ internal sealed class WordServiceTests public void Setup() { _wordRepo = new WordRepositoryMock(); - _wordService = new WordService(_wordRepo); + var countRepo = new SemanticDomainCountRepositoryMock(); + _countService = new SemanticDomainCountService(countRepo, _wordRepo); + _wordService = new WordService(_wordRepo, _countService); } [Test] diff --git a/Backend/Interfaces/ISemanticDomainCountRepository.cs b/Backend/Interfaces/ISemanticDomainCountRepository.cs new file mode 100644 index 0000000000..445fec5338 --- /dev/null +++ b/Backend/Interfaces/ISemanticDomainCountRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using BackendFramework.Models; + +namespace BackendFramework.Interfaces +{ + public interface ISemanticDomainCountRepository + { + Task GetCount(string projectId, string domainId); + Task> GetAllCounts(string projectId); + Task Create(ProjectSemanticDomainCount count); + Task Increment(string projectId, string domainId, int amount = 1); + Task DeleteAllCounts(string projectId); + } +} diff --git a/Backend/Interfaces/ISemanticDomainCountService.cs b/Backend/Interfaces/ISemanticDomainCountService.cs new file mode 100644 index 0000000000..27deff994f --- /dev/null +++ b/Backend/Interfaces/ISemanticDomainCountService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using BackendFramework.Models; + +namespace BackendFramework.Interfaces +{ + public interface ISemanticDomainCountService + { + Task UpdateCountsForWord(Word word); + Task UpdateCountsForWords(List words); + Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord); + Task MigrateCounts(string projectId); + } +} diff --git a/Backend/Models/ProjectSemanticDomainCount.cs b/Backend/Models/ProjectSemanticDomainCount.cs new file mode 100644 index 0000000000..fbfde3f979 --- /dev/null +++ b/Backend/Models/ProjectSemanticDomainCount.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace BackendFramework.Models +{ + /// + /// Tracks the number of senses in each semantic domain for a project. + /// Used for performant statistics queries without scanning the entire Frontier collection. + /// + public class ProjectSemanticDomainCount + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + [Required] + [BsonElement("projectId")] + public string ProjectId { get; set; } + + [Required] + [BsonElement("domainId")] + public string DomainId { get; set; } + + [Required] + [BsonElement("count")] + public int Count { get; set; } + + public ProjectSemanticDomainCount() + { + Id = ""; + ProjectId = ""; + DomainId = ""; + Count = 0; + } + + public ProjectSemanticDomainCount(string projectId, string domainId, int count = 0) + { + Id = ""; + ProjectId = projectId; + DomainId = domainId; + Count = count; + } + } +} diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs new file mode 100644 index 0000000000..4196f7e50e --- /dev/null +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using BackendFramework.Interfaces; +using BackendFramework.Models; +using BackendFramework.Otel; +using MongoDB.Driver; + +namespace BackendFramework.Repositories +{ + /// Atomic database functions for s. + [ExcludeFromCodeCoverage] + public class SemanticDomainCountRepository(IMongoDbContext dbContext) : ISemanticDomainCountRepository + { + private readonly IMongoCollection _counts = + dbContext.Db.GetCollection("SemanticDomainCountCollection"); + + private const string otelTagName = "otel.SemanticDomainCountRepository"; + + /// Gets the count for a specific semantic domain in a project + public async Task GetCount(string projectId, string domainId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "getting semantic domain count"); + + var filterDef = new FilterDefinitionBuilder(); + var filter = filterDef.And( + filterDef.Eq(c => c.ProjectId, projectId), + filterDef.Eq(c => c.DomainId, domainId)); + + var result = await _counts.FindAsync(filter); + return await result.FirstOrDefaultAsync(); + } + + /// Gets all counts for a project + public async Task> GetAllCounts(string projectId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "getting all semantic domain counts"); + + var filterDef = new FilterDefinitionBuilder(); + var filter = filterDef.Eq(c => c.ProjectId, projectId); + + return await _counts.Find(filter).ToListAsync(); + } + + /// Creates a new semantic domain count entry + public async Task Create(ProjectSemanticDomainCount count) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "creating semantic domain count"); + + await _counts.InsertOneAsync(count); + return count; + } + + /// Increments (or decrements if negative) the count for a semantic domain + /// true if the count was updated, false if it doesn't exist + public async Task Increment(string projectId, string domainId, int amount = 1) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "incrementing semantic domain count"); + + var filterDef = new FilterDefinitionBuilder(); + var filter = filterDef.And( + filterDef.Eq(c => c.ProjectId, projectId), + filterDef.Eq(c => c.DomainId, domainId)); + + var updateDef = new UpdateDefinitionBuilder(); + var update = updateDef.Inc(c => c.Count, amount); + + var options = new FindOneAndUpdateOptions + { + IsUpsert = true, + ReturnDocument = ReturnDocument.After + }; + + var result = await _counts.FindOneAndUpdateAsync(filter, update, options); + return result is not null; + } + + /// Deletes all counts for a project + public async Task DeleteAllCounts(string projectId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting all semantic domain counts"); + + var filterDef = new FilterDefinitionBuilder(); + var filter = filterDef.Eq(c => c.ProjectId, projectId); + + var result = await _counts.DeleteManyAsync(filter); + return result.DeletedCount > 0; + } + } +} diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs new file mode 100644 index 0000000000..f2953f68a1 --- /dev/null +++ b/Backend/Services/SemanticDomainCountService.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BackendFramework.Interfaces; +using BackendFramework.Models; +using BackendFramework.Otel; + +namespace BackendFramework.Services +{ + /// Service for managing semantic domain sense counts + public class SemanticDomainCountService : ISemanticDomainCountService + { + private readonly ISemanticDomainCountRepository _countRepo; + private readonly IWordRepository _wordRepo; + + private const string otelTagName = "otel.SemanticDomainCountService"; + + public SemanticDomainCountService( + ISemanticDomainCountRepository countRepo, + IWordRepository wordRepo) + { + _countRepo = countRepo; + _wordRepo = wordRepo; + } + + /// Updates counts when a new word is added + public async Task UpdateCountsForWord(Word word) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for word"); + + foreach (var sense in word.Senses) + { + foreach (var domain in sense.SemanticDomains) + { + await _countRepo.Increment(word.ProjectId, domain.Id, 1); + } + } + } + + /// Updates counts when multiple new words are added + public async Task UpdateCountsForWords(List words) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for words"); + + // Group by project and domain for efficient bulk updates + var domainCounts = new Dictionary>(); + + foreach (var word in words) + { + if (!domainCounts.TryGetValue(word.ProjectId, out var projectDict)) + { + projectDict = new Dictionary(); + domainCounts[word.ProjectId] = projectDict; + } + + foreach (var sense in word.Senses) + { + foreach (var domain in sense.SemanticDomains) + { + if (!projectDict.TryGetValue(domain.Id, out _)) + { + projectDict[domain.Id] = 0; + } + projectDict[domain.Id]++; + } + } + } + + // Apply all increments + foreach (var projectEntry in domainCounts) + { + var projectId = projectEntry.Key; + foreach (var domainEntry in projectEntry.Value) + { + await _countRepo.Increment(projectId, domainEntry.Key, domainEntry.Value); + } + } + } + + /// Updates counts when a word is modified + public async Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts after word update"); + + // Get old domains + var oldDomains = new Dictionary(); + foreach (var sense in oldWord.Senses) + { + foreach (var domain in sense.SemanticDomains) + { + oldDomains[domain.Id] = oldDomains.GetValueOrDefault(domain.Id, 0) + 1; + } + } + + // Get new domains + var newDomains = new Dictionary(); + foreach (var sense in newWord.Senses) + { + foreach (var domain in sense.SemanticDomains) + { + newDomains[domain.Id] = newDomains.GetValueOrDefault(domain.Id, 0) + 1; + } + } + + // Calculate differences + var allDomainIds = oldDomains.Keys.Union(newDomains.Keys).ToHashSet(); + foreach (var domainId in allDomainIds) + { + var oldCount = oldDomains.GetValueOrDefault(domainId, 0); + var newCount = newDomains.GetValueOrDefault(domainId, 0); + var diff = newCount - oldCount; + + if (diff != 0) + { + await _countRepo.Increment(newWord.ProjectId, domainId, diff); + } + } + } + + /// Migrates counts for an existing project by scanning all Frontier words + public async Task MigrateCounts(string projectId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "migrating counts for project"); + + // Clear existing counts for the project + await _countRepo.DeleteAllCounts(projectId); + + // Get all words in the frontier + var words = await _wordRepo.GetFrontier(projectId); + + // Build count map + var domainCounts = new Dictionary(); + foreach (var word in words) + { + foreach (var sense in word.Senses) + { + foreach (var domain in sense.SemanticDomains) + { + domainCounts[domain.Id] = domainCounts.GetValueOrDefault(domain.Id, 0) + 1; + } + } + } + + // Create count entries + foreach (var entry in domainCounts) + { + await _countRepo.Create(new ProjectSemanticDomainCount(projectId, entry.Key, entry.Value)); + } + } + } +} diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index 5c32dd7913..0a6a37134a 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -11,12 +11,14 @@ namespace BackendFramework.Services public class WordService : IWordService { private readonly IWordRepository _wordRepo; + private readonly ISemanticDomainCountService _domainCountService; private const string otelTagName = "otel.WordService"; - public WordService(IWordRepository wordRepo) + public WordService(IWordRepository wordRepo, ISemanticDomainCountService domainCountService) { _wordRepo = wordRepo; + _domainCountService = domainCountService; } /// @@ -40,7 +42,9 @@ public async Task Create(string userId, Word word) { using var activity = OtelService.StartActivityWithTag(otelTagName, "creating a word"); - return await _wordRepo.Create(PrepEditedData(userId, word)); + var createdWord = await _wordRepo.Create(PrepEditedData(userId, word)); + await _domainCountService.UpdateCountsForWord(createdWord); + return createdWord; } /// Creates new words with updated edited data. @@ -49,7 +53,9 @@ public async Task> Create(string userId, List words) { using var activity = OtelService.StartActivityWithTag(otelTagName, "creating words"); - return await _wordRepo.Create(words.Select(w => PrepEditedData(userId, w)).ToList()); + var createdWords = await _wordRepo.Create(words.Select(w => PrepEditedData(userId, w)).ToList()); + await _domainCountService.UpdateCountsForWords(createdWords); + return createdWords; } /// Adds a new word with updated edited data. @@ -65,6 +71,12 @@ public async Task Delete(string projectId, string userId, string wordId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word"); + var wordToDelete = await _wordRepo.GetWord(projectId, wordId); + if (wordToDelete is null) + { + return false; + } + var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); // We only want to add the deleted word if the word started in the frontier. @@ -73,11 +85,10 @@ public async Task Delete(string projectId, string userId, string wordId) return wordIsInFrontier; } - var wordToDelete = await _wordRepo.GetWord(projectId, wordId); - if (wordToDelete is null) - { - return false; - } + // Decrement counts for the deleted word's semantic domains + await _domainCountService.UpdateCountsAfterWordUpdate( + wordToDelete, + new Word { ProjectId = projectId, Senses = new List() }); wordToDelete.EditedBy = new List(); wordToDelete.History = new List { wordId }; @@ -88,7 +99,7 @@ public async Task Delete(string projectId, string userId, string wordId) senseAcc.Accessibility = Status.Deleted; } - await Create(userId, wordToDelete); + await Add(userId, wordToDelete); return wordIsInFrontier; } @@ -129,18 +140,23 @@ public async Task Delete(string projectId, string userId, string wordId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word from Frontier"); - var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); - if (!wordIsInFrontier) + var word = await _wordRepo.GetWord(projectId, wordId); + if (word is null) { return null; } - var word = await _wordRepo.GetWord(projectId, wordId); - if (word is null) + var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); + if (!wordIsInFrontier) { return null; } + // Decrement counts for the deleted word's semantic domains + await _domainCountService.UpdateCountsAfterWordUpdate( + word, + new Word { ProjectId = projectId, Senses = new List() }); + word.ProjectId = projectId; word.Accessibility = Status.Deleted; word.History.Add(wordId); @@ -165,6 +181,7 @@ public async Task RestoreFrontierWords(string projectId, List word words.Add(word); } await _wordRepo.AddFrontier(words); + await _domainCountService.UpdateCountsForWords(words); return true; } @@ -174,6 +191,12 @@ public async Task Update(string projectId, string userId, string wordId, W { using var activity = OtelService.StartActivityWithTag(otelTagName, "updating a word in Frontier"); + var oldWord = await _wordRepo.GetWord(projectId, wordId); + if (oldWord is null) + { + return false; + } + var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); // We only want to update words that are in the frontier @@ -185,12 +208,13 @@ public async Task Update(string projectId, string userId, string wordId, W // If an imported word was using the citation form for its Vernacular, // only keep UsingCitationForm true if the Vernacular hasn't changed. word.UsingCitationForm &= - word.Vernacular == (await _wordRepo.GetWord(projectId, wordId))!.Vernacular; + word.Vernacular == oldWord.Vernacular; word.ProjectId = projectId; word.History.Add(wordId); - await Create(userId, word); + var createdWord = await _wordRepo.Create(PrepEditedData(userId, word)); + await _domainCountService.UpdateCountsAfterWordUpdate(oldWord, createdWord); return wordIsInFrontier; } diff --git a/Backend/Startup.cs b/Backend/Startup.cs index b6df0ca033..2731ddc44e 100644 --- a/Backend/Startup.cs +++ b/Backend/Startup.cs @@ -269,6 +269,8 @@ public void ConfigureServices(IServiceCollection services) // Semantic Domain types services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); // Speaker types services.AddTransient(); From 7220b098c070576edb91f981fcf6087a6525408c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:04:45 +0000 Subject: [PATCH 03/22] Add LIFT import integration for semantic domain count updates - Update LiftService to accept ISemanticDomainCountService dependency - Update LiftMerger to accept and use ISemanticDomainCountService - Update SaveImportEntries to call UpdateCountsForWords after creating words - Update test files to provide SemanticDomainCountService to LiftService Co-authored-by: imnasnainaec <6411521+imnasnainaec@users.noreply.github.com> --- .../Controllers/LiftControllerTests.cs | 2 +- Backend.Tests/Services/LiftServiceTests.cs | 5 ++++- Backend/Services/LiftService.cs | 18 +++++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index 45fa5d6a73..4595b74e27 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -48,9 +48,9 @@ public void Setup() _projRepo = new ProjectRepositoryMock(); _speakerRepo = new SpeakerRepositoryMock(); _wordRepo = new WordRepositoryMock(); - _liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo); var countRepo = new SemanticDomainCountRepositoryMock(); var countService = new SemanticDomainCountService(countRepo, _wordRepo); + _liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo, countService); _wordService = new WordService(_wordRepo, countService); _liftController = new LiftController(_wordRepo, _projRepo, new PermissionServiceMock(), _liftService, new HubContextMock(), new MockLogger()); diff --git a/Backend.Tests/Services/LiftServiceTests.cs b/Backend.Tests/Services/LiftServiceTests.cs index 3200b37d8d..564d156b78 100644 --- a/Backend.Tests/Services/LiftServiceTests.cs +++ b/Backend.Tests/Services/LiftServiceTests.cs @@ -27,7 +27,10 @@ public void Setup() { _semDomRepo = new SemanticDomainRepositoryMock(); _speakerRepo = new SpeakerRepositoryMock(); - _liftService = new LiftService(_semDomRepo, _speakerRepo); + var wordRepo = new WordRepositoryMock(); + var countRepo = new SemanticDomainCountRepositoryMock(); + var countService = new SemanticDomainCountService(countRepo, wordRepo); + _liftService = new LiftService(_semDomRepo, _speakerRepo, countService); } [Test] diff --git a/Backend/Services/LiftService.cs b/Backend/Services/LiftService.cs index 2febcd6c4b..eb596dfedb 100644 --- a/Backend/Services/LiftService.cs +++ b/Backend/Services/LiftService.cs @@ -104,6 +104,7 @@ public class LiftService : ILiftService { private readonly ISemanticDomainRepository _semDomRepo; private readonly ISpeakerRepository _speakerRepo; + private readonly ISemanticDomainCountService _domainCountService; /// /// A dictionary shared by all Projects for tracking exported projects. @@ -120,10 +121,14 @@ public class LiftService : ILiftService private const string InProgress = "IN_PROGRESS"; private const string otelTagName = "otel.LiftService"; - public LiftService(ISemanticDomainRepository semDomRepo, ISpeakerRepository speakerRepo) + public LiftService( + ISemanticDomainRepository semDomRepo, + ISpeakerRepository speakerRepo, + ISemanticDomainCountService domainCountService) { _semDomRepo = semDomRepo; _speakerRepo = speakerRepo; + _domainCountService = domainCountService; if (!Sldr.IsInitialized) { @@ -682,7 +687,7 @@ public static string MakeSafeXmlAttribute(string sInput) public ILiftMerger GetLiftImporterExporter(string projectId, string vernLang, IWordRepository wordRepo) { - return new LiftMerger(projectId, vernLang, wordRepo); + return new LiftMerger(projectId, vernLang, wordRepo, _domainCountService); } private static void WriteRangeElement(XmlWriter liftRangesWriter, @@ -722,13 +727,19 @@ private sealed class LiftMerger : ILiftMerger private readonly List _customSemDoms = new(); private readonly string _vernLang; private readonly IWordRepository _wordRepo; + private readonly ISemanticDomainCountService _domainCountService; private readonly List _importEntries = new(); - public LiftMerger(string projectId, string vernLang, IWordRepository wordRepo) + public LiftMerger( + string projectId, + string vernLang, + IWordRepository wordRepo, + ISemanticDomainCountService domainCountService) { _projectId = projectId; _vernLang = vernLang; _wordRepo = wordRepo; + _domainCountService = domainCountService; } /// @@ -778,6 +789,7 @@ public List GetImportAnalysisWritingSystems() public async Task> SaveImportEntries() { var savedWords = new List(await _wordRepo.Create(_importEntries)); + await _domainCountService.UpdateCountsForWords(savedWords); _importEntries.Clear(); return savedWords; } From 8bdcd225ca2129da364140893f5546ac4447bd11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:29:27 +0000 Subject: [PATCH 04/22] Refactor SemanticDomainCountService and simplify WordService deletion logic - Add private GetDomainCounts method to extract domain counts from words - Refactor all methods in SemanticDomainCountService to use GetDomainCounts - Add UpdateCountsForWordDeletion method to simplify deletion operations - Update WordService.Delete to use UpdateCountsForWordDeletion instead of UpdateCountsAfterWordUpdate - Revert WordService.Delete to use Create instead of Add for consistency - Add test for UpdateCountsForWordDeletion method Co-authored-by: imnasnainaec <6411521+imnasnainaec@users.noreply.github.com> --- .../SemanticDomainCountServiceTests.cs | 37 +++++++++ .../Interfaces/ISemanticDomainCountService.cs | 1 + .../Services/SemanticDomainCountService.cs | 75 +++++++++---------- Backend/Services/WordService.cs | 10 +-- 4 files changed, 78 insertions(+), 45 deletions(-) diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs index bc6fc02c93..ffabb550a3 100644 --- a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -214,5 +214,42 @@ public async Task TestMigrateClearsOldCounts() var counts = await _countRepo.GetAllCounts(ProjId); Assert.That(counts, Is.Empty); } + + [Test] + public async Task TestUpdateCountsForWordDeletion() + { + var word = new Word + { + ProjectId = ProjId, + Senses = new List + { + new() + { + SemanticDomains = new List + { + new() { Id = DomainId1 }, + new() { Id = DomainId2 } + } + } + } + }; + + // First add the word to get initial counts + await _countService.UpdateCountsForWord(word); + + var count1Before = await _countRepo.GetCount(ProjId, DomainId1); + var count2Before = await _countRepo.GetCount(ProjId, DomainId2); + Assert.That(count1Before!.Count, Is.EqualTo(1)); + Assert.That(count2Before!.Count, Is.EqualTo(1)); + + // Now delete it + await _countService.UpdateCountsForWordDeletion(word); + + var count1After = await _countRepo.GetCount(ProjId, DomainId1); + var count2After = await _countRepo.GetCount(ProjId, DomainId2); + + Assert.That(count1After!.Count, Is.EqualTo(0)); + Assert.That(count2After!.Count, Is.EqualTo(0)); + } } } diff --git a/Backend/Interfaces/ISemanticDomainCountService.cs b/Backend/Interfaces/ISemanticDomainCountService.cs index 27deff994f..921cb2cabe 100644 --- a/Backend/Interfaces/ISemanticDomainCountService.cs +++ b/Backend/Interfaces/ISemanticDomainCountService.cs @@ -9,6 +9,7 @@ public interface ISemanticDomainCountService Task UpdateCountsForWord(Word word); Task UpdateCountsForWords(List words); Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord); + Task UpdateCountsForWordDeletion(Word word); Task MigrateCounts(string projectId); } } diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs index f2953f68a1..293ac72c3c 100644 --- a/Backend/Services/SemanticDomainCountService.cs +++ b/Backend/Services/SemanticDomainCountService.cs @@ -23,18 +23,30 @@ public SemanticDomainCountService( _wordRepo = wordRepo; } - /// Updates counts when a new word is added - public async Task UpdateCountsForWord(Word word) + /// Extracts domain counts from a word + private static Dictionary GetDomainCounts(Word word) { - using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for word"); - + var domainCounts = new Dictionary(); foreach (var sense in word.Senses) { foreach (var domain in sense.SemanticDomains) { - await _countRepo.Increment(word.ProjectId, domain.Id, 1); + domainCounts[domain.Id] = domainCounts.GetValueOrDefault(domain.Id, 0) + 1; } } + return domainCounts; + } + + /// Updates counts when a new word is added + public async Task UpdateCountsForWord(Word word) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for word"); + + var domainCounts = GetDomainCounts(word); + foreach (var entry in domainCounts) + { + await _countRepo.Increment(word.ProjectId, entry.Key, entry.Value); + } } /// Updates counts when multiple new words are added @@ -53,16 +65,10 @@ public async Task UpdateCountsForWords(List words) domainCounts[word.ProjectId] = projectDict; } - foreach (var sense in word.Senses) + var wordDomainCounts = GetDomainCounts(word); + foreach (var entry in wordDomainCounts) { - foreach (var domain in sense.SemanticDomains) - { - if (!projectDict.TryGetValue(domain.Id, out _)) - { - projectDict[domain.Id] = 0; - } - projectDict[domain.Id]++; - } + projectDict[entry.Key] = projectDict.GetValueOrDefault(entry.Key, 0) + entry.Value; } } @@ -82,25 +88,8 @@ public async Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord) { using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts after word update"); - // Get old domains - var oldDomains = new Dictionary(); - foreach (var sense in oldWord.Senses) - { - foreach (var domain in sense.SemanticDomains) - { - oldDomains[domain.Id] = oldDomains.GetValueOrDefault(domain.Id, 0) + 1; - } - } - - // Get new domains - var newDomains = new Dictionary(); - foreach (var sense in newWord.Senses) - { - foreach (var domain in sense.SemanticDomains) - { - newDomains[domain.Id] = newDomains.GetValueOrDefault(domain.Id, 0) + 1; - } - } + var oldDomains = GetDomainCounts(oldWord); + var newDomains = GetDomainCounts(newWord); // Calculate differences var allDomainIds = oldDomains.Keys.Union(newDomains.Keys).ToHashSet(); @@ -117,6 +106,18 @@ public async Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord) } } + /// Updates counts when a word is deleted + public async Task UpdateCountsForWordDeletion(Word word) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for word deletion"); + + var domainCounts = GetDomainCounts(word); + foreach (var entry in domainCounts) + { + await _countRepo.Increment(word.ProjectId, entry.Key, -entry.Value); + } + } + /// Migrates counts for an existing project by scanning all Frontier words public async Task MigrateCounts(string projectId) { @@ -132,12 +133,10 @@ public async Task MigrateCounts(string projectId) var domainCounts = new Dictionary(); foreach (var word in words) { - foreach (var sense in word.Senses) + var wordDomainCounts = GetDomainCounts(word); + foreach (var entry in wordDomainCounts) { - foreach (var domain in sense.SemanticDomains) - { - domainCounts[domain.Id] = domainCounts.GetValueOrDefault(domain.Id, 0) + 1; - } + domainCounts[entry.Key] = domainCounts.GetValueOrDefault(entry.Key, 0) + entry.Value; } } diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index 0a6a37134a..8ff2b302ae 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -86,9 +86,7 @@ public async Task Delete(string projectId, string userId, string wordId) } // Decrement counts for the deleted word's semantic domains - await _domainCountService.UpdateCountsAfterWordUpdate( - wordToDelete, - new Word { ProjectId = projectId, Senses = new List() }); + await _domainCountService.UpdateCountsForWordDeletion(wordToDelete); wordToDelete.EditedBy = new List(); wordToDelete.History = new List { wordId }; @@ -99,7 +97,7 @@ await _domainCountService.UpdateCountsAfterWordUpdate( senseAcc.Accessibility = Status.Deleted; } - await Add(userId, wordToDelete); + await Create(userId, wordToDelete); return wordIsInFrontier; } @@ -153,9 +151,7 @@ await _domainCountService.UpdateCountsAfterWordUpdate( } // Decrement counts for the deleted word's semantic domains - await _domainCountService.UpdateCountsAfterWordUpdate( - word, - new Word { ProjectId = projectId, Senses = new List() }); + await _domainCountService.UpdateCountsForWordDeletion(word); word.ProjectId = projectId; word.Accessibility = Status.Deleted; From a605dc636fc232aac74f3724e4e1ceb049f62554 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 16 Dec 2025 17:18:11 -0500 Subject: [PATCH 05/22] Simplify --- .../SemanticDomainCountServiceTests.cs | 63 --------------- .../Interfaces/ISemanticDomainCountService.cs | 1 - .../SemanticDomainCountRepository.cs | 34 ++++---- .../Services/SemanticDomainCountService.cs | 79 ++++--------------- 4 files changed, 33 insertions(+), 144 deletions(-) diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs index ffabb550a3..3d26d9d1be 100644 --- a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -152,69 +152,6 @@ public async Task TestUpdateCountsAfterWordUpdate() Assert.That(count2!.Count, Is.EqualTo(0)); // Decremented by 1 } - [Test] - public async Task TestMigrateCounts() - { - // Create some frontier words - var word1 = new Word - { - ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 } - } - } - } - }; - - var word2 = new Word - { - ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 }, - new() { Id = DomainId2 } - } - } - } - }; - - await _wordRepo.Create(word1); - await _wordRepo.Create(word2); - - // Migrate counts - await _countService.MigrateCounts(ProjId); - - var count1 = await _countRepo.GetCount(ProjId, DomainId1); - var count2 = await _countRepo.GetCount(ProjId, DomainId2); - - Assert.That(count1, Is.Not.Null); - Assert.That(count1!.Count, Is.EqualTo(2)); - Assert.That(count2, Is.Not.Null); - Assert.That(count2!.Count, Is.EqualTo(1)); - } - - [Test] - public async Task TestMigrateClearsOldCounts() - { - // Add an old count - await _countRepo.Create(new ProjectSemanticDomainCount(ProjId, DomainId1, 99)); - - // Migrate with no words - await _countService.MigrateCounts(ProjId); - - var counts = await _countRepo.GetAllCounts(ProjId); - Assert.That(counts, Is.Empty); - } - [Test] public async Task TestUpdateCountsForWordDeletion() { diff --git a/Backend/Interfaces/ISemanticDomainCountService.cs b/Backend/Interfaces/ISemanticDomainCountService.cs index 921cb2cabe..5d4d3a0b89 100644 --- a/Backend/Interfaces/ISemanticDomainCountService.cs +++ b/Backend/Interfaces/ISemanticDomainCountService.cs @@ -10,6 +10,5 @@ public interface ISemanticDomainCountService Task UpdateCountsForWords(List words); Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord); Task UpdateCountsForWordDeletion(Word word); - Task MigrateCounts(string projectId); } } diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index 4196f7e50e..87f90242af 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -17,17 +17,24 @@ public class SemanticDomainCountRepository(IMongoDbContext dbContext) : ISemanti private const string otelTagName = "otel.SemanticDomainCountRepository"; + private static FilterDefinition? ProjectFilter(string projectId) + { + var filterDef = new FilterDefinitionBuilder(); + return filterDef.Eq(c => c.ProjectId, projectId); + } + + private static FilterDefinition? ProjectDomainFilter(string projectId, string domainId) + { + var filterDef = new FilterDefinitionBuilder(); + return filterDef.And(filterDef.Eq(c => c.ProjectId, projectId), filterDef.Eq(c => c.DomainId, domainId)); + } + /// Gets the count for a specific semantic domain in a project public async Task GetCount(string projectId, string domainId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "getting semantic domain count"); - var filterDef = new FilterDefinitionBuilder(); - var filter = filterDef.And( - filterDef.Eq(c => c.ProjectId, projectId), - filterDef.Eq(c => c.DomainId, domainId)); - - var result = await _counts.FindAsync(filter); + var result = await _counts.FindAsync(ProjectDomainFilter(projectId, domainId)); return await result.FirstOrDefaultAsync(); } @@ -36,10 +43,7 @@ public async Task> GetAllCounts(string projectI { using var activity = OtelService.StartActivityWithTag(otelTagName, "getting all semantic domain counts"); - var filterDef = new FilterDefinitionBuilder(); - var filter = filterDef.Eq(c => c.ProjectId, projectId); - - return await _counts.Find(filter).ToListAsync(); + return await _counts.Find(ProjectFilter(projectId)).ToListAsync(); } /// Creates a new semantic domain count entry @@ -57,10 +61,7 @@ public async Task Increment(string projectId, string domainId, int amount { using var activity = OtelService.StartActivityWithTag(otelTagName, "incrementing semantic domain count"); - var filterDef = new FilterDefinitionBuilder(); - var filter = filterDef.And( - filterDef.Eq(c => c.ProjectId, projectId), - filterDef.Eq(c => c.DomainId, domainId)); + var filter = ProjectDomainFilter(projectId, domainId); var updateDef = new UpdateDefinitionBuilder(); var update = updateDef.Inc(c => c.Count, amount); @@ -80,10 +81,7 @@ public async Task DeleteAllCounts(string projectId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting all semantic domain counts"); - var filterDef = new FilterDefinitionBuilder(); - var filter = filterDef.Eq(c => c.ProjectId, projectId); - - var result = await _counts.DeleteManyAsync(filter); + var result = await _counts.DeleteManyAsync(ProjectFilter(projectId)); return result.DeletedCount > 0; } } diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs index 293ac72c3c..715dbb2d67 100644 --- a/Backend/Services/SemanticDomainCountService.cs +++ b/Backend/Services/SemanticDomainCountService.cs @@ -8,25 +8,18 @@ namespace BackendFramework.Services { /// Service for managing semantic domain sense counts - public class SemanticDomainCountService : ISemanticDomainCountService + public class SemanticDomainCountService(ISemanticDomainCountRepository countRepo, IWordRepository wordRepo) + : ISemanticDomainCountService { - private readonly ISemanticDomainCountRepository _countRepo; - private readonly IWordRepository _wordRepo; + private readonly ISemanticDomainCountRepository _countRepo = countRepo; + private readonly IWordRepository _wordRepo = wordRepo; private const string otelTagName = "otel.SemanticDomainCountService"; - public SemanticDomainCountService( - ISemanticDomainCountRepository countRepo, - IWordRepository wordRepo) - { - _countRepo = countRepo; - _wordRepo = wordRepo; - } - /// Extracts domain counts from a word - private static Dictionary GetDomainCounts(Word word) + private static Dictionary GetDomainCounts(Word word, Dictionary? domainCounts = null) { - var domainCounts = new Dictionary(); + domainCounts ??= []; foreach (var sense in word.Senses) { foreach (var domain in sense.SemanticDomains) @@ -50,36 +43,27 @@ public async Task UpdateCountsForWord(Word word) } /// Updates counts when multiple new words are added + /// Assumes all words belong to the same project public async Task UpdateCountsForWords(List words) { using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for words"); - // Group by project and domain for efficient bulk updates - var domainCounts = new Dictionary>(); + if (words.Count == 0) + { + return; + } + var projectId = words.First().ProjectId; + + var domainCounts = new Dictionary(); foreach (var word in words) { - if (!domainCounts.TryGetValue(word.ProjectId, out var projectDict)) - { - projectDict = new Dictionary(); - domainCounts[word.ProjectId] = projectDict; - } - - var wordDomainCounts = GetDomainCounts(word); - foreach (var entry in wordDomainCounts) - { - projectDict[entry.Key] = projectDict.GetValueOrDefault(entry.Key, 0) + entry.Value; - } + GetDomainCounts(word, domainCounts); } - // Apply all increments - foreach (var projectEntry in domainCounts) + foreach (var entry in domainCounts) { - var projectId = projectEntry.Key; - foreach (var domainEntry in projectEntry.Value) - { - await _countRepo.Increment(projectId, domainEntry.Key, domainEntry.Value); - } + await _countRepo.Increment(projectId, entry.Key, entry.Value); } } @@ -117,34 +101,5 @@ public async Task UpdateCountsForWordDeletion(Word word) await _countRepo.Increment(word.ProjectId, entry.Key, -entry.Value); } } - - /// Migrates counts for an existing project by scanning all Frontier words - public async Task MigrateCounts(string projectId) - { - using var activity = OtelService.StartActivityWithTag(otelTagName, "migrating counts for project"); - - // Clear existing counts for the project - await _countRepo.DeleteAllCounts(projectId); - - // Get all words in the frontier - var words = await _wordRepo.GetFrontier(projectId); - - // Build count map - var domainCounts = new Dictionary(); - foreach (var word in words) - { - var wordDomainCounts = GetDomainCounts(word); - foreach (var entry in wordDomainCounts) - { - domainCounts[entry.Key] = domainCounts.GetValueOrDefault(entry.Key, 0) + entry.Value; - } - } - - // Create count entries - foreach (var entry in domainCounts) - { - await _countRepo.Create(new ProjectSemanticDomainCount(projectId, entry.Key, entry.Value)); - } - } } } From 0fc6780e06bd1d25b39ed3cf662d6ebbcfd723e3 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 17 Dec 2025 14:04:01 -0500 Subject: [PATCH 06/22] Condense test objects --- .../SemanticDomainCountServiceTests.cs | 70 ++----------------- 1 file changed, 6 insertions(+), 64 deletions(-) diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs index 3d26d9d1be..4cd42c4ab0 100644 --- a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -32,17 +32,7 @@ public async Task TestUpdateCountsForWord() var word = new Word { ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 }, - new() { Id = DomainId2 } - } - } - } + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }], }; await _countService.UpdateCountsForWord(word); @@ -64,31 +54,12 @@ public async Task TestUpdateCountsForWords() new() { ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 } - } - } - } + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }] }] }, new() { ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 }, - new() { Id = DomainId2 } - } - } - } + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }], } }; @@ -109,32 +80,13 @@ public async Task TestUpdateCountsAfterWordUpdate() var oldWord = new Word { ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 }, - new() { Id = DomainId2 } - } - } - } + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }], }; var newWord = new Word { ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 } - } - } - } + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }] }] }; // Start with counts from old word @@ -158,17 +110,7 @@ public async Task TestUpdateCountsForWordDeletion() var word = new Word { ProjectId = ProjId, - Senses = new List - { - new() - { - SemanticDomains = new List - { - new() { Id = DomainId1 }, - new() { Id = DomainId2 } - } - } - } + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }], }; // First add the word to get initial counts From d610c504106cecd1b6eb042d2c4f4b7dcf8d1d71 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 17 Dec 2025 14:47:49 -0500 Subject: [PATCH 07/22] Move from WordController to StatisticsController --- .../Controllers/StatisticsControllerTests.cs | 18 ++- .../Controllers/WordControllerTests.cs | 16 --- .../SemanticDomainCountRepositoryMock.cs | 4 +- Backend.Tests/Mocks/StatisticsServiceMock.cs | 5 + Backend.Tests/Mocks/WordRepositoryMock.cs | 7 - .../SemanticDomainCountServiceTests.cs | 27 ++-- .../Services/StatisticsServiceTests.cs | 30 ++-- Backend/Controllers/StatisticsController.cs | 28 ++-- Backend/Controllers/WordController.cs | 17 --- .../ISemanticDomainCountRepository.cs | 2 +- Backend/Interfaces/IStatisticsService.cs | 1 + Backend/Interfaces/IWordRepository.cs | 1 - .../SemanticDomainCountRepository.cs | 4 +- Backend/Repositories/WordRepository.cs | 19 --- Backend/Services/StatisticsService.cs | 55 ++++---- src/api/api/statistics-api.ts | 132 +++++++++++++++++ src/api/api/word-api.ts | 133 ------------------ src/backend/index.ts | 16 +-- .../TreeDepiction/DomainCountBadge.tsx | 4 +- .../TreeDepiction/tests/CurrentRow.test.tsx | 6 +- .../TreeDepiction/tests/index.test.tsx | 6 +- 21 files changed, 246 insertions(+), 285 deletions(-) diff --git a/Backend.Tests/Controllers/StatisticsControllerTests.cs b/Backend.Tests/Controllers/StatisticsControllerTests.cs index 03f4fbcef3..6014957377 100644 --- a/Backend.Tests/Controllers/StatisticsControllerTests.cs +++ b/Backend.Tests/Controllers/StatisticsControllerTests.cs @@ -33,7 +33,7 @@ public async Task Setup() _projRepo = new ProjectRepositoryMock(); _userRepo = new UserRepositoryMock(); _permService = new PermissionServiceMock(_userRepo); - _statsController = new StatisticsController(new StatisticsServiceMock(), _permService, _projRepo) + _statsController = new StatisticsController(_projRepo, _permService, new StatisticsServiceMock()) { // Mock the Http Context because this isn't an actual call controller ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } @@ -46,6 +46,22 @@ public async Task Setup() _projId = (await _projRepo.Create(new Project { Name = "StatisticsControllerTests" }))!.Id; } + [Test] + public async Task TestGetDomainCountNoPermission() + { + _statsController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); + + var result = await _statsController.GetDomainCount(_projId, "1"); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task TestGetDomainCount() + { + var result = await _statsController.GetDomainCount(_projId, "1"); + Assert.That(result, Is.InstanceOf()); + } + [Test] public async Task TestGetSemanticDomainCountsNoPermission() { diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 5a5fc8d0f5..3dead436c6 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -412,21 +412,5 @@ public async Task TestRestoreWordMissingWord() var wordResult = await _wordController.RestoreWord(ProjId, MissingId); Assert.That(wordResult, Is.InstanceOf()); } - - [Test] - public async Task TestGetDomainWordCountNoPermission() - { - _wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); - - var result = await _wordController.GetDomainWordCount(ProjId, "1"); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public async Task TestGetDomainWordCount() - { - var result = await _wordController.GetDomainWordCount(ProjId, "1"); - Assert.That(result, Is.InstanceOf()); - } } } diff --git a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs index 9163d51494..f980676623 100644 --- a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs +++ b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs @@ -15,10 +15,10 @@ public SemanticDomainCountRepositoryMock() _counts = new List(); } - public Task GetCount(string projectId, string domainId) + public Task GetCount(string projectId, string domainId) { var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId); - return Task.FromResult(count); + return Task.FromResult(count?.Count ?? 0); } public Task> GetAllCounts(string projectId) diff --git a/Backend.Tests/Mocks/StatisticsServiceMock.cs b/Backend.Tests/Mocks/StatisticsServiceMock.cs index 4e55c3abd6..4c45e470a2 100644 --- a/Backend.Tests/Mocks/StatisticsServiceMock.cs +++ b/Backend.Tests/Mocks/StatisticsServiceMock.cs @@ -8,6 +8,11 @@ namespace Backend.Tests.Mocks { internal sealed class StatisticsServiceMock : IStatisticsService { + public Task GetDomainCount(string projectId, string domainId) + { + return Task.FromResult(0); + } + public Task> GetSemanticDomainCounts(string projectId, string lang) { return Task.FromResult(new List()); diff --git a/Backend.Tests/Mocks/WordRepositoryMock.cs b/Backend.Tests/Mocks/WordRepositoryMock.cs index 40e2d80ae2..677481670f 100644 --- a/Backend.Tests/Mocks/WordRepositoryMock.cs +++ b/Backend.Tests/Mocks/WordRepositoryMock.cs @@ -131,12 +131,5 @@ public Task Add(Word word) _words.Add(word.Clone()); return Task.FromResult(word); } - - public Task CountFrontierWordsWithDomain(string projectId, string domainId) - { - var count = _frontier.Count( - w => w.ProjectId == projectId && w.Senses.Any(s => s.SemanticDomains.Any(sd => sd.Id == domainId))); - return Task.FromResult(count); - } } } diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs index 4cd42c4ab0..508e447f43 100644 --- a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -40,10 +40,8 @@ public async Task TestUpdateCountsForWord() var count1 = await _countRepo.GetCount(ProjId, DomainId1); var count2 = await _countRepo.GetCount(ProjId, DomainId2); - Assert.That(count1, Is.Not.Null); - Assert.That(count1!.Count, Is.EqualTo(1)); - Assert.That(count2, Is.Not.Null); - Assert.That(count2!.Count, Is.EqualTo(1)); + Assert.That(count1, Is.EqualTo(1)); + Assert.That(count2, Is.EqualTo(1)); } [Test] @@ -68,10 +66,8 @@ public async Task TestUpdateCountsForWords() var count1 = await _countRepo.GetCount(ProjId, DomainId1); var count2 = await _countRepo.GetCount(ProjId, DomainId2); - Assert.That(count1, Is.Not.Null); - Assert.That(count1!.Count, Is.EqualTo(2)); - Assert.That(count2, Is.Not.Null); - Assert.That(count2!.Count, Is.EqualTo(1)); + Assert.That(count1, Is.EqualTo(2)); + Assert.That(count2, Is.EqualTo(1)); } [Test] @@ -98,10 +94,8 @@ public async Task TestUpdateCountsAfterWordUpdate() var count1 = await _countRepo.GetCount(ProjId, DomainId1); var count2 = await _countRepo.GetCount(ProjId, DomainId2); - Assert.That(count1, Is.Not.Null); - Assert.That(count1!.Count, Is.EqualTo(1)); // Unchanged - Assert.That(count2, Is.Not.Null); - Assert.That(count2!.Count, Is.EqualTo(0)); // Decremented by 1 + Assert.That(count1, Is.EqualTo(1)); // Unchanged + Assert.That(count2, Is.EqualTo(0)); // Decremented by 1 } [Test] @@ -118,17 +112,16 @@ public async Task TestUpdateCountsForWordDeletion() var count1Before = await _countRepo.GetCount(ProjId, DomainId1); var count2Before = await _countRepo.GetCount(ProjId, DomainId2); - Assert.That(count1Before!.Count, Is.EqualTo(1)); - Assert.That(count2Before!.Count, Is.EqualTo(1)); + Assert.That(count1Before, Is.EqualTo(1)); + Assert.That(count2Before, Is.EqualTo(1)); // Now delete it await _countService.UpdateCountsForWordDeletion(word); var count1After = await _countRepo.GetCount(ProjId, DomainId1); var count2After = await _countRepo.GetCount(ProjId, DomainId2); - - Assert.That(count1After!.Count, Is.EqualTo(0)); - Assert.That(count2After!.Count, Is.EqualTo(0)); + Assert.That(count1After, Is.EqualTo(0)); + Assert.That(count2After, Is.EqualTo(0)); } } } diff --git a/Backend.Tests/Services/StatisticsServiceTests.cs b/Backend.Tests/Services/StatisticsServiceTests.cs index 789f884c2b..acddd2dc5d 100644 --- a/Backend.Tests/Services/StatisticsServiceTests.cs +++ b/Backend.Tests/Services/StatisticsServiceTests.cs @@ -12,18 +12,19 @@ namespace Backend.Tests.Services internal sealed class StatisticsServiceTests { private ISemanticDomainRepository _domainRepo = null!; + private ISemanticDomainCountRepository _domainCountRepo = null!; private IUserRepository _userRepo = null!; private IWordRepository _wordRepo = null!; private IStatisticsService _statsService = null!; private const string ProjId = "StatsServiceTestProjId"; private const string SemDomId = "StatsServiceTestSemDomId"; - private readonly List NonEmptySchedule = new() { DateTime.Now }; - private readonly List TreeNodes = new() { new(new SemanticDomain { Id = SemDomId }) }; + private readonly List NonEmptySchedule = [DateTime.Now]; + private readonly List TreeNodes = [new(new SemanticDomain { Id = SemDomId })]; private static Sense GetSenseWithDomain(string semDomId = SemDomId) { - return new() { SemanticDomains = new() { new() { Id = semDomId } } }; + return new() { SemanticDomains = [new() { Id = semDomId }] }; } private static User GetUserWithProjId(string projId = ProjId) { @@ -37,7 +38,7 @@ private static Word GetWordWithDomain(string semDomId = SemDomId) { Id = Util.RandString(10), ProjectId = ProjId, - Senses = new() { GetSenseWithDomain(semDomId) }, + Senses = [GetSenseWithDomain(semDomId)], Vernacular = Util.RandString(10) }; } @@ -46,16 +47,17 @@ private static Word GetWordWithDomain(string semDomId = SemDomId) public void Setup() { _domainRepo = new SemanticDomainRepositoryMock(); + _domainCountRepo = new SemanticDomainCountRepositoryMock(); _userRepo = new UserRepositoryMock(); _wordRepo = new WordRepositoryMock(); - _statsService = new StatisticsService(_wordRepo, _domainRepo, _userRepo); + _statsService = new StatisticsService(_domainRepo, _domainCountRepo, _userRepo, _wordRepo); } [Test] public void GetSemanticDomainCountsTestNullDomainList() { - // Add a word to the database and leave the semantic domain list null - _wordRepo.AddFrontier(GetWordWithDomain()); + // Add a domain count to the database and leave the semantic domain list null + _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, SemDomId, 1)); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); @@ -64,16 +66,16 @@ public void GetSemanticDomainCountsTestNullDomainList() [Test] public void GetSemanticDomainCountsTestEmptyDomainList() { - // Add to the database a word and an empty list of semantic domains + // Add to the database an empty list of semantic domains and a domain count ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(new List()); - _wordRepo.AddFrontier(GetWordWithDomain()); + _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, SemDomId, 1)); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); } [Test] - public void GetSemanticDomainCountsTestEmptyFrontier() + public void GetSemanticDomainCountsTestEmptyCounts() { // Add to the database a semantic domain but no word ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); @@ -85,9 +87,9 @@ public void GetSemanticDomainCountsTestEmptyFrontier() [Test] public void GetSemanticDomainCountsTestIdMismatch() { - // Add to the database a semantic domain and a word with a different semantic domain + // Add to the database a semantic domain and count with a different domain id ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); - _wordRepo.AddFrontier(GetWordWithDomain("different-id")); + _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, "DifferentId", 1)); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Has.Count.EqualTo(1)); @@ -97,9 +99,9 @@ public void GetSemanticDomainCountsTestIdMismatch() [Test] public void GetSemanticDomainCountsTestIdMatch() { - // Add to the database a semantic domain and a word with the same semantic domain + // Add to the database a semantic domain and a corresponding count ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); - _wordRepo.AddFrontier(GetWordWithDomain()); + _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, SemDomId, 1)); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Has.Count.EqualTo(1)); diff --git a/Backend/Controllers/StatisticsController.cs b/Backend/Controllers/StatisticsController.cs index 092535f824..1a41b28267 100644 --- a/Backend/Controllers/StatisticsController.cs +++ b/Backend/Controllers/StatisticsController.cs @@ -13,20 +13,30 @@ namespace BackendFramework.Controllers [Produces("application/json")] [Route("v1/projects/{projectId}/statistics")] - public class StatisticsController : Controller + public class StatisticsController( + IProjectRepository projRepo, IPermissionService permissionService, IStatisticsService statService) : Controller { - private readonly IStatisticsService _statService; - private readonly IPermissionService _permissionService; - private readonly IProjectRepository _projRepo; + private readonly IProjectRepository _projRepo = projRepo; + private readonly IPermissionService _permissionService = permissionService; + private readonly IStatisticsService _statService = statService; private const string otelTagName = "otel.StatisticsController"; - public StatisticsController( - IStatisticsService statService, IPermissionService permissionService, IProjectRepository projRepo) + /// Get the count of senses in a specific semantic domain + /// An integer count + [HttpGet("domaincount/{domainId}", Name = "GetDomainCount")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetDomainCount(string projectId, string domainId) { - _statService = statService; - _permissionService = permissionService; - _projRepo = projRepo; + using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain count"); + + if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId)) + { + return Forbid(); + } + + return Ok(await _statService.GetDomainCount(projectId, domainId)); } /// Get a list of s of a specific project in order diff --git a/Backend/Controllers/WordController.cs b/Backend/Controllers/WordController.cs index f625a4bbef..98a16eab2f 100644 --- a/Backend/Controllers/WordController.cs +++ b/Backend/Controllers/WordController.cs @@ -305,22 +305,5 @@ public async Task RevertWords( } return Ok(updates); } - - /// Get the count of frontier words with senses in a specific semantic domain - /// An integer count - [HttpGet("domainwordcount/{domainId}", Name = "GetDomainWordCount")] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task GetDomainWordCount(string projectId, string domainId) - { - using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain word count"); - - if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId)) - { - return Forbid(); - } - - return Ok(await _wordRepo.CountFrontierWordsWithDomain(projectId, domainId)); - } } } diff --git a/Backend/Interfaces/ISemanticDomainCountRepository.cs b/Backend/Interfaces/ISemanticDomainCountRepository.cs index 445fec5338..770fbe78f1 100644 --- a/Backend/Interfaces/ISemanticDomainCountRepository.cs +++ b/Backend/Interfaces/ISemanticDomainCountRepository.cs @@ -6,7 +6,7 @@ namespace BackendFramework.Interfaces { public interface ISemanticDomainCountRepository { - Task GetCount(string projectId, string domainId); + Task GetCount(string projectId, string domainId); Task> GetAllCounts(string projectId); Task Create(ProjectSemanticDomainCount count); Task Increment(string projectId, string domainId, int amount = 1); diff --git a/Backend/Interfaces/IStatisticsService.cs b/Backend/Interfaces/IStatisticsService.cs index 09229d5289..bd4102efad 100644 --- a/Backend/Interfaces/IStatisticsService.cs +++ b/Backend/Interfaces/IStatisticsService.cs @@ -7,6 +7,7 @@ namespace BackendFramework.Interfaces { public interface IStatisticsService { + Task GetDomainCount(string projectId, string domainId); Task> GetSemanticDomainCounts(string projectId, string lang); Task> GetWordsPerDayPerUserCounts(string projectId); Task GetProgressEstimationLineChartRoot(string projectId, List schedule); diff --git a/Backend/Interfaces/IWordRepository.cs b/Backend/Interfaces/IWordRepository.cs index a5606c6f82..4dd65b628e 100644 --- a/Backend/Interfaces/IWordRepository.cs +++ b/Backend/Interfaces/IWordRepository.cs @@ -23,6 +23,5 @@ public interface IWordRepository Task> AddFrontier(List words); Task DeleteFrontier(string projectId, string wordId); Task DeleteFrontier(string projectId, List wordIds); - Task CountFrontierWordsWithDomain(string projectId, string domainId); } } diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index 87f90242af..b7523c8534 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -30,12 +30,12 @@ public class SemanticDomainCountRepository(IMongoDbContext dbContext) : ISemanti } /// Gets the count for a specific semantic domain in a project - public async Task GetCount(string projectId, string domainId) + public async Task GetCount(string projectId, string domainId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "getting semantic domain count"); var result = await _counts.FindAsync(ProjectDomainFilter(projectId, domainId)); - return await result.FirstOrDefaultAsync(); + return (await result.FirstOrDefaultAsync())?.Count ?? 0; } /// Gets all counts for a project diff --git a/Backend/Repositories/WordRepository.cs b/Backend/Repositories/WordRepository.cs index 5884e14c7b..d6ad340997 100644 --- a/Backend/Repositories/WordRepository.cs +++ b/Backend/Repositories/WordRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Tasks; using BackendFramework.Helper; using BackendFramework.Interfaces; @@ -269,23 +268,5 @@ public async Task DeleteFrontier(string projectId, List wordIds) var deleted = await _frontier.DeleteManyAsync(GetProjectWordsFilter(projectId, wordIds)); return deleted.DeletedCount; } - - /// - /// Counts the number of Frontier words that have the specified semantic domain. - /// - /// The project id - /// The semantic domain id - /// The count of words containing at least one sense with the specified domain. - public async Task CountFrontierWordsWithDomain(string projectId, string domainId) - { - using var activity = OtelService.StartActivityWithTag(otelTagName, "counting frontier words with domain"); - - var filterDef = new FilterDefinitionBuilder(); - var filter = filterDef.And( - filterDef.Eq(w => w.ProjectId, projectId), - filterDef.ElemMatch(w => w.Senses, s => s.SemanticDomains.Any(sd => sd.Id == domainId))); - - return (int)await _frontier.CountDocumentsAsync(filter); - } } } diff --git a/Backend/Services/StatisticsService.cs b/Backend/Services/StatisticsService.cs index 2ad4d7a829..37f014186c 100644 --- a/Backend/Services/StatisticsService.cs +++ b/Backend/Services/StatisticsService.cs @@ -9,22 +9,17 @@ namespace BackendFramework.Services { - public class StatisticsService : IStatisticsService + public class StatisticsService(ISemanticDomainRepository domainRepo, + ISemanticDomainCountRepository domainCountRepo, IUserRepository userRepo, IWordRepository wordRepo) + : IStatisticsService { - private readonly IWordRepository _wordRepo; - private readonly ISemanticDomainRepository _domainRepo; - private readonly IUserRepository _userRepo; + private readonly ISemanticDomainRepository _domainRepo = domainRepo; + private readonly ISemanticDomainCountRepository _domainCountRepo = domainCountRepo; + private readonly IUserRepository _userRepo = userRepo; + private readonly IWordRepository _wordRepo = wordRepo; private const string otelTagName = "otel.StatisticsService"; - public StatisticsService( - IWordRepository wordRepo, ISemanticDomainRepository domainRepo, IUserRepository userRepo) - { - _wordRepo = wordRepo; - _domainRepo = domainRepo; - _userRepo = userRepo; - } - // Statistic names (TODO: localize) const string StatAverage = "Average"; const string StatBurstProjection = "Burst Projection"; @@ -32,6 +27,16 @@ public StatisticsService( const string StatProjection = "Projection"; const string StatRunningTotal = "Running Total"; + /// + /// Get a count of the number of senses associated with a semantic domain + /// + public Task GetDomainCount(string projectId, string domainId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain count"); + + return _domainCountRepo.GetCount(projectId, domainId); + } + /// /// Get a to generate a SemanticDomain statistics /// @@ -39,32 +44,22 @@ public async Task> GetSemanticDomainCounts(string proj { using var activity = OtelService.StartActivityWithTag(otelTagName, "getting semantic domain counts"); - var hashMap = new Dictionary(); var domainTreeNodeList = await _domainRepo.GetAllSemanticDomainTreeNodes(lang); - var wordList = await _wordRepo.GetFrontier(projectId); - - if (domainTreeNodeList is null || domainTreeNodeList.Count == 0 || wordList.Count == 0) + if (domainTreeNodeList is null || domainTreeNodeList.Count == 0) { return []; } - foreach (var word in wordList) + var domainCounts = await _domainCountRepo.GetAllCounts(projectId); + if (domainCounts.Count == 0) { - foreach (var sense in word.Senses) - { - foreach (var sd in sense.SemanticDomains) - { - hashMap[sd.Id] = hashMap.GetValueOrDefault(sd.Id, 0) + 1; - } - } + return []; } + var domainCountDict = domainCounts.ToDictionary(dc => dc.DomainId, dc => dc.Count); - var resList = new List(); - foreach (var domainTreeNode in domainTreeNodeList) - { - resList.Add(new(domainTreeNode, hashMap.GetValueOrDefault(domainTreeNode.Id, 0))); - } - return resList; + return domainTreeNodeList.Select(domainTreeNode => + new SemanticDomainCount(domainTreeNode, domainCountDict.GetValueOrDefault(domainTreeNode.Id, 0)) + ).ToList(); } /// diff --git a/src/api/api/statistics-api.ts b/src/api/api/statistics-api.ts index 96205a7008..89d4421e73 100644 --- a/src/api/api/statistics-api.ts +++ b/src/api/api/statistics-api.ts @@ -52,6 +52,55 @@ export const StatisticsApiAxiosParamCreator = function ( configuration?: Configuration ) { return { + /** + * + * @param {string} projectId + * @param {string} domainId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDomainCount: async ( + projectId: string, + domainId: string, + options: any = {} + ): Promise => { + // verify required parameter 'projectId' is not null or undefined + assertParamExists("getDomainCount", "projectId", projectId); + // verify required parameter 'domainId' is not null or undefined + assertParamExists("getDomainCount", "domainId", domainId); + const localVarPath = + `/v1/projects/{projectId}/statistics/domaincount/{domainId}` + .replace(`{${"projectId"}}`, encodeURIComponent(String(projectId))) + .replace(`{${"domainId"}}`, encodeURIComponent(String(domainId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} projectId @@ -303,6 +352,32 @@ export const StatisticsApiFp = function (configuration?: Configuration) { const localVarAxiosParamCreator = StatisticsApiAxiosParamCreator(configuration); return { + /** + * + * @param {string} projectId + * @param {string} domainId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getDomainCount( + projectId: string, + domainId: string, + options?: any + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.getDomainCount( + projectId, + domainId, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, /** * * @param {string} projectId @@ -449,6 +524,22 @@ export const StatisticsApiFactory = function ( ) { const localVarFp = StatisticsApiFp(configuration); return { + /** + * + * @param {string} projectId + * @param {string} domainId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDomainCount( + projectId: string, + domainId: string, + options?: any + ): AxiosPromise { + return localVarFp + .getDomainCount(projectId, domainId, options) + .then((request) => request(axios, basePath)); + }, /** * * @param {string} projectId @@ -524,6 +615,27 @@ export const StatisticsApiFactory = function ( }; }; +/** + * Request parameters for getDomainCount operation in StatisticsApi. + * @export + * @interface StatisticsApiGetDomainCountRequest + */ +export interface StatisticsApiGetDomainCountRequest { + /** + * + * @type {string} + * @memberof StatisticsApiGetDomainCount + */ + readonly projectId: string; + + /** + * + * @type {string} + * @memberof StatisticsApiGetDomainCount + */ + readonly domainId: string; +} + /** * Request parameters for getLineChartRootData operation in StatisticsApi. * @export @@ -608,6 +720,26 @@ export interface StatisticsApiGetWordsPerDayPerUserCountsRequest { * @extends {BaseAPI} */ export class StatisticsApi extends BaseAPI { + /** + * + * @param {StatisticsApiGetDomainCountRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StatisticsApi + */ + public getDomainCount( + requestParameters: StatisticsApiGetDomainCountRequest, + options?: any + ) { + return StatisticsApiFp(this.configuration) + .getDomainCount( + requestParameters.projectId, + requestParameters.domainId, + options + ) + .then((request) => request(this.axios, this.basePath)); + } + /** * * @param {StatisticsApiGetLineChartRootDataRequest} requestParameters Request parameters. diff --git a/src/api/api/word-api.ts b/src/api/api/word-api.ts index 64d6f9ddc5..d8a587a3bf 100644 --- a/src/api/api/word-api.ts +++ b/src/api/api/word-api.ts @@ -207,55 +207,6 @@ export const WordApiAxiosParamCreator = function ( options: localVarRequestOptions, }; }, - /** - * - * @param {string} projectId - * @param {string} domainId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getDomainWordCount: async ( - projectId: string, - domainId: string, - options: any = {} - ): Promise => { - // verify required parameter 'projectId' is not null or undefined - assertParamExists("getDomainWordCount", "projectId", projectId); - // verify required parameter 'domainId' is not null or undefined - assertParamExists("getDomainWordCount", "domainId", domainId); - const localVarPath = - `/v1/projects/{projectId}/words/domainwordcount/{domainId}` - .replace(`{${"projectId"}}`, encodeURIComponent(String(projectId))) - .replace(`{${"domainId"}}`, encodeURIComponent(String(domainId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { - method: "GET", - ...baseOptions, - ...options, - }; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - }; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {string} projectId @@ -857,33 +808,6 @@ export const WordApiFp = function (configuration?: Configuration) { configuration ); }, - /** - * - * @param {string} projectId - * @param {string} domainId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getDomainWordCount( - projectId: string, - domainId: string, - options?: any - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = - await localVarAxiosParamCreator.getDomainWordCount( - projectId, - domainId, - options - ); - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ); - }, /** * * @param {string} projectId @@ -1205,22 +1129,6 @@ export const WordApiFactory = function ( .deleteFrontierWord(projectId, wordId, options) .then((request) => request(axios, basePath)); }, - /** - * - * @param {string} projectId - * @param {string} domainId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getDomainWordCount( - projectId: string, - domainId: string, - options?: any - ): AxiosPromise { - return localVarFp - .getDomainWordCount(projectId, domainId, options) - .then((request) => request(axios, basePath)); - }, /** * * @param {string} projectId @@ -1442,27 +1350,6 @@ export interface WordApiDeleteFrontierWordRequest { readonly wordId: string; } -/** - * Request parameters for getDomainWordCount operation in WordApi. - * @export - * @interface WordApiGetDomainWordCountRequest - */ -export interface WordApiGetDomainWordCountRequest { - /** - * - * @type {string} - * @memberof WordApiGetDomainWordCount - */ - readonly projectId: string; - - /** - * - * @type {string} - * @memberof WordApiGetDomainWordCount - */ - readonly domainId: string; -} - /** * Request parameters for getDuplicateId operation in WordApi. * @export @@ -1729,26 +1616,6 @@ export class WordApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {WordApiGetDomainWordCountRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof WordApi - */ - public getDomainWordCount( - requestParameters: WordApiGetDomainWordCountRequest, - options?: any - ) { - return WordApiFp(this.configuration) - .getDomainWordCount( - requestParameters.projectId, - requestParameters.domainId, - options - ) - .then((request) => request(this.axios, this.basePath)); - } - /** * * @param {WordApiGetDuplicateIdRequest} requestParameters Request parameters. diff --git a/src/backend/index.ts b/src/backend/index.ts index a5f2cff803..2863b9db7c 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -644,6 +644,14 @@ export async function getConsentImageSrc(speaker: Speaker): Promise { /* StatisticsController.cs */ +export async function getDomainCount(domainId: string): Promise { + const response = await statisticsApi.getDomainCount( + { projectId: LocalStorage.getProjectId(), domainId }, + defaultOptions() + ); + return response.data; +} + export async function getSemanticDomainCounts( projectId: string, lang?: string @@ -966,11 +974,3 @@ export async function updateWord(word: Word): Promise { ); return { ...word, id: resp.data }; } - -export async function getDomainWordCount(domainId: string): Promise { - const response = await wordApi.getDomainWordCount( - { projectId: LocalStorage.getProjectId(), domainId }, - defaultOptions() - ); - return response.data; -} diff --git a/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx b/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx index dab9ad8b7c..9325efdc66 100644 --- a/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx +++ b/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx @@ -2,7 +2,7 @@ import { Badge, Tooltip } from "@mui/material"; import { ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { getDomainWordCount } from "backend"; +import { getDomainCount } from "backend"; import { rootId } from "types/semanticDomain"; interface DomainCountBadgeProps { @@ -21,7 +21,7 @@ export default function DomainCountBadge( useEffect(() => { setWordCount(undefined); if (domainId && domainId !== rootId) { - getDomainWordCount(domainId) + getDomainCount(domainId) .then(setWordCount) .catch(() => console.warn(`Failed to get word count for domain ${domainId}.`) diff --git a/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx b/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx index a6b6446be3..a76c63f73a 100644 --- a/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx +++ b/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx @@ -9,15 +9,15 @@ import testDomainMap, { } from "components/TreeView/tests/SemanticDomainMock"; jest.mock("backend", () => ({ - getDomainWordCount: () => mockGetDomainWordCount(), + getDomainCount: () => mockGetDomainCount(), })); const mockAnimate = jest.fn(); -const mockGetDomainWordCount = jest.fn(); +const mockGetDomainCount = jest.fn(); beforeEach(() => { jest.clearAllMocks(); - mockGetDomainWordCount.mockResolvedValue(0); + mockGetDomainCount.mockResolvedValue(0); }); describe("CurrentRow", () => { diff --git a/src/components/TreeView/TreeDepiction/tests/index.test.tsx b/src/components/TreeView/TreeDepiction/tests/index.test.tsx index 95e586096f..d63f5a6a3c 100644 --- a/src/components/TreeView/TreeDepiction/tests/index.test.tsx +++ b/src/components/TreeView/TreeDepiction/tests/index.test.tsx @@ -6,13 +6,13 @@ import testDomainMap, { } from "components/TreeView/tests/SemanticDomainMock"; jest.mock("backend", () => ({ - getDomainWordCount: () => mockGetDomainWordCount(), + getDomainCount: () => mockGetDomainCount(), })); -const mockGetDomainWordCount = jest.fn(); +const mockGetDomainCount = jest.fn(); beforeEach(() => { - mockGetDomainWordCount.mockResolvedValue(0); + mockGetDomainCount.mockResolvedValue(0); }); describe("TreeDepiction", () => { From 00b10d7e85039e6f3976bb80b3370fe475f032f7 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 18 Dec 2025 14:28:47 -0500 Subject: [PATCH 08/22] Condense --- .../Controllers/AudioControllerTests.cs | 3 +- .../Controllers/LiftControllerTests.cs | 3 +- .../Controllers/MergeControllerTests.cs | 3 +- .../Controllers/WordControllerTests.cs | 3 +- Backend.Tests/Services/MergeServiceTests.cs | 3 +- Backend/Services/LiftService.cs | 28 ++++++------------- 6 files changed, 14 insertions(+), 29 deletions(-) diff --git a/Backend.Tests/Controllers/AudioControllerTests.cs b/Backend.Tests/Controllers/AudioControllerTests.cs index 03999507fb..1c13a1f9f6 100644 --- a/Backend.Tests/Controllers/AudioControllerTests.cs +++ b/Backend.Tests/Controllers/AudioControllerTests.cs @@ -37,8 +37,7 @@ public void Setup() _projRepo = new ProjectRepositoryMock(); _wordRepo = new WordRepositoryMock(); _permissionService = new PermissionServiceMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - var countService = new SemanticDomainCountService(countRepo, _wordRepo); + var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); _wordService = new WordService(_wordRepo, countService); _audioController = new AudioController(_wordRepo, _wordService, _permissionService); diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index 4595b74e27..b11288abce 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -48,8 +48,7 @@ public void Setup() _projRepo = new ProjectRepositoryMock(); _speakerRepo = new SpeakerRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - var countService = new SemanticDomainCountService(countRepo, _wordRepo); + var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); _liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo, countService); _wordService = new WordService(_wordRepo, countService); _liftController = new LiftController(_wordRepo, _projRepo, new PermissionServiceMock(), _liftService, diff --git a/Backend.Tests/Controllers/MergeControllerTests.cs b/Backend.Tests/Controllers/MergeControllerTests.cs index 5f78f57477..dfd738c65e 100644 --- a/Backend.Tests/Controllers/MergeControllerTests.cs +++ b/Backend.Tests/Controllers/MergeControllerTests.cs @@ -34,8 +34,7 @@ public void Setup() _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - var countService = new SemanticDomainCountService(countRepo, _wordRepo); + var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); _wordService = new WordService(_wordRepo, countService); _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); _mergeController = new MergeController( diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 3dead436c6..1da847ca7d 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -32,8 +32,7 @@ public void Dispose() public void Setup() { _wordRepo = new WordRepositoryMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - var countService = new SemanticDomainCountService(countRepo, _wordRepo); + var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); _wordService = new WordService(_wordRepo, countService); _permissionService = new PermissionServiceMock(); _wordController = new WordController(_wordRepo, _wordService, _permissionService); diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index a63023d70a..d1ce631f95 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -25,8 +25,7 @@ public void Setup() _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - var countService = new SemanticDomainCountService(countRepo, _wordRepo); + var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); _wordService = new WordService(_wordRepo, countService); _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); } diff --git a/Backend/Services/LiftService.cs b/Backend/Services/LiftService.cs index eb596dfedb..b50c5ad77b 100644 --- a/Backend/Services/LiftService.cs +++ b/Backend/Services/LiftService.cs @@ -721,26 +721,16 @@ private static void WriteFormElement(XmlWriter liftRangesWriter, string element, liftRangesWriter.WriteEndElement(); // end element } - private sealed class LiftMerger : ILiftMerger + private sealed class LiftMerger(string projectId, string vernLang, IWordRepository wordRepo, + ISemanticDomainCountService domainCountService) : ILiftMerger { - private readonly string _projectId; - private readonly List _customSemDoms = new(); - private readonly string _vernLang; - private readonly IWordRepository _wordRepo; - private readonly ISemanticDomainCountService _domainCountService; - private readonly List _importEntries = new(); - - public LiftMerger( - string projectId, - string vernLang, - IWordRepository wordRepo, - ISemanticDomainCountService domainCountService) - { - _projectId = projectId; - _vernLang = vernLang; - _wordRepo = wordRepo; - _domainCountService = domainCountService; - } + private readonly string _projectId = projectId; + private readonly string _vernLang = vernLang; + private readonly IWordRepository _wordRepo = wordRepo; + private readonly ISemanticDomainCountService _domainCountService = domainCountService; + + private readonly List _customSemDoms = []; + private readonly List _importEntries = []; /// /// Check for any Definitions in the private field From 0adbf00de11a79948e05bbd6f763fd1697b86b2a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 8 Jan 2026 11:01:54 -0500 Subject: [PATCH 09/22] Remove unused stuff --- .../Controllers/AudioControllerTests.cs | 4 ++-- .../Controllers/LiftControllerTests.cs | 2 +- .../Controllers/MergeControllerTests.cs | 4 ++-- .../Controllers/WordControllerTests.cs | 4 ++-- .../Mocks/SemanticDomainCountRepositoryMock.cs | 18 +++--------------- Backend.Tests/Services/LiftServiceTests.cs | 6 ++---- Backend.Tests/Services/MergeServiceTests.cs | 4 ++-- .../SemanticDomainCountServiceTests.cs | 4 +--- .../Services/StatisticsServiceTests.cs | 8 ++++---- Backend.Tests/Services/WordServiceTests.cs | 6 ++---- .../ISemanticDomainCountRepository.cs | 3 +-- Backend/Models/ProjectSemanticDomainCount.cs | 3 +-- .../SemanticDomainCountRepository.cs | 15 +++------------ Backend/Services/SemanticDomainCountService.cs | 4 +--- 14 files changed, 27 insertions(+), 58 deletions(-) diff --git a/Backend.Tests/Controllers/AudioControllerTests.cs b/Backend.Tests/Controllers/AudioControllerTests.cs index 1c13a1f9f6..422e76024e 100644 --- a/Backend.Tests/Controllers/AudioControllerTests.cs +++ b/Backend.Tests/Controllers/AudioControllerTests.cs @@ -37,8 +37,8 @@ public void Setup() _projRepo = new ProjectRepositoryMock(); _wordRepo = new WordRepositoryMock(); _permissionService = new PermissionServiceMock(); - var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); - _wordService = new WordService(_wordRepo, countService); + _wordService = + new WordService(_wordRepo, new SemanticDomainCountService(new SemanticDomainCountRepositoryMock())); _audioController = new AudioController(_wordRepo, _wordService, _permissionService); _projId = _projRepo.Create(new Project { Name = "AudioControllerTests" }).Result!.Id; diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index b11288abce..ae77da441d 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -48,7 +48,7 @@ public void Setup() _projRepo = new ProjectRepositoryMock(); _speakerRepo = new SpeakerRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); + var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); _liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo, countService); _wordService = new WordService(_wordRepo, countService); _liftController = new LiftController(_wordRepo, _projRepo, new PermissionServiceMock(), _liftService, diff --git a/Backend.Tests/Controllers/MergeControllerTests.cs b/Backend.Tests/Controllers/MergeControllerTests.cs index dfd738c65e..4e5bea685c 100644 --- a/Backend.Tests/Controllers/MergeControllerTests.cs +++ b/Backend.Tests/Controllers/MergeControllerTests.cs @@ -34,8 +34,8 @@ public void Setup() _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); - _wordService = new WordService(_wordRepo, countService); + _wordService = + new WordService(_wordRepo, new SemanticDomainCountService(new SemanticDomainCountRepositoryMock())); _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); _mergeController = new MergeController( _mergeService, new HubContextMock(), new PermissionServiceMock()); diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 1da847ca7d..adaf3926d5 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -32,8 +32,8 @@ public void Dispose() public void Setup() { _wordRepo = new WordRepositoryMock(); - var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); - _wordService = new WordService(_wordRepo, countService); + _wordService = + new WordService(_wordRepo, new SemanticDomainCountService(new SemanticDomainCountRepositoryMock())); _permissionService = new PermissionServiceMock(); _wordController = new WordController(_wordRepo, _wordService, _permissionService); } diff --git a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs index f980676623..b4c405e039 100644 --- a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs +++ b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs @@ -8,12 +8,7 @@ namespace Backend.Tests.Mocks { public sealed class SemanticDomainCountRepositoryMock : ISemanticDomainCountRepository { - private readonly List _counts; - - public SemanticDomainCountRepositoryMock() - { - _counts = new List(); - } + private readonly List _counts = []; public Task GetCount(string projectId, string domainId) { @@ -27,14 +22,7 @@ public Task> GetAllCounts(string projectId) return Task.FromResult(counts); } - public Task Create(ProjectSemanticDomainCount count) - { - count.Id = Util.RandString(); - _counts.Add(count); - return Task.FromResult(count); - } - - public Task Increment(string projectId, string domainId, int amount = 1) + public Task Increment(string projectId, string domainId, int amount = 1) { var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId); if (count is null) @@ -49,7 +37,7 @@ public Task Increment(string projectId, string domainId, int amount = 1) { count.Count += amount; } - return Task.FromResult(true); + return Task.FromResult(count.Count); } public Task DeleteAllCounts(string projectId) diff --git a/Backend.Tests/Services/LiftServiceTests.cs b/Backend.Tests/Services/LiftServiceTests.cs index 564d156b78..8c441f5207 100644 --- a/Backend.Tests/Services/LiftServiceTests.cs +++ b/Backend.Tests/Services/LiftServiceTests.cs @@ -27,10 +27,8 @@ public void Setup() { _semDomRepo = new SemanticDomainRepositoryMock(); _speakerRepo = new SpeakerRepositoryMock(); - var wordRepo = new WordRepositoryMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - var countService = new SemanticDomainCountService(countRepo, wordRepo); - _liftService = new LiftService(_semDomRepo, _speakerRepo, countService); + _liftService = new LiftService( + _semDomRepo, _speakerRepo, new SemanticDomainCountService(new SemanticDomainCountRepositoryMock())); } [Test] diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index d1ce631f95..1526f68a20 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -25,8 +25,8 @@ public void Setup() _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock(), _wordRepo); - _wordService = new WordService(_wordRepo, countService); + _wordService = + new WordService(_wordRepo, new SemanticDomainCountService(new SemanticDomainCountRepositoryMock())); _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); } diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs index 508e447f43..58b5bc10ed 100644 --- a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -11,7 +11,6 @@ namespace Backend.Tests.Services internal sealed class SemanticDomainCountServiceTests { private ISemanticDomainCountRepository _countRepo = null!; - private IWordRepository _wordRepo = null!; private ISemanticDomainCountService _countService = null!; private const string ProjId = "CountServiceTestProjId"; @@ -22,8 +21,7 @@ internal sealed class SemanticDomainCountServiceTests public void Setup() { _countRepo = new SemanticDomainCountRepositoryMock(); - _wordRepo = new WordRepositoryMock(); - _countService = new SemanticDomainCountService(_countRepo, _wordRepo); + _countService = new SemanticDomainCountService(_countRepo); } [Test] diff --git a/Backend.Tests/Services/StatisticsServiceTests.cs b/Backend.Tests/Services/StatisticsServiceTests.cs index acddd2dc5d..13dbce3e49 100644 --- a/Backend.Tests/Services/StatisticsServiceTests.cs +++ b/Backend.Tests/Services/StatisticsServiceTests.cs @@ -57,7 +57,7 @@ public void Setup() public void GetSemanticDomainCountsTestNullDomainList() { // Add a domain count to the database and leave the semantic domain list null - _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, SemDomId, 1)); + _domainCountRepo.Increment(ProjId, SemDomId).Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); @@ -68,7 +68,7 @@ public void GetSemanticDomainCountsTestEmptyDomainList() { // Add to the database an empty list of semantic domains and a domain count ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(new List()); - _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, SemDomId, 1)); + _domainCountRepo.Increment(ProjId, SemDomId).Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); @@ -89,7 +89,7 @@ public void GetSemanticDomainCountsTestIdMismatch() { // Add to the database a semantic domain and count with a different domain id ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); - _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, "DifferentId", 1)); + _domainCountRepo.Increment(ProjId, "DifferentId").Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Has.Count.EqualTo(1)); @@ -101,7 +101,7 @@ public void GetSemanticDomainCountsTestIdMatch() { // Add to the database a semantic domain and a corresponding count ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); - _domainCountRepo.Create(new ProjectSemanticDomainCount(ProjId, SemDomId, 1)); + _domainCountRepo.Increment(ProjId, SemDomId).Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Has.Count.EqualTo(1)); diff --git a/Backend.Tests/Services/WordServiceTests.cs b/Backend.Tests/Services/WordServiceTests.cs index 87f45fade8..006872266a 100644 --- a/Backend.Tests/Services/WordServiceTests.cs +++ b/Backend.Tests/Services/WordServiceTests.cs @@ -11,7 +11,6 @@ namespace Backend.Tests.Services internal sealed class WordServiceTests { private IWordRepository _wordRepo = null!; - private ISemanticDomainCountService _countService = null!; private IWordService _wordService = null!; private const string ProjId = "WordServiceTestProjId"; @@ -22,9 +21,8 @@ internal sealed class WordServiceTests public void Setup() { _wordRepo = new WordRepositoryMock(); - var countRepo = new SemanticDomainCountRepositoryMock(); - _countService = new SemanticDomainCountService(countRepo, _wordRepo); - _wordService = new WordService(_wordRepo, _countService); + _wordService = + new WordService(_wordRepo, new SemanticDomainCountService(new SemanticDomainCountRepositoryMock())); } [Test] diff --git a/Backend/Interfaces/ISemanticDomainCountRepository.cs b/Backend/Interfaces/ISemanticDomainCountRepository.cs index 770fbe78f1..13ab16e53e 100644 --- a/Backend/Interfaces/ISemanticDomainCountRepository.cs +++ b/Backend/Interfaces/ISemanticDomainCountRepository.cs @@ -8,8 +8,7 @@ public interface ISemanticDomainCountRepository { Task GetCount(string projectId, string domainId); Task> GetAllCounts(string projectId); - Task Create(ProjectSemanticDomainCount count); - Task Increment(string projectId, string domainId, int amount = 1); + Task Increment(string projectId, string domainId, int amount = 1); Task DeleteAllCounts(string projectId); } } diff --git a/Backend/Models/ProjectSemanticDomainCount.cs b/Backend/Models/ProjectSemanticDomainCount.cs index fbfde3f979..afd2db3b97 100644 --- a/Backend/Models/ProjectSemanticDomainCount.cs +++ b/Backend/Models/ProjectSemanticDomainCount.cs @@ -34,9 +34,8 @@ public ProjectSemanticDomainCount() Count = 0; } - public ProjectSemanticDomainCount(string projectId, string domainId, int count = 0) + public ProjectSemanticDomainCount(string projectId, string domainId, int count = 0) : this() { - Id = ""; ProjectId = projectId; DomainId = domainId; Count = count; diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index b7523c8534..db196201c6 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -46,18 +46,9 @@ public async Task> GetAllCounts(string projectI return await _counts.Find(ProjectFilter(projectId)).ToListAsync(); } - /// Creates a new semantic domain count entry - public async Task Create(ProjectSemanticDomainCount count) - { - using var activity = OtelService.StartActivityWithTag(otelTagName, "creating semantic domain count"); - - await _counts.InsertOneAsync(count); - return count; - } - /// Increments (or decrements if negative) the count for a semantic domain - /// true if the count was updated, false if it doesn't exist - public async Task Increment(string projectId, string domainId, int amount = 1) + /// the new count after incrementing + public async Task Increment(string projectId, string domainId, int amount = 1) { using var activity = OtelService.StartActivityWithTag(otelTagName, "incrementing semantic domain count"); @@ -73,7 +64,7 @@ public async Task Increment(string projectId, string domainId, int amount }; var result = await _counts.FindOneAndUpdateAsync(filter, update, options); - return result is not null; + return result?.Count ?? 0; } /// Deletes all counts for a project diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs index 715dbb2d67..3f5d396e9d 100644 --- a/Backend/Services/SemanticDomainCountService.cs +++ b/Backend/Services/SemanticDomainCountService.cs @@ -8,11 +8,9 @@ namespace BackendFramework.Services { /// Service for managing semantic domain sense counts - public class SemanticDomainCountService(ISemanticDomainCountRepository countRepo, IWordRepository wordRepo) - : ISemanticDomainCountService + public class SemanticDomainCountService(ISemanticDomainCountRepository countRepo) : ISemanticDomainCountService { private readonly ISemanticDomainCountRepository _countRepo = countRepo; - private readonly IWordRepository _wordRepo = wordRepo; private const string otelTagName = "otel.SemanticDomainCountService"; From 56a131c25af2ba2106bef8d3b71b925b3c6d3455 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 8 Jan 2026 11:16:18 -0500 Subject: [PATCH 10/22] Clear domain counts when clearing frontier --- .../Controllers/LiftControllerTests.cs | 7 +++-- Backend/Controllers/LiftController.cs | 31 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index ae77da441d..abdd368a87 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -46,13 +46,14 @@ public void Dispose() public void Setup() { _projRepo = new ProjectRepositoryMock(); + var semanticDomainCountRepo = new SemanticDomainCountRepositoryMock(); _speakerRepo = new SpeakerRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var countService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); + var countService = new SemanticDomainCountService(semanticDomainCountRepo); _liftService = new LiftService(new SemanticDomainRepositoryMock(), _speakerRepo, countService); _wordService = new WordService(_wordRepo, countService); - _liftController = new LiftController(_wordRepo, _projRepo, new PermissionServiceMock(), _liftService, - new HubContextMock(), new MockLogger()); + _liftController = new LiftController(_projRepo, semanticDomainCountRepo, _wordRepo, _liftService, + new HubContextMock(), new PermissionServiceMock(), new MockLogger()); _projId = _projRepo.Create(new Project { Name = ProjName }).Result!.Id; _file = new FormFile(_stream, 0, _stream.Length, "Name", FileName); diff --git a/Backend/Controllers/LiftController.cs b/Backend/Controllers/LiftController.cs index 0554c42208..ca1a1859c5 100644 --- a/Backend/Controllers/LiftController.cs +++ b/Backend/Controllers/LiftController.cs @@ -22,29 +22,21 @@ namespace BackendFramework.Controllers [Authorize] [Produces("application/json")] [Route("v1/projects/{projectId}/lift")] - public class LiftController : Controller + public class LiftController(IProjectRepository projRepo, + ISemanticDomainCountRepository semanticDomainCountRepository, IWordRepository wordRepo, + ILiftService liftService, IHubContext notifyService, IPermissionService permissionService, + ILogger logger) : Controller { - private readonly IProjectRepository _projRepo; - private readonly IWordRepository _wordRepo; - private readonly ILiftService _liftService; - private readonly IHubContext _notifyService; - private readonly IPermissionService _permissionService; - private readonly ILogger _logger; + private readonly IProjectRepository _projRepo = projRepo; + private readonly ISemanticDomainCountRepository _semanticDomainCountRepository = semanticDomainCountRepository; + private readonly IWordRepository _wordRepo = wordRepo; + private readonly ILiftService _liftService = liftService; + private readonly IHubContext _notifyService = notifyService; + private readonly IPermissionService _permissionService = permissionService; + private readonly ILogger _logger = logger; private const string otelTagName = "otel.LiftController"; - public LiftController( - IWordRepository wordRepo, IProjectRepository projRepo, IPermissionService permissionService, - ILiftService liftService, IHubContext notifyService, ILogger logger) - { - _projRepo = projRepo; - _wordRepo = wordRepo; - _liftService = liftService; - _notifyService = notifyService; - _permissionService = permissionService; - _logger = logger; - } - /// /// Extract a LIFT file to a temporary folder. /// Get all vernacular writing systems from the extracted location. @@ -117,6 +109,7 @@ public async Task DeleteFrontierAndFinishUploadLiftFile(string pr // Delete all frontier words and load the LIFT data await _wordRepo.DeleteAllFrontierWords(projectId); + await _semanticDomainCountRepository.DeleteAllCounts(projectId); return await FinishUploadLiftFile(projectId, userId, true); } From ca760cab3cd8cc1dabcca8a64c980e715ff8664c Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 14 Jan 2026 16:50:33 -0500 Subject: [PATCH 11/22] Refactor word repo/service balance --- .../Controllers/LiftControllerTests.cs | 2 +- Backend.Tests/Mocks/WordRepositoryMock.cs | 23 +-- Backend.Tests/Services/WordServiceTests.cs | 48 +++---- Backend/Controllers/AudioController.cs | 2 +- Backend/Controllers/WordController.cs | 2 +- Backend/Interfaces/IWordRepository.cs | 3 +- Backend/Interfaces/IWordService.cs | 9 +- Backend/Repositories/WordRepository.cs | 27 ++-- Backend/Services/MergeService.cs | 4 +- Backend/Services/WordService.cs | 131 +++++++----------- 10 files changed, 107 insertions(+), 144 deletions(-) diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index cbd206c86b..ff31dec6bc 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -416,7 +416,7 @@ public async Task TestDeletedWordsExportToLift() word.Vernacular = "updated"; await _wordService.Update(_projId, UserId, wordToUpdate.Id, word); - await _wordService.DeleteFrontierWord(_projId, UserId, wordToDelete.Id); + await _wordService.MakeFrontierDeleted(_projId, UserId, wordToDelete.Id); _liftService.SetExportInProgress(UserId, ExportId); await _liftController.CreateLiftExportThenSignal(_projId, UserId, ExportId); diff --git a/Backend.Tests/Mocks/WordRepositoryMock.cs b/Backend.Tests/Mocks/WordRepositoryMock.cs index 53911ff989..8f73654f7b 100644 --- a/Backend.Tests/Mocks/WordRepositoryMock.cs +++ b/Backend.Tests/Mocks/WordRepositoryMock.cs @@ -127,19 +127,20 @@ public Task> AddFrontier(List words) return Task.FromResult(words); } - public Task DeleteFrontier(string projectId, string wordId) + public Task DeleteFrontier(string projectId, string wordId, string? fileName = null) { - var origLength = _frontier.Count; - _frontier.RemoveAll(word => word.ProjectId == projectId && word.Id == wordId); - return Task.FromResult(origLength != _frontier.Count); - } + var index = _frontier.FindIndex( + w => w.ProjectId == projectId && w.Id == wordId && + (fileName is null || w.Audio.Any(a => a.FileName == fileName))); - public Task DeleteFrontier(string projectId, List wordIds) - { - long deletedCount = 0; - wordIds.ForEach(id => deletedCount += _frontier.RemoveAll( - word => word.ProjectId == projectId && word.Id == id)); - return Task.FromResult(deletedCount); + if (index == -1) + { + return Task.FromResult(null); + } + + var word = _frontier[index]; + _frontier.RemoveAt(index); + return Task.FromResult(word); } public Task Add(Word word) diff --git a/Backend.Tests/Services/WordServiceTests.cs b/Backend.Tests/Services/WordServiceTests.cs index 9140fb9adb..9fdeaf4acd 100644 --- a/Backend.Tests/Services/WordServiceTests.cs +++ b/Backend.Tests/Services/WordServiceTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using Backend.Tests.Mocks; using BackendFramework.Interfaces; @@ -28,7 +27,7 @@ public void Setup() [Test] public void TestCreateAddsUserId() { - var word = _wordService.Create(UserId, new Word { EditedBy = new List { "other" } }).Result; + var word = _wordService.Create(UserId, new Word { EditedBy = ["other"] }).Result; Assert.That(word.EditedBy, Has.Count.EqualTo(2)); Assert.That(word.EditedBy.Last(), Is.EqualTo(UserId)); } @@ -36,15 +35,14 @@ public void TestCreateAddsUserId() [Test] public void TestCreateDoesNotAddDuplicateUserId() { - var word = _wordService.Create(UserId, new Word { EditedBy = new List { UserId } }).Result; + var word = _wordService.Create(UserId, new Word { EditedBy = [UserId] }).Result; Assert.That(word.EditedBy, Has.Count.EqualTo(1)); } [Test] public void TestCreateMultipleWords() { - _ = _wordService.Create( - UserId, new List { new() { ProjectId = ProjId }, new() { ProjectId = ProjId } }).Result; + _ = _wordService.Create(UserId, [new() { ProjectId = ProjId }, new() { ProjectId = ProjId }]).Result; Assert.That(_wordRepo.GetAllWords(ProjId).Result, Has.Count.EqualTo(2)); Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(2)); } @@ -54,10 +52,10 @@ public void TestDeleteAudioBadInputNull() { var fileName = "audio.mp3"; var wordInFrontier = _wordRepo.Create( - new Word() { Audio = new() { new() { FileName = fileName } }, ProjectId = ProjId }).Result; - Assert.That(_wordService.Delete("non-project-id", UserId, wordInFrontier.Id, fileName).Result, Is.Null); - Assert.That(_wordService.Delete(ProjId, UserId, "non-word-id", fileName).Result, Is.Null); - Assert.That(_wordService.Delete(ProjId, UserId, wordInFrontier.Id, "non-file-name").Result, Is.Null); + new Word() { Audio = [new() { FileName = fileName }], ProjectId = ProjId }).Result; + Assert.That(_wordService.DeleteAudio("non-proj-id", UserId, wordInFrontier.Id, fileName).Result, Is.Null); + Assert.That(_wordService.DeleteAudio(ProjId, UserId, "non-word-id", fileName).Result, Is.Null); + Assert.That(_wordService.DeleteAudio(ProjId, UserId, wordInFrontier.Id, "non-file-name").Result, Is.Null); } [Test] @@ -65,8 +63,8 @@ public void TestDeleteAudioNotInFrontierNull() { var fileName = "audio.mp3"; var wordNotInFrontier = _wordRepo.Add( - new() { Audio = new() { new() { FileName = fileName } }, ProjectId = ProjId }).Result; - Assert.That(_wordService.Delete(ProjId, UserId, wordNotInFrontier.Id, fileName).Result, Is.Null); + new() { Audio = [new() { FileName = fileName }], ProjectId = ProjId }).Result; + Assert.That(_wordService.DeleteAudio(ProjId, UserId, wordNotInFrontier.Id, fileName).Result, Is.Null); } [Test] @@ -74,8 +72,8 @@ public void TestDeleteAudio() { var fileName = "audio.mp3"; var wordInFrontier = _wordRepo.Create( - new Word() { Audio = new() { new() { FileName = fileName } }, ProjectId = ProjId }).Result; - var result = _wordService.Delete(ProjId, UserId, wordInFrontier.Id, fileName).Result; + new Word() { Audio = [new() { FileName = fileName }], ProjectId = ProjId }).Result; + var result = _wordService.DeleteAudio(ProjId, UserId, wordInFrontier.Id, fileName).Result; Assert.That(result!.EditedBy.Last(), Is.EqualTo(UserId)); Assert.That(result!.History.Last(), Is.EqualTo(wordInFrontier.Id)); Assert.That(_wordRepo.IsInFrontier(ProjId, result.Id).Result, Is.True); @@ -83,9 +81,9 @@ public void TestDeleteAudio() } [Test] - public void TestUpdateNotInFrontierFalse() + public void TestUpdateNotInFrontierNull() { - Assert.That(_wordService.Update(ProjId, UserId, WordId, new Word()).Result, Is.False); + Assert.That(_wordService.Update(ProjId, UserId, WordId, new Word()).Result, Is.Null); } [Test] @@ -95,7 +93,7 @@ public void TestUpdateReplacesFrontierWord() Assert.That(word, Is.Not.Null); var oldId = word.Id; word.Vernacular = "NewVern"; - Assert.That(_wordService.Update(ProjId, UserId, oldId, word).Result, Is.True); + Assert.That(_wordService.Update(ProjId, UserId, oldId, word).Result, Is.Not.Null); var frontier = _wordRepo.GetFrontier(ProjId).Result; Assert.That(frontier, Has.Count.EqualTo(1)); var newWord = frontier.First(); @@ -126,8 +124,7 @@ public void TestUpdateUsingCitationForm() public void TestRestoreFrontierWordsMissingWordFalse() { var word = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; - Assert.That(_wordService.RestoreFrontierWords( - ProjId, new List { "NotAnId", word.Id }).Result, Is.False); + Assert.That(_wordService.RestoreFrontierWords(ProjId, ["NotAnId", word.Id]).Result, Is.False); } [Test] @@ -136,8 +133,8 @@ public void TestRestoreFrontierWordsFrontierWordFalse() var wordNoFrontier = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; var wordYesFrontier = _wordRepo.Create(new Word { ProjectId = ProjId }).Result; Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(1)); - Assert.That(_wordService.RestoreFrontierWords( - ProjId, new List { wordNoFrontier.Id, wordYesFrontier.Id }).Result, Is.False); + Assert.That( + _wordService.RestoreFrontierWords(ProjId, [wordNoFrontier.Id, wordYesFrontier.Id]).Result, Is.False); } [Test] @@ -146,8 +143,7 @@ public void TestRestoreFrontierWordsTrue() var word1 = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; var word2 = _wordRepo.Add(new Word { ProjectId = ProjId }).Result; Assert.That(_wordRepo.GetFrontier(ProjId).Result, Is.Empty); - Assert.That(_wordService.RestoreFrontierWords( - ProjId, new List { word1.Id, word2.Id }).Result, Is.True); + Assert.That(_wordService.RestoreFrontierWords(ProjId, [word1.Id, word2.Id]).Result, Is.True); Assert.That(_wordRepo.GetFrontier(ProjId).Result, Has.Count.EqualTo(2)); } @@ -187,7 +183,7 @@ public void TestFindContainingWordSameVernSubsetSense() // Sense of new word is subset of one sense of old word. var oldSense = Util.RandomSense(); - newWord.Senses = new List { oldSense.Clone() }; + newWord.Senses = [oldSense.Clone()]; oldSense.Definitions.Add(Util.RandomDefinition()); oldSense.Glosses.Add(Util.RandomGloss()); oldWord.Senses.Add(oldSense); @@ -208,9 +204,9 @@ public void TestFindContainingWordSameVernEmptySensesDiffDoms() // New word sense with no definitions and blank gloss. var newSense = oldWord.Senses.First().Clone(); newSense.Definitions.Clear(); - newSense.Glosses = new List { new Gloss() }; + newSense.Glosses = [new Gloss()]; newSense.SemanticDomains.Add(Util.RandomSemanticDomain()); - newWord.Senses = new List { newSense }; + newWord.Senses = [newSense]; var dupId = _wordService.FindContainingWord(newWord).Result; Assert.That(dupId, Is.Null); @@ -228,7 +224,7 @@ public void TestFindContainingWordSameVernEmptySensesSameDoms() var emptySense = Util.RandomSense(); emptySense.Definitions.Clear(); emptySense.Glosses.Clear(); - newWord.Senses = new List { emptySense.Clone() }; + newWord.Senses = [emptySense.Clone()]; emptySense.SemanticDomains.Add(Util.RandomSemanticDomain()); oldWord.Senses.Add(emptySense); oldWord = _wordRepo.Create(oldWord).Result; diff --git a/Backend/Controllers/AudioController.cs b/Backend/Controllers/AudioController.cs index bb8fba2bbd..d25175efd4 100644 --- a/Backend/Controllers/AudioController.cs +++ b/Backend/Controllers/AudioController.cs @@ -177,7 +177,7 @@ public async Task DeleteAudioFile(string projectId, string wordId return new UnsupportedMediaTypeResult(); } - var newWord = await _wordService.Delete(projectId, userId, wordId, fileName); + var newWord = await _wordService.DeleteAudio(projectId, userId, wordId, fileName); if (newWord is not null) { return Ok(newWord.Id); diff --git a/Backend/Controllers/WordController.cs b/Backend/Controllers/WordController.cs index 98a16eab2f..9aa1620d8d 100644 --- a/Backend/Controllers/WordController.cs +++ b/Backend/Controllers/WordController.cs @@ -43,7 +43,7 @@ public async Task DeleteFrontierWord(string projectId, string wor } var userId = _permissionService.GetUserId(HttpContext); - var deletedWordId = await _wordService.DeleteFrontierWord(projectId, userId, wordId); + var deletedWordId = await _wordService.MakeFrontierDeleted(projectId, userId, wordId); return deletedWordId is null ? NotFound() : Ok(); } diff --git a/Backend/Interfaces/IWordRepository.cs b/Backend/Interfaces/IWordRepository.cs index 4dd65b628e..5919256bd6 100644 --- a/Backend/Interfaces/IWordRepository.cs +++ b/Backend/Interfaces/IWordRepository.cs @@ -21,7 +21,6 @@ public interface IWordRepository Task> GetFrontierWithVernacular(string projectId, string vernacular); Task AddFrontier(Word word); Task> AddFrontier(List words); - Task DeleteFrontier(string projectId, string wordId); - Task DeleteFrontier(string projectId, List wordIds); + Task DeleteFrontier(string projectId, string wordId, string? fileName = null); } } diff --git a/Backend/Interfaces/IWordService.cs b/Backend/Interfaces/IWordService.cs index e8b9ddd557..4f2bc2624f 100644 --- a/Backend/Interfaces/IWordService.cs +++ b/Backend/Interfaces/IWordService.cs @@ -8,10 +8,11 @@ public interface IWordService { Task Create(string userId, Word word); Task> Create(string userId, List words); - Task Update(string projectId, string userId, string wordId, Word word); - Task Delete(string projectId, string userId, string wordId); - Task Delete(string projectId, string userId, string wordId, string fileName); - Task DeleteFrontierWord(string projectId, string userId, string wordId); + Task Update(string projectId, string userId, string wordId, Word word); + Task DeleteAudio(string projectId, string userId, string wordId, string fileName); + Task DeleteFrontierWord(string projectId, string wordId); + Task DeleteFrontierWords(string projectId, List wordIds); + Task MakeFrontierDeleted(string projectId, string userId, string wordId); Task RestoreFrontierWords(string projectId, List wordIds); Task FindContainingWord(Word word); } diff --git a/Backend/Repositories/WordRepository.cs b/Backend/Repositories/WordRepository.cs index d6ad340997..13ad047a9f 100644 --- a/Backend/Repositories/WordRepository.cs +++ b/Backend/Repositories/WordRepository.cs @@ -39,6 +39,16 @@ private static FilterDefinition GetProjectWordFilter(string projectId, str return filterDef.And(filterDef.Eq(w => w.ProjectId, projectId), filterDef.Eq(w => w.Id, wordId)); } + /// Creates a mongo filter for project words with specified wordId and audio. + private static FilterDefinition GetProjectWordFilter(string projectId, string wordId, string fileName) + { + var filterDef = new FilterDefinitionBuilder(); + return filterDef.And( + filterDef.Eq(w => w.ProjectId, projectId), + filterDef.Eq(w => w.Id, wordId), + filterDef.ElemMatch(w => w.Audio, a => a.FileName == fileName)); + } + /// Creates a mongo filter for words in a specified project with specified wordIds. private static FilterDefinition GetProjectWordsFilter(string projectId, List wordIds) { @@ -251,22 +261,13 @@ public async Task> AddFrontier(List words) /// Removes from the Frontier with specified wordId and projectId /// A bool: success of operation - public async Task DeleteFrontier(string projectId, string wordId) + public async Task DeleteFrontier(string projectId, string wordId, string? fileName = null) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word from Frontier"); - var deleted = await _frontier.DeleteOneAsync(GetProjectWordFilter(projectId, wordId)); - return deleted.DeletedCount > 0; - } - - /// Removes s from the Frontier with specified wordIds and projectId - /// Number of words deleted - public async Task DeleteFrontier(string projectId, List wordIds) - { - using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting words from Frontier"); - - var deleted = await _frontier.DeleteManyAsync(GetProjectWordsFilter(projectId, wordIds)); - return deleted.DeletedCount; + return fileName is null + ? await _frontier.FindOneAndDeleteAsync(GetProjectWordFilter(projectId, wordId)) + : await _frontier.FindOneAndDeleteAsync(GetProjectWordFilter(projectId, wordId, fileName)); } } } diff --git a/Backend/Services/MergeService.cs b/Backend/Services/MergeService.cs index 38834bd397..90af7f37aa 100644 --- a/Backend/Services/MergeService.cs +++ b/Backend/Services/MergeService.cs @@ -120,7 +120,7 @@ private async Task MergePrepParent(string projectId, MergeWords mergeWords private async Task MergeDeleteChildren(string projectId, MergeWords mergeWords) { var childIds = mergeWords.Children.Select(c => c.SrcWordId).ToList(); - return await _wordRepo.DeleteFrontier(projectId, childIds); + return await _wordService.DeleteFrontierWords(projectId, childIds); } /// @@ -160,7 +160,7 @@ public async Task UndoMerge(string projectId, string userId, MergeUndoIds } foreach (var parentId in ids.ParentIds) { - await _wordService.DeleteFrontierWord(projectId, userId, parentId); + await _wordService.MakeFrontierDeleted(projectId, userId, parentId); } return true; } diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index 8ff2b302ae..f9839ae53c 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -8,19 +8,13 @@ namespace BackendFramework.Services { /// More complex functions and application logic for s - public class WordService : IWordService + public class WordService(IWordRepository wordRepo, ISemanticDomainCountService domainCountService) : IWordService { - private readonly IWordRepository _wordRepo; - private readonly ISemanticDomainCountService _domainCountService; + private readonly IWordRepository _wordRepo = wordRepo; + private readonly ISemanticDomainCountService _domainCountService = domainCountService; private const string otelTagName = "otel.WordService"; - public WordService(IWordRepository wordRepo, ISemanticDomainCountService domainCountService) - { - _wordRepo = wordRepo; - _domainCountService = domainCountService; - } - /// /// Clear the given word's Id and Metadata to be generated by the word repo, /// and add the given userId to EditedBy if it's not already last on the list. @@ -65,94 +59,72 @@ private async Task Add(string userId, Word word) return await _wordRepo.Add(PrepEditedData(userId, word)); } - /// Makes a new word in Frontier that has deleted tag on each sense - /// A bool: success of operation - public async Task Delete(string projectId, string userId, string wordId) + /// Removes audio with specified fileName from a word + /// New word + public async Task DeleteAudio(string projectId, string userId, string wordId, string fileName) { - using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word"); - - var wordToDelete = await _wordRepo.GetWord(projectId, wordId); - if (wordToDelete is null) - { - return false; - } - - var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); - - // We only want to add the deleted word if the word started in the frontier. - if (!wordIsInFrontier) - { - return wordIsInFrontier; - } - - // Decrement counts for the deleted word's semantic domains - await _domainCountService.UpdateCountsForWordDeletion(wordToDelete); - - wordToDelete.EditedBy = new List(); - wordToDelete.History = new List { wordId }; - wordToDelete.Accessibility = Status.Deleted; + using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting an audio"); - foreach (var senseAcc in wordToDelete.Senses) + // We only want to update words that are in the frontier + var wordWithAudioToDelete = await _wordRepo.DeleteFrontier(projectId, wordId, fileName); + if (wordWithAudioToDelete is null) { - senseAcc.Accessibility = Status.Deleted; + return null; } - await Create(userId, wordToDelete); + wordWithAudioToDelete.Audio.RemoveAll(a => a.FileName == fileName); + wordWithAudioToDelete.History.Add(wordId); - return wordIsInFrontier; + return await Create(userId, wordWithAudioToDelete); } - /// Removes audio with specified fileName from a word - /// New word - public async Task Delete(string projectId, string userId, string wordId, string fileName) + public async Task DeleteFrontierWord(string projectId, string wordId) { - using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting an audio"); + using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word from Frontier"); - var wordWithAudioToDelete = await _wordRepo.GetWord(projectId, wordId); - if (wordWithAudioToDelete is null) + var word = await _wordRepo.DeleteFrontier(projectId, wordId); + if (word is null) { return null; } - var audioToRemove = wordWithAudioToDelete.Audio.Find(a => a.FileName == fileName); - if (audioToRemove is null) - { - return null; - } + // Decrement counts for the deleted word's semantic domains + await _domainCountService.UpdateCountsForWordDeletion(word); + return word; + } - // We only want to update words that are in the frontier - if (!await _wordRepo.DeleteFrontier(projectId, wordId)) + public async Task DeleteFrontierWords(string projectId, List wordIds) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting words from Frontier"); + + var deletedCount = 0; + foreach (var wordId in wordIds) { - return null; + if (await _wordRepo.DeleteFrontier(projectId, wordId) is not null) + { + deletedCount++; + } } - - wordWithAudioToDelete.Audio.Remove(audioToRemove); - wordWithAudioToDelete.History.Add(wordId); - - return await Create(userId, wordWithAudioToDelete); + return deletedCount; } - /// Deletes word in frontier collection and adds word with deleted tag in word collection + /// + /// Deletes word in frontier collection. Conditionally adds word with deleted tag in word collection + /// + /// The project id + /// The user id + /// The word id /// A string: id of new word - public async Task DeleteFrontierWord(string projectId, string userId, string wordId) + public async Task MakeFrontierDeleted(string projectId, string userId, string wordId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word from Frontier"); - var word = await _wordRepo.GetWord(projectId, wordId); + var word = await DeleteFrontierWord(projectId, wordId); if (word is null) { return null; } - var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); - if (!wordIsInFrontier) - { - return null; - } - - // Decrement counts for the deleted word's semantic domains - await _domainCountService.UpdateCountsForWordDeletion(word); - word.ProjectId = projectId; word.Accessibility = Status.Deleted; word.History.Add(wordId); @@ -182,29 +154,22 @@ public async Task RestoreFrontierWords(string projectId, List word } /// Makes a new word in the Frontier with changes made - /// A bool: success of operation - public async Task Update(string projectId, string userId, string wordId, Word word) + /// Id of the updated word + public async Task Update(string projectId, string userId, string wordId, Word word) { using var activity = OtelService.StartActivityWithTag(otelTagName, "updating a word in Frontier"); - var oldWord = await _wordRepo.GetWord(projectId, wordId); - if (oldWord is null) - { - return false; - } - - var wordIsInFrontier = await _wordRepo.DeleteFrontier(projectId, wordId); + var oldWord = await _wordRepo.DeleteFrontier(projectId, wordId); // We only want to update words that are in the frontier - if (!wordIsInFrontier) + if (oldWord is null) { - return wordIsInFrontier; + return null; } // If an imported word was using the citation form for its Vernacular, // only keep UsingCitationForm true if the Vernacular hasn't changed. - word.UsingCitationForm &= - word.Vernacular == oldWord.Vernacular; + word.UsingCitationForm &= word.Vernacular == oldWord.Vernacular; word.ProjectId = projectId; word.History.Add(wordId); @@ -212,7 +177,7 @@ public async Task Update(string projectId, string userId, string wordId, W var createdWord = await _wordRepo.Create(PrepEditedData(userId, word)); await _domainCountService.UpdateCountsAfterWordUpdate(oldWord, createdWord); - return wordIsInFrontier; + return createdWord.Id; } /// Checks if a word being added is a duplicate of a preexisting word. From faf07eb115192afd2de66cce648cf2a716b66f78 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 16 Jan 2026 16:43:56 -0500 Subject: [PATCH 12/22] Move sem-dom count updates into WordService --- .../Controllers/AudioControllerTests.cs | 5 +++-- .../Controllers/LiftControllerTests.cs | 4 ++-- .../Controllers/MergeControllerTests.cs | 5 +++-- .../Controllers/WordControllerTests.cs | 5 +++-- Backend.Tests/Services/MergeServiceTests.cs | 5 +++-- Backend.Tests/Services/WordServiceTests.cs | 5 +++-- Backend/Controllers/LiftController.cs | 11 +++++------ Backend/Interfaces/ILiftService.cs | 3 +-- Backend/Interfaces/IWordService.cs | 1 + Backend/Services/LiftService.cs | 14 +++++--------- Backend/Services/WordService.cs | 18 ++++++++++++++++-- 11 files changed, 45 insertions(+), 31 deletions(-) diff --git a/Backend.Tests/Controllers/AudioControllerTests.cs b/Backend.Tests/Controllers/AudioControllerTests.cs index 0ff1ba8696..eb3d07ca3d 100644 --- a/Backend.Tests/Controllers/AudioControllerTests.cs +++ b/Backend.Tests/Controllers/AudioControllerTests.cs @@ -35,10 +35,11 @@ public void Dispose() public void Setup() { _projRepo = new ProjectRepositoryMock(); + var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); _permissionService = new PermissionServiceMock(); - var semDomCountService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); - _wordService = new WordService(_wordRepo, semDomCountService); + var semDomCountService = new SemanticDomainCountService(semDomCountRepo); + _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); _audioController = new AudioController(_wordRepo, _wordService, _permissionService); _projId = _projRepo.Create(new Project { Name = "AudioControllerTests" }).Result!.Id; diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index ff31dec6bc..956f64ca2e 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -51,10 +51,10 @@ public void Setup() _wordRepo = new WordRepositoryMock(); _liftService = new LiftService(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(_wordRepo, semDomCountService); + _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); _liftController = new LiftController(_projRepo, new SemanticDomainRepositoryMock(), semDomCountRepo, _speakerRepo, _wordRepo, _liftService, new HubContextMock(), new PermissionServiceMock(), - semDomCountService, new MockLogger()); + _wordService, new MockLogger()); _projId = _projRepo.Create(new Project { Name = ProjName }).Result!.Id; _file = new FormFile(_stream, 0, _stream.Length, "Name", FileName); diff --git a/Backend.Tests/Controllers/MergeControllerTests.cs b/Backend.Tests/Controllers/MergeControllerTests.cs index 5b9154aa5b..186176ddd4 100644 --- a/Backend.Tests/Controllers/MergeControllerTests.cs +++ b/Backend.Tests/Controllers/MergeControllerTests.cs @@ -38,9 +38,10 @@ public void Setup() new ServiceCollection().AddMemoryCache().BuildServiceProvider().GetRequiredService(); _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); + var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var semDomCountService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); - _wordService = new WordService(_wordRepo, semDomCountService); + var semDomCountService = new SemanticDomainCountService(semDomCountRepo); + _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); _mergeService = new MergeService(_cache, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); var notifyService = new HubContextMock(); var permissionService = new PermissionServiceMock(); diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 6699ffdaca..31a99cd21b 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -31,9 +31,10 @@ public void Dispose() [SetUp] public void Setup() { + var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var semDomCountService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); - _wordService = new WordService(_wordRepo, semDomCountService); + var semDomCountService = new SemanticDomainCountService(semDomCountRepo); + _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); _permissionService = new PermissionServiceMock(); _wordController = new WordController(_wordRepo, _wordService, _permissionService); } diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index 7cc2e86775..9f0ad48db1 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -30,9 +30,10 @@ public void Setup() new ServiceCollection().AddMemoryCache().BuildServiceProvider().GetRequiredService(); _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); + var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var semDomCountService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); - _wordService = new WordService(_wordRepo, semDomCountService); + var semDomCountService = new SemanticDomainCountService(semDomCountRepo); + _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); _mergeService = new MergeService(_cache, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); } diff --git a/Backend.Tests/Services/WordServiceTests.cs b/Backend.Tests/Services/WordServiceTests.cs index de3841d0e3..e465e48ce9 100644 --- a/Backend.Tests/Services/WordServiceTests.cs +++ b/Backend.Tests/Services/WordServiceTests.cs @@ -19,9 +19,10 @@ internal sealed class WordServiceTests [SetUp] public void Setup() { + var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); - var semDomCountService = new SemanticDomainCountService(new SemanticDomainCountRepositoryMock()); - _wordService = new WordService(_wordRepo, semDomCountService); + var semDomCountService = new SemanticDomainCountService(semDomCountRepo); + _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); } [Test] diff --git a/Backend/Controllers/LiftController.cs b/Backend/Controllers/LiftController.cs index 33fcf60c8f..63bf11d41c 100644 --- a/Backend/Controllers/LiftController.cs +++ b/Backend/Controllers/LiftController.cs @@ -25,7 +25,7 @@ namespace BackendFramework.Controllers public class LiftController(IProjectRepository projRepo, ISemanticDomainRepository semDomRepo, ISemanticDomainCountRepository semDomCountRepository, ISpeakerRepository speakerRepo, IWordRepository wordRepo, ILiftService liftService, IHubContext notifyService, IPermissionService permissionService, - ISemanticDomainCountService semDomCountService, ILogger logger) : Controller + IWordService wordService, ILogger logger) : Controller { private readonly IProjectRepository _projRepo = projRepo; private readonly ISemanticDomainRepository _semDomRepo = semDomRepo; @@ -35,7 +35,7 @@ public class LiftController(IProjectRepository projRepo, ISemanticDomainReposito private readonly ILiftService _liftService = liftService; private readonly IHubContext _notifyService = notifyService; private readonly IPermissionService _permissionService = permissionService; - private readonly ISemanticDomainCountService _semDomCountService = semDomCountService; + private readonly IWordService _wordService = wordService; private readonly ILogger _logger = logger; private const string otelTagName = "otel.LiftController"; @@ -111,8 +111,7 @@ public async Task DeleteFrontierAndFinishUploadLiftFile(string pr } // Delete all frontier words and load the LIFT data - await _wordRepo.DeleteAllFrontierWords(projectId); - await _semDomCountRepository.DeleteAllCounts(projectId); + await _wordService.ClearFrontier(projectId); return await FinishUploadLiftFile(projectId, userId, true); } @@ -264,8 +263,8 @@ private async Task AddImportToProject(string liftStoragePath, str int countWordsImported; // Sets the projectId of our parser to add words to that project - var liftMerger = _liftService.GetLiftImporterExporter( - projectId, proj.VernacularWritingSystem.Bcp47, _wordRepo, _semDomCountService); + var liftMerger = + _liftService.GetLiftImporterExporter(projectId, proj.VernacularWritingSystem.Bcp47, _wordService); var importedAnalysisWritingSystems = new List(); var doesImportHaveDefinitions = false; var doesImportHaveGrammaticalInfo = false; diff --git a/Backend/Interfaces/ILiftService.cs b/Backend/Interfaces/ILiftService.cs index dc0466cf27..413f99d2e1 100644 --- a/Backend/Interfaces/ILiftService.cs +++ b/Backend/Interfaces/ILiftService.cs @@ -8,8 +8,7 @@ namespace BackendFramework.Interfaces { public interface ILiftService : IDisposable { - ILiftMerger GetLiftImporterExporter(string projectId, string vernLang, IWordRepository wordRepo, - ISemanticDomainCountService semDomCountService); + ILiftMerger GetLiftImporterExporter(string projectId, string vernLang, IWordService wordService); Task LdmlImport(string dirPath, IProjectRepository projRepo, Project project); Task LiftExport(string projectId, IProjectRepository projRepo, ISemanticDomainRepository semDomRepo, ISpeakerRepository speakerRepo, IWordRepository wordRepo); diff --git a/Backend/Interfaces/IWordService.cs b/Backend/Interfaces/IWordService.cs index 4f2bc2624f..942a2f9700 100644 --- a/Backend/Interfaces/IWordService.cs +++ b/Backend/Interfaces/IWordService.cs @@ -14,6 +14,7 @@ public interface IWordService Task DeleteFrontierWords(string projectId, List wordIds); Task MakeFrontierDeleted(string projectId, string userId, string wordId); Task RestoreFrontierWords(string projectId, List wordIds); + Task ClearFrontier(string projectId); Task FindContainingWord(Word word); } } diff --git a/Backend/Services/LiftService.cs b/Backend/Services/LiftService.cs index 2ef1240990..f625277862 100644 --- a/Backend/Services/LiftService.cs +++ b/Backend/Services/LiftService.cs @@ -670,10 +670,9 @@ public static string MakeSafeXmlAttribute(string sInput) return SecurityElement.Escape(sInput); } - public ILiftMerger GetLiftImporterExporter(string projectId, string vernLang, IWordRepository wordRepo, - ISemanticDomainCountService semDomCountService) + public ILiftMerger GetLiftImporterExporter(string projectId, string vernLang, IWordService wordService) { - return new LiftMerger(projectId, vernLang, wordRepo, semDomCountService); + return new LiftMerger(projectId, vernLang, wordService); } private static void WriteRangeElement(XmlWriter liftRangesWriter, @@ -707,13 +706,11 @@ private static void WriteFormElement(XmlWriter liftRangesWriter, string element, liftRangesWriter.WriteEndElement(); // end element } - private sealed class LiftMerger(string projectId, string vernLang, IWordRepository wordRepo, - ISemanticDomainCountService semDomCountService) : ILiftMerger + private sealed class LiftMerger(string projectId, string vernLang, IWordService wordService) : ILiftMerger { private readonly string _projectId = projectId; private readonly string _vernLang = vernLang; - private readonly IWordRepository _wordRepo = wordRepo; - private readonly ISemanticDomainCountService _semDomCountService = semDomCountService; + private readonly IWordService _wordService = wordService; private readonly List _customSemDoms = []; private readonly List _importEntries = []; @@ -764,8 +761,7 @@ public List GetImportAnalysisWritingSystems() /// The words saved. public async Task> SaveImportEntries() { - var savedWords = new List(await _wordRepo.Create(_importEntries)); - await _semDomCountService.UpdateCountsForWords(savedWords); + var savedWords = new List(await _wordService.Create("", _importEntries)); _importEntries.Clear(); return savedWords; } diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index 7a987ebcd4..4f7d895a9d 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -8,8 +8,10 @@ namespace BackendFramework.Services { /// More complex functions and application logic for s - public class WordService(IWordRepository wordRepo, ISemanticDomainCountService semDomCountService) : IWordService + public class WordService(ISemanticDomainCountRepository semDomCountRepo, IWordRepository wordRepo, + ISemanticDomainCountService semDomCountService) : IWordService { + private readonly ISemanticDomainCountRepository _semDomCountRepo = semDomCountRepo; private readonly IWordRepository _wordRepo = wordRepo; private readonly ISemanticDomainCountService _semDomCountService = semDomCountService; @@ -17,7 +19,7 @@ public class WordService(IWordRepository wordRepo, ISemanticDomainCountService s /// /// Clear the given word's Id and Metadata to be generated by the word repo, - /// and add the given userId to EditedBy if it's not already last on the list. + /// and add the given userId to EditedBy if it's nonempty and not already last on the list. /// private static Word PrepEditedData(string userId, Word word) { @@ -179,6 +181,18 @@ public async Task RestoreFrontierWords(string projectId, List word return createdWord.Id; } + public async Task ClearFrontier(string projectId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "clearing the Frontier"); + + var success = await _wordRepo.DeleteAllFrontierWords(projectId); + if (success) + { + await _semDomCountRepo.DeleteAllCounts(projectId); + } + return success; + } + /// Checks if a word being added is a duplicate of a preexisting word. /// The id string of the existing word, or null if none. public async Task FindContainingWord(Word word) From b640d60241db15681f17f76c9b6cca3bdde9170a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 16 Jan 2026 17:08:52 -0500 Subject: [PATCH 13/22] Tidy --- .../Controllers/LiftControllerTests.cs | 6 ++--- .../SemanticDomainCountRepositoryMock.cs | 5 +--- .../Services/StatisticsServiceTests.cs | 26 +++++++++---------- Backend/Controllers/LiftController.cs | 7 +++-- Backend/Services/StatisticsService.cs | 16 ++++++------ Backend/Services/WordService.cs | 13 +++++----- 6 files changed, 34 insertions(+), 39 deletions(-) diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index 956f64ca2e..bbdc18ce20 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -52,9 +52,9 @@ public void Setup() _liftService = new LiftService(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); - _liftController = new LiftController(_projRepo, new SemanticDomainRepositoryMock(), semDomCountRepo, - _speakerRepo, _wordRepo, _liftService, new HubContextMock(), new PermissionServiceMock(), - _wordService, new MockLogger()); + _liftController = new LiftController(_projRepo, new SemanticDomainRepositoryMock(), _speakerRepo, + _wordRepo, _liftService, new HubContextMock(), new PermissionServiceMock(), _wordService, + new MockLogger()); _projId = _projRepo.Create(new Project { Name = ProjName }).Result!.Id; _file = new FormFile(_stream, 0, _stream.Length, "Name", FileName); diff --git a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs index b4c405e039..266cb91edf 100644 --- a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs +++ b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs @@ -27,10 +27,7 @@ public Task Increment(string projectId, string domainId, int amount = 1) var count = _counts.FirstOrDefault(c => c.ProjectId == projectId && c.DomainId == domainId); if (count is null) { - count = new ProjectSemanticDomainCount(projectId, domainId, amount) - { - Id = Util.RandString() - }; + count = new(projectId, domainId, amount) { Id = Util.RandString() }; _counts.Add(count); } else diff --git a/Backend.Tests/Services/StatisticsServiceTests.cs b/Backend.Tests/Services/StatisticsServiceTests.cs index 13dbce3e49..c8477ea37c 100644 --- a/Backend.Tests/Services/StatisticsServiceTests.cs +++ b/Backend.Tests/Services/StatisticsServiceTests.cs @@ -11,8 +11,8 @@ namespace Backend.Tests.Services { internal sealed class StatisticsServiceTests { - private ISemanticDomainRepository _domainRepo = null!; - private ISemanticDomainCountRepository _domainCountRepo = null!; + private ISemanticDomainRepository _semDomRepo = null!; + private ISemanticDomainCountRepository _semDomCountRepo = null!; private IUserRepository _userRepo = null!; private IWordRepository _wordRepo = null!; private IStatisticsService _statsService = null!; @@ -46,18 +46,18 @@ private static Word GetWordWithDomain(string semDomId = SemDomId) [SetUp] public void Setup() { - _domainRepo = new SemanticDomainRepositoryMock(); - _domainCountRepo = new SemanticDomainCountRepositoryMock(); + _semDomRepo = new SemanticDomainRepositoryMock(); + _semDomCountRepo = new SemanticDomainCountRepositoryMock(); _userRepo = new UserRepositoryMock(); _wordRepo = new WordRepositoryMock(); - _statsService = new StatisticsService(_domainRepo, _domainCountRepo, _userRepo, _wordRepo); + _statsService = new StatisticsService(_semDomRepo, _semDomCountRepo, _userRepo, _wordRepo); } [Test] public void GetSemanticDomainCountsTestNullDomainList() { // Add a domain count to the database and leave the semantic domain list null - _domainCountRepo.Increment(ProjId, SemDomId).Wait(); + _semDomCountRepo.Increment(ProjId, SemDomId).Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); @@ -67,8 +67,8 @@ public void GetSemanticDomainCountsTestNullDomainList() public void GetSemanticDomainCountsTestEmptyDomainList() { // Add to the database an empty list of semantic domains and a domain count - ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(new List()); - _domainCountRepo.Increment(ProjId, SemDomId).Wait(); + ((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(new List()); + _semDomCountRepo.Increment(ProjId, SemDomId).Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); @@ -78,7 +78,7 @@ public void GetSemanticDomainCountsTestEmptyDomainList() public void GetSemanticDomainCountsTestEmptyCounts() { // Add to the database a semantic domain but no word - ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); + ((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Is.Empty); @@ -88,8 +88,8 @@ public void GetSemanticDomainCountsTestEmptyCounts() public void GetSemanticDomainCountsTestIdMismatch() { // Add to the database a semantic domain and count with a different domain id - ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); - _domainCountRepo.Increment(ProjId, "DifferentId").Wait(); + ((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes); + _semDomCountRepo.Increment(ProjId, "DifferentId").Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Has.Count.EqualTo(1)); @@ -100,8 +100,8 @@ public void GetSemanticDomainCountsTestIdMismatch() public void GetSemanticDomainCountsTestIdMatch() { // Add to the database a semantic domain and a corresponding count - ((SemanticDomainRepositoryMock)_domainRepo).SetNextResponse(TreeNodes); - _domainCountRepo.Increment(ProjId, SemDomId).Wait(); + ((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes); + _semDomCountRepo.Increment(ProjId, SemDomId).Wait(); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; Assert.That(result, Has.Count.EqualTo(1)); diff --git a/Backend/Controllers/LiftController.cs b/Backend/Controllers/LiftController.cs index 63bf11d41c..93e1877521 100644 --- a/Backend/Controllers/LiftController.cs +++ b/Backend/Controllers/LiftController.cs @@ -23,13 +23,12 @@ namespace BackendFramework.Controllers [Produces("application/json")] [Route("v1/projects/{projectId}/lift")] public class LiftController(IProjectRepository projRepo, ISemanticDomainRepository semDomRepo, - ISemanticDomainCountRepository semDomCountRepository, ISpeakerRepository speakerRepo, IWordRepository wordRepo, - ILiftService liftService, IHubContext notifyService, IPermissionService permissionService, - IWordService wordService, ILogger logger) : Controller + ISpeakerRepository speakerRepo, IWordRepository wordRepo, ILiftService liftService, + IHubContext notifyService, IPermissionService permissionService, IWordService wordService, + ILogger logger) : Controller { private readonly IProjectRepository _projRepo = projRepo; private readonly ISemanticDomainRepository _semDomRepo = semDomRepo; - private readonly ISemanticDomainCountRepository _semDomCountRepository = semDomCountRepository; private readonly ISpeakerRepository _speakerRepo = speakerRepo; private readonly IWordRepository _wordRepo = wordRepo; private readonly ILiftService _liftService = liftService; diff --git a/Backend/Services/StatisticsService.cs b/Backend/Services/StatisticsService.cs index 37f014186c..8f351c4f0e 100644 --- a/Backend/Services/StatisticsService.cs +++ b/Backend/Services/StatisticsService.cs @@ -9,12 +9,12 @@ namespace BackendFramework.Services { - public class StatisticsService(ISemanticDomainRepository domainRepo, - ISemanticDomainCountRepository domainCountRepo, IUserRepository userRepo, IWordRepository wordRepo) + public class StatisticsService(ISemanticDomainRepository semDomRepo, + ISemanticDomainCountRepository semDomCountRepo, IUserRepository userRepo, IWordRepository wordRepo) : IStatisticsService { - private readonly ISemanticDomainRepository _domainRepo = domainRepo; - private readonly ISemanticDomainCountRepository _domainCountRepo = domainCountRepo; + private readonly ISemanticDomainRepository _semDomRepo = semDomRepo; + private readonly ISemanticDomainCountRepository _semDomCountRepo = semDomCountRepo; private readonly IUserRepository _userRepo = userRepo; private readonly IWordRepository _wordRepo = wordRepo; @@ -30,11 +30,11 @@ public class StatisticsService(ISemanticDomainRepository domainRepo, /// /// Get a count of the number of senses associated with a semantic domain /// - public Task GetDomainCount(string projectId, string domainId) + public async Task GetDomainCount(string projectId, string domainId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain count"); - return _domainCountRepo.GetCount(projectId, domainId); + return await _semDomCountRepo.GetCount(projectId, domainId); } /// @@ -44,13 +44,13 @@ public async Task> GetSemanticDomainCounts(string proj { using var activity = OtelService.StartActivityWithTag(otelTagName, "getting semantic domain counts"); - var domainTreeNodeList = await _domainRepo.GetAllSemanticDomainTreeNodes(lang); + var domainTreeNodeList = await _semDomRepo.GetAllSemanticDomainTreeNodes(lang); if (domainTreeNodeList is null || domainTreeNodeList.Count == 0) { return []; } - var domainCounts = await _domainCountRepo.GetAllCounts(projectId); + var domainCounts = await _semDomCountRepo.GetAllCounts(projectId); if (domainCounts.Count == 0) { return []; diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index 4f7d895a9d..c10fde9619 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -80,6 +80,8 @@ private async Task Add(string userId, Word word) return await Create(userId, wordWithAudioToDelete); } + /// Deletes a word from the frontier + /// The deleted word, or null if not found public async Task DeleteFrontierWord(string projectId, string wordId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word from Frontier"); @@ -95,6 +97,8 @@ private async Task Add(string userId, Word word) return word; } + /// Deletes words from the frontier + /// The number of words deleted public async Task DeleteFrontierWords(string projectId, List wordIds) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting words from Frontier"); @@ -110,13 +114,8 @@ public async Task DeleteFrontierWords(string projectId, List wordId return deletedCount; } - /// - /// Deletes word in frontier collection. Conditionally adds word with deleted tag in word collection - /// - /// The project id - /// The user id - /// The word id - /// A string: id of new word + /// Deletes frontier word and updates it as deleted in word collection + /// The id of new word, or null if not found public async Task MakeFrontierDeleted(string projectId, string userId, string wordId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting a word from Frontier"); From 2bf374bd3de2e98a843187b32c7b090a70c65f60 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 20 Jan 2026 12:08:58 -0500 Subject: [PATCH 14/22] Fix bugs found by the bunny --- Backend/Services/StatisticsService.cs | 13 ++++--------- Backend/Services/WordService.cs | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Backend/Services/StatisticsService.cs b/Backend/Services/StatisticsService.cs index 8f351c4f0e..24dc35f6f9 100644 --- a/Backend/Services/StatisticsService.cs +++ b/Backend/Services/StatisticsService.cs @@ -50,16 +50,11 @@ public async Task> GetSemanticDomainCounts(string proj return []; } - var domainCounts = await _semDomCountRepo.GetAllCounts(projectId); - if (domainCounts.Count == 0) - { - return []; - } - var domainCountDict = domainCounts.ToDictionary(dc => dc.DomainId, dc => dc.Count); + var domainCounts = + (await _semDomCountRepo.GetAllCounts(projectId)).ToDictionary(dc => dc.DomainId, dc => dc.Count); - return domainTreeNodeList.Select(domainTreeNode => - new SemanticDomainCount(domainTreeNode, domainCountDict.GetValueOrDefault(domainTreeNode.Id, 0)) - ).ToList(); + return domainTreeNodeList + .Select(node => new SemanticDomainCount(node, domainCounts.GetValueOrDefault(node.Id, 0))).ToList(); } /// diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index c10fde9619..b7daf57aea 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -106,7 +106,7 @@ public async Task DeleteFrontierWords(string projectId, List wordId var deletedCount = 0; foreach (var wordId in wordIds) { - if (await _wordRepo.DeleteFrontier(projectId, wordId) is not null) + if (await DeleteFrontierWord(projectId, wordId) is not null) { deletedCount++; } From d1c44e1c86de87c90cd651ed11a0f7de5bc3ed6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:14:56 +0000 Subject: [PATCH 15/22] Add unique compound index on ProjectId and DomainId - Create unique compound index on (ProjectId, DomainId) in SemanticDomainCountRepository constructor - Index optimizes queries and enforces uniqueness constraint - Uses Builders.IndexKeys.Ascending for both fields with Unique = true option - Index creation happens at repository initialization Co-authored-by: imnasnainaec <6411521+imnasnainaec@users.noreply.github.com> --- .../SemanticDomainCountRepository.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index db196201c6..1d31f8b8af 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -10,13 +10,27 @@ namespace BackendFramework.Repositories { /// Atomic database functions for s. [ExcludeFromCodeCoverage] - public class SemanticDomainCountRepository(IMongoDbContext dbContext) : ISemanticDomainCountRepository + public class SemanticDomainCountRepository : ISemanticDomainCountRepository { - private readonly IMongoCollection _counts = - dbContext.Db.GetCollection("SemanticDomainCountCollection"); + private readonly IMongoCollection _counts; private const string otelTagName = "otel.SemanticDomainCountRepository"; + public SemanticDomainCountRepository(IMongoDbContext dbContext) + { + _counts = dbContext.Db.GetCollection("SemanticDomainCountCollection"); + + // Create unique compound index on (ProjectId, DomainId) to optimize queries and enforce uniqueness + var indexKeys = Builders.IndexKeys + .Ascending(p => p.ProjectId) + .Ascending(p => p.DomainId); + var indexOptions = new CreateIndexOptions { Unique = true }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + + // Create the index if it doesn't already exist + _counts.Indexes.CreateOne(indexModel); + } + private static FilterDefinition? ProjectFilter(string projectId) { var filterDef = new FilterDefinitionBuilder(); From 55ff0d446c3b2da98c6a24b0e2b8136b6265db5b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 20 Jan 2026 12:28:59 -0500 Subject: [PATCH 16/22] Respond to the bunny's nitpicks --- .../SemanticDomainCountRepositoryMock.cs | 2 +- .../SemanticDomainCountServiceTests.cs | 4 ++-- Backend/Models/ProjectSemanticDomainCount.cs | 11 +++++++++++ .../SemanticDomainCountRepository.cs | 4 ++-- .../Services/SemanticDomainCountService.cs | 19 ++++++++++++------- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs index 266cb91edf..6367457d26 100644 --- a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs +++ b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs @@ -18,7 +18,7 @@ public Task GetCount(string projectId, string domainId) public Task> GetAllCounts(string projectId) { - var counts = _counts.Where(c => c.ProjectId == projectId).ToList(); + var counts = _counts.Where(c => c.ProjectId == projectId).Select(c => c.Clone()).ToList(); return Task.FromResult(counts); } diff --git a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs index 58b5bc10ed..9f53fb38c0 100644 --- a/Backend.Tests/Services/SemanticDomainCountServiceTests.cs +++ b/Backend.Tests/Services/SemanticDomainCountServiceTests.cs @@ -50,12 +50,12 @@ public async Task TestUpdateCountsForWords() new() { ProjectId = ProjId, - Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }] }] + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }] }] }, new() { ProjectId = ProjId, - Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }], + Senses = [new() { SemanticDomains = [new() { Id = DomainId1 }, new() { Id = DomainId2 }] }], } }; diff --git a/Backend/Models/ProjectSemanticDomainCount.cs b/Backend/Models/ProjectSemanticDomainCount.cs index afd2db3b97..6eced2554e 100644 --- a/Backend/Models/ProjectSemanticDomainCount.cs +++ b/Backend/Models/ProjectSemanticDomainCount.cs @@ -40,5 +40,16 @@ public ProjectSemanticDomainCount(string projectId, string domainId, int count = DomainId = domainId; Count = count; } + + public ProjectSemanticDomainCount Clone() + { + return new() + { + Id = Id, + ProjectId = ProjectId, + DomainId = DomainId, + Count = Count + }; + } } } diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index db196201c6..3b1c268011 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -17,13 +17,13 @@ public class SemanticDomainCountRepository(IMongoDbContext dbContext) : ISemanti private const string otelTagName = "otel.SemanticDomainCountRepository"; - private static FilterDefinition? ProjectFilter(string projectId) + private static FilterDefinition ProjectFilter(string projectId) { var filterDef = new FilterDefinitionBuilder(); return filterDef.Eq(c => c.ProjectId, projectId); } - private static FilterDefinition? ProjectDomainFilter(string projectId, string domainId) + private static FilterDefinition ProjectDomainFilter(string projectId, string domainId) { var filterDef = new FilterDefinitionBuilder(); return filterDef.And(filterDef.Eq(c => c.ProjectId, projectId), filterDef.Eq(c => c.DomainId, domainId)); diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs index 3f5d396e9d..d27736705e 100644 --- a/Backend/Services/SemanticDomainCountService.cs +++ b/Backend/Services/SemanticDomainCountService.cs @@ -41,7 +41,6 @@ public async Task UpdateCountsForWord(Word word) } /// Updates counts when multiple new words are added - /// Assumes all words belong to the same project public async Task UpdateCountsForWords(List words) { using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts for words"); @@ -51,17 +50,23 @@ public async Task UpdateCountsForWords(List words) return; } - var projectId = words.First().ProjectId; - - var domainCounts = new Dictionary(); + var domainCounts = new Dictionary>(); foreach (var word in words) { - GetDomainCounts(word, domainCounts); + if (!domainCounts.TryGetValue(word.ProjectId, out var projectDomainCounts)) + { + projectDomainCounts = []; + domainCounts[word.ProjectId] = projectDomainCounts; + } + GetDomainCounts(word, projectDomainCounts); } - foreach (var entry in domainCounts) + foreach (var projectEntry in domainCounts) { - await _countRepo.Increment(projectId, entry.Key, entry.Value); + foreach (var domainEntry in projectEntry.Value) + { + await _countRepo.Increment(projectEntry.Key, domainEntry.Key, domainEntry.Value); + } } } From 9a90281aa4c8f13f011b5db1963bf546eadc9ab6 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 20 Jan 2026 12:44:27 -0500 Subject: [PATCH 17/22] Update test --- Backend.Tests/Services/StatisticsServiceTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Backend.Tests/Services/StatisticsServiceTests.cs b/Backend.Tests/Services/StatisticsServiceTests.cs index c8477ea37c..330b95d715 100644 --- a/Backend.Tests/Services/StatisticsServiceTests.cs +++ b/Backend.Tests/Services/StatisticsServiceTests.cs @@ -81,7 +81,8 @@ public void GetSemanticDomainCountsTestEmptyCounts() ((SemanticDomainRepositoryMock)_semDomRepo).SetNextResponse(TreeNodes); var result = _statsService.GetSemanticDomainCounts(ProjId, "").Result; - Assert.That(result, Is.Empty); + Assert.That(result, Is.Not.Empty); + Assert.That(result.First().Count, Is.EqualTo(0)); } [Test] From ccccabe07b969acf1c47c5594cb4cca2807aa6c0 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 20 Jan 2026 13:00:14 -0500 Subject: [PATCH 18/22] Respond to the bunny's nitpicks --- Backend.Tests/Controllers/AudioControllerTests.cs | 2 +- Backend.Tests/Controllers/LiftControllerTests.cs | 2 +- Backend.Tests/Controllers/MergeControllerTests.cs | 2 +- Backend.Tests/Controllers/WordControllerTests.cs | 2 +- .../Mocks/SemanticDomainCountRepositoryMock.cs | 5 ++--- Backend.Tests/Services/MergeServiceTests.cs | 2 +- Backend.Tests/Services/WordServiceTests.cs | 2 +- .../Interfaces/ISemanticDomainCountRepository.cs | 2 +- Backend/Interfaces/ISemanticDomainCountService.cs | 1 + .../Repositories/SemanticDomainCountRepository.cs | 5 ++--- Backend/Services/SemanticDomainCountService.cs | 13 +++++++++++++ Backend/Services/WordService.cs | 6 ++---- 12 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Backend.Tests/Controllers/AudioControllerTests.cs b/Backend.Tests/Controllers/AudioControllerTests.cs index eb3d07ca3d..edd44dd6e8 100644 --- a/Backend.Tests/Controllers/AudioControllerTests.cs +++ b/Backend.Tests/Controllers/AudioControllerTests.cs @@ -39,7 +39,7 @@ public void Setup() _wordRepo = new WordRepositoryMock(); _permissionService = new PermissionServiceMock(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); + _wordService = new WordService(_wordRepo, semDomCountService); _audioController = new AudioController(_wordRepo, _wordService, _permissionService); _projId = _projRepo.Create(new Project { Name = "AudioControllerTests" }).Result!.Id; diff --git a/Backend.Tests/Controllers/LiftControllerTests.cs b/Backend.Tests/Controllers/LiftControllerTests.cs index bbdc18ce20..066545fd29 100644 --- a/Backend.Tests/Controllers/LiftControllerTests.cs +++ b/Backend.Tests/Controllers/LiftControllerTests.cs @@ -51,7 +51,7 @@ public void Setup() _wordRepo = new WordRepositoryMock(); _liftService = new LiftService(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); + _wordService = new WordService(_wordRepo, semDomCountService); _liftController = new LiftController(_projRepo, new SemanticDomainRepositoryMock(), _speakerRepo, _wordRepo, _liftService, new HubContextMock(), new PermissionServiceMock(), _wordService, new MockLogger()); diff --git a/Backend.Tests/Controllers/MergeControllerTests.cs b/Backend.Tests/Controllers/MergeControllerTests.cs index 186176ddd4..84f234e8d2 100644 --- a/Backend.Tests/Controllers/MergeControllerTests.cs +++ b/Backend.Tests/Controllers/MergeControllerTests.cs @@ -41,7 +41,7 @@ public void Setup() var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); + _wordService = new WordService(_wordRepo, semDomCountService); _mergeService = new MergeService(_cache, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); var notifyService = new HubContextMock(); var permissionService = new PermissionServiceMock(); diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 31a99cd21b..8c1d80e473 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -34,7 +34,7 @@ public void Setup() var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); + _wordService = new WordService(_wordRepo, semDomCountService); _permissionService = new PermissionServiceMock(); _wordController = new WordController(_wordRepo, _wordService, _permissionService); } diff --git a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs index 6367457d26..5e3881f005 100644 --- a/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs +++ b/Backend.Tests/Mocks/SemanticDomainCountRepositoryMock.cs @@ -37,10 +37,9 @@ public Task Increment(string projectId, string domainId, int amount = 1) return Task.FromResult(count.Count); } - public Task DeleteAllCounts(string projectId) + public Task DeleteAllCounts(string projectId) { - var removed = _counts.RemoveAll(c => c.ProjectId == projectId); - return Task.FromResult(removed > 0); + return Task.FromResult(_counts.RemoveAll(c => c.ProjectId == projectId)); } } } diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index 9f0ad48db1..b8ed3fa3b7 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -33,7 +33,7 @@ public void Setup() var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); + _wordService = new WordService(_wordRepo, semDomCountService); _mergeService = new MergeService(_cache, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); } diff --git a/Backend.Tests/Services/WordServiceTests.cs b/Backend.Tests/Services/WordServiceTests.cs index e465e48ce9..3d05c396c1 100644 --- a/Backend.Tests/Services/WordServiceTests.cs +++ b/Backend.Tests/Services/WordServiceTests.cs @@ -22,7 +22,7 @@ public void Setup() var semDomCountRepo = new SemanticDomainCountRepositoryMock(); _wordRepo = new WordRepositoryMock(); var semDomCountService = new SemanticDomainCountService(semDomCountRepo); - _wordService = new WordService(semDomCountRepo, _wordRepo, semDomCountService); + _wordService = new WordService(_wordRepo, semDomCountService); } [Test] diff --git a/Backend/Interfaces/ISemanticDomainCountRepository.cs b/Backend/Interfaces/ISemanticDomainCountRepository.cs index 13ab16e53e..7167331e59 100644 --- a/Backend/Interfaces/ISemanticDomainCountRepository.cs +++ b/Backend/Interfaces/ISemanticDomainCountRepository.cs @@ -9,6 +9,6 @@ public interface ISemanticDomainCountRepository Task GetCount(string projectId, string domainId); Task> GetAllCounts(string projectId); Task Increment(string projectId, string domainId, int amount = 1); - Task DeleteAllCounts(string projectId); + Task DeleteAllCounts(string projectId); } } diff --git a/Backend/Interfaces/ISemanticDomainCountService.cs b/Backend/Interfaces/ISemanticDomainCountService.cs index 5d4d3a0b89..aa3f1b48d8 100644 --- a/Backend/Interfaces/ISemanticDomainCountService.cs +++ b/Backend/Interfaces/ISemanticDomainCountService.cs @@ -6,6 +6,7 @@ namespace BackendFramework.Interfaces { public interface ISemanticDomainCountService { + Task ClearCountsForProject(string projectId); Task UpdateCountsForWord(Word word); Task UpdateCountsForWords(List words); Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord); diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index 3b1c268011..4924bb0b79 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -68,12 +68,11 @@ public async Task Increment(string projectId, string domainId, int amount = } /// Deletes all counts for a project - public async Task DeleteAllCounts(string projectId) + public async Task DeleteAllCounts(string projectId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "deleting all semantic domain counts"); - var result = await _counts.DeleteManyAsync(ProjectFilter(projectId)); - return result.DeletedCount > 0; + return (int)(await _counts.DeleteManyAsync(ProjectFilter(projectId))).DeletedCount; } } } diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs index d27736705e..8ef7ee416c 100644 --- a/Backend/Services/SemanticDomainCountService.cs +++ b/Backend/Services/SemanticDomainCountService.cs @@ -28,6 +28,14 @@ private static Dictionary GetDomainCounts(Word word, Dictionary Clears all counts for a project + public async Task ClearCountsForProject(string projectId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "clearing counts for project"); + + await _countRepo.DeleteAllCounts(projectId); + } + /// Updates counts when a new word is added public async Task UpdateCountsForWord(Word word) { @@ -75,6 +83,11 @@ public async Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord) { using var activity = OtelService.StartActivityWithTag(otelTagName, "updating counts after word update"); + if (oldWord.ProjectId != newWord.ProjectId) + { + throw new System.ArgumentException("Old and new words must belong to the same project"); + } + var oldDomains = GetDomainCounts(oldWord); var newDomains = GetDomainCounts(newWord); diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index b7daf57aea..05efacab38 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -8,10 +8,8 @@ namespace BackendFramework.Services { /// More complex functions and application logic for s - public class WordService(ISemanticDomainCountRepository semDomCountRepo, IWordRepository wordRepo, - ISemanticDomainCountService semDomCountService) : IWordService + public class WordService(IWordRepository wordRepo, ISemanticDomainCountService semDomCountService) : IWordService { - private readonly ISemanticDomainCountRepository _semDomCountRepo = semDomCountRepo; private readonly IWordRepository _wordRepo = wordRepo; private readonly ISemanticDomainCountService _semDomCountService = semDomCountService; @@ -187,7 +185,7 @@ public async Task ClearFrontier(string projectId) var success = await _wordRepo.DeleteAllFrontierWords(projectId); if (success) { - await _semDomCountRepo.DeleteAllCounts(projectId); + await _semDomCountService.ClearCountsForProject(projectId); } return success; } From 2fc4747f2be84d6e62f3ee221f58880d91af26df Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 21 Jan 2026 12:07:23 -0500 Subject: [PATCH 19/22] Reimplement compound index --- .../SemanticDomainCountRepository.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Backend/Repositories/SemanticDomainCountRepository.cs b/Backend/Repositories/SemanticDomainCountRepository.cs index 4924bb0b79..a425677572 100644 --- a/Backend/Repositories/SemanticDomainCountRepository.cs +++ b/Backend/Repositories/SemanticDomainCountRepository.cs @@ -10,20 +10,33 @@ namespace BackendFramework.Repositories { /// Atomic database functions for s. [ExcludeFromCodeCoverage] - public class SemanticDomainCountRepository(IMongoDbContext dbContext) : ISemanticDomainCountRepository + public class SemanticDomainCountRepository : ISemanticDomainCountRepository { - private readonly IMongoCollection _counts = - dbContext.Db.GetCollection("SemanticDomainCountCollection"); + private readonly IMongoCollection _counts; private const string otelTagName = "otel.SemanticDomainCountRepository"; + public SemanticDomainCountRepository(IMongoDbContext dbContext) + { + _counts = dbContext.Db.GetCollection("SemanticDomainCountCollection"); + + // Create unique compound index on (ProjectId, DomainId) to optimize queries and enforce uniqueness + var indexKeys = + Builders.IndexKeys.Ascending(p => p.ProjectId).Ascending(p => p.DomainId); + var indexModel = new CreateIndexModel(indexKeys, new() { Unique = true }); + + // Create the index if it doesn't already exist + _counts.Indexes.CreateOne(indexModel); + } + private static FilterDefinition ProjectFilter(string projectId) { var filterDef = new FilterDefinitionBuilder(); return filterDef.Eq(c => c.ProjectId, projectId); } - private static FilterDefinition ProjectDomainFilter(string projectId, string domainId) + private static FilterDefinition ProjectDomainFilter( + string projectId, string domainId) { var filterDef = new FilterDefinitionBuilder(); return filterDef.And(filterDef.Eq(c => c.ProjectId, projectId), filterDef.Eq(c => c.DomainId, domainId)); From 7a92e7145093367f09281d1ed37f1b6377175dc7 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 21 Jan 2026 12:24:42 -0500 Subject: [PATCH 20/22] Fix DeleteAudio domain count updating --- Backend/Services/WordService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index 05efacab38..4e7de54ff0 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -71,10 +71,10 @@ private async Task Add(string userId, Word word) { return null; } + await _semDomCountService.UpdateCountsForWordDeletion(wordWithAudioToDelete); wordWithAudioToDelete.Audio.RemoveAll(a => a.FileName == fileName); wordWithAudioToDelete.History.Add(wordId); - return await Create(userId, wordWithAudioToDelete); } From 25eff94f70e6183364d800fd5afb47d64f2f178c Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 6 Feb 2026 15:03:20 -0500 Subject: [PATCH 21/22] Move domain count api function back from stats to word controller --- .../Controllers/StatisticsControllerTests.cs | 19 +-- .../Controllers/WordControllerTests.cs | 18 ++- Backend.Tests/Mocks/StatisticsServiceMock.cs | 5 - Backend/Controllers/StatisticsController.cs | 28 ++-- Backend/Controllers/WordController.cs | 34 +++-- Backend/Interfaces/IStatisticsService.cs | 1 - .../Services/SemanticDomainCountService.cs | 14 +- Backend/Services/StatisticsService.cs | 10 -- src/api/api/statistics-api.ts | 132 ----------------- src/api/api/word-api.ts | 133 ++++++++++++++++++ src/backend/index.ts | 16 +-- .../TreeDepiction/DomainCountBadge.tsx | 4 +- .../TreeDepiction/tests/CurrentRow.test.tsx | 6 +- .../TreeDepiction/tests/index.test.tsx | 6 +- 14 files changed, 207 insertions(+), 219 deletions(-) diff --git a/Backend.Tests/Controllers/StatisticsControllerTests.cs b/Backend.Tests/Controllers/StatisticsControllerTests.cs index 6014957377..dc482ef418 100644 --- a/Backend.Tests/Controllers/StatisticsControllerTests.cs +++ b/Backend.Tests/Controllers/StatisticsControllerTests.cs @@ -33,7 +33,8 @@ public async Task Setup() _projRepo = new ProjectRepositoryMock(); _userRepo = new UserRepositoryMock(); _permService = new PermissionServiceMock(_userRepo); - _statsController = new StatisticsController(_projRepo, _permService, new StatisticsServiceMock()) + var statsService = new StatisticsServiceMock(); + _statsController = new StatisticsController(statsService, _permService, _projRepo) { // Mock the Http Context because this isn't an actual call controller ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } @@ -46,22 +47,6 @@ public async Task Setup() _projId = (await _projRepo.Create(new Project { Name = "StatisticsControllerTests" }))!.Id; } - [Test] - public async Task TestGetDomainCountNoPermission() - { - _statsController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); - - var result = await _statsController.GetDomainCount(_projId, "1"); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public async Task TestGetDomainCount() - { - var result = await _statsController.GetDomainCount(_projId, "1"); - Assert.That(result, Is.InstanceOf()); - } - [Test] public async Task TestGetSemanticDomainCountsNoPermission() { diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index 57a4c2f765..12e57adf3f 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -36,7 +36,7 @@ public void Setup() var semDomCountService = new SemanticDomainCountService(semDomCountRepo); _wordService = new WordService(_wordRepo, semDomCountService); _permissionService = new PermissionServiceMock(); - _wordController = new WordController(_wordRepo, _wordService, _permissionService); + _wordController = new WordController(semDomCountRepo, _wordRepo, _wordService, _permissionService); } [Test] @@ -409,5 +409,21 @@ public async Task TestRestoreWordMissingWord() var wordResult = await _wordController.RestoreWord(ProjId, MissingId); Assert.That(wordResult, Is.InstanceOf()); } + + [Test] + public async Task TestGetDomainWordCountNoPermission() + { + _wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); + + var result = await _wordController.GetDomainWordCount(ProjId, "1"); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task TestGetDomainWordCount() + { + var result = await _wordController.GetDomainWordCount(ProjId, "1"); + Assert.That(result, Is.InstanceOf()); + } } } diff --git a/Backend.Tests/Mocks/StatisticsServiceMock.cs b/Backend.Tests/Mocks/StatisticsServiceMock.cs index 4c45e470a2..4e55c3abd6 100644 --- a/Backend.Tests/Mocks/StatisticsServiceMock.cs +++ b/Backend.Tests/Mocks/StatisticsServiceMock.cs @@ -8,11 +8,6 @@ namespace Backend.Tests.Mocks { internal sealed class StatisticsServiceMock : IStatisticsService { - public Task GetDomainCount(string projectId, string domainId) - { - return Task.FromResult(0); - } - public Task> GetSemanticDomainCounts(string projectId, string lang) { return Task.FromResult(new List()); diff --git a/Backend/Controllers/StatisticsController.cs b/Backend/Controllers/StatisticsController.cs index 1a41b28267..092535f824 100644 --- a/Backend/Controllers/StatisticsController.cs +++ b/Backend/Controllers/StatisticsController.cs @@ -13,30 +13,20 @@ namespace BackendFramework.Controllers [Produces("application/json")] [Route("v1/projects/{projectId}/statistics")] - public class StatisticsController( - IProjectRepository projRepo, IPermissionService permissionService, IStatisticsService statService) : Controller + public class StatisticsController : Controller { - private readonly IProjectRepository _projRepo = projRepo; - private readonly IPermissionService _permissionService = permissionService; - private readonly IStatisticsService _statService = statService; + private readonly IStatisticsService _statService; + private readonly IPermissionService _permissionService; + private readonly IProjectRepository _projRepo; private const string otelTagName = "otel.StatisticsController"; - /// Get the count of senses in a specific semantic domain - /// An integer count - [HttpGet("domaincount/{domainId}", Name = "GetDomainCount")] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task GetDomainCount(string projectId, string domainId) + public StatisticsController( + IStatisticsService statService, IPermissionService permissionService, IProjectRepository projRepo) { - using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain count"); - - if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId)) - { - return Forbid(); - } - - return Ok(await _statService.GetDomainCount(projectId, domainId)); + _statService = statService; + _permissionService = permissionService; + _projRepo = projRepo; } /// Get a list of s of a specific project in order diff --git a/Backend/Controllers/WordController.cs b/Backend/Controllers/WordController.cs index cd2bcbde9b..af14f57d89 100644 --- a/Backend/Controllers/WordController.cs +++ b/Backend/Controllers/WordController.cs @@ -13,21 +13,16 @@ namespace BackendFramework.Controllers [Authorize] [Produces("application/json")] [Route("v1/projects/{projectId}/words")] - public class WordController : Controller + public class WordController(ISemanticDomainCountRepository semDomCountRepo, IWordRepository wordRepo, + IWordService wordService, IPermissionService permissionService) : Controller { - private readonly IWordRepository _wordRepo; - private readonly IPermissionService _permissionService; - private readonly IWordService _wordService; + private readonly ISemanticDomainCountRepository _semDomCountRepo = semDomCountRepo; + private readonly IWordRepository _wordRepo = wordRepo; + private readonly IPermissionService _permissionService = permissionService; + private readonly IWordService _wordService = wordService; private const string otelTagName = "otel.WordController"; - public WordController(IWordRepository repo, IWordService wordService, IPermissionService permissionService) - { - _wordRepo = repo; - _permissionService = permissionService; - _wordService = wordService; - } - /// Deletes specified Frontier . [HttpDelete("frontier/{wordId}", Name = "DeleteFrontierWord")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -305,5 +300,22 @@ public async Task RevertWords( } return Ok(updates); } + + /// Get the count of frontier word senses in a specific semantic domain + /// An integer count + [HttpGet("domainwordcount/{domainId}", Name = "GetDomainWordCount")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetDomainWordCount(string projectId, string domainId) + { + using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain word count"); + + if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId)) + { + return Forbid(); + } + + return Ok(await _semDomCountRepo.GetCount(projectId, domainId)); + } } } diff --git a/Backend/Interfaces/IStatisticsService.cs b/Backend/Interfaces/IStatisticsService.cs index bd4102efad..09229d5289 100644 --- a/Backend/Interfaces/IStatisticsService.cs +++ b/Backend/Interfaces/IStatisticsService.cs @@ -7,7 +7,6 @@ namespace BackendFramework.Interfaces { public interface IStatisticsService { - Task GetDomainCount(string projectId, string domainId); Task> GetSemanticDomainCounts(string projectId, string lang); Task> GetWordsPerDayPerUserCounts(string projectId); Task GetProgressEstimationLineChartRoot(string projectId, List schedule); diff --git a/Backend/Services/SemanticDomainCountService.cs b/Backend/Services/SemanticDomainCountService.cs index 8ef7ee416c..fa50321836 100644 --- a/Backend/Services/SemanticDomainCountService.cs +++ b/Backend/Services/SemanticDomainCountService.cs @@ -8,9 +8,9 @@ namespace BackendFramework.Services { /// Service for managing semantic domain sense counts - public class SemanticDomainCountService(ISemanticDomainCountRepository countRepo) : ISemanticDomainCountService + public class SemanticDomainCountService(ISemanticDomainCountRepository semDomCountRepo) : ISemanticDomainCountService { - private readonly ISemanticDomainCountRepository _countRepo = countRepo; + private readonly ISemanticDomainCountRepository _semDomCountRepo = semDomCountRepo; private const string otelTagName = "otel.SemanticDomainCountService"; @@ -33,7 +33,7 @@ public async Task ClearCountsForProject(string projectId) { using var activity = OtelService.StartActivityWithTag(otelTagName, "clearing counts for project"); - await _countRepo.DeleteAllCounts(projectId); + await _semDomCountRepo.DeleteAllCounts(projectId); } /// Updates counts when a new word is added @@ -44,7 +44,7 @@ public async Task UpdateCountsForWord(Word word) var domainCounts = GetDomainCounts(word); foreach (var entry in domainCounts) { - await _countRepo.Increment(word.ProjectId, entry.Key, entry.Value); + await _semDomCountRepo.Increment(word.ProjectId, entry.Key, entry.Value); } } @@ -73,7 +73,7 @@ public async Task UpdateCountsForWords(List words) { foreach (var domainEntry in projectEntry.Value) { - await _countRepo.Increment(projectEntry.Key, domainEntry.Key, domainEntry.Value); + await _semDomCountRepo.Increment(projectEntry.Key, domainEntry.Key, domainEntry.Value); } } } @@ -101,7 +101,7 @@ public async Task UpdateCountsAfterWordUpdate(Word oldWord, Word newWord) if (diff != 0) { - await _countRepo.Increment(newWord.ProjectId, domainId, diff); + await _semDomCountRepo.Increment(newWord.ProjectId, domainId, diff); } } } @@ -114,7 +114,7 @@ public async Task UpdateCountsForWordDeletion(Word word) var domainCounts = GetDomainCounts(word); foreach (var entry in domainCounts) { - await _countRepo.Increment(word.ProjectId, entry.Key, -entry.Value); + await _semDomCountRepo.Increment(word.ProjectId, entry.Key, -entry.Value); } } } diff --git a/Backend/Services/StatisticsService.cs b/Backend/Services/StatisticsService.cs index 24dc35f6f9..65b5b0c99c 100644 --- a/Backend/Services/StatisticsService.cs +++ b/Backend/Services/StatisticsService.cs @@ -27,16 +27,6 @@ public class StatisticsService(ISemanticDomainRepository semDomRepo, const string StatProjection = "Projection"; const string StatRunningTotal = "Running Total"; - /// - /// Get a count of the number of senses associated with a semantic domain - /// - public async Task GetDomainCount(string projectId, string domainId) - { - using var activity = OtelService.StartActivityWithTag(otelTagName, "getting domain count"); - - return await _semDomCountRepo.GetCount(projectId, domainId); - } - /// /// Get a to generate a SemanticDomain statistics /// diff --git a/src/api/api/statistics-api.ts b/src/api/api/statistics-api.ts index 89d4421e73..96205a7008 100644 --- a/src/api/api/statistics-api.ts +++ b/src/api/api/statistics-api.ts @@ -52,55 +52,6 @@ export const StatisticsApiAxiosParamCreator = function ( configuration?: Configuration ) { return { - /** - * - * @param {string} projectId - * @param {string} domainId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getDomainCount: async ( - projectId: string, - domainId: string, - options: any = {} - ): Promise => { - // verify required parameter 'projectId' is not null or undefined - assertParamExists("getDomainCount", "projectId", projectId); - // verify required parameter 'domainId' is not null or undefined - assertParamExists("getDomainCount", "domainId", domainId); - const localVarPath = - `/v1/projects/{projectId}/statistics/domaincount/{domainId}` - .replace(`{${"projectId"}}`, encodeURIComponent(String(projectId))) - .replace(`{${"domainId"}}`, encodeURIComponent(String(domainId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { - method: "GET", - ...baseOptions, - ...options, - }; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); - let headersFromBaseOptions = - baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = { - ...localVarHeaderParameter, - ...headersFromBaseOptions, - ...options.headers, - }; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {string} projectId @@ -352,32 +303,6 @@ export const StatisticsApiFp = function (configuration?: Configuration) { const localVarAxiosParamCreator = StatisticsApiAxiosParamCreator(configuration); return { - /** - * - * @param {string} projectId - * @param {string} domainId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getDomainCount( - projectId: string, - domainId: string, - options?: any - ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise - > { - const localVarAxiosArgs = await localVarAxiosParamCreator.getDomainCount( - projectId, - domainId, - options - ); - return createRequestFunction( - localVarAxiosArgs, - globalAxios, - BASE_PATH, - configuration - ); - }, /** * * @param {string} projectId @@ -524,22 +449,6 @@ export const StatisticsApiFactory = function ( ) { const localVarFp = StatisticsApiFp(configuration); return { - /** - * - * @param {string} projectId - * @param {string} domainId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getDomainCount( - projectId: string, - domainId: string, - options?: any - ): AxiosPromise { - return localVarFp - .getDomainCount(projectId, domainId, options) - .then((request) => request(axios, basePath)); - }, /** * * @param {string} projectId @@ -615,27 +524,6 @@ export const StatisticsApiFactory = function ( }; }; -/** - * Request parameters for getDomainCount operation in StatisticsApi. - * @export - * @interface StatisticsApiGetDomainCountRequest - */ -export interface StatisticsApiGetDomainCountRequest { - /** - * - * @type {string} - * @memberof StatisticsApiGetDomainCount - */ - readonly projectId: string; - - /** - * - * @type {string} - * @memberof StatisticsApiGetDomainCount - */ - readonly domainId: string; -} - /** * Request parameters for getLineChartRootData operation in StatisticsApi. * @export @@ -720,26 +608,6 @@ export interface StatisticsApiGetWordsPerDayPerUserCountsRequest { * @extends {BaseAPI} */ export class StatisticsApi extends BaseAPI { - /** - * - * @param {StatisticsApiGetDomainCountRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof StatisticsApi - */ - public getDomainCount( - requestParameters: StatisticsApiGetDomainCountRequest, - options?: any - ) { - return StatisticsApiFp(this.configuration) - .getDomainCount( - requestParameters.projectId, - requestParameters.domainId, - options - ) - .then((request) => request(this.axios, this.basePath)); - } - /** * * @param {StatisticsApiGetLineChartRootDataRequest} requestParameters Request parameters. diff --git a/src/api/api/word-api.ts b/src/api/api/word-api.ts index 4bbce45fbf..53ac796c1b 100644 --- a/src/api/api/word-api.ts +++ b/src/api/api/word-api.ts @@ -207,6 +207,55 @@ export const WordApiAxiosParamCreator = function ( options: localVarRequestOptions, }; }, + /** + * + * @param {string} projectId + * @param {string} domainId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDomainWordCount: async ( + projectId: string, + domainId: string, + options: any = {} + ): Promise => { + // verify required parameter 'projectId' is not null or undefined + assertParamExists("getDomainWordCount", "projectId", projectId); + // verify required parameter 'domainId' is not null or undefined + assertParamExists("getDomainWordCount", "domainId", domainId); + const localVarPath = + `/v1/projects/{projectId}/words/domainwordcount/{domainId}` + .replace(`{${"projectId"}}`, encodeURIComponent(String(projectId))) + .replace(`{${"domainId"}}`, encodeURIComponent(String(domainId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} projectId @@ -809,6 +858,33 @@ export const WordApiFp = function (configuration?: Configuration) { configuration ); }, + /** + * + * @param {string} projectId + * @param {string} domainId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getDomainWordCount( + projectId: string, + domainId: string, + options?: any + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.getDomainWordCount( + projectId, + domainId, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, /** * * @param {string} projectId @@ -1128,6 +1204,22 @@ export const WordApiFactory = function ( .deleteFrontierWord(projectId, wordId, options) .then((request) => request(axios, basePath)); }, + /** + * + * @param {string} projectId + * @param {string} domainId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDomainWordCount( + projectId: string, + domainId: string, + options?: any + ): AxiosPromise { + return localVarFp + .getDomainWordCount(projectId, domainId, options) + .then((request) => request(axios, basePath)); + }, /** * * @param {string} projectId @@ -1346,6 +1438,27 @@ export interface WordApiDeleteFrontierWordRequest { readonly wordId: string; } +/** + * Request parameters for getDomainWordCount operation in WordApi. + * @export + * @interface WordApiGetDomainWordCountRequest + */ +export interface WordApiGetDomainWordCountRequest { + /** + * + * @type {string} + * @memberof WordApiGetDomainWordCount + */ + readonly projectId: string; + + /** + * + * @type {string} + * @memberof WordApiGetDomainWordCount + */ + readonly domainId: string; +} + /** * Request parameters for getDuplicateId operation in WordApi. * @export @@ -1612,6 +1725,26 @@ export class WordApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {WordApiGetDomainWordCountRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WordApi + */ + public getDomainWordCount( + requestParameters: WordApiGetDomainWordCountRequest, + options?: any + ) { + return WordApiFp(this.configuration) + .getDomainWordCount( + requestParameters.projectId, + requestParameters.domainId, + options + ) + .then((request) => request(this.axios, this.basePath)); + } + /** * * @param {WordApiGetDuplicateIdRequest} requestParameters Request parameters. diff --git a/src/backend/index.ts b/src/backend/index.ts index fee2cf139c..254b7d48fb 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -659,14 +659,6 @@ export async function getConsentImageSrc(speaker: Speaker): Promise { /* StatisticsController.cs */ -export async function getDomainCount(domainId: string): Promise { - const response = await statisticsApi.getDomainCount( - { projectId: LocalStorage.getProjectId(), domainId }, - defaultOptions() - ); - return response.data; -} - export async function getSemanticDomainCounts( projectId: string, lang?: string @@ -998,3 +990,11 @@ export async function updateWord(word: Word): Promise { ); return { ...word, id: resp.data }; } + +export async function getDomainWordCount(domainId: string): Promise { + const response = await wordApi.getDomainWordCount( + { projectId: LocalStorage.getProjectId(), domainId }, + defaultOptions() + ); + return response.data; +} diff --git a/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx b/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx index 9325efdc66..dab9ad8b7c 100644 --- a/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx +++ b/src/components/TreeView/TreeDepiction/DomainCountBadge.tsx @@ -2,7 +2,7 @@ import { Badge, Tooltip } from "@mui/material"; import { ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { getDomainCount } from "backend"; +import { getDomainWordCount } from "backend"; import { rootId } from "types/semanticDomain"; interface DomainCountBadgeProps { @@ -21,7 +21,7 @@ export default function DomainCountBadge( useEffect(() => { setWordCount(undefined); if (domainId && domainId !== rootId) { - getDomainCount(domainId) + getDomainWordCount(domainId) .then(setWordCount) .catch(() => console.warn(`Failed to get word count for domain ${domainId}.`) diff --git a/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx b/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx index a76c63f73a..a6b6446be3 100644 --- a/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx +++ b/src/components/TreeView/TreeDepiction/tests/CurrentRow.test.tsx @@ -9,15 +9,15 @@ import testDomainMap, { } from "components/TreeView/tests/SemanticDomainMock"; jest.mock("backend", () => ({ - getDomainCount: () => mockGetDomainCount(), + getDomainWordCount: () => mockGetDomainWordCount(), })); const mockAnimate = jest.fn(); -const mockGetDomainCount = jest.fn(); +const mockGetDomainWordCount = jest.fn(); beforeEach(() => { jest.clearAllMocks(); - mockGetDomainCount.mockResolvedValue(0); + mockGetDomainWordCount.mockResolvedValue(0); }); describe("CurrentRow", () => { diff --git a/src/components/TreeView/TreeDepiction/tests/index.test.tsx b/src/components/TreeView/TreeDepiction/tests/index.test.tsx index d63f5a6a3c..95e586096f 100644 --- a/src/components/TreeView/TreeDepiction/tests/index.test.tsx +++ b/src/components/TreeView/TreeDepiction/tests/index.test.tsx @@ -6,13 +6,13 @@ import testDomainMap, { } from "components/TreeView/tests/SemanticDomainMock"; jest.mock("backend", () => ({ - getDomainCount: () => mockGetDomainCount(), + getDomainWordCount: () => mockGetDomainWordCount(), })); -const mockGetDomainCount = jest.fn(); +const mockGetDomainWordCount = jest.fn(); beforeEach(() => { - mockGetDomainCount.mockResolvedValue(0); + mockGetDomainWordCount.mockResolvedValue(0); }); describe("TreeDepiction", () => { From fae569ac8a1b15ed11292d43cad72c902a445d44 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 6 Feb 2026 15:09:56 -0500 Subject: [PATCH 22/22] Revert out-of-scope change --- Backend.Tests/Controllers/StatisticsControllerTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Backend.Tests/Controllers/StatisticsControllerTests.cs b/Backend.Tests/Controllers/StatisticsControllerTests.cs index dc482ef418..03f4fbcef3 100644 --- a/Backend.Tests/Controllers/StatisticsControllerTests.cs +++ b/Backend.Tests/Controllers/StatisticsControllerTests.cs @@ -33,8 +33,7 @@ public async Task Setup() _projRepo = new ProjectRepositoryMock(); _userRepo = new UserRepositoryMock(); _permService = new PermissionServiceMock(_userRepo); - var statsService = new StatisticsServiceMock(); - _statsController = new StatisticsController(statsService, _permService, _projRepo) + _statsController = new StatisticsController(new StatisticsServiceMock(), _permService, _projRepo) { // Mock the Http Context because this isn't an actual call controller ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }