From fef0e61daa48309444832c697897457410b9caec Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 16 Aug 2026 00:22:05 +0700 Subject: [PATCH 1/4] pull direct harmony change writing into it's own class --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 163 +++++++----------- .../LcmCrdt/Harmony/HarmonyChangeWriter.cs | 50 ++++++ 2 files changed, 112 insertions(+), 101 deletions(-) create mode 100644 backend/FwLite/LcmCrdt/Harmony/HarmonyChangeWriter.cs diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index b982cc7d37..5fe4fbf789 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -1,4 +1,3 @@ -using System.Data; using FluentValidation; using SIL.Harmony; using SIL.Harmony.Changes; @@ -9,68 +8,30 @@ using LcmCrdt.Changes.ExampleSentences; using LcmCrdt.Data; using LcmCrdt.FullTextSearch; +using LcmCrdt.Harmony; using LcmCrdt.MediaServer; using LcmCrdt.Objects; -using LinqToDB; using LinqToDB.Async; using LinqToDB.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using MiniLcm.Exceptions; using MiniLcm.SyncHelpers; -using SIL.Harmony.Core; -using MiniLcm.Culture; using MiniLcm.Media; using SystemTextJsonPatch; namespace LcmCrdt; public class CrdtMiniLcmApi( - DataModel dataModel, + HarmonyChangeWriter harmonyChangeWriter, CurrentProjectService projectService, MiniLcmRepositoryFactory repoFactory, - IOptions config, ILogger logger, LcmMediaService lcmMediaService, LocalCommentReadStatusService commentReadStatusService, - CommitMetadataInterceptor commitMetadataInterceptor, EntrySearchService? entrySearchService = null) : IMiniLcmApi { - private Guid ClientId { get; } = projectService.ProjectData.ClientId; public ProjectData ProjectData => projectService.ProjectData; public CrdtProject Project => projectService.Project; - private LcmCrdtConfig LcmConfig => config.Value; - - private CommitMetadata NewMetadata() - { - var metadata = new CommitMetadata - { - ClientVersion = AppVersion.Version, - //todo, if a user logs out and in with another account, this will be out of date until the next sync - AuthorName = ProjectData.LastUserName ?? config.Value.DefaultAuthorForCommits, - AuthorId = ProjectData.LastUserId - }; - commitMetadataInterceptor.Apply(metadata); - return metadata; - } - private async Task AddChange(IChange change) - { - AssertWritable(); - var commit = await dataModel.AddChange(ClientId, change, commitMetadata: NewMetadata()); - return commit; - } - - private async Task AddChanges(IEnumerable changes) - { - AssertWritable(); - await dataModel.AddManyChanges(ClientId, changes, commitMetadata: NewMetadata); - } - - private void AssertWritable() - { - if (ProjectData.IsReadonly) - throw new ReadOnlyException($"project is readonly because you are logged in with the {ProjectData.Role} role. If your role recently changed, try refreshing the server project list on the home page."); - } public async Task GetWritingSystems() { @@ -92,7 +53,7 @@ public async Task CreateWritingSystem(WritingSystem writingSystem if (exists) throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} ({wsType}) already exists"); var betweenIds = between is null ? null : await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, wsType))?.Id); var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(ws => ws.Type == wsType), betweenIds); - await AddChange(new CreateWritingSystemChange(writingSystem, entityId, order)); + await harmonyChangeWriter.AddChange(new CreateWritingSystemChange(writingSystem, entityId, order)); return await repo.GetWritingSystem(writingSystem.WsId, wsType) ?? throw NotFoundException.ForWs(writingSystem); } @@ -101,7 +62,7 @@ public async Task UpdateWritingSystem(WritingSystemId id, Writing await using var repo = await repoFactory.CreateRepoAsync(); var ws = await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); var patchChange = new JsonPatchChange(ws.Id, update.Patch); - await AddChange(patchChange); + await harmonyChangeWriter.AddChange(patchChange); return await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); } @@ -117,7 +78,7 @@ public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, var ws = await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); var betweenIds = await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, type))?.Id); var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds); - await AddChange(new Changes.SetOrderChange(ws.Id, order)); + await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(ws.Id, order)); } public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) @@ -144,13 +105,13 @@ public async IAsyncEnumerable GetPartsOfSpeech() public async Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) { if (partOfSpeech.Id == Guid.Empty) partOfSpeech.Id = Guid.NewGuid(); - await AddChange(new CreatePartOfSpeechChange(partOfSpeech.Id, partOfSpeech.Name, partOfSpeech.Predefined)); + await harmonyChangeWriter.AddChange(new CreatePartOfSpeechChange(partOfSpeech.Id, partOfSpeech.Name, partOfSpeech.Predefined)); return await GetPartOfSpeech(partOfSpeech.Id) ?? throw NotFoundException.ForType(partOfSpeech.Id); } public async Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) { - await AddChanges(update.Patch.ToChanges(id)); + await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(id)); } public async Task UpdatePartOfSpeech(Guid id, UpdateObjectInput update) @@ -167,7 +128,7 @@ public async Task UpdatePartOfSpeech(PartOfSpeech before, PartOfSp public async Task DeletePartOfSpeech(Guid id) { - await AddChange(new DeleteChange(id)); + await harmonyChangeWriter.AddChange(new DeleteChange(id)); } public async IAsyncEnumerable GetPublications() @@ -187,7 +148,7 @@ public async IAsyncEnumerable GetPublications() public async Task CreatePublication(Publication pub) { - await AddChange(new CreatePublicationChange(pub.Id, pub.Name, pub.IsMain)); + await harmonyChangeWriter.AddChange(new CreatePublicationChange(pub.Id, pub.Name, pub.IsMain)); return await GetPublication(pub.Id) ?? throw NotFoundException.ForType(pub.Id); } @@ -202,11 +163,11 @@ public async Task SubmitUpdatePublication(Guid id, UpdateObjectInput 0) await AddChanges(changes); + if (changes.Count > 0) await harmonyChangeWriter.AddChanges(changes); } else if (update.Patch.Operations.Count > 0) { - await AddChanges(update.Patch.ToChanges(id)); + await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(id)); } } @@ -226,18 +187,18 @@ public async Task UpdatePublication(Publication before, Publication public async Task DeletePublication(Guid id) { - await AddChange(new DeleteChange(id)); + await harmonyChangeWriter.AddChange(new DeleteChange(id)); } public async Task AddPublication(Guid entryId, Guid publicationId) { var pub = await GetPublication(publicationId) ?? throw NotFoundException.ForType(publicationId); - await AddChange(new AddPublicationChange(entryId, pub)); + await harmonyChangeWriter.AddChange(new AddPublicationChange(entryId, pub)); } public async Task RemovePublication(Guid entryId, Guid publicationId) { - await AddChange(new RemovePublicationChange(entryId, publicationId)); + await harmonyChangeWriter.AddChange(new RemovePublicationChange(entryId, publicationId)); } public async IAsyncEnumerable GetSemanticDomains() @@ -257,13 +218,13 @@ public async IAsyncEnumerable GetSemanticDomains() public async Task CreateSemanticDomain(SemanticDomain semanticDomain) { - await AddChange(new CreateSemanticDomainChange(semanticDomain.Id, semanticDomain.Name, semanticDomain.Code, semanticDomain.Predefined)); + await harmonyChangeWriter.AddChange(new CreateSemanticDomainChange(semanticDomain.Id, semanticDomain.Name, semanticDomain.Code, semanticDomain.Predefined)); return await GetSemanticDomain(semanticDomain.Id) ?? throw NotFoundException.ForType(semanticDomain.Id); } public async Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) { - await AddChanges(update.Patch.ToChanges(id)); + await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(id)); } public async Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) @@ -280,12 +241,12 @@ public async Task UpdateSemanticDomain(SemanticDomain before, Se public async Task DeleteSemanticDomain(Guid id) { - await AddChange(new DeleteChange(id)); + await harmonyChangeWriter.AddChange(new DeleteChange(id)); } public async Task BulkImportSemanticDomains(IAsyncEnumerable semanticDomains) { - await AddChanges(await semanticDomains.Select(sd => new CreateSemanticDomainChange(sd.Id, sd.Name, sd.Code, sd.Predefined)).ToArrayAsync()); + await harmonyChangeWriter.AddChanges(await semanticDomains.Select(sd => new CreateSemanticDomainChange(sd.Id, sd.Name, sd.Code, sd.Predefined)).ToArrayAsync()); } public async IAsyncEnumerable GetComplexFormTypes() @@ -307,13 +268,13 @@ public async Task CreateComplexFormType(ComplexFormType complex { await using var repo = await repoFactory.CreateRepoAsync(); if (complexFormType.Id == default) complexFormType.Id = Guid.NewGuid(); - await AddChange(new CreateComplexFormType(complexFormType.Id, complexFormType.Name)); + await harmonyChangeWriter.AddChange(new CreateComplexFormType(complexFormType.Id, complexFormType.Name)); return await repo.ComplexFormTypes.SingleAsync(c => c.Id == complexFormType.Id); } public async Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) { - await AddChange(new JsonPatchChange(id, update.Patch)); + await harmonyChangeWriter.AddChange(new JsonPatchChange(id, update.Patch)); } public async Task UpdateComplexFormType(Guid id, UpdateObjectInput update) @@ -330,7 +291,7 @@ public async Task UpdateComplexFormType(ComplexFormType before, public async Task DeleteComplexFormType(Guid id) { - await AddChange(new DeleteChange(id)); + await harmonyChangeWriter.AddChange(new DeleteChange(id)); } public async Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? between = null) @@ -345,7 +306,7 @@ public async Task SubmitCreateComplexFormComponent(ComplexFormComponent complexF // Harmony duplicate-ID pitfalls during sync. complexFormComponent.Id = Guid.NewGuid(); var addEntryComponentChange = await repo.CreateComplexFormComponentChange(complexFormComponent, betweenIds); - await AddChange(addEntryComponentChange); + await harmonyChangeWriter.AddChange(addEntryComponentChange); return; } @@ -386,7 +347,7 @@ private async Task MoveComplexFormComponent(ComplexFormComponent component, Betw } var betweenIds = await between.MapAsync(async c => (await repo.FindComplexFormComponent(c))?.Id); var order = await OrderPicker.PickOrder(repo.ComplexFormComponents.Where(s => s.ComplexFormEntryId == component.ComplexFormEntryId), betweenIds); - await AddChange(new Changes.SetOrderChange(id.Value, order)); + await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(id.Value, order)); } public async Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) @@ -394,18 +355,18 @@ public async Task DeleteComplexFormComponent(ComplexFormComponent complexFormCom await using var repo = await repoFactory.CreateRepoAsync(); var existing = await repo.FindComplexFormComponent(complexFormComponent); if (existing is null) return; - await AddChange(new DeleteChange(existing.Id)); + await harmonyChangeWriter.AddChange(new DeleteChange(existing.Id)); } public async Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) { await using var repo = await repoFactory.CreateRepoAsync(); - await AddChange(new AddComplexFormTypeChange(entryId, await repo.ComplexFormTypes.SingleAsync(ct => ct.Id == complexFormTypeId))); + await harmonyChangeWriter.AddChange(new AddComplexFormTypeChange(entryId, await repo.ComplexFormTypes.SingleAsync(ct => ct.Id == complexFormTypeId))); } public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) { - await AddChange(new RemoveComplexFormTypeChange(entryId, complexFormTypeId)); + await harmonyChangeWriter.AddChange(new RemoveComplexFormTypeChange(entryId, complexFormTypeId)); } public async IAsyncEnumerable GetMorphTypes() @@ -433,13 +394,13 @@ public async Task CreateMorphType(MorphType morphType) { //I don't like returning a different object than what the user requested, it feels very unexpected, however this is pretty much what happens in the change anyway and that can't be avoided if (await GetMorphType(morphType.Kind) is {} actualMorphType) return actualMorphType; - await AddChange(new CreateMorphTypeChange(morphType)); + await harmonyChangeWriter.AddChange(new CreateMorphTypeChange(morphType)); return await GetMorphType(morphType.Id) ?? throw NotFoundException.ForType(morphType.Id); } public async Task UpdateMorphType(Guid id, UpdateObjectInput update) { - await AddChange(new JsonPatchChange(id, update.Patch)); + await harmonyChangeWriter.AddChange(new JsonPatchChange(id, update.Patch)); return await GetMorphType(id) ?? throw NotFoundException.ForType(id); } @@ -497,14 +458,14 @@ public async Task BulkCreateEntries(IAsyncEnumerable entries) createdEntryIds.Add(entry.Id); if (changeList.Count > 1000) { - await AddChanges(changeList); + await harmonyChangeWriter.AddChanges(changeList); changeList.Clear(); logger.LogInformation("Added {Count} entries so far", entryCount); } } if (changeList.Count > 0) { - await AddChanges(changeList); + await harmonyChangeWriter.AddChanges(changeList); } await (entrySearchService?.RegenerateEntrySearchTable() ?? Task.CompletedTask); @@ -589,7 +550,7 @@ public async Task CreateEntry(Entry entry, CreateEntryOptions? options = entry.PublishIn.Add(mainPublication); } } - await AddChanges([ + await harmonyChangeWriter.AddChanges((IEnumerable)[ new CreateEntryChange(entry), ..homographPromotionChange is null ? [] : new[] { homographPromotionChange }, ..await entry.Senses.ToAsyncEnumerable() @@ -696,7 +657,7 @@ private async ValueTask IsEntryDeleted(Guid id) public async Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) { - await AddChanges(update.Patch.ToChanges(id)); + await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(id)); } public async Task UpdateEntry(Guid id, @@ -716,7 +677,7 @@ public async Task UpdateEntry(Entry before, Entry after, IMiniLcmApi? api public async Task DeleteEntry(Guid id) { - await AddChange(new DeleteChange(id)); + await harmonyChangeWriter.AddChange(new DeleteChange(id)); } private async IAsyncEnumerable CreateSenseChanges(Guid entryId, @@ -760,7 +721,7 @@ public async Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? { await using var repo = await repoFactory.CreateRepoAsync(); sense.Order = await OrderPicker.PickOrder(repo.Senses.Where(s => s.EntryId == entryId), between); - await AddChanges(await CreateSenseChanges(entryId, sense, repo.SemanticDomains).ToArrayAsync()); + await harmonyChangeWriter.AddChanges(await CreateSenseChanges(entryId, sense, repo.SemanticDomains).ToArrayAsync()); } public async Task CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null) @@ -777,7 +738,7 @@ public async Task CreateSense(Guid entryId, Sense sense, BetweenPosition? public async Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) { - await AddChanges(update.Patch.ToChanges(senseId)); + await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(senseId)); } public async Task UpdateSense(Guid entryId, @@ -806,32 +767,32 @@ public async Task MoveSense(Guid entryId, Guid senseId, BetweenPosition between) var currentEntryId = await repo.Senses.Where(s => s.Id == senseId).Select(s => s.EntryId).FirstOrDefaultAsync(); if (currentEntryId != default && currentEntryId != entryId) { - await AddChange(new MoveSenseToEntryChange(senseId, entryId, order)); + await harmonyChangeWriter.AddChange(new MoveSenseToEntryChange(senseId, entryId, order)); } else { - await AddChange(new Changes.SetOrderChange(senseId, order)); + await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(senseId, order)); } } public async Task DeleteSense(Guid entryId, Guid senseId) { - await AddChange(new DeleteChange(senseId)); + await harmonyChangeWriter.AddChange(new DeleteChange(senseId)); } public async Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain) { - await AddChange(new AddSemanticDomainChange(semanticDomain, senseId)); + await harmonyChangeWriter.AddChange(new AddSemanticDomainChange(semanticDomain, senseId)); } public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) { - await AddChange(new RemoveSemanticDomainChange(semanticDomainId, senseId)); + await harmonyChangeWriter.AddChange(new RemoveSemanticDomainChange(semanticDomainId, senseId)); } public async Task SetSensePartOfSpeech(Guid senseId, Guid? partOfSpeechId) { - await AddChange(new SetPartOfSpeechChange(senseId, partOfSpeechId)); + await harmonyChangeWriter.AddChange(new SetPartOfSpeechChange(senseId, partOfSpeechId)); } public async Task SubmitCreateExampleSentence(Guid entryId, @@ -841,7 +802,7 @@ public async Task SubmitCreateExampleSentence(Guid entryId, { await using var repo = await repoFactory.CreateRepoAsync(); exampleSentence.Order = await OrderPicker.PickOrder(repo.ExampleSentences.Where(s => s.SenseId == senseId), between); - await AddChange(new CreateExampleSentenceChange(exampleSentence, senseId)); + await harmonyChangeWriter.AddChange(new CreateExampleSentenceChange(exampleSentence, senseId)); } public async Task CreateExampleSentence(Guid entryId, @@ -864,7 +825,7 @@ public async Task SubmitUpdateExampleSentence(Guid entryId, Guid exampleSentenceId, UpdateObjectInput update) { - await AddChange(new JsonPatchExampleSentenceChange(exampleSentenceId, update.Patch)); + await harmonyChangeWriter.AddChange(new JsonPatchExampleSentenceChange(exampleSentenceId, update.Patch)); } public async Task UpdateExampleSentence(Guid entryId, @@ -890,23 +851,23 @@ public async Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleId { await using var repo = await repoFactory.CreateRepoAsync(); var order = await OrderPicker.PickOrder(repo.ExampleSentences.Where(s => s.SenseId == senseId), between); - await AddChange(new Changes.SetOrderChange(exampleId, order)); + await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(exampleId, order)); } public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) { - await AddChange(new DeleteChange(exampleSentenceId)); + await harmonyChangeWriter.AddChange(new DeleteChange(exampleSentenceId)); } public async Task AddTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Translation translation) { if (translation.Id == Guid.Empty) translation.Id = Guid.NewGuid(); - await AddChange(new AddTranslationChange(exampleSentenceId, translation)); + await harmonyChangeWriter.AddChange(new AddTranslationChange(exampleSentenceId, translation)); } public async Task RemoveTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Guid translationId) { - await AddChange(new RemoveTranslationChange(exampleSentenceId, translationId)); + await harmonyChangeWriter.AddChange(new RemoveTranslationChange(exampleSentenceId, translationId)); } public async Task UpdateTranslation(Guid entryId, @@ -916,7 +877,7 @@ public async Task UpdateTranslation(Guid entryId, UpdateObjectInput update) { var jsonPatch = update.Patch; - await AddChange(new UpdateTranslationChange(exampleSentenceId, translationId, jsonPatch)); + await harmonyChangeWriter.AddChange(new UpdateTranslationChange(exampleSentenceId, translationId, jsonPatch)); } [Obsolete($"Use {nameof(AddTranslation)} instead")] @@ -924,7 +885,7 @@ public async Task SetFirstTranslationIds(IDictionary exampleSentence { var changes = exampleSentenceIdToTranslationId .Select(kv => GetSetFirstTranslationIdChange(kv.Key, kv.Value)); - await AddChanges(changes); + await harmonyChangeWriter.AddChanges(changes); static SetFirstTranslationIdChange GetSetFirstTranslationIdChange(Guid exampleSentenceId, Guid translationId) { @@ -945,7 +906,7 @@ public async Task CreatePicture(Guid entryId, { await using var repo = await repoFactory.CreateRepoAsync(); var change = new CreateSensePictureChange(picture, senseId, between); - await AddChange(change); + await harmonyChangeWriter.AddChange(change); return await GetPicture(entryId, senseId, change.PictureId) ?? throw NotFoundException.ForType(change.PictureId); } @@ -963,7 +924,7 @@ public async Task SubmitUpdatePicture(Guid entryId, { var jsonPatch = update.Patch; var patchChange = new UpdateSensePictureChange(pictureId, senseId, jsonPatch); - await AddChange(patchChange); + await harmonyChangeWriter.AddChange(patchChange); } public async Task UpdatePicture(Guid entryId, @@ -991,12 +952,12 @@ public async Task MovePicture(Guid entryId, Guid senseId, Guid pictureId, Betwee var sense = await repo.GetSense(senseId); if (sense is null) throw NotFoundException.ForType(senseId); var order = OrderPicker.PickOrder(sense.Pictures, between); - await AddChange(new ReorderSensePictureChange(pictureId, senseId, order)); + await harmonyChangeWriter.AddChange(new ReorderSensePictureChange(pictureId, senseId, order)); } public async Task DeletePicture(Guid entryId, Guid senseId, Guid pictureId) { - await AddChange(new RemoveSensePictureChange(pictureId, senseId)); + await harmonyChangeWriter.AddChange(new RemoveSensePictureChange(pictureId, senseId)); } public async Task GetFileStream(MediaUri mediaUri, bool downloadIfMissing = true) @@ -1040,7 +1001,7 @@ public async Task CreateCustomView(CustomView customView) { AssertManagerRoleForCustomViewWrite(); if (customView.Id == Guid.Empty) customView.Id = Guid.NewGuid(); - await AddChange(new CreateCustomViewChange(customView.Id, customView)); + await harmonyChangeWriter.AddChange(new CreateCustomViewChange(customView.Id, customView)); return await GetCustomView(customView.Id) ?? throw NotFoundException.ForType(customView.Id); } @@ -1050,7 +1011,7 @@ public async Task UpdateCustomView(CustomView customView) await using var repo = await repoFactory.CreateRepoAsync(); var id = customView.Id; var _ = await repo.GetCustomView(id) ?? throw NotFoundException.ForType(id); - await AddChange(new EditCustomViewChange(id, customView)); + await harmonyChangeWriter.AddChange(new EditCustomViewChange(id, customView)); return await repo.GetCustomView(id) ?? throw NotFoundException.ForType(id); } @@ -1059,7 +1020,7 @@ public async Task DeleteCustomView(Guid id) AssertManagerRoleForCustomViewWrite(); await using var repo = await repoFactory.CreateRepoAsync(); _ = await repo.GetCustomView(id) ?? throw NotFoundException.ForType(id); - await AddChange(new DeleteChange(id)); + await harmonyChangeWriter.AddChange(new DeleteChange(id)); } private void AssertManagerRoleForCustomViewWrite() @@ -1139,7 +1100,7 @@ public async Task CreateCommentThread(CommentThread thread, UserC firstComment.CommentThreadId = thread.Id; StampCommentAuthor(firstComment, now); - await AddChanges([ + await harmonyChangeWriter.AddChanges((IEnumerable)[ new CreateCommentThreadChange(thread), new CreateUserCommentChange(firstComment) ]); @@ -1153,7 +1114,7 @@ public async Task AddUserComment(Guid threadId, UserComment comment comment.CommentThreadId = threadId; StampCommentAuthor(comment, DateTimeOffset.UtcNow); - await AddChange(new CreateUserCommentChange(comment)); + await harmonyChangeWriter.AddChange(new CreateUserCommentChange(comment)); return await repo.GetUserComment(comment.Id) ?? throw NotFoundException.ForType(comment.Id); } @@ -1162,7 +1123,7 @@ public async Task EditUserComment(Guid commentId, string text) await using var repo = await repoFactory.CreateRepoAsync(); var comment = await repo.GetUserComment(commentId) ?? throw NotFoundException.ForType(commentId); AssertCurrentUserCanChangeComment(comment); - await AddChange(new EditUserCommentChange(commentId, text, DateTimeOffset.UtcNow)); + await harmonyChangeWriter.AddChange(new EditUserCommentChange(commentId, text, DateTimeOffset.UtcNow)); return await repo.GetUserComment(commentId) ?? throw NotFoundException.ForType(commentId); } @@ -1170,7 +1131,7 @@ public async Task SetCommentThreadStatus(Guid threadId, ThreadSta { await using var repo = await repoFactory.CreateRepoAsync(); _ = await repo.GetCommentThread(threadId) ?? throw NotFoundException.ForType(threadId); - await AddChange(new SetCommentThreadStatusChange(threadId, status, DateTimeOffset.UtcNow)); + await harmonyChangeWriter.AddChange(new SetCommentThreadStatusChange(threadId, status, DateTimeOffset.UtcNow)); return await repo.GetCommentThread(threadId) ?? throw NotFoundException.ForType(threadId); } @@ -1179,7 +1140,7 @@ public async Task DeleteUserComment(Guid commentId) await using var repo = await repoFactory.CreateRepoAsync(); var comment = await repo.GetUserComment(commentId) ?? throw NotFoundException.ForType(commentId); AssertCurrentUserCanChangeComment(comment); - await AddChange(new DeleteChange(commentId)); + await harmonyChangeWriter.AddChange(new DeleteChange(commentId)); await commentReadStatusService.RemoveUnreadComments([commentId]); } @@ -1187,7 +1148,7 @@ public async Task DeleteCommentThread(Guid threadId) { await using var repo = await repoFactory.CreateRepoAsync(); _ = await repo.GetCommentThread(threadId) ?? throw NotFoundException.ForType(threadId); - await AddChange(new DeleteChange(threadId)); + await harmonyChangeWriter.AddChange(new DeleteChange(threadId)); await commentReadStatusService.MarkThreadRead(threadId); } diff --git a/backend/FwLite/LcmCrdt/Harmony/HarmonyChangeWriter.cs b/backend/FwLite/LcmCrdt/Harmony/HarmonyChangeWriter.cs new file mode 100644 index 0000000000..497f36434b --- /dev/null +++ b/backend/FwLite/LcmCrdt/Harmony/HarmonyChangeWriter.cs @@ -0,0 +1,50 @@ +using System.Data; +using Microsoft.Extensions.Options; +using SIL.Harmony; +using SIL.Harmony.Changes; +using SIL.Harmony.Core; + +namespace LcmCrdt.Harmony; + +public class HarmonyChangeWriter( + DataModel dataModel, + CommitMetadataInterceptor commitMetadataInterceptor, + IOptions config, + CurrentProjectService projectService +) +{ + private Guid ClientId { get; } = projectService.ProjectData.ClientId; + public ProjectData ProjectData => projectService.ProjectData; + private CommitMetadata NewMetadata() + { + var metadata = new CommitMetadata + { + ClientVersion = AppVersion.Version, + //todo, if a user logs out and in with another account, this will be out of date until the next sync + AuthorName = ProjectData.LastUserName ?? config.Value.DefaultAuthorForCommits, + AuthorId = ProjectData.LastUserId + }; + commitMetadataInterceptor.Apply(metadata); + return metadata; + } + + public async Task AddChange(IChange change) + { + AssertWritable(); + var commit = await dataModel.AddChange(ClientId, change, commitMetadata: NewMetadata()); + return commit; + } + + public async Task AddChanges(IEnumerable changes) + { + AssertWritable(); + await dataModel.AddManyChanges(ClientId, changes, commitMetadata: NewMetadata); + } + + private void AssertWritable() + { + if (ProjectData.IsReadonly) + throw new ReadOnlyException( + $"project is readonly because you are logged in with the {ProjectData.Role} role. If your role recently changed, try refreshing the server project list on the home page."); + } +} From 3dc70e952a5f1492a4c550653608083bc969ab25 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 16 Aug 2026 00:38:55 +0700 Subject: [PATCH 2/4] break WritingSystem implementation out into it's own class --- .../Import/ResumableTests.cs | 2 +- .../Import/ResumableImportApi.cs | 2 +- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 53 +++++--------- .../LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs | 70 +++++++++++++++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 17 +++-- 5 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs index 7bb31a7e67..27e9e0a60d 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs @@ -211,7 +211,7 @@ Task IMiniLcmWriteApi.CreatePublication(Publication publication) ResumableTests.MaybeThrowRandom(random, 0.2); return _api.CreatePublication(publication); } - Task IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystems, BetweenPosition? between) + Task IMiniLcmWritingSystemApi.CreateWritingSystem(WritingSystem writingSystems, BetweenPosition? between) { ResumableTests.MaybeThrowRandom(random, 0.2); return _api.CreateWritingSystem(writingSystems, between); diff --git a/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs b/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs index 942b81f900..2f9bbd43f4 100644 --- a/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs +++ b/backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs @@ -70,7 +70,7 @@ async Task IMiniLcmWriteApi.CreatePublication(Publication publicati { return await HasCreated(publication, _api.GetPublications(), () => _api.CreatePublication(publication)); } - async Task IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between) + async Task IMiniLcmWritingSystemApi.CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between) { return await HasCreated(writingSystem, AsyncWs(), () => _api.CreateWritingSystem(writingSystem, between), ws => ws.Type + ws.WsId.Code); } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 5fe4fbf789..2435fa7441 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -10,6 +10,7 @@ using LcmCrdt.FullTextSearch; using LcmCrdt.Harmony; using LcmCrdt.MediaServer; +using LcmCrdt.MiniLcm; using LcmCrdt.Objects; using LinqToDB.Async; using LinqToDB.EntityFrameworkCore; @@ -28,63 +29,43 @@ public class CrdtMiniLcmApi( ILogger logger, LcmMediaService lcmMediaService, LocalCommentReadStatusService commentReadStatusService, + CrdtWritingSystemApi writingSystemApi, EntrySearchService? entrySearchService = null) : IMiniLcmApi { public ProjectData ProjectData => projectService.ProjectData; public CrdtProject Project => projectService.Project; - public async Task GetWritingSystems() + public Task GetWritingSystems() { - await using var repo = await repoFactory.CreateRepoAsync(); - var systems = await repo.WritingSystemsOrdered.ToArrayAsync(); - return new WritingSystems - { - Analysis = [.. systems.Where(ws => ws.Type == WritingSystemType.Analysis)], - Vernacular = [.. systems.Where(ws => ws.Type == WritingSystemType.Vernacular)] - }; + return writingSystemApi.GetWritingSystems(); } - public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) + public Task CreateWritingSystem(WritingSystem writingSystem, + BetweenPosition? between = null) { - await using var repo = await repoFactory.CreateRepoAsync(); - var entityId = writingSystem.MaybeId ?? Guid.NewGuid(); - var wsType = writingSystem.Type; - var exists = await repo.WritingSystems.AnyAsync(ws => ws.WsId == writingSystem.WsId && ws.Type == wsType); - if (exists) throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} ({wsType}) already exists"); - var betweenIds = between is null ? null : await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, wsType))?.Id); - var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(ws => ws.Type == wsType), betweenIds); - await harmonyChangeWriter.AddChange(new CreateWritingSystemChange(writingSystem, entityId, order)); - return await repo.GetWritingSystem(writingSystem.WsId, wsType) ?? throw NotFoundException.ForWs(writingSystem); + return writingSystemApi.CreateWritingSystem(writingSystem, between); } - public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) + public Task UpdateWritingSystem(WritingSystemId id, + WritingSystemType type, + UpdateObjectInput update) { - await using var repo = await repoFactory.CreateRepoAsync(); - var ws = await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); - var patchChange = new JsonPatchChange(ws.Id, update.Patch); - await harmonyChangeWriter.AddChange(patchChange); - return await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); + return writingSystemApi.UpdateWritingSystem(id, type, update); } - public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null) + public Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null) { - await WritingSystemSync.Sync(before, after, api ?? this); - return await GetWritingSystem(after.WsId, after.Type) ?? throw NotFoundException.ForWs(after); + return writingSystemApi.UpdateWritingSystem(before, after, api ?? this); } - public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) + public Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) { - await using var repo = await repoFactory.CreateRepoAsync(); - var ws = await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); - var betweenIds = await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, type))?.Id); - var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds); - await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(ws.Id, order)); + return writingSystemApi.MoveWritingSystem(id, type, between); } - public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) + public Task GetWritingSystem(WritingSystemId id, WritingSystemType type) { - await using var repo = await repoFactory.CreateRepoAsync(); - return await repo.GetWritingSystem(id, type); + return writingSystemApi.GetWritingSystem(id, type); } public async IAsyncEnumerable GetPartsOfSpeech() diff --git a/backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs b/backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs new file mode 100644 index 0000000000..991db49b35 --- /dev/null +++ b/backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs @@ -0,0 +1,70 @@ + + +using LcmCrdt.Changes; +using LcmCrdt.Data; +using LcmCrdt.Harmony; +using LinqToDB.Async; +using MiniLcm.Exceptions; +using MiniLcm.SyncHelpers; + +namespace LcmCrdt.MiniLcm; + +public class CrdtWritingSystemApi( + MiniLcmRepositoryFactory repoFactory, + HarmonyChangeWriter harmonyChangeWriter +) : IMiniLcmWritingSystemApi +{ + public async Task GetWritingSystems() + { + await using var repo = await repoFactory.CreateRepoAsync(); + var systems = await repo.WritingSystemsOrdered.ToArrayAsync(); + return new WritingSystems + { + Analysis = [.. systems.Where(ws => ws.Type == WritingSystemType.Analysis)], + Vernacular = [.. systems.Where(ws => ws.Type == WritingSystemType.Vernacular)] + }; + } + + public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) + { + await using var repo = await repoFactory.CreateRepoAsync(); + var entityId = writingSystem.MaybeId ?? Guid.NewGuid(); + var wsType = writingSystem.Type; + var exists = await repo.WritingSystems.AnyAsync(ws => ws.WsId == writingSystem.WsId && ws.Type == wsType); + if (exists) throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} ({wsType}) already exists"); + var betweenIds = between is null ? null : await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, wsType))?.Id); + var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(ws => ws.Type == wsType), betweenIds); + await harmonyChangeWriter.AddChange(new CreateWritingSystemChange(writingSystem, entityId, order)); + return await repo.GetWritingSystem(writingSystem.WsId, wsType) ?? throw NotFoundException.ForWs(writingSystem); + } + + public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) + { + await using var repo = await repoFactory.CreateRepoAsync(); + var ws = await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); + var patchChange = new JsonPatchChange(ws.Id, update.Patch); + await harmonyChangeWriter.AddChange(patchChange); + return await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); + } + + public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi api) + { + await WritingSystemSync.Sync(before, after, api); + return await GetWritingSystem(after.WsId, after.Type) ?? throw NotFoundException.ForWs(after); + } + + public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) + { + await using var repo = await repoFactory.CreateRepoAsync(); + var ws = await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); + var betweenIds = await between.MapAsync(async wsId => wsId is null ? null : (await repo.GetWritingSystem(wsId.Value, type))?.Id); + var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds); + await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(ws.Id, order)); + } + + public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) + { + await using var repo = await repoFactory.CreateRepoAsync(); + return await repo.GetWritingSystem(id, type); + } +} diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index a09f955573..3f663c301c 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -6,16 +6,23 @@ namespace MiniLcm; -public interface IMiniLcmWriteApi +// Note there's no Task DeleteWritingSystem(Guid id) because deleting writing systems needs careful consideration, as it can cause a massive cascade of data deletion +public interface IMiniLcmWritingSystemApi { Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null); - Task UpdateWritingSystem(WritingSystemId id, + Task UpdateWritingSystem( + WritingSystemId id, WritingSystemType type, - UpdateObjectInput update); - Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null); - // Note there's no Task DeleteWritingSystem(Guid id) because deleting writing systems needs careful consideration, as it can cause a massive cascade of data deletion + UpdateObjectInput update + ); + + Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi api); Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between); +} + +public interface IMiniLcmWriteApi : IMiniLcmWritingSystemApi +{ #region PartOfSpeech Task CreatePartOfSpeech(PartOfSpeech partOfSpeech); From 9d56fa201f9556aa08ce7f3513c1af9585501849 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 16 Aug 2026 00:49:03 +0700 Subject: [PATCH 3/4] fix UpdateWritingSystem --- .../FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 4 +++- .../FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs | 4 +++- .../FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs | 4 +++- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 6 ++++-- .../{MiniLcm => MiniLcmImp}/CrdtWritingSystemApi.cs | 10 +++++----- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 6 +++++- .../MiniLcmApiWriteNormalizationWrapper.cs | 4 +++- .../FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs | 2 +- 8 files changed, 27 insertions(+), 13 deletions(-) rename backend/FwLite/LcmCrdt/{MiniLcm => MiniLcmImp}/CrdtWritingSystemApi.cs (94%) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index f2256115b5..dfa509ea1c 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -221,7 +221,9 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem", return await GetWritingSystem(id, type) ?? throw new NullReferenceException($"unable to find writing system with id {id}"); } - public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null) + public async Task UpdateWritingSystem(WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api) { await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem", "Revert WritingSystem", diff --git a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs index 522b57fe87..7a1e59c90a 100644 --- a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs @@ -38,7 +38,9 @@ public async Task UpdateWritingSystem(WritingSystemId id, return await _api.UpdateWritingSystem(id, type, update); } - public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api) + public async Task UpdateWritingSystem(WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api) { RunRecords.Add(new RunRecord(nameof(UpdateWritingSystem), $"Update {after.Type} writing system {after.WsId}")); return await _api.UpdateWritingSystem(before, after, api); diff --git a/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs index e7d903a445..fd11cb506d 100644 --- a/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs @@ -32,7 +32,9 @@ public async Task UpdateWritingSystem(WritingSystemId id, }).First(w => w.WsId == id); } - public Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api) + public Task UpdateWritingSystem(WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api) { return Task.FromResult(after); } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 2435fa7441..521edf7522 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -10,7 +10,7 @@ using LcmCrdt.FullTextSearch; using LcmCrdt.Harmony; using LcmCrdt.MediaServer; -using LcmCrdt.MiniLcm; +using LcmCrdt.MiniLcmImp; using LcmCrdt.Objects; using LinqToDB.Async; using LinqToDB.EntityFrameworkCore; @@ -53,7 +53,9 @@ public Task UpdateWritingSystem(WritingSystemId id, return writingSystemApi.UpdateWritingSystem(id, type, update); } - public Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null) + public Task UpdateWritingSystem(WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api = null) { return writingSystemApi.UpdateWritingSystem(before, after, api ?? this); } diff --git a/backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs b/backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs similarity index 94% rename from backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs rename to backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs index 991db49b35..e81491fc82 100644 --- a/backend/FwLite/LcmCrdt/MiniLcm/CrdtWritingSystemApi.cs +++ b/backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs @@ -1,5 +1,3 @@ - - using LcmCrdt.Changes; using LcmCrdt.Data; using LcmCrdt.Harmony; @@ -7,7 +5,7 @@ using MiniLcm.Exceptions; using MiniLcm.SyncHelpers; -namespace LcmCrdt.MiniLcm; +namespace LcmCrdt.MiniLcmImp; public class CrdtWritingSystemApi( MiniLcmRepositoryFactory repoFactory, @@ -47,9 +45,11 @@ public async Task UpdateWritingSystem(WritingSystemId id, Writing return await repo.GetWritingSystem(id, type) ?? throw NotFoundException.ForWs(id, type); } - public async Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi api) + public async Task UpdateWritingSystem(WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api = null) { - await WritingSystemSync.Sync(before, after, api); + await WritingSystemSync.Sync(before, after, api ?? this); return await GetWritingSystem(after.WsId, after.Type) ?? throw NotFoundException.ForWs(after); } diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 3f663c301c..d8feccf24b 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -17,7 +17,11 @@ Task UpdateWritingSystem( UpdateObjectInput update ); - Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi api); + Task UpdateWritingSystem( + WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api = null + ); Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between); } diff --git a/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs b/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs index a43847bb50..9f4f4bbb45 100644 --- a/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs +++ b/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs @@ -65,7 +65,9 @@ public Task UpdateWritingSystem(WritingSystemId id, WritingSystem } - public Task UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null) + public Task UpdateWritingSystem(WritingSystem before, + WritingSystem after, + IMiniLcmWritingSystemApi? api) { return _api.UpdateWritingSystem(before, after, api); } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs index cb58ec4e48..4a79d4f71a 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs @@ -20,7 +20,7 @@ public static async Task Sync(WritingSystem[] beforeWritingSystems, return await writingSystemsDiffApi.Diff(beforeWritingSystems, afterWritingSystems); } - public static async Task Sync(WritingSystem beforeWs, WritingSystem afterWs, IMiniLcmApi api) + public static async Task Sync(WritingSystem beforeWs, WritingSystem afterWs, IMiniLcmWritingSystemApi api) { var updateObjectInput = WritingSystemDiffToUpdate(beforeWs, afterWs); if (updateObjectInput is not null) await api.UpdateWritingSystem(afterWs.WsId, afterWs.Type, updateObjectInput); From 2c61a0b72c0e64cf32c6eb874110debdf76dc339 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Sun, 16 Aug 2026 01:04:40 +0700 Subject: [PATCH 4/4] register some missing dependencies, pull the read side out into it's own api --- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 4 ++++ .../LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs | 14 +++++++------- backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 6 +++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 5922d26bb3..c729f9f551 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -34,6 +34,8 @@ using LcmCrdt.Project; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Text.Json.Serialization.Metadata; +using LcmCrdt.Harmony; +using LcmCrdt.MiniLcmImp; using MiniLcm.Media; namespace LcmCrdt; @@ -72,6 +74,8 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se harmonyConfig.LocalResourceCachePath = Path.Combine(lcmConfig.Value.ProjectPath, "localResourcesCache"); }); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddMiniLcmValidators(); diff --git a/backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs b/backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs index e81491fc82..4ea806ff9e 100644 --- a/backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs +++ b/backend/FwLite/LcmCrdt/MiniLcmImp/CrdtWritingSystemApi.cs @@ -10,7 +10,7 @@ namespace LcmCrdt.MiniLcmImp; public class CrdtWritingSystemApi( MiniLcmRepositoryFactory repoFactory, HarmonyChangeWriter harmonyChangeWriter -) : IMiniLcmWritingSystemApi +) : IMiniLcmWritingSystemApi, IMiniLcmReadWritingSystemApi { public async Task GetWritingSystems() { @@ -23,6 +23,12 @@ public async Task GetWritingSystems() }; } + public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) + { + await using var repo = await repoFactory.CreateRepoAsync(); + return await repo.GetWritingSystem(id, type); + } + public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? between = null) { await using var repo = await repoFactory.CreateRepoAsync(); @@ -61,10 +67,4 @@ public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, var order = await OrderPicker.PickOrder(repo.WritingSystems.Where(s => s.Type == type), betweenIds); await harmonyChangeWriter.AddChange(new Changes.SetOrderChange(ws.Id, order)); } - - public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) - { - await using var repo = await repoFactory.CreateRepoAsync(); - return await repo.GetWritingSystem(id, type); - } } diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index 25105cbd6b..311c8cc383 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs @@ -7,10 +7,14 @@ namespace MiniLcm; -public interface IMiniLcmReadApi +public interface IMiniLcmReadWritingSystemApi { Task GetWritingSystems(); Task GetWritingSystem(WritingSystemId id, WritingSystemType type); +} + +public interface IMiniLcmReadApi : IMiniLcmReadWritingSystemApi +{ IAsyncEnumerable GetPartsOfSpeech(); IAsyncEnumerable GetPublications(); IAsyncEnumerable GetSemanticDomains();