From 63a7a802798fea0b060498a404f761adc4d2cab2 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 17 Oct 2024 14:54:55 +0700 Subject: [PATCH 01/38] refactor diff code out of the sync service --- .../MultiStringDiffTests.cs | 15 +- .../UpdateDiffTests.cs | 9 +- .../CrdtFwdataProjectSyncService.cs | 223 +----------------- .../SyncHelpers/DiffCollection.cs | 39 +++ .../SyncHelpers/EntrySync.cs | 64 +++++ .../SyncHelpers/ExampleSentenceSync.cs | 61 +++++ .../SyncHelpers/MultiStringDiff.cs | 33 +++ .../SyncHelpers/SenseSync.cs | 64 +++++ 8 files changed, 277 insertions(+), 231 deletions(-) create mode 100644 backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs create mode 100644 backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs create mode 100644 backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs create mode 100644 backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs create mode 100644 backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs diff --git a/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs index 07b0d7910..13b7fdb2f 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs @@ -1,4 +1,5 @@ -using MiniLcm.Models; +using FwLiteProjectSync.SyncHelpers; +using MiniLcm.Models; using Spart.Parsers; using SystemTextJsonPatch.Operations; @@ -13,7 +14,7 @@ public void DiffEmptyDoesNothing() { var previous = new MultiString(); var current = new MultiString(); - var result = CrdtFwdataProjectSyncService.GetMultiStringDiff("test", previous, current); + var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); result.Should().BeEmpty(); } @@ -23,7 +24,7 @@ public void DiffOneToEmptyAddsOne() var previous = new MultiString(); var current = new MultiString(); current.Values.Add("en", "hello"); - var result = CrdtFwdataProjectSyncService.GetMultiStringDiff("test", previous, current); + var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); result.Should().BeEquivalentTo([ new Operation("add", "/test/en", null, "hello") ]); @@ -36,7 +37,7 @@ public void DiffOneToOneReplacesOne() previous.Values.Add("en", "hello"); var current = new MultiString(); current.Values.Add("en", "world"); - var result = CrdtFwdataProjectSyncService.GetMultiStringDiff("test", previous, current); + var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); result.Should().BeEquivalentTo([ new Operation("replace", "/test/en", null, "world") ]); @@ -48,7 +49,7 @@ public void DiffNoneToOneRemovesOne() var previous = new MultiString(); previous.Values.Add("en", "hello"); var current = new MultiString(); - var result = CrdtFwdataProjectSyncService.GetMultiStringDiff("test", previous, current); + var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); result.Should().BeEquivalentTo([ new Operation("remove", "/test/en", null) ]); @@ -63,7 +64,7 @@ public void DiffMatchingDoesNothing() current.Values.Add("en", new string(['h', 'e', 'l', 'l', 'o'])); //ensure the strings are not the same instance ReferenceEquals(previous["en"], current["en"]).Should().BeFalse(); - var result = CrdtFwdataProjectSyncService.GetMultiStringDiff("test", previous, current); + var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); result.Should().BeEmpty(); } @@ -76,7 +77,7 @@ public void DiffWithMultipleAddsRemovesEach() var current = new MultiString(); current.Values.Add("en", "world"); current.Values.Add("fr", "monde"); - var result = CrdtFwdataProjectSyncService.GetMultiStringDiff("test", previous, current); + var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); result.Should().BeEquivalentTo([ new Operation("replace", "/test/en", null, "world"), new Operation("add", "/test/fr", null, "monde"), diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index 6bf34b549..871806a14 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -1,4 +1,5 @@ -using FwLiteProjectSync.Tests.Fixtures; +using FwLiteProjectSync.SyncHelpers; +using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; using Soenneker.Utils.AutoBogus; using Soenneker.Utils.AutoBogus.Config; @@ -17,7 +18,7 @@ public void EntryDiffShouldUpdateAllFields() { var previous = new Entry(); var current = _autoFaker.Generate(); - var entryDiffToUpdate = CrdtFwdataProjectSyncService.EntryDiffToUpdate(previous, current); + var entryDiffToUpdate = EntrySync.EntryDiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(entryDiffToUpdate); entryDiffToUpdate.Apply(previous); previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id) @@ -32,7 +33,7 @@ public async Task SenseDiffShouldUpdateAllFields() { var previous = new Sense(); var current = _autoFaker.Generate(); - var senseDiffToUpdate = await CrdtFwdataProjectSyncService.SenseDiffToUpdate(previous, current); + var senseDiffToUpdate = await SenseSync.SenseDiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(senseDiffToUpdate); senseDiffToUpdate.Apply(previous); previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id).Excluding(x => x.ExampleSentences)); @@ -43,7 +44,7 @@ public void ExampleSentenceDiffShouldUpdateAllFields() { var previous = new ExampleSentence(); var current = _autoFaker.Generate(); - var exampleSentenceDiffToUpdate = CrdtFwdataProjectSyncService.ExampleDiffToUpdate(previous, current); + var exampleSentenceDiffToUpdate = ExampleSentenceSync.DiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(exampleSentenceDiffToUpdate); exampleSentenceDiffToUpdate.Apply(previous); previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id)); diff --git a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs index 4ccff1d34..87ad8007b 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -1,5 +1,6 @@ using System.Text.Json; using FwDataMiniLcmBridge.Api; +using FwLiteProjectSync.SyncHelpers; using LcmCrdt; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -45,10 +46,10 @@ private async Task Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, //todo sync complex form types, parts of speech, semantic domains, writing systems var currentFwDataEntries = await fwdataApi.GetEntries().ToArrayAsync(); - var crdtChanges = await EntrySync(currentFwDataEntries, projectSnapshot.Entries, crdtApi); + var crdtChanges = await EntrySync.Sync(currentFwDataEntries, projectSnapshot.Entries, crdtApi); LogDryRun(crdtApi, "crdt"); - var fwdataChanges = await EntrySync(await crdtApi.GetEntries().ToArrayAsync(), currentFwDataEntries, fwdataApi); + var fwdataChanges = await EntrySync.Sync(await crdtApi.GetEntries().ToArrayAsync(), currentFwDataEntries, fwdataApi); LogDryRun(fwdataApi, "fwdata"); //todo push crdt changes to lexbox @@ -84,222 +85,4 @@ private async Task SaveProjectSnapshot(string projectName, ProjectSnapshot proje await JsonSerializer.SerializeAsync(file, projectSnapshot); } - private async Task EntrySync(Entry[] currentEntries, - Entry[] previousEntries, - IMiniLcmApi api) - { - return await DiffCollection(api, - previousEntries, - currentEntries, - async (api, currentEntry) => - { - await api.CreateEntry(currentEntry); - return 1; - }, - async (api, previousEntry) => - { - await api.DeleteEntry(previousEntry.Id); - return 1; - }, - async (api, previousEntry, currentEntry) => - { - var updateObjectInput = EntryDiffToUpdate(previousEntry, currentEntry); - if (updateObjectInput is not null) await api.UpdateEntry(currentEntry.Id, updateObjectInput); - var changes = await SenseSync(currentEntry.Id, currentEntry.Senses, previousEntry.Senses, api); - return changes + (updateObjectInput is null ? 0 : 1); - }); - } - - private async Task SenseSync(Guid entryId, - IList currentSenses, - IList previousSenses, - IMiniLcmApi api) - { - return await DiffCollection(api, - previousSenses, - currentSenses, - async (api, currentSense) => - { - await api.CreateSense(entryId, currentSense); - return 1; - }, - async (api, previousSense) => - { - await api.DeleteSense(entryId, previousSense.Id); - return 1; - }, - async (api, previousSense, currentSense) => - { - var updateObjectInput = await SenseDiffToUpdate(previousSense, currentSense); - if (updateObjectInput is not null) await api.UpdateSense(entryId, previousSense.Id, updateObjectInput); - var changes = await ExampleSentenceSync(entryId, - previousSense.Id, - currentSense.ExampleSentences, - previousSense.ExampleSentences, - api); - return changes + (updateObjectInput is null ? 0 : 1); - }); - } - - private async Task ExampleSentenceSync(Guid entryId, - Guid senseId, - IList currentExampleSentences, - IList previousExampleSentences, - IMiniLcmApi api) - { - return await DiffCollection(api, - previousExampleSentences, - currentExampleSentences, - async (api, currentExampleSentence) => - { - await api.CreateExampleSentence(entryId, senseId, currentExampleSentence); - return 1; - }, - async (api, previousExampleSentence) => - { - await api.DeleteExampleSentence(entryId, senseId, previousExampleSentence.Id); - return 1; - }, - async (api, previousExampleSentence, currentExampleSentence) => - { - var updateObjectInput = ExampleDiffToUpdate(previousExampleSentence, currentExampleSentence); - if (updateObjectInput is null) return 0; - await api.UpdateExampleSentence(entryId, senseId, previousExampleSentence.Id, updateObjectInput); - return 1; - }); - } - - public static UpdateObjectInput? EntryDiffToUpdate(Entry previousEntry, Entry currentEntry) - { - JsonPatchDocument patchDocument = new(); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(Entry.LexemeForm), - previousEntry.LexemeForm, - currentEntry.LexemeForm)); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(Entry.CitationForm), - previousEntry.CitationForm, - currentEntry.CitationForm)); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(Entry.Note), - previousEntry.Note, - currentEntry.Note)); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(Entry.LiteralMeaning), - previousEntry.LiteralMeaning, - currentEntry.LiteralMeaning)); - if (patchDocument.Operations.Count == 0) return null; - return new UpdateObjectInput(patchDocument); - } - - public static async Task?> SenseDiffToUpdate(Sense previousSense, Sense currentSense) - { - JsonPatchDocument patchDocument = new(); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(Sense.Gloss), - previousSense.Gloss, - currentSense.Gloss)); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(Sense.Definition), - previousSense.Definition, - currentSense.Definition)); - if (previousSense.PartOfSpeech != currentSense.PartOfSpeech) - { - patchDocument.Replace(sense => sense.PartOfSpeech, currentSense.PartOfSpeech); - } - - if (previousSense.PartOfSpeechId != currentSense.PartOfSpeechId) - { - patchDocument.Replace(sense => sense.PartOfSpeechId, currentSense.PartOfSpeechId); - } - - await DiffCollection(null!, - previousSense.SemanticDomains, - currentSense.SemanticDomains, - (_, domain) => - { - patchDocument.Add(sense => sense.SemanticDomains, domain); - return Task.FromResult(1); - }, - (_, previousDomain) => - { - patchDocument.Remove(sense => sense.SemanticDomains, previousSense.SemanticDomains.IndexOf(previousDomain)); - return Task.FromResult(1); - }, - (_, previousDomain, currentDomain) => - { - //do nothing, semantic domains are not editable here - return Task.FromResult(0); - }); - if (patchDocument.Operations.Count == 0) return null; - return new UpdateObjectInput(patchDocument); - } - - public static UpdateObjectInput? ExampleDiffToUpdate(ExampleSentence previousExampleSentence, - ExampleSentence currentExampleSentence) - { - JsonPatchDocument patchDocument = new(); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(ExampleSentence.Sentence), - previousExampleSentence.Sentence, - currentExampleSentence.Sentence)); - patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(ExampleSentence.Translation), - previousExampleSentence.Translation, - currentExampleSentence.Translation)); - if (previousExampleSentence.Reference != currentExampleSentence.Reference) - { - patchDocument.Replace(exampleSentence => exampleSentence.Reference, currentExampleSentence.Reference); - } - - if (patchDocument.Operations.Count == 0) return null; - return new UpdateObjectInput(patchDocument); - } - - public static IEnumerable> GetMultiStringDiff(string path, MultiString previous, MultiString current) - where T : class - { - var currentKeys = current.Values.Keys.ToHashSet(); - foreach (var (key, previousValue) in previous.Values) - { - if (current.Values.TryGetValue(key, out var currentValue)) - { - if (!previousValue.Equals(currentValue)) - yield return new Operation("replace", $"/{path}/{key}", null, currentValue); - } - else - { - yield return new Operation("remove", $"/{path}/{key}", null); - } - - currentKeys.Remove(key); - } - - foreach (var key in currentKeys) - { - yield return new Operation("add", $"/{path}/{key}", null, current.Values[key]); - } - } - - private static async Task DiffCollection( - IMiniLcmApi api, - IList previous, - IList current, - Func> add, - Func> remove, - Func> replace) where T : IObjectWithId - { - var changes = 0; - var currentEntriesDict = current.ToDictionary(entry => entry.Id); - foreach (var previousEntry in previous) - { - if (currentEntriesDict.TryGetValue(previousEntry.Id, out var currentEntry)) - { - changes += await replace(api, previousEntry, currentEntry); - } - else - { - changes += await remove(api, previousEntry); - } - - currentEntriesDict.Remove(previousEntry.Id); - } - foreach (var value in currentEntriesDict.Values) - { - changes += await add(api, value); - } - return changes; - } } diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs new file mode 100644 index 000000000..c31d0f9cc --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs @@ -0,0 +1,39 @@ +using MiniLcm; +using MiniLcm.Models; + +namespace FwLiteProjectSync.SyncHelpers; + +public static class DiffCollection +{ + public static async Task Diff( + IMiniLcmApi api, + IList previous, + IList current, + Func> add, + Func> remove, + Func> replace) where T : IObjectWithId + { + var changes = 0; + var currentEntriesDict = current.ToDictionary(entry => entry.Id); + foreach (var previousEntry in previous) + { + if (currentEntriesDict.TryGetValue(previousEntry.Id, out var currentEntry)) + { + changes += await replace(api, previousEntry, currentEntry); + } + else + { + changes += await remove(api, previousEntry); + } + + currentEntriesDict.Remove(previousEntry.Id); + } + + foreach (var value in currentEntriesDict.Values) + { + changes += await add(api, value); + } + + return changes; + } +} diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs new file mode 100644 index 000000000..a54e7ff2d --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs @@ -0,0 +1,64 @@ +using MiniLcm; +using MiniLcm.Models; +using SystemTextJsonPatch; + +namespace FwLiteProjectSync.SyncHelpers; + +public static class EntrySync +{ + public static async Task Sync(Entry[] currentEntries, + Entry[] previousEntries, + IMiniLcmApi api) + { + Func> add = static async (api, currentEntry) => + { + await api.CreateEntry(currentEntry); + return 1; + }; + Func> remove = static async (api, previousEntry) => + { + await api.DeleteEntry(previousEntry.Id); + return 1; + }; + Func> replace = static async (api, previousEntry, currentEntry) => await Sync(currentEntry, previousEntry, api); + return await DiffCollection.Diff(api, previousEntries, currentEntries, add, remove, replace); + } + + public static async Task Sync(Entry currentEntry, Entry previousEntry, IMiniLcmApi api) + { + var updateObjectInput = EntryDiffToUpdate(previousEntry, currentEntry); + if (updateObjectInput is not null) await api.UpdateEntry(currentEntry.Id, updateObjectInput); + var changes = await SensesSync(currentEntry.Id, currentEntry.Senses, previousEntry.Senses, api); + return changes + (updateObjectInput is null ? 0 : 1); + } + + private static async Task SensesSync(Guid entryId, + IList currentSenses, + IList previousSenses, + IMiniLcmApi api) + { + Func> add = async (api, currentSense) => + { + await api.CreateSense(entryId, currentSense); + return 1; + }; + Func> remove = async (api, previousSense) => + { + await api.DeleteSense(entryId, previousSense.Id); + return 1; + }; + Func> replace = async (api, previousSense, currentSense) => await SenseSync.Sync(entryId, currentSense, previousSense, api); + return await DiffCollection.Diff(api, previousSenses, currentSenses, add, remove, replace); + } + + public static UpdateObjectInput? EntryDiffToUpdate(Entry previousEntry, Entry currentEntry) + { + JsonPatchDocument patchDocument = new(); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LexemeForm), previousEntry.LexemeForm, currentEntry.LexemeForm)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.CitationForm), previousEntry.CitationForm, currentEntry.CitationForm)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.Note), previousEntry.Note, currentEntry.Note)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LiteralMeaning), previousEntry.LiteralMeaning, currentEntry.LiteralMeaning)); + if (patchDocument.Operations.Count == 0) return null; + return new UpdateObjectInput(patchDocument); + } +} diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs new file mode 100644 index 000000000..42dc075f9 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs @@ -0,0 +1,61 @@ +using MiniLcm; +using MiniLcm.Models; +using SystemTextJsonPatch; + +namespace FwLiteProjectSync.SyncHelpers; + +public static class ExampleSentenceSync +{ + public static async Task Sync(Guid entryId, + Guid senseId, + IList currentExampleSentences, + IList previousExampleSentences, + IMiniLcmApi api) + { + Func> add = async (api, currentExampleSentence) => + { + await api.CreateExampleSentence(entryId, senseId, currentExampleSentence); + return 1; + }; + Func> remove = async (api, previousExampleSentence) => + { + await api.DeleteExampleSentence(entryId, senseId, previousExampleSentence.Id); + return 1; + }; + Func> replace = + async (api, previousExampleSentence, currentExampleSentence) => + { + var updateObjectInput = DiffToUpdate(previousExampleSentence, currentExampleSentence); + if (updateObjectInput is null) return 0; + await api.UpdateExampleSentence(entryId, senseId, previousExampleSentence.Id, updateObjectInput); + return 1; + }; + return await DiffCollection.Diff(api, + previousExampleSentences, + currentExampleSentences, + add, + remove, + replace); + } + + public static UpdateObjectInput? DiffToUpdate(ExampleSentence previousExampleSentence, + ExampleSentence currentExampleSentence) + { + JsonPatchDocument patchDocument = new(); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff( + nameof(ExampleSentence.Sentence), + previousExampleSentence.Sentence, + currentExampleSentence.Sentence)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff( + nameof(ExampleSentence.Translation), + previousExampleSentence.Translation, + currentExampleSentence.Translation)); + if (previousExampleSentence.Reference != currentExampleSentence.Reference) + { + patchDocument.Replace(exampleSentence => exampleSentence.Reference, currentExampleSentence.Reference); + } + + if (patchDocument.Operations.Count == 0) return null; + return new UpdateObjectInput(patchDocument); + } +} diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs new file mode 100644 index 000000000..576373bf7 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs @@ -0,0 +1,33 @@ +using MiniLcm.Models; +using SystemTextJsonPatch.Operations; + +namespace FwLiteProjectSync.SyncHelpers; + +public static class MultiStringDiff +{ + public static IEnumerable> GetMultiStringDiff(string path, + MultiString previous, + MultiString current) where T : class + { + var currentKeys = current.Values.Keys.ToHashSet(); + foreach (var (key, previousValue) in previous.Values) + { + if (current.Values.TryGetValue(key, out var currentValue)) + { + if (!previousValue.Equals(currentValue)) + yield return new Operation("replace", $"/{path}/{key}", null, currentValue); + } + else + { + yield return new Operation("remove", $"/{path}/{key}", null); + } + + currentKeys.Remove(key); + } + + foreach (var key in currentKeys) + { + yield return new Operation("add", $"/{path}/{key}", null, current.Values[key]); + } + } +} diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs new file mode 100644 index 000000000..27c9cecd9 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs @@ -0,0 +1,64 @@ +using MiniLcm; +using MiniLcm.Models; +using SystemTextJsonPatch; + +namespace FwLiteProjectSync.SyncHelpers; + +public static class SenseSync +{ + public static async Task Sync(Guid entryId, + Sense currentSense, + Sense previousSense, + IMiniLcmApi api) + { + var updateObjectInput = await SenseDiffToUpdate(previousSense, currentSense); + if (updateObjectInput is not null) await api.UpdateSense(entryId, previousSense.Id, updateObjectInput); + var changes = await ExampleSentenceSync.Sync(entryId, + previousSense.Id, + currentSense.ExampleSentences, + previousSense.ExampleSentences, + api); + return changes + (updateObjectInput is null ? 0 : 1); + } + + public static async Task?> SenseDiffToUpdate(Sense previousSense, Sense currentSense) + { + JsonPatchDocument patchDocument = new(); + patchDocument.Operations.AddRange( + MultiStringDiff.GetMultiStringDiff(nameof(Sense.Gloss), previousSense.Gloss, currentSense.Gloss)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Sense.Definition), + previousSense.Definition, + currentSense.Definition)); + if (previousSense.PartOfSpeech != currentSense.PartOfSpeech) + { + patchDocument.Replace(sense => sense.PartOfSpeech, currentSense.PartOfSpeech); + } + + if (previousSense.PartOfSpeechId != currentSense.PartOfSpeechId) + { + patchDocument.Replace(sense => sense.PartOfSpeechId, currentSense.PartOfSpeechId); + } + + await DiffCollection.Diff(null!, + previousSense.SemanticDomains, + currentSense.SemanticDomains, + (_, domain) => + { + patchDocument.Add(sense => sense.SemanticDomains, domain); + return Task.FromResult(1); + }, + (_, previousDomain) => + { + patchDocument.Remove(sense => sense.SemanticDomains, + previousSense.SemanticDomains.IndexOf(previousDomain)); + return Task.FromResult(1); + }, + (_, previousDomain, currentDomain) => + { + //do nothing, semantic domains are not editable here + return Task.FromResult(0); + }); + if (patchDocument.Operations.Count == 0) return null; + return new UpdateObjectInput(patchDocument); + } +} From f39073f22c742030be9648df609d0115e5b68bb0 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 17 Oct 2024 15:07:16 +0700 Subject: [PATCH 02/38] change terminiology was current/previous now before after --- .../MultiStringDiffTests.cs | 58 +++++++++---------- .../UpdateDiffTests.cs | 30 +++++----- .../SyncHelpers/DiffCollection.cs | 18 +++--- .../SyncHelpers/EntrySync.cs | 50 ++++++++-------- .../SyncHelpers/ExampleSentenceSync.cs | 38 ++++++------ .../SyncHelpers/MultiStringDiff.cs | 20 +++---- .../SyncHelpers/SenseSync.cs | 40 ++++++------- 7 files changed, 127 insertions(+), 127 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs index 13b7fdb2f..75dd5e927 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs @@ -12,19 +12,19 @@ private record Placeholder(); [Fact] public void DiffEmptyDoesNothing() { - var previous = new MultiString(); - var current = new MultiString(); - var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); + var before = new MultiString(); + var after = new MultiString(); + var result = MultiStringDiff.GetMultiStringDiff("test", before, after); result.Should().BeEmpty(); } [Fact] public void DiffOneToEmptyAddsOne() { - var previous = new MultiString(); - var current = new MultiString(); - current.Values.Add("en", "hello"); - var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); + var before = new MultiString(); + var after = new MultiString(); + after.Values.Add("en", "hello"); + var result = MultiStringDiff.GetMultiStringDiff("test", before, after); result.Should().BeEquivalentTo([ new Operation("add", "/test/en", null, "hello") ]); @@ -33,11 +33,11 @@ public void DiffOneToEmptyAddsOne() [Fact] public void DiffOneToOneReplacesOne() { - var previous = new MultiString(); - previous.Values.Add("en", "hello"); - var current = new MultiString(); - current.Values.Add("en", "world"); - var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); + var before = new MultiString(); + before.Values.Add("en", "hello"); + var after = new MultiString(); + after.Values.Add("en", "world"); + var result = MultiStringDiff.GetMultiStringDiff("test", before, after); result.Should().BeEquivalentTo([ new Operation("replace", "/test/en", null, "world") ]); @@ -46,10 +46,10 @@ public void DiffOneToOneReplacesOne() [Fact] public void DiffNoneToOneRemovesOne() { - var previous = new MultiString(); - previous.Values.Add("en", "hello"); - var current = new MultiString(); - var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); + var before = new MultiString(); + before.Values.Add("en", "hello"); + var after = new MultiString(); + var result = MultiStringDiff.GetMultiStringDiff("test", before, after); result.Should().BeEquivalentTo([ new Operation("remove", "/test/en", null) ]); @@ -58,26 +58,26 @@ public void DiffNoneToOneRemovesOne() [Fact] public void DiffMatchingDoesNothing() { - var previous = new MultiString(); - previous.Values.Add("en", "hello"); - var current = new MultiString(); - current.Values.Add("en", new string(['h', 'e', 'l', 'l', 'o'])); + var before = new MultiString(); + before.Values.Add("en", "hello"); + var after = new MultiString(); + after.Values.Add("en", new string(['h', 'e', 'l', 'l', 'o'])); //ensure the strings are not the same instance - ReferenceEquals(previous["en"], current["en"]).Should().BeFalse(); - var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); + ReferenceEquals(before["en"], after["en"]).Should().BeFalse(); + var result = MultiStringDiff.GetMultiStringDiff("test", before, after); result.Should().BeEmpty(); } [Fact] public void DiffWithMultipleAddsRemovesEach() { - var previous = new MultiString(); - previous.Values.Add("en", "hello"); - previous.Values.Add("es", "hola"); - var current = new MultiString(); - current.Values.Add("en", "world"); - current.Values.Add("fr", "monde"); - var result = MultiStringDiff.GetMultiStringDiff("test", previous, current); + var before = new MultiString(); + before.Values.Add("en", "hello"); + before.Values.Add("es", "hola"); + var after = new MultiString(); + after.Values.Add("en", "world"); + after.Values.Add("fr", "monde"); + var result = MultiStringDiff.GetMultiStringDiff("test", before, after); result.Should().BeEquivalentTo([ new Operation("replace", "/test/en", null, "world"), new Operation("add", "/test/fr", null, "monde"), diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index 871806a14..a0c4f59f9 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -16,12 +16,12 @@ public class UpdateDiffTests [Fact] public void EntryDiffShouldUpdateAllFields() { - var previous = new Entry(); - var current = _autoFaker.Generate(); - var entryDiffToUpdate = EntrySync.EntryDiffToUpdate(previous, current); + var before = new Entry(); + var after = _autoFaker.Generate(); + var entryDiffToUpdate = EntrySync.EntryDiffToUpdate(before, after); ArgumentNullException.ThrowIfNull(entryDiffToUpdate); - entryDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id) + entryDiffToUpdate.Apply(before); + before.Should().BeEquivalentTo(after, options => options.Excluding(x => x.Id) .Excluding(x => x.Senses) .Excluding(x => x.Components) .Excluding(x => x.ComplexForms) @@ -31,22 +31,22 @@ public void EntryDiffShouldUpdateAllFields() [Fact] public async Task SenseDiffShouldUpdateAllFields() { - var previous = new Sense(); - var current = _autoFaker.Generate(); - var senseDiffToUpdate = await SenseSync.SenseDiffToUpdate(previous, current); + var before = new Sense(); + var after = _autoFaker.Generate(); + var senseDiffToUpdate = await SenseSync.SenseDiffToUpdate(before, after); ArgumentNullException.ThrowIfNull(senseDiffToUpdate); - senseDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id).Excluding(x => x.ExampleSentences)); + senseDiffToUpdate.Apply(before); + before.Should().BeEquivalentTo(after, options => options.Excluding(x => x.Id).Excluding(x => x.ExampleSentences)); } [Fact] public void ExampleSentenceDiffShouldUpdateAllFields() { - var previous = new ExampleSentence(); - var current = _autoFaker.Generate(); - var exampleSentenceDiffToUpdate = ExampleSentenceSync.DiffToUpdate(previous, current); + var before = new ExampleSentence(); + var after = _autoFaker.Generate(); + var exampleSentenceDiffToUpdate = ExampleSentenceSync.DiffToUpdate(before, after); ArgumentNullException.ThrowIfNull(exampleSentenceDiffToUpdate); - exampleSentenceDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id)); + exampleSentenceDiffToUpdate.Apply(before); + before.Should().BeEquivalentTo(after, options => options.Excluding(x => x.Id)); } } diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs index c31d0f9cc..7602b6cd5 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs @@ -7,29 +7,29 @@ public static class DiffCollection { public static async Task Diff( IMiniLcmApi api, - IList previous, - IList current, + IList before, + IList after, Func> add, Func> remove, Func> replace) where T : IObjectWithId { var changes = 0; - var currentEntriesDict = current.ToDictionary(entry => entry.Id); - foreach (var previousEntry in previous) + var afterEntriesDict = after.ToDictionary(entry => entry.Id); + foreach (var beforeEntry in before) { - if (currentEntriesDict.TryGetValue(previousEntry.Id, out var currentEntry)) + if (afterEntriesDict.TryGetValue(beforeEntry.Id, out var afterEntry)) { - changes += await replace(api, previousEntry, currentEntry); + changes += await replace(api, beforeEntry, afterEntry); } else { - changes += await remove(api, previousEntry); + changes += await remove(api, beforeEntry); } - currentEntriesDict.Remove(previousEntry.Id); + afterEntriesDict.Remove(beforeEntry.Id); } - foreach (var value in currentEntriesDict.Values) + foreach (var value in afterEntriesDict.Values) { changes += await add(api, value); } diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs index a54e7ff2d..32c239279 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs @@ -6,58 +6,58 @@ namespace FwLiteProjectSync.SyncHelpers; public static class EntrySync { - public static async Task Sync(Entry[] currentEntries, - Entry[] previousEntries, + public static async Task Sync(Entry[] afterEntries, + Entry[] beforeEntries, IMiniLcmApi api) { - Func> add = static async (api, currentEntry) => + Func> add = static async (api, afterEntry) => { - await api.CreateEntry(currentEntry); + await api.CreateEntry(afterEntry); return 1; }; - Func> remove = static async (api, previousEntry) => + Func> remove = static async (api, beforeEntry) => { - await api.DeleteEntry(previousEntry.Id); + await api.DeleteEntry(beforeEntry.Id); return 1; }; - Func> replace = static async (api, previousEntry, currentEntry) => await Sync(currentEntry, previousEntry, api); - return await DiffCollection.Diff(api, previousEntries, currentEntries, add, remove, replace); + Func> replace = static async (api, beforeEntry, afterEntry) => await Sync(afterEntry, beforeEntry, api); + return await DiffCollection.Diff(api, beforeEntries, afterEntries, add, remove, replace); } - public static async Task Sync(Entry currentEntry, Entry previousEntry, IMiniLcmApi api) + public static async Task Sync(Entry afterEntry, Entry beforeEntry, IMiniLcmApi api) { - var updateObjectInput = EntryDiffToUpdate(previousEntry, currentEntry); - if (updateObjectInput is not null) await api.UpdateEntry(currentEntry.Id, updateObjectInput); - var changes = await SensesSync(currentEntry.Id, currentEntry.Senses, previousEntry.Senses, api); + var updateObjectInput = EntryDiffToUpdate(beforeEntry, afterEntry); + if (updateObjectInput is not null) await api.UpdateEntry(afterEntry.Id, updateObjectInput); + var changes = await SensesSync(afterEntry.Id, afterEntry.Senses, beforeEntry.Senses, api); return changes + (updateObjectInput is null ? 0 : 1); } private static async Task SensesSync(Guid entryId, - IList currentSenses, - IList previousSenses, + IList afterSenses, + IList beforeSenses, IMiniLcmApi api) { - Func> add = async (api, currentSense) => + Func> add = async (api, afterSense) => { - await api.CreateSense(entryId, currentSense); + await api.CreateSense(entryId, afterSense); return 1; }; - Func> remove = async (api, previousSense) => + Func> remove = async (api, beforeSense) => { - await api.DeleteSense(entryId, previousSense.Id); + await api.DeleteSense(entryId, beforeSense.Id); return 1; }; - Func> replace = async (api, previousSense, currentSense) => await SenseSync.Sync(entryId, currentSense, previousSense, api); - return await DiffCollection.Diff(api, previousSenses, currentSenses, add, remove, replace); + Func> replace = async (api, beforeSense, afterSense) => await SenseSync.Sync(entryId, afterSense, beforeSense, api); + return await DiffCollection.Diff(api, beforeSenses, afterSenses, add, remove, replace); } - public static UpdateObjectInput? EntryDiffToUpdate(Entry previousEntry, Entry currentEntry) + public static UpdateObjectInput? EntryDiffToUpdate(Entry beforeEntry, Entry afterEntry) { JsonPatchDocument patchDocument = new(); - patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LexemeForm), previousEntry.LexemeForm, currentEntry.LexemeForm)); - patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.CitationForm), previousEntry.CitationForm, currentEntry.CitationForm)); - patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.Note), previousEntry.Note, currentEntry.Note)); - patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LiteralMeaning), previousEntry.LiteralMeaning, currentEntry.LiteralMeaning)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LexemeForm), beforeEntry.LexemeForm, afterEntry.LexemeForm)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.CitationForm), beforeEntry.CitationForm, afterEntry.CitationForm)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.Note), beforeEntry.Note, afterEntry.Note)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LiteralMeaning), beforeEntry.LiteralMeaning, afterEntry.LiteralMeaning)); if (patchDocument.Operations.Count == 0) return null; return new UpdateObjectInput(patchDocument); } diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs index 42dc075f9..84014a917 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs @@ -8,51 +8,51 @@ public static class ExampleSentenceSync { public static async Task Sync(Guid entryId, Guid senseId, - IList currentExampleSentences, - IList previousExampleSentences, + IList afterExampleSentences, + IList beforeExampleSentences, IMiniLcmApi api) { - Func> add = async (api, currentExampleSentence) => + Func> add = async (api, afterExampleSentence) => { - await api.CreateExampleSentence(entryId, senseId, currentExampleSentence); + await api.CreateExampleSentence(entryId, senseId, afterExampleSentence); return 1; }; - Func> remove = async (api, previousExampleSentence) => + Func> remove = async (api, beforeExampleSentence) => { - await api.DeleteExampleSentence(entryId, senseId, previousExampleSentence.Id); + await api.DeleteExampleSentence(entryId, senseId, beforeExampleSentence.Id); return 1; }; Func> replace = - async (api, previousExampleSentence, currentExampleSentence) => + async (api, beforeExampleSentence, afterExampleSentence) => { - var updateObjectInput = DiffToUpdate(previousExampleSentence, currentExampleSentence); + var updateObjectInput = DiffToUpdate(beforeExampleSentence, afterExampleSentence); if (updateObjectInput is null) return 0; - await api.UpdateExampleSentence(entryId, senseId, previousExampleSentence.Id, updateObjectInput); + await api.UpdateExampleSentence(entryId, senseId, beforeExampleSentence.Id, updateObjectInput); return 1; }; return await DiffCollection.Diff(api, - previousExampleSentences, - currentExampleSentences, + beforeExampleSentences, + afterExampleSentences, add, remove, replace); } - public static UpdateObjectInput? DiffToUpdate(ExampleSentence previousExampleSentence, - ExampleSentence currentExampleSentence) + public static UpdateObjectInput? DiffToUpdate(ExampleSentence beforeExampleSentence, + ExampleSentence afterExampleSentence) { JsonPatchDocument patchDocument = new(); patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff( nameof(ExampleSentence.Sentence), - previousExampleSentence.Sentence, - currentExampleSentence.Sentence)); + beforeExampleSentence.Sentence, + afterExampleSentence.Sentence)); patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff( nameof(ExampleSentence.Translation), - previousExampleSentence.Translation, - currentExampleSentence.Translation)); - if (previousExampleSentence.Reference != currentExampleSentence.Reference) + beforeExampleSentence.Translation, + afterExampleSentence.Translation)); + if (beforeExampleSentence.Reference != afterExampleSentence.Reference) { - patchDocument.Replace(exampleSentence => exampleSentence.Reference, currentExampleSentence.Reference); + patchDocument.Replace(exampleSentence => exampleSentence.Reference, afterExampleSentence.Reference); } if (patchDocument.Operations.Count == 0) return null; diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs index 576373bf7..2d9bb72f1 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs @@ -6,28 +6,28 @@ namespace FwLiteProjectSync.SyncHelpers; public static class MultiStringDiff { public static IEnumerable> GetMultiStringDiff(string path, - MultiString previous, - MultiString current) where T : class + MultiString before, + MultiString after) where T : class { - var currentKeys = current.Values.Keys.ToHashSet(); - foreach (var (key, previousValue) in previous.Values) + var afterKeys = after.Values.Keys.ToHashSet(); + foreach (var (key, beforeValue) in before.Values) { - if (current.Values.TryGetValue(key, out var currentValue)) + if (after.Values.TryGetValue(key, out var afterValue)) { - if (!previousValue.Equals(currentValue)) - yield return new Operation("replace", $"/{path}/{key}", null, currentValue); + if (!beforeValue.Equals(afterValue)) + yield return new Operation("replace", $"/{path}/{key}", null, afterValue); } else { yield return new Operation("remove", $"/{path}/{key}", null); } - currentKeys.Remove(key); + afterKeys.Remove(key); } - foreach (var key in currentKeys) + foreach (var key in afterKeys) { - yield return new Operation("add", $"/{path}/{key}", null, current.Values[key]); + yield return new Operation("add", $"/{path}/{key}", null, after.Values[key]); } } } diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs index 27c9cecd9..26c062df1 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs @@ -7,53 +7,53 @@ namespace FwLiteProjectSync.SyncHelpers; public static class SenseSync { public static async Task Sync(Guid entryId, - Sense currentSense, - Sense previousSense, + Sense afterSense, + Sense beforeSense, IMiniLcmApi api) { - var updateObjectInput = await SenseDiffToUpdate(previousSense, currentSense); - if (updateObjectInput is not null) await api.UpdateSense(entryId, previousSense.Id, updateObjectInput); + var updateObjectInput = await SenseDiffToUpdate(beforeSense, afterSense); + if (updateObjectInput is not null) await api.UpdateSense(entryId, beforeSense.Id, updateObjectInput); var changes = await ExampleSentenceSync.Sync(entryId, - previousSense.Id, - currentSense.ExampleSentences, - previousSense.ExampleSentences, + beforeSense.Id, + afterSense.ExampleSentences, + beforeSense.ExampleSentences, api); return changes + (updateObjectInput is null ? 0 : 1); } - public static async Task?> SenseDiffToUpdate(Sense previousSense, Sense currentSense) + public static async Task?> SenseDiffToUpdate(Sense beforeSense, Sense afterSense) { JsonPatchDocument patchDocument = new(); patchDocument.Operations.AddRange( - MultiStringDiff.GetMultiStringDiff(nameof(Sense.Gloss), previousSense.Gloss, currentSense.Gloss)); + MultiStringDiff.GetMultiStringDiff(nameof(Sense.Gloss), beforeSense.Gloss, afterSense.Gloss)); patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Sense.Definition), - previousSense.Definition, - currentSense.Definition)); - if (previousSense.PartOfSpeech != currentSense.PartOfSpeech) + beforeSense.Definition, + afterSense.Definition)); + if (beforeSense.PartOfSpeech != afterSense.PartOfSpeech) { - patchDocument.Replace(sense => sense.PartOfSpeech, currentSense.PartOfSpeech); + patchDocument.Replace(sense => sense.PartOfSpeech, afterSense.PartOfSpeech); } - if (previousSense.PartOfSpeechId != currentSense.PartOfSpeechId) + if (beforeSense.PartOfSpeechId != afterSense.PartOfSpeechId) { - patchDocument.Replace(sense => sense.PartOfSpeechId, currentSense.PartOfSpeechId); + patchDocument.Replace(sense => sense.PartOfSpeechId, afterSense.PartOfSpeechId); } await DiffCollection.Diff(null!, - previousSense.SemanticDomains, - currentSense.SemanticDomains, + beforeSense.SemanticDomains, + afterSense.SemanticDomains, (_, domain) => { patchDocument.Add(sense => sense.SemanticDomains, domain); return Task.FromResult(1); }, - (_, previousDomain) => + (_, beforeDomain) => { patchDocument.Remove(sense => sense.SemanticDomains, - previousSense.SemanticDomains.IndexOf(previousDomain)); + beforeSense.SemanticDomains.IndexOf(beforeDomain)); return Task.FromResult(1); }, - (_, previousDomain, currentDomain) => + (_, beforeDomain, afterDomain) => { //do nothing, semantic domains are not editable here return Task.FromResult(0); From 99a8524786a4aa2c247a98088d3f8d7404052e5b Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 17 Oct 2024 16:06:00 +0700 Subject: [PATCH 03/38] write test trying to sync changes between an entrys components --- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 48 +++++++++++++++++ .../Fixtures/SyncFixture.cs | 51 +++++++++++++------ .../SyncHelpers/DiffCollection.cs | 45 ++++++++++++++-- .../SyncHelpers/EntrySync.cs | 27 ++++++++++ backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 1 + backend/FwLite/MiniLcm/Models/Entry.cs | 2 +- 6 files changed, 152 insertions(+), 22 deletions(-) create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs new file mode 100644 index 000000000..a6c5c1a82 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -0,0 +1,48 @@ +using FwLiteProjectSync.SyncHelpers; +using FwLiteProjectSync.Tests.Fixtures; +using MiniLcm.Models; + +namespace FwLiteProjectSync.Tests; + +public class EntrySyncTests : IClassFixture +{ + public EntrySyncTests(SyncFixture fixture) + { + fixture.DisableFwData(); + _fixture = fixture; + } + + private readonly SyncFixture _fixture; + + [Fact] + public async Task CanChangeComplexFormViaSync() + { + var component1 = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var component2 = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "component2" } } }); + var complexFormId = Guid.NewGuid(); + var complexForm = await _fixture.CrdtApi.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + Entry after = (Entry) ((LcmCrdt.Objects.Entry)complexForm).Copy(); + after.Components[0].ComponentEntryId = component2.Id; + after.Components[0].ComponentHeadword = component2.Headword(); + + await EntrySync.Sync(after, complexForm, _fixture.CrdtApi); + + var actual = await _fixture.CrdtApi.GetEntry(after.Id); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(after); + } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index e6aaf3e91..022bdfb02 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -18,6 +18,8 @@ public class SyncFixture : IAsyncLifetime public CrdtFwdataProjectSyncService SyncService => _services.ServiceProvider.GetRequiredService(); private readonly string _projectName; + private bool _crdtEnabled = true; + private bool _fwDataEnabled = true; public static SyncFixture Create(string projectName) => new(projectName); @@ -41,23 +43,30 @@ public SyncFixture(): this("sena-3") public async Task InitializeAsync() { - var projectsFolder = _services.ServiceProvider.GetRequiredService>().Value - .ProjectsFolder; - if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true); - Directory.CreateDirectory(projectsFolder); - var lcmCache = _services.ServiceProvider.GetRequiredService() - .NewProject($"{_projectName}.fwdata", "en", "fr"); - var projectGuid = lcmCache.LanguageProject.Guid; - FwDataApi = _services.ServiceProvider.GetRequiredService().GetFwDataMiniLcmApi(_projectName, false); + Guid projectGuid = Guid.NewGuid(); + if (_fwDataEnabled) + { + var projectsFolder = _services.ServiceProvider.GetRequiredService>().Value + .ProjectsFolder; + if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true); + Directory.CreateDirectory(projectsFolder); + var lcmCache = _services.ServiceProvider.GetRequiredService() + .NewProject($"{_projectName}.fwdata", "en", "fr"); + projectGuid = lcmCache.LanguageProject.Guid; + FwDataApi = _services.ServiceProvider.GetRequiredService() + .GetFwDataMiniLcmApi(_projectName, false); + } - var crdtProjectsFolder = - _services.ServiceProvider.GetRequiredService>().Value.ProjectPath; - if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); - Directory.CreateDirectory(crdtProjectsFolder); - var crdtProject = await _services.ServiceProvider.GetRequiredService() - .CreateProject(new(_projectName, projectGuid)); - _services.ServiceProvider.GetRequiredService().Project = crdtProject; - CrdtApi = _services.ServiceProvider.GetRequiredService(); + if (_crdtEnabled) + { + var crdtProjectsFolder = _services.ServiceProvider.GetRequiredService>().Value.ProjectPath; + if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); + Directory.CreateDirectory(crdtProjectsFolder); + var crdtProject = await _services.ServiceProvider.GetRequiredService() + .CreateProject(new(_projectName, projectGuid)); + _services.ServiceProvider.GetRequiredService().Project = crdtProject; + CrdtApi = _services.ServiceProvider.GetRequiredService(); + } } public async Task DisposeAsync() @@ -67,6 +76,16 @@ public async Task DisposeAsync() public IMiniLcmApi CrdtApi { get; set; } = null!; public FwDataMiniLcmApi FwDataApi { get; set; } = null!; + + public void DisableCrdt() + { + _crdtEnabled = false; + } + + public void DisableFwData() + { + _fwDataEnabled = false; + } } public class MockProjectContext(CrdtProject project) : ProjectContext diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs index 7602b6cd5..f739d51fd 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs @@ -5,19 +5,20 @@ namespace FwLiteProjectSync.SyncHelpers; public static class DiffCollection { - public static async Task Diff( + public static async Task Diff( IMiniLcmApi api, IList before, IList after, + Func identity, Func> add, Func> remove, - Func> replace) where T : IObjectWithId + Func> replace) where TId : notnull { var changes = 0; - var afterEntriesDict = after.ToDictionary(entry => entry.Id); + var afterEntriesDict = after.ToDictionary(identity); foreach (var beforeEntry in before) { - if (afterEntriesDict.TryGetValue(beforeEntry.Id, out var afterEntry)) + if (afterEntriesDict.TryGetValue(identity(beforeEntry), out var afterEntry)) { changes += await replace(api, beforeEntry, afterEntry); } @@ -26,7 +27,7 @@ public static async Task Diff( changes += await remove(api, beforeEntry); } - afterEntriesDict.Remove(beforeEntry.Id); + afterEntriesDict.Remove(identity(beforeEntry)); } foreach (var value in afterEntriesDict.Values) @@ -36,4 +37,38 @@ public static async Task Diff( return changes; } + public static async Task Diff( + IMiniLcmApi api, + IList before, + IList after, + Func> add, + Func> remove, + Func> replace) where T : IObjectWithId + { + return await Diff(api, before, after, t => t.Id, add, remove, replace); + } + + public static async Task Diff( + IMiniLcmApi api, + IList before, + IList after, + Func add, + Func remove, + Func> replace) where T : IObjectWithId + { + return await Diff(api, + before, + after, + async (api, entry) => + { + await add(api, entry); + return 1; + }, + async (api, entry) => + { + await remove(api, entry); + return 1; + }, + async (api, beforeEntry, afterEntry) => await replace(api, beforeEntry, afterEntry)); + } } diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs b/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs index 32c239279..d67251fb6 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs @@ -29,6 +29,33 @@ public static async Task Sync(Entry afterEntry, Entry beforeEntry, IMiniLcm var updateObjectInput = EntryDiffToUpdate(beforeEntry, afterEntry); if (updateObjectInput is not null) await api.UpdateEntry(afterEntry.Id, updateObjectInput); var changes = await SensesSync(afterEntry.Id, afterEntry.Senses, beforeEntry.Senses, api); + + changes += await DiffCollection.Diff(api, + beforeEntry.Components, + afterEntry.Components, + component => (component.ComplexFormEntryId, component.ComponentEntryId, component.ComponentSenseId), + static async (api, afterComponent) => + { + await api.CreateComplexFormComponent(afterComponent); + return 1; + }, + static async (api, beforeComponent) => + { + await api.DeleteComplexFormComponent(beforeComponent); + return 1; + }, + static async (api, beforeComponent, afterComponent) => + { + if (beforeComponent.ComplexFormEntryId == afterComponent.ComplexFormEntryId && + beforeComponent.ComponentEntryId == afterComponent.ComponentEntryId && + beforeComponent.ComponentSenseId == afterComponent.ComponentSenseId) + { + return 0; + } + await api.ReplaceComplexFormComponent(beforeComponent, afterComponent); + return 1; + } + ); return changes + (updateObjectInput is null ? 0 : 1); } diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index aeb0e4cdb..2b1ab3f37 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -154,6 +154,7 @@ public static void ConfigureCrdt(CrdtConfig config) .Add() .Add() .Add() + .Add() .Add(); } } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index c8492deda..2945a0d21 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -31,7 +31,7 @@ public string Headword() } } -public record ComplexFormComponent +public record ComplexFormComponent: IObjectWithId { public static ComplexFormComponent FromEntries(Entry complexFormEntry, Entry componentEntry, Guid? componentSenseId = null) { From 0a014873573bfda355e283d38cf71fb3d23dfea9 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 17 Oct 2024 16:06:18 +0700 Subject: [PATCH 04/38] implement Component API --- .../Api/FwDataMiniLcmApi.cs | 40 +++++++++++++++++++ .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 18 +++++++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 36 +++++++++++++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 4 ++ backend/FwLite/MiniLcm/InMemoryApi.cs | 15 +++++++ 5 files changed, 113 insertions(+) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 26e415466..5040c15a0 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -499,6 +499,46 @@ public async Task CreateEntry(Entry entry) return await GetEntry(entry.Id) ?? throw new InvalidOperationException("Entry was not created"); } + public Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) + { + UndoableUnitOfWorkHelper.Do("Create Complex Form Component", + "Remove Complex Form Component", + Cache.ServiceLocator.ActionHandler, + () => + { + var lexEntry = EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId); + AddComplexFormComponent(lexEntry, complexFormComponent); + }); + return Task.FromResult(ToComplexFormComponents(EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId)).Single(c => c.ComponentEntryId == complexFormComponent.ComponentEntryId)); + } + + public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) + { + UndoableUnitOfWorkHelper.Do("Delete Complex Form Component", + "Add Complex Form Component", + Cache.ServiceLocator.ActionHandler, + () => + { + var lexEntry = EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId); + RemoveComplexFormComponent(lexEntry, complexFormComponent); + }); + return Task.CompletedTask; + } + + public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new) + { + UndoableUnitOfWorkHelper.Do("Replace Complex Form Component", + "Replace Complex Form Component", + Cache.ServiceLocator.ActionHandler, + () => + { + var lexEntry = EntriesRepository.GetObject(old.ComplexFormEntryId); + RemoveComplexFormComponent(lexEntry, old); + AddComplexFormComponent(lexEntry, @new); + }); + return Task.CompletedTask; + } + /// /// must be called as part of an lcm action /// diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 30e1b3f8b..3f0d882f4 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -149,4 +149,22 @@ public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSenten DryRunRecords.Add(new DryRunRecord(nameof(DeleteExampleSentence), $"Delete example sentence {exampleSentenceId}")); return Task.CompletedTask; } + + public Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) + { + DryRunRecords.Add(new DryRunRecord(nameof(CreateComplexFormComponent), $"Create complex form component complex entry: {complexFormComponent.ComplexFormHeadword}, component entry: {complexFormComponent.ComponentHeadword}")); + return Task.FromResult(complexFormComponent); + } + + public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) + { + DryRunRecords.Add(new DryRunRecord(nameof(DeleteComplexFormComponent), $"Delete complex form component complex entry: {complexFormComponent.ComplexFormHeadword}, component entry: {complexFormComponent.ComponentHeadword}")); + return Task.CompletedTask; + } + + public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new) + { + DryRunRecords.Add(new DryRunRecord(nameof(ReplaceComplexFormComponent), $"Replace complex form component complex entry: {old.ComplexFormHeadword}, component entry: {old.ComponentHeadword} with complex entry: {@new.ComplexFormHeadword}, component entry: {@new.ComponentHeadword}")); + return Task.CompletedTask; + } } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index f10daa8b3..7fd84cd1a 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -111,6 +111,42 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexF return await ComplexFormTypes.SingleAsync(c => c.Id == complexFormType.Id); } + public async Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) + { + var addEntryComponentChange = new AddEntryComponentChange(complexFormComponent); + await dataModel.AddChange(ClientId, addEntryComponentChange); + return await ComplexFormComponents.SingleAsync(c => c.Id == addEntryComponentChange.EntityId); + } + + public async Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) + { + await dataModel.AddChange(ClientId, new DeleteChange(complexFormComponent.Id)); + } + + public async Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new) + { + IChange change; + if (old.ComplexFormEntryId != @new.ComplexFormEntryId) + { + change = SetComplexFormComponentChange.NewComplexForm(old.Id, @new.ComplexFormEntryId); + } + else if (old.ComponentEntryId != @new.ComponentEntryId) + { + change = SetComplexFormComponentChange.NewComponent(old.Id, @new.ComponentEntryId); + } + else if (old.ComponentSenseId != @new.ComponentSenseId) + { + change = SetComplexFormComponentChange.NewComponentSense(old.Id, + @new.ComponentEntryId, + @new.ComponentSenseId); + } + else + { + return; + } + await dataModel.AddChange(ClientId, change); + } + public IAsyncEnumerable GetEntries(QueryOptions? options = null) { return GetEntriesAsyncEnum(predicate: null, options); diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index f7ebc2209..5ae2dfdae 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -12,6 +12,10 @@ Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update); + Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent); + Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent); + Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new); + Task CreatePartOfSpeech(PartOfSpeech partOfSpeech); Task CreateSemanticDomain(SemanticDomain semanticDomain); Task CreateComplexFormType(ComplexFormType complexFormType); diff --git a/backend/FwLite/MiniLcm/InMemoryApi.cs b/backend/FwLite/MiniLcm/InMemoryApi.cs index fc6bfe0e1..37f2516ac 100644 --- a/backend/FwLite/MiniLcm/InMemoryApi.cs +++ b/backend/FwLite/MiniLcm/InMemoryApi.cs @@ -160,6 +160,21 @@ public Task CreateComplexFormType(ComplexFormType complexFormTy throw new NotImplementedException(); } + public Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) + { + throw new NotImplementedException(); + } + + public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) + { + throw new NotImplementedException(); + } + + public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new) + { + throw new NotImplementedException(); + } + private readonly string[] _exemplars = Enumerable.Range('a', 'z').Select(c => ((char)c).ToString()).ToArray(); public Task CreateEntry(Entry entry) From 33eb0960c646e876b78335eba5e732ff5834ca52 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 17 Oct 2024 16:39:26 +0700 Subject: [PATCH 05/38] implement semantic domain API --- .../Api/FwDataMiniLcmApi.cs | 26 +++++++++++++++++++ .../Api/UpdateProxy/UpdateSenseProxy.cs | 9 +++---- .../FwLiteProjectSync.Tests/SyncTests.cs | 2 +- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 12 +++++++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 10 +++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 3 +++ backend/FwLite/MiniLcm/InMemoryApi.cs | 10 +++++++ 7 files changed, 66 insertions(+), 6 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 5040c15a0..8fccff6c1 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -695,6 +695,32 @@ public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput + { + var lexSense = SenseRepository.GetObject(senseId); + lexSense.SemanticDomainsRC.Add(GetLcmSemanticDomain(semanticDomain.Id)); + }); + return Task.CompletedTask; + } + + public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) + { + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Remove Semantic Domain from Sense", + "Add Semantic Domain to Sense", + Cache.ServiceLocator.ActionHandler, + () => + { + var lexSense = SenseRepository.GetObject(senseId); + lexSense.SemanticDomainsRC.Remove(lexSense.SemanticDomainsRC.First(sd => sd.Guid == semanticDomainId)); + }); + return Task.CompletedTask; + } + public Task DeleteSense(Guid entryId, Guid senseId) { var lexSense = SenseRepository.GetObject(senseId); diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs index dba63fbfd..404c35125 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs @@ -64,14 +64,13 @@ public override IList SemanticDomains new UpdateListProxy( semanticDomain => { - if (semanticDomain.Id != default) - sense.SemanticDomainsRC.Add(lexboxLcmApi.GetLcmSemanticDomain(semanticDomain.Id)); +#pragma warning disable VSTHRD002 + lexboxLcmApi.AddSemanticDomainToSense(sense.Guid, semanticDomain).Wait(); }, semanticDomain => { - if (semanticDomain.Id != default) - sense.SemanticDomainsRC.Remove( - sense.SemanticDomainsRC.First(sd => sd.Guid == semanticDomain.Id)); + lexboxLcmApi.RemoveSemanticDomainFromSense(sense.Guid, semanticDomain.Id).Wait(); +#pragma warning restore VSTHRD002 }, i => new UpdateProxySemanticDomain(sense.SemanticDomainsRC, sense.SemanticDomainsRC.ElementAt(i).Guid, diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 9174e6ebd..bd71ff752 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -124,7 +124,7 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = new WritingSystemId("fr"), Name = "French", Abbreviation = "fr", Font = "Arial" }); await _syncService.Sync(crdtApi, fwdataApi); - await crdtApi.UpdateEntry(_testEntry.Id, new UpdateObjectInput().Set(entry => entry.LexemeForm["es"],"Manzana")); + await crdtApi.UpdateEntry(_testEntry.Id, new UpdateObjectInput().Set(entry => entry.LexemeForm["es"], "Manzana")); await fwdataApi.UpdateEntry(_testEntry.Id, new UpdateObjectInput().Set(entry => entry.LexemeForm["fr"], "Pomme")); var results = await _syncService.Sync(crdtApi, fwdataApi); diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 3f0d882f4..a8066d29d 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -124,6 +124,18 @@ public Task DeleteSense(Guid entryId, Guid senseId) return Task.CompletedTask; } + public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain) + { + DryRunRecords.Add(new DryRunRecord(nameof(AddSemanticDomainToSense), $"Add semantic domain {semanticDomain.Name}")); + return Task.CompletedTask; + } + + public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) + { + DryRunRecords.Add(new DryRunRecord(nameof(RemoveSemanticDomainFromSense), $"Remove semantic domain {semanticDomainId}")); + return Task.CompletedTask; + } + public Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence) { DryRunRecords.Add(new DryRunRecord(nameof(CreateExampleSentence), $"Create example sentence {exampleSentence.Sentence}")); diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 7fd84cd1a..60b21b708 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -390,6 +390,16 @@ public async Task DeleteSense(Guid entryId, Guid senseId) await dataModel.AddChange(ClientId, new DeleteChange(senseId)); } + public async Task AddSemanticDomainToSense(Guid senseId, MiniLcm.Models.SemanticDomain semanticDomain) + { + await dataModel.AddChange(ClientId, new AddSemanticDomainChange(semanticDomain, senseId)); + } + + public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) + { + await dataModel.AddChange(ClientId, new RemoveSemanticDomainChange(semanticDomainId, senseId)); + } + public async Task CreateExampleSentence(Guid entryId, Guid senseId, MiniLcm.Models.ExampleSentence exampleSentence) diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 5ae2dfdae..3274007b8 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -25,6 +25,9 @@ Task UpdateWritingSystem(WritingSystemId id, Task CreateSense(Guid entryId, Sense sense); Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update); Task DeleteSense(Guid entryId, Guid senseId); + Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain); + Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId); + Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence); Task UpdateExampleSentence(Guid entryId, diff --git a/backend/FwLite/MiniLcm/InMemoryApi.cs b/backend/FwLite/MiniLcm/InMemoryApi.cs index 37f2516ac..ae424121f 100644 --- a/backend/FwLite/MiniLcm/InMemoryApi.cs +++ b/backend/FwLite/MiniLcm/InMemoryApi.cs @@ -293,6 +293,16 @@ public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput Date: Fri, 18 Oct 2024 16:43:17 +0700 Subject: [PATCH 06/38] GitButler WIP Commit --- .idea/.idea.LexBox/.idea/dataSources.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.LexBox/.idea/dataSources.xml b/.idea/.idea.LexBox/.idea/dataSources.xml index 5661a55a2..2e5912227 100644 --- a/.idea/.idea.LexBox/.idea/dataSources.xml +++ b/.idea/.idea.LexBox/.idea/dataSources.xml @@ -72,7 +72,7 @@ $ProjectFileDir$ - sqlite.xerial + 1ad76a0a-03ae-4990-8fbe-1c2f61731f3d true org.sqlite.JDBC jdbc:sqlite:C:\dev\LexBox\backend\LocalWebApp\sena-3.sqlite From a411b00c75a9765875df8b9b2366b1cf28a00c0f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 09:23:04 +0700 Subject: [PATCH 07/38] being making API to update an entry by simply passing it in --- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 4 +- .../MultiStringDiffTests.cs | 4 +- .../UpdateDiffTests.cs | 4 +- .../CrdtFwdataProjectSyncService.cs | 2 +- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 39 ++++++++++++------- backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | 1 + backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 3 ++ .../FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 11 ++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 20 ++++++++-- backend/FwLite/MiniLcm/Models/Entry.cs | 1 + .../SyncHelpers/DiffCollection.cs | 5 +-- .../SyncHelpers/EntrySync.cs | 5 +-- .../SyncHelpers/ExampleSentenceSync.cs | 5 +-- .../SyncHelpers/MultiStringDiff.cs | 2 +- .../SyncHelpers/SenseSync.cs | 5 +-- 15 files changed, 74 insertions(+), 37 deletions(-) rename backend/FwLite/{FwLiteProjectSync => MiniLcm}/SyncHelpers/DiffCollection.cs (96%) rename backend/FwLite/{FwLiteProjectSync => MiniLcm}/SyncHelpers/EntrySync.cs (98%) rename backend/FwLite/{FwLiteProjectSync => MiniLcm}/SyncHelpers/ExampleSentenceSync.cs (96%) rename backend/FwLite/{FwLiteProjectSync => MiniLcm}/SyncHelpers/MultiStringDiff.cs (95%) rename backend/FwLite/{FwLiteProjectSync => MiniLcm}/SyncHelpers/SenseSync.cs (96%) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 10876aca8..492655206 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -1,6 +1,6 @@ -using FwLiteProjectSync.SyncHelpers; -using FwLiteProjectSync.Tests.Fixtures; +using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; +using MiniLcm.SyncHelpers; namespace FwLiteProjectSync.Tests; diff --git a/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs index 75dd5e927..58f1298f8 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/MultiStringDiffTests.cs @@ -1,5 +1,5 @@ -using FwLiteProjectSync.SyncHelpers; -using MiniLcm.Models; +using MiniLcm.Models; +using MiniLcm.SyncHelpers; using Spart.Parsers; using SystemTextJsonPatch.Operations; diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index a9dc3b072..a9f13f49c 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -1,6 +1,6 @@ -using FwLiteProjectSync.SyncHelpers; -using FwLiteProjectSync.Tests.Fixtures; +using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; +using MiniLcm.SyncHelpers; using Soenneker.Utils.AutoBogus; using Soenneker.Utils.AutoBogus.Config; diff --git a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs index 87ad8007b..76a08503e 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -1,11 +1,11 @@ using System.Text.Json; using FwDataMiniLcmBridge.Api; -using FwLiteProjectSync.SyncHelpers; using LcmCrdt; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MiniLcm; using MiniLcm.Models; +using MiniLcm.SyncHelpers; using SystemTextJsonPatch; using SystemTextJsonPatch.Operations; diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index f39e776b1..c0b0a46a5 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -6,11 +6,11 @@ using LcmCrdt.Data; using LcmCrdt.Objects; using LinqToDB; -using LinqToDB.EntityFrameworkCore; +using MiniLcm.SyncHelpers; namespace LcmCrdt; -public class CrdtMiniLcmApi(DataModel dataModel, CurrentProjectService projectService) : IMiniLcmApi +public class CrdtMiniLcmApi(DataModel dataModel, CurrentProjectService projectService, LcmCrdtDbContext dbContext) : IMiniLcmApi { private Guid ClientId { get; } = projectService.ProjectData.ClientId; @@ -154,18 +154,14 @@ public IAsyncEnumerable SearchEntries(string? query, QueryOptions? option return GetEntriesAsyncEnum(Filtering.SearchFilter(query), options); } - private async IAsyncEnumerable GetEntriesAsyncEnum( + private IAsyncEnumerable GetEntriesAsyncEnum( Expression>? predicate = null, QueryOptions? options = null) { - var entries = await GetEntries(predicate, options); - foreach (var entry in entries) - { - yield return entry; - } + return GetEntries(predicate, options); } - private async Task GetEntries( + private async IAsyncEnumerable GetEntries( Expression>? predicate = null, QueryOptions? options = null) { @@ -193,10 +189,11 @@ private async Task GetEntries( .ThenBy(e => e.Id) .Skip(options.Offset) .Take(options.Count); - var entries = await queryable - .ToArrayAsyncLinqToDB(); - - return entries; + var entries = queryable.AsAsyncEnumerable(); + await foreach (var entry in entries) + { + yield return entry; + } } public async Task GetEntry(Guid id) @@ -290,6 +287,22 @@ public async Task UpdateEntry(Guid id, return await GetEntry(id) ?? throw new NullReferenceException(); } + public async Task UpdateEntry(Entry entry) + { + if (!Guid.TryParse(entry.Version, out var snapshotId)) + { + throw new InvalidOperationException($"Unable to parse snapshot id '{entry.Version}'"); + } + + //todo this will not work for properties not in the snapshot, but we don't have a way to get the full snapshot yet + var beforeChanges = await dataModel.GetBySnapshotId(snapshotId); + //workaround to avoid syncing senses, which are not in the snapshot + beforeChanges.Senses = []; + entry.Senses = []; + await EntrySync.Sync(entry, beforeChanges, this); + return await GetEntry(entry.Id) ?? throw new NullReferenceException(); + } + public async Task DeleteEntry(Guid id) { await dataModel.AddChange(ClientId, new DeleteChange(id)); diff --git a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs index 3a303f643..525d1d8e6 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs @@ -10,6 +10,7 @@ namespace LcmCrdt; public class LcmCrdtDbContext(DbContextOptions dbContextOptions, IOptions options): DbContext(dbContextOptions), ICrdtDbContext { public DbSet ProjectData => Set(); + public IQueryable Snapshots => ((ICrdtDbContext)this).Snapshots; protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 5e7db207f..4fc88c22a 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SIL.Harmony.Db; namespace LcmCrdt; @@ -51,6 +52,7 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.DateTime))) .HasAttribute(new ColumnAttribute(nameof(HybridDateTime.Counter), nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter))) + .Entity().Member(e => e.Version).HasColumnName(ObjectSnapshot.ShadowRefName) .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -61,6 +63,7 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio }); } + public static void ConfigureCrdt(CrdtConfig config) { config.EnableProjectedTables = true; diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index 56f76f650..061dc1820 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -298,6 +298,17 @@ public async Task UpdateEntry() updatedEntry.LexemeForm.Values["en"].Should().Be("updated"); } + [Fact] + public async Task UpdateEntry_SimpleApi() + { + var entry = await Api.GetEntry(_entry1Id); + ArgumentNullException.ThrowIfNull(entry); + entry.LexemeForm["en"] = "updated"; + var updatedEntry = await Api.UpdateEntry(entry); + updatedEntry.LexemeForm["en"].Should().Be("updated"); + updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); + } + [Fact] public async Task UpdateEntryNote() { diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 3274007b8..e83b1b9a6 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -12,30 +12,42 @@ Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update); - Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent); - Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent); - Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new); Task CreatePartOfSpeech(PartOfSpeech partOfSpeech); Task CreateSemanticDomain(SemanticDomain semanticDomain); Task CreateComplexFormType(ComplexFormType complexFormType); + + #region Entry Task CreateEntry(Entry entry); Task UpdateEntry(Guid id, UpdateObjectInput update); + + Task UpdateEntry(Entry entry) + { + throw new NotImplementedException(); + } Task DeleteEntry(Guid id); + Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent); + Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent); + Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new); + #endregion + + #region Sense Task CreateSense(Guid entryId, Sense sense); Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update); Task DeleteSense(Guid entryId, Guid senseId); Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain); Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId); + #endregion + #region ExampleSentence Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence); - Task UpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update); Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId); + #endregion } /// diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 98dc6c9c9..234111c34 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -4,6 +4,7 @@ public class Entry : IObjectWithId { public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public virtual MultiString LexemeForm { get; set; } = new(); diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs b/backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs similarity index 96% rename from backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs rename to backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs index f739d51fd..2cc251f2b 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/DiffCollection.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs @@ -1,7 +1,6 @@ -using MiniLcm; -using MiniLcm.Models; +using MiniLcm.Models; -namespace FwLiteProjectSync.SyncHelpers; +namespace MiniLcm.SyncHelpers; public static class DiffCollection { diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs similarity index 98% rename from backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs rename to backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs index d67251fb6..5805d2941 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs @@ -1,8 +1,7 @@ -using MiniLcm; -using MiniLcm.Models; +using MiniLcm.Models; using SystemTextJsonPatch; -namespace FwLiteProjectSync.SyncHelpers; +namespace MiniLcm.SyncHelpers; public static class EntrySync { diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/ExampleSentenceSync.cs similarity index 96% rename from backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs rename to backend/FwLite/MiniLcm/SyncHelpers/ExampleSentenceSync.cs index 84014a917..5652e77de 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/ExampleSentenceSync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/ExampleSentenceSync.cs @@ -1,8 +1,7 @@ -using MiniLcm; -using MiniLcm.Models; +using MiniLcm.Models; using SystemTextJsonPatch; -namespace FwLiteProjectSync.SyncHelpers; +namespace MiniLcm.SyncHelpers; public static class ExampleSentenceSync { diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs b/backend/FwLite/MiniLcm/SyncHelpers/MultiStringDiff.cs similarity index 95% rename from backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs rename to backend/FwLite/MiniLcm/SyncHelpers/MultiStringDiff.cs index 2d9bb72f1..7a24c58fd 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/MultiStringDiff.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/MultiStringDiff.cs @@ -1,7 +1,7 @@ using MiniLcm.Models; using SystemTextJsonPatch.Operations; -namespace FwLiteProjectSync.SyncHelpers; +namespace MiniLcm.SyncHelpers; public static class MultiStringDiff { diff --git a/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs similarity index 96% rename from backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs rename to backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs index 26c062df1..1b3b7c437 100644 --- a/backend/FwLite/FwLiteProjectSync/SyncHelpers/SenseSync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs @@ -1,8 +1,7 @@ -using MiniLcm; -using MiniLcm.Models; +using MiniLcm.Models; using SystemTextJsonPatch; -namespace FwLiteProjectSync.SyncHelpers; +namespace MiniLcm.SyncHelpers; public static class SenseSync { From e03f1e7f882f29762f667102bf46e66151efd52e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 12:53:38 +0700 Subject: [PATCH 08/38] add logging from MiniLcmApiFixture to xunit --- .../FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs | 52 ++++++++++++++++--- .../MiniLcmTests/BasicApiTests.cs | 22 ++++++-- .../FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj | 1 + 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs index e2f05d114..7bee8b47f 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -1,34 +1,40 @@ using LcmCrdt.Tests.Mocks; +using Meziantou.Extensions.Logging.Xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using MiniLcm; using MiniLcm.Models; +using Xunit.Abstractions; namespace LcmCrdt.Tests; public class MiniLcmApiFixture : IAsyncLifetime { - private readonly AsyncServiceScope _services; - private readonly LcmCrdtDbContext _crdtDbContext; + private AsyncServiceScope _services; + private LcmCrdtDbContext? _crdtDbContext; public CrdtMiniLcmApi Api => (CrdtMiniLcmApi)_services.ServiceProvider.GetRequiredService(); public DataModel DataModel => _services.ServiceProvider.GetRequiredService(); public MiniLcmApiFixture() + { + } + + public async Task InitializeAsync() { var services = new ServiceCollection() .AddLcmCrdtClient() - .AddLogging(builder => builder.AddDebug()) + .AddLogging(builder => builder.AddDebug() + .AddProvider(new LateXUnitLoggerProvider(this)) + .AddFilter("LinqToDB", LogLevel.Trace) + .SetMinimumLevel(LogLevel.Error)) .RemoveAll(typeof(ProjectContext)) - .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", ":memory:"))) + .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", "test.sqlite"))) .BuildServiceProvider(); _services = services.CreateAsyncScope(); _crdtDbContext = _services.ServiceProvider.GetRequiredService(); - } - - public async Task InitializeAsync() - { await _crdtDbContext.Database.OpenConnectionAsync(); //can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db. await ProjectsService.InitProjectDb(_crdtDbContext, @@ -58,6 +64,36 @@ await Api.CreateWritingSystem(WritingSystemType.Analysis, }); } + ITestOutputHelper? _outputHelper; + public void LogTo(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + private class LateXUnitLoggerProvider(MiniLcmApiFixture fixture) : ILoggerProvider + { + private ILoggerProvider? _loggerProvider; + public void Dispose() + { + } + + public ILogger CreateLogger(string categoryName) + { + if (_loggerProvider is null) + { + if (fixture._outputHelper is null) + { + return NullLogger.Instance; + } + _loggerProvider = new XUnitLoggerProvider(fixture._outputHelper, new XUnitLoggerOptions() + { + IncludeCategory = true + }); + } + return _loggerProvider.CreateLogger(categoryName); + } + } + public async Task DisposeAsync() { await _services.DisposeAsync(); diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs index ed109b039..47ec1b108 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs @@ -1,9 +1,25 @@ -namespace LcmCrdt.Tests.MiniLcmTests; +using Xunit.Abstractions; -public class BasicApiTests(MiniLcmApiFixture fixture): BasicApiTestsBase, IClassFixture +namespace LcmCrdt.Tests.MiniLcmTests; + +public class BasicApiTests(ITestOutputHelper output): BasicApiTestsBase { + private readonly MiniLcmApiFixture _fixture = new(); + public override async Task InitializeAsync() + { + _fixture.LogTo(output); + await _fixture.InitializeAsync(); + await base.InitializeAsync(); + } + protected override Task NewApi() { - return Task.FromResult(fixture.Api); + return Task.FromResult(_fixture.Api); + } + + public override async Task DisposeAsync() + { + await base.DisposeAsync(); + await _fixture.DisposeAsync(); } } diff --git a/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj b/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj index 370a55ee6..6243d7eb5 100644 --- a/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj +++ b/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj @@ -11,6 +11,7 @@ + From f889baa312dbbeccc4ca547b1ef2562caef50a35 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 22 Oct 2024 13:11:48 +0700 Subject: [PATCH 09/38] comment out version stuff and make a 2 entry update api for now --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 30 ++++++++++--------- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 2 -- .../FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 3 +- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 2 +- backend/FwLite/MiniLcm/Models/Entry.cs | 1 + 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index c0b0a46a5..e48e11320 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -6,6 +6,7 @@ using LcmCrdt.Data; using LcmCrdt.Objects; using LinqToDB; +using LinqToDB.EntityFrameworkCore; using MiniLcm.SyncHelpers; namespace LcmCrdt; @@ -287,20 +288,21 @@ public async Task UpdateEntry(Guid id, return await GetEntry(id) ?? throw new NullReferenceException(); } - public async Task UpdateEntry(Entry entry) - { - if (!Guid.TryParse(entry.Version, out var snapshotId)) - { - throw new InvalidOperationException($"Unable to parse snapshot id '{entry.Version}'"); - } - - //todo this will not work for properties not in the snapshot, but we don't have a way to get the full snapshot yet - var beforeChanges = await dataModel.GetBySnapshotId(snapshotId); - //workaround to avoid syncing senses, which are not in the snapshot - beforeChanges.Senses = []; - entry.Senses = []; - await EntrySync.Sync(entry, beforeChanges, this); - return await GetEntry(entry.Id) ?? throw new NullReferenceException(); + public async Task UpdateEntry(Entry before, Entry after) + { + //todo version stuff isn't working yet. + // if (!Guid.TryParse(entry.Version, out var snapshotId)) + // { + // throw new InvalidOperationException($"Unable to parse snapshot id '{entry.Version}'"); + // } + // + // //todo this will not work for properties not in the snapshot, but we don't have a way to get the full snapshot yet + // var beforeChanges = await dataModel.GetBySnapshotId(snapshotId); + // //workaround to avoid syncing senses, which are not in the snapshot + // beforeChanges.Senses = []; + // entry.Senses = []; + await EntrySync.Sync(after, before, this); + return await GetEntry(after.Id) ?? throw new NullReferenceException(); } public async Task DeleteEntry(Guid id) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 4fc88c22a..f72ac4ca5 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -52,7 +52,6 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.DateTime))) .HasAttribute(new ColumnAttribute(nameof(HybridDateTime.Counter), nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter))) - .Entity().Member(e => e.Version).HasColumnName(ObjectSnapshot.ShadowRefName) .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -71,7 +70,6 @@ public static void ConfigureCrdt(CrdtConfig config) .CustomAdapter() .Add(builder => { - builder.Ignore(e => e.Senses); builder.HasMany(e => e.Components) .WithOne() .HasPrincipalKey(entry => entry.Id) diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index 061dc1820..e28c6cbca 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -303,8 +303,9 @@ public async Task UpdateEntry_SimpleApi() { var entry = await Api.GetEntry(_entry1Id); ArgumentNullException.ThrowIfNull(entry); + var before = (Entry) entry.Copy(); entry.LexemeForm["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(entry); + var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.LexemeForm["en"].Should().Be("updated"); updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); } diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index e83b1b9a6..35d2f06df 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -21,7 +21,7 @@ Task UpdateWritingSystem(WritingSystemId id, Task CreateEntry(Entry entry); Task UpdateEntry(Guid id, UpdateObjectInput update); - Task UpdateEntry(Entry entry) + Task UpdateEntry(Entry before, Entry after) { throw new NotImplementedException(); } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 234111c34..36aba3362 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -41,6 +41,7 @@ public IObjectWithId Copy() { Id = Id, DeletedAt = DeletedAt, + Version = Version, LexemeForm = LexemeForm.Copy(), CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), From 804c8fa6292b803fec99ae55836ea61dd4e1fff4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:27:54 +0700 Subject: [PATCH 10/38] remove version from entry --- backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 2 +- backend/FwLite/MiniLcm/Models/Entry.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index e28c6cbca..9e6119e1b 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -307,7 +307,7 @@ public async Task UpdateEntry_SimpleApi() entry.LexemeForm["en"] = "updated"; var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); + updatedEntry.Should().BeEquivalentTo(entry); } [Fact] diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 36aba3362..2112a4550 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -4,7 +4,6 @@ public class Entry : IObjectWithId { public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public virtual MultiString LexemeForm { get; set; } = new(); @@ -41,7 +40,7 @@ public IObjectWithId Copy() { Id = Id, DeletedAt = DeletedAt, - Version = Version, + // Version = Version, LexemeForm = LexemeForm.Copy(), CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), From 2ccf0e23242cd3e3c73f7998bfe775a2840c30fb Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:28:40 +0700 Subject: [PATCH 11/38] fix some test bugs related to complex forms getting deleted and then recreated with the same Id --- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 1 - .../Fixtures/SyncFixture.cs | 43 ++++++------------- .../FwLiteProjectSync.Tests/SyncTests.cs | 2 +- .../FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs | 2 +- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 3 +- .../FwLite/MiniLcm/SyncHelpers/EntrySync.cs | 4 ++ 6 files changed, 21 insertions(+), 34 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 492655206..97b94e87a 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -8,7 +8,6 @@ public class EntrySyncTests : IClassFixture { public EntrySyncTests(SyncFixture fixture) { - fixture.DisableFwData(); _fixture = fixture; } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index 7005237fe..881ab4c07 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -47,27 +47,20 @@ public SyncFixture(): this("sena-3") public async Task InitializeAsync() { - Guid projectGuid = Guid.NewGuid(); - if (_fwDataEnabled) - { - var projectsFolder = _services.ServiceProvider.GetRequiredService>().Value - .ProjectsFolder; - if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true); - Directory.CreateDirectory(projectsFolder); - var lcmCache = _services.ServiceProvider.GetRequiredService() - .NewProject(new FwDataProject(_projectName, projectsFolder), "en", "fr"); - projectGuid = lcmCache.LanguageProject.Guid; - FwDataApi = _services.ServiceProvider.GetRequiredService().GetFwDataMiniLcmApi(_projectName, false); - } - if (_crdtEnabled) - { - var crdtProjectsFolder = _services.ServiceProvider.GetRequiredService>().Value.ProjectPath; - if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); - Directory.CreateDirectory(crdtProjectsFolder); - var crdtProject = await _services.ServiceProvider.GetRequiredService() - .CreateProject(new(_projectName, FwProjectId: FwDataApi.ProjectId)); + var projectsFolder = _services.ServiceProvider.GetRequiredService>().Value + .ProjectsFolder; + if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true); + Directory.CreateDirectory(projectsFolder); + var lcmCache = _services.ServiceProvider.GetRequiredService() + .NewProject(new FwDataProject(_projectName, projectsFolder), "en", "fr"); + FwDataApi = _services.ServiceProvider.GetRequiredService().GetFwDataMiniLcmApi(_projectName, false); + + var crdtProjectsFolder = _services.ServiceProvider.GetRequiredService>().Value.ProjectPath; + if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); + Directory.CreateDirectory(crdtProjectsFolder); + var crdtProject = await _services.ServiceProvider.GetRequiredService() + .CreateProject(new(_projectName, FwProjectId: FwDataApi.ProjectId)); CrdtApi = (CrdtMiniLcmApi) await _services.ServiceProvider.OpenCrdtProject(crdtProject); - } } public async Task DisposeAsync() @@ -77,16 +70,6 @@ public async Task DisposeAsync() public CrdtMiniLcmApi CrdtApi { get; set; } = null!; public FwDataMiniLcmApi FwDataApi { get; set; } = null!; - - public void DisableCrdt() - { - _crdtEnabled = false; - } - - public void DisableFwData() - { - _fwDataEnabled = false; - } } public class MockProjectContext(CrdtProject? project) : ProjectContext diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index e15450f23..6aefac3e8 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -60,7 +60,7 @@ public async Task DisposeAsync() { await _fixture.FwDataApi.DeleteEntry(entry.Id); } - await foreach (var entry in _fixture.CrdtApi.GetEntries()) + foreach (var entry in await _fixture.CrdtApi.GetEntries().ToArrayAsync()) { await _fixture.CrdtApi.DeleteEntry(entry.Id); } diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs index 7bee8b47f..bc93339cc 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -31,7 +31,7 @@ public async Task InitializeAsync() .AddFilter("LinqToDB", LogLevel.Trace) .SetMinimumLevel(LogLevel.Error)) .RemoveAll(typeof(ProjectContext)) - .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", "test.sqlite"))) + .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", ":memory:"))) .BuildServiceProvider(); _services = services.CreateAsyncScope(); _crdtDbContext = _services.ServiceProvider.GetRequiredService(); diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index ce554551a..045d07aa5 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -8,6 +8,7 @@ using LinqToDB; using LinqToDB.EntityFrameworkCore; using MiniLcm.SyncHelpers; +using SIL.Harmony.Db; namespace LcmCrdt; @@ -199,7 +200,7 @@ private async IAsyncEnumerable GetEntries( public async Task GetEntry(Guid id) { - var entry = await Entries + var entry = await Entries.AsTracking(false) .LoadWith(e => e.Senses) .ThenLoad(s => s.ExampleSentences) .LoadWith(e => e.ComplexForms) diff --git a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs index 5805d2941..49b2fa975 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs @@ -32,9 +32,13 @@ public static async Task Sync(Entry afterEntry, Entry beforeEntry, IMiniLcm changes += await DiffCollection.Diff(api, beforeEntry.Components, afterEntry.Components, + //we can't use the ID as there's none defined by Fw so it won't work as a sync key component => (component.ComplexFormEntryId, component.ComponentEntryId, component.ComponentSenseId), static async (api, afterComponent) => { + //change id, since we're not using the id as the key for this collection + //the id may be the same, which is not what we want here + afterComponent.Id = Guid.NewGuid(); await api.CreateComplexFormComponent(afterComponent); return 1; }, From f2c3eac606f2bf2a839a0c4284cb47237bb9048e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:29:06 +0700 Subject: [PATCH 12/38] use locking to prevent a race condition from calling Project loader init twice --- .../LcmUtils/ProjectLoader.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs b/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs index 847eee223..c42358022 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs @@ -26,6 +26,7 @@ public class ProjectLoader(IOptions config) : IProjectLoader { protected string TemplatesFolder => config.Value.TemplatesFolder; private static bool _init; + private static readonly object _initLock = new(); public static void Init() { @@ -34,13 +35,22 @@ public static void Init() return; } - Icu.Wrapper.Init(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + lock (_initLock) { - Debug.Assert(Icu.Wrapper.IcuVersion == "72.1.0.3"); + if (_init) + { + return; + } + + Icu.Wrapper.Init(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Debug.Assert(Icu.Wrapper.IcuVersion == "72.1.0.3"); + } + + Sldr.Initialize(); + _init = true; } - Sldr.Initialize(); - _init = true; } public virtual LcmCache LoadCache(FwDataProject project) From 62289359c1b47d2523d7135a0f65539f3ecfc53d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:35:17 +0700 Subject: [PATCH 13/38] support syncing Entry.ComplexForms in addition to Components --- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 33 ++++++++++++++++++- .../FwLite/MiniLcm/SyncHelpers/EntrySync.cs | 14 +++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 97b94e87a..57a5db9d4 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -14,7 +14,7 @@ public EntrySyncTests(SyncFixture fixture) private readonly SyncFixture _fixture; [Fact] - public async Task CanChangeComplexFormViaSync() + public async Task CanChangeComplexFormVisSync_Components() { var component1 = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); var component2 = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "component2" } } }); @@ -40,6 +40,37 @@ public async Task CanChangeComplexFormViaSync() await EntrySync.Sync(after, complexForm, _fixture.CrdtApi); + var actual = await _fixture.CrdtApi.GetEntry(after.Id); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(after); + } + [Fact] + public async Task CanChangeComplexFormViaSync_ComplexForms() + { + var complexForm1 = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "complexForm1" } } }); + var complexForm2 = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "complexForm2" } } }); + var componentId = Guid.NewGuid(); + var component = await _fixture.CrdtApi.CreateEntry(new() + { + Id = componentId, + LexemeForm = { { "en", "component" } }, + ComplexForms = + [ + new ComplexFormComponent() + { + ComponentEntryId = componentId, + ComponentHeadword = "component", + ComplexFormEntryId = complexForm1.Id, + ComplexFormHeadword = complexForm1.Headword() + } + ] + }); + Entry after = (Entry) component.Copy(); + after.ComplexForms[0].ComplexFormEntryId = complexForm2.Id; + after.ComplexForms[0].ComplexFormHeadword = complexForm2.Headword(); + + await EntrySync.Sync(after, component, _fixture.CrdtApi); + var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); actual.Should().BeEquivalentTo(after); diff --git a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs index 49b2fa975..7f98c28cb 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs @@ -29,9 +29,16 @@ public static async Task Sync(Entry afterEntry, Entry beforeEntry, IMiniLcm if (updateObjectInput is not null) await api.UpdateEntry(afterEntry.Id, updateObjectInput); var changes = await SensesSync(afterEntry.Id, afterEntry.Senses, beforeEntry.Senses, api); - changes += await DiffCollection.Diff(api, - beforeEntry.Components, - afterEntry.Components, + changes += await Sync(afterEntry.Components, beforeEntry.Components, api); + changes += await Sync(afterEntry.ComplexForms, beforeEntry.ComplexForms, api); + return changes + (updateObjectInput is null ? 0 : 1); + } + + private static async Task Sync(IList afterComponents, IList beforeComponents, IMiniLcmApi api) + { + return await DiffCollection.Diff(api, + beforeComponents, + afterComponents, //we can't use the ID as there's none defined by Fw so it won't work as a sync key component => (component.ComplexFormEntryId, component.ComponentEntryId, component.ComponentSenseId), static async (api, afterComponent) => @@ -59,7 +66,6 @@ static async (api, beforeComponent, afterComponent) => return 1; } ); - return changes + (updateObjectInput is null ? 0 : 1); } private static async Task SensesSync(Guid entryId, From 80fb4c32b20bc213e4cb8204ebd5711663825234 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:52:36 +0700 Subject: [PATCH 14/38] define AddComplexFormType and RemoveComplexFormType on MiniLcmApi --- .../Api/FwDataMiniLcmApi.cs | 24 +++++++++++++++++++ .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 12 ++++++++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 10 ++++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 2 ++ backend/FwLite/MiniLcm/InMemoryApi.cs | 10 ++++++++ 5 files changed, 58 insertions(+) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 04e40b25d..2f3fbfaa7 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -558,6 +558,30 @@ public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormCom return Task.CompletedTask; } + public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) + { + UndoableUnitOfWorkHelper.Do("Add Complex Form Type", + "Remove Complex Form Type", + Cache.ServiceLocator.ActionHandler, + () => + { + AddComplexFormType(EntriesRepository.GetObject(entryId), complexFormTypeId); + }); + return Task.CompletedTask; + } + + public Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) + { + UndoableUnitOfWorkHelper.Do("Remove Complex Form Type", + "Add Complex Form Type", + Cache.ServiceLocator.ActionHandler, + () => + { + RemoveComplexFormType(EntriesRepository.GetObject(entryId), complexFormTypeId); + }); + return Task.CompletedTask; + } + /// /// must be called as part of an lcm action /// diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 4befbc1d2..11fff8575 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -102,6 +102,12 @@ public Task DeleteEntry(Guid id) return Task.CompletedTask; } + public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) + { + DryRunRecords.Add(new DryRunRecord(nameof(RemoveComplexFormType), $"Remove complex form type {complexFormTypeId}, from entry {entryId}")); + await Task.CompletedTask; + } + public Task CreateSense(Guid entryId, Sense sense) { DryRunRecords.Add(new DryRunRecord(nameof(CreateSense), $"Create sense {sense.Gloss}")); @@ -179,4 +185,10 @@ public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormCom DryRunRecords.Add(new DryRunRecord(nameof(ReplaceComplexFormComponent), $"Replace complex form component complex entry: {old.ComplexFormHeadword}, component entry: {old.ComponentHeadword} with complex entry: {@new.ComplexFormHeadword}, component entry: {@new.ComponentHeadword}")); return Task.CompletedTask; } + + public async Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) + { + DryRunRecords.Add(new DryRunRecord(nameof(AddComplexFormType), $"Add complex form type {complexFormTypeId}, to entry {entryId}")); + await Task.CompletedTask; + } } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 045d07aa5..fd34d2e1c 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -144,6 +144,16 @@ public async Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexF await dataModel.AddChange(ClientId, change); } + public async Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) + { + await dataModel.AddChange(ClientId, new AddComplexFormTypeChange(entryId, await ComplexFormTypes.SingleAsync(ct => ct.Id == complexFormTypeId))); + } + + public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) + { + await dataModel.AddChange(ClientId, new RemoveComplexFormTypeChange(entryId, complexFormTypeId)); + } + public IAsyncEnumerable GetEntries(QueryOptions? options = null) { return GetEntriesAsyncEnum(predicate: null, options); diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 35d2f06df..d21398e08 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -29,6 +29,8 @@ Task UpdateEntry(Entry before, Entry after) Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent); Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent); Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new); + Task AddComplexFormType(Guid entryId, Guid complexFormTypeId); + Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId); #endregion #region Sense diff --git a/backend/FwLite/MiniLcm/InMemoryApi.cs b/backend/FwLite/MiniLcm/InMemoryApi.cs index c77851f40..4894650b8 100644 --- a/backend/FwLite/MiniLcm/InMemoryApi.cs +++ b/backend/FwLite/MiniLcm/InMemoryApi.cs @@ -175,6 +175,16 @@ public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormCom throw new NotImplementedException(); } + public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) + { + throw new NotImplementedException(); + } + + public Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) + { + throw new NotImplementedException(); + } + private readonly string[] _exemplars = Enumerable.Range('a', 'z').Select(c => ((char)c).ToString()).ToArray(); public Task CreateEntry(Entry entry) From 90c44b4cd3e1a45431e29ea9af1378ed48f0b7e7 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:52:58 +0700 Subject: [PATCH 15/38] preseed complex form types in crdt projects --- backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs | 10 ++++++++++ backend/FwLite/LcmCrdt/ProjectsService.cs | 1 + 2 files changed, 11 insertions(+) diff --git a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs index b69837bd0..a9bc4984a 100644 --- a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs +++ b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs @@ -5,6 +5,16 @@ namespace LcmCrdt.Objects; public static class PreDefinedData { + internal static async Task PredefinedComplexFormTypes(DataModel dataModel, Guid clientId) + { + await dataModel.AddChanges(clientId, + [ + new CreateComplexFormType(new Guid("c36f55ed-d1ea-4069-90b3-3f35ff696273"), new MultiString() { { "en", "Compound" } } ), + new CreateComplexFormType(new Guid("eeb78fce-6009-4932-aaa6-85faeb180c69"), new MultiString() { { "en", "Unspecified" } }) + ], + new Guid("dc60d2a9-0cc2-48ed-803c-a238a14b6eae")); + } + internal static async Task PredefinedSemanticDomains(DataModel dataModel, Guid clientId) { //todo load from xml instead of hardcoding and use real IDs diff --git a/backend/FwLite/LcmCrdt/ProjectsService.cs b/backend/FwLite/LcmCrdt/ProjectsService.cs index 4a9950e8b..a9cbeb43f 100644 --- a/backend/FwLite/LcmCrdt/ProjectsService.cs +++ b/backend/FwLite/LcmCrdt/ProjectsService.cs @@ -81,6 +81,7 @@ internal static async Task InitProjectDb(LcmCrdtDbContext db, ProjectData data) internal static async Task SeedSystemData(DataModel dataModel, Guid clientId) { + await PreDefinedData.PredefinedComplexFormTypes(dataModel, clientId); await PreDefinedData.PredefinedPartsOfSpeech(dataModel, clientId); await PreDefinedData.PredefinedSemanticDomains(dataModel, clientId); } From be5fa9ef55886cd88e14dfb2b0c9695435b131b8 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 29 Oct 2024 16:53:18 +0700 Subject: [PATCH 16/38] implement syncing entry complex form types --- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 15 ++++++++++++ .../FwLite/MiniLcm/SyncHelpers/EntrySync.cs | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 57a5db9d4..a076fa01f 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -44,6 +44,7 @@ public async Task CanChangeComplexFormVisSync_Components() actual.Should().NotBeNull(); actual.Should().BeEquivalentTo(after); } + [Fact] public async Task CanChangeComplexFormViaSync_ComplexForms() { @@ -75,4 +76,18 @@ public async Task CanChangeComplexFormViaSync_ComplexForms() actual.Should().NotBeNull(); actual.Should().BeEquivalentTo(after); } + + [Fact] + public async Task CanChangeComplexFormTypeViaSync() + { + var entry = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "complexForm1" } } }); + var complexFormType = await _fixture.CrdtApi.GetComplexFormTypes().FirstAsync(); + var after = (Entry) entry.Copy(); + after.ComplexFormTypes = [complexFormType]; + await EntrySync.Sync(after, entry, _fixture.CrdtApi); + + var actual = await _fixture.CrdtApi.GetEntry(after.Id); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(after); + } } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs index 7f98c28cb..59dc1f506 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs @@ -31,9 +31,33 @@ public static async Task Sync(Entry afterEntry, Entry beforeEntry, IMiniLcm changes += await Sync(afterEntry.Components, beforeEntry.Components, api); changes += await Sync(afterEntry.ComplexForms, beforeEntry.ComplexForms, api); + changes += await Sync(afterEntry.Id, afterEntry.ComplexFormTypes, beforeEntry.ComplexFormTypes, api); return changes + (updateObjectInput is null ? 0 : 1); } + private static async Task Sync(Guid entryId, + IList afterComplexFormTypes, + IList beforeComplexFormTypes, + IMiniLcmApi api) + { + return await DiffCollection.Diff(api, + beforeComplexFormTypes, + afterComplexFormTypes, + complexFormType => complexFormType.Id, + async (api, afterComplexFormType) => + { + await api.AddComplexFormType(entryId, afterComplexFormType.Id); + return 1; + }, + async (api, beforeComplexFormType) => + { + await api.RemoveComplexFormType(entryId, beforeComplexFormType.Id); + return 1; + }, + //do nothing, complex form types are not editable, ignore any changes to them here + static (api, beforeComplexFormType, afterComplexFormType) => Task.FromResult(0)); + } + private static async Task Sync(IList afterComponents, IList beforeComponents, IMiniLcmApi api) { return await DiffCollection.Diff(api, From 9d4611eb46ccfa5c23662900539a220e6d85d08c Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 12:51:38 +0700 Subject: [PATCH 17/38] make complex form type a record --- .../Api/UpdateProxy/UpdateComplexFormTypeProxy.cs | 2 +- backend/FwLite/MiniLcm/Models/ComplexFormType.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs index 3dad6ad23..b3a6eeeb0 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs @@ -4,7 +4,7 @@ namespace FwDataMiniLcmBridge.Api.UpdateProxy; -public class UpdateComplexFormTypeProxy : ComplexFormType +public record UpdateComplexFormTypeProxy : ComplexFormType { private readonly ILexEntryType _lexEntryType; private readonly ILexEntry _lcmEntry; diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs index 4e3268cba..1a0cd095b 100644 --- a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs +++ b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs @@ -1,7 +1,7 @@ namespace MiniLcm.Models; //todo support an order for the complex form types, might be here, or on the entry -public class ComplexFormType : IObjectWithId +public record ComplexFormType : IObjectWithId { public virtual Guid Id { get; set; } public required MultiString Name { get; set; } From f783ca141dd64cbae0936558ee53f507adad3dff Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 12:52:26 +0700 Subject: [PATCH 18/38] test creating an entry with a generated entry --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 60 +++++++++++++- .../AutoFakerHelpers/EntryFakerHelper.cs | 81 +++++++++++++++++++ .../AutoFakerHelpers}/MultiStringOverride.cs | 2 +- .../AutoFakerHelpers/ObjectWithIdOverride.cs | 21 +++++ .../MiniLcm.Tests/CreateEntryTestsBase.cs | 19 ++++- .../FwLite/MiniLcm.Tests/MiniLcmTestBase.cs | 9 ++- 6 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs rename backend/FwLite/{FwLiteProjectSync.Tests/Fixtures => MiniLcm.Tests/AutoFakerHelpers}/MultiStringOverride.cs (93%) create mode 100644 backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index fd34d2e1c..85b171750 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -282,13 +282,67 @@ await dataModel.AddChanges(ClientId, ..await entry.Senses.ToAsyncEnumerable() .SelectMany(s => CreateSenseChanges(entry.Id, s)) .ToArrayAsync(), - ..entry.Components.Select(c => new AddEntryComponentChange(c)), - ..entry.ComplexForms.Select(c => new AddEntryComponentChange(c)), - ..entry.ComplexFormTypes.Select(c => new AddComplexFormTypeChange(entry.Id, c)) + ..await ToComplexFormComponents(entry.Components).ToArrayAsync(), + ..await ToComplexFormComponents(entry.ComplexForms).ToArrayAsync(), + ..await ToComplexFormTypes(entry.ComplexFormTypes).ToArrayAsync() ]); return await GetEntry(entry.Id) ?? throw new NullReferenceException(); + + async IAsyncEnumerable ToComplexFormComponents(IList complexFormComponents) + { + foreach (var complexFormComponent in complexFormComponents) + { + if (complexFormComponent.ComponentEntryId == default) complexFormComponent.ComponentEntryId = entry.Id; + if (complexFormComponent.ComplexFormEntryId == default) complexFormComponent.ComplexFormEntryId = entry.Id; + if (complexFormComponent.ComponentEntryId == complexFormComponent.ComplexFormEntryId) + { + throw new InvalidOperationException($"Complex form component {complexFormComponent} has the same component id as its complex form"); + } + if (complexFormComponent.ComponentEntryId != entry.Id && + await IsEntryDeleted(complexFormComponent.ComponentEntryId)) + { + throw new InvalidOperationException($"Complex form component {complexFormComponent} references deleted entry {complexFormComponent.ComponentEntryId} as its component"); + } + if (complexFormComponent.ComplexFormEntryId != entry.Id && + await IsEntryDeleted(complexFormComponent.ComplexFormEntryId)) + { + throw new InvalidOperationException($"Complex form component {complexFormComponent} references deleted entry {complexFormComponent.ComplexFormEntryId} as its complex form"); + } + + if (complexFormComponent.ComponentSenseId != null && + !await Senses.AnyAsyncEF(s => s.Id == complexFormComponent.ComponentSenseId.Value)) + { + throw new InvalidOperationException($"Complex form component {complexFormComponent} references deleted sense {complexFormComponent.ComponentSenseId} as its component"); + } + yield return new AddEntryComponentChange(complexFormComponent); + } + } + + async IAsyncEnumerable ToComplexFormTypes(IList complexFormTypes) + { + foreach (var complexFormType in complexFormTypes) + { + if (complexFormType.Id == default) + { + throw new InvalidOperationException("Complex form type must have an id"); + } + + if (!await ComplexFormTypes.AnyAsyncEF(t => t.Id == complexFormType.Id)) + { + throw new InvalidOperationException($"Complex form type {complexFormType} does not exist"); + } + yield return new AddComplexFormTypeChange(entry.Id, complexFormType); + } + } } + private async ValueTask IsEntryDeleted(Guid id) + { + return !await Entries.AnyAsyncEF(e => e.Id == id); + } + + + public async Task UpdateEntry(Guid id, UpdateObjectInput update) { diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs new file mode 100644 index 000000000..15daa6501 --- /dev/null +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs @@ -0,0 +1,81 @@ +using MiniLcm.Models; +using Soenneker.Utils.AutoBogus; + +namespace MiniLcm.Tests.AutoFakerHelpers; + +public static class EntryFakerHelper +{ + public static async Task EntryReadyForCreation(this AutoFaker autoFaker, + IMiniLcmApi api, + Guid? entryId = null, + bool createComplexForms = true, + bool createComplexFormTypes = true, + bool createComponents = true) + { + var entry = autoFaker.Generate(); + if (entryId.HasValue) entry.Id = entryId.Value; + if (createComponents) await CreateComplexFormComponentEntry(entry.Id, true, entry.Components, api); + if (createComplexForms) await CreateComplexFormComponentEntry(entry.Id, false, entry.ComplexForms, api); + if (createComplexFormTypes) await CreateComplexFormTypes(entry.ComplexFormTypes, api); + foreach (var sense in entry.Senses) + { + sense.EntryId = entry.Id; + foreach (var exampleSentence in sense.ExampleSentences) + { + exampleSentence.SenseId = sense.Id; + } + } + return entry; + static async Task CreateComplexFormComponentEntry(Guid entryId, + bool isComponent, + IList complexFormComponents, + IMiniLcmApi api) + { + int i = 1; + foreach (var complexFormComponent in complexFormComponents) + { + //generated entries won't have the expected ids, so fix them up here + if (isComponent) + { + complexFormComponent.ComplexFormEntryId = entryId; + } + else + { + complexFormComponent.ComponentEntryId = entryId; + complexFormComponent.ComponentSenseId = null; + } + + var name = $"test {(isComponent ? "component" : "complex form")} {i}"; + await api.CreateEntry(new() + { + Id = isComponent + ? complexFormComponent.ComponentEntryId + : complexFormComponent.ComplexFormEntryId, + LexemeForm = { { "en", name } }, + Senses = + [ + ..complexFormComponent.ComponentSenseId.HasValue && + isComponent + ? + [ + new Sense + { + Id = complexFormComponent.ComponentSenseId.Value, Gloss = { { "en", name } } + } + ] + : (ReadOnlySpan) [] + ] + }); + i++; + } + } + + static async Task CreateComplexFormTypes(IList complexFormTypes, IMiniLcmApi api) + { + foreach (var complexFormType in complexFormTypes) + { + await api.CreateComplexFormType(complexFormType); + } + } + } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MultiStringOverride.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs similarity index 93% rename from backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MultiStringOverride.cs rename to backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs index cb6af2072..b73b77b47 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MultiStringOverride.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs @@ -2,7 +2,7 @@ using Soenneker.Utils.AutoBogus.Context; using Soenneker.Utils.AutoBogus.Override; -namespace FwLiteProjectSync.Tests.Fixtures; +namespace MiniLcm.Tests.AutoFakerHelpers; public class MultiStringOverride: AutoFakerOverride { diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs new file mode 100644 index 000000000..5125236e6 --- /dev/null +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs @@ -0,0 +1,21 @@ +using MiniLcm.Models; +using Soenneker.Utils.AutoBogus.Context; +using Soenneker.Utils.AutoBogus.Generators; + +namespace MiniLcm.Tests.AutoFakerHelpers; + +public class ObjectWithIdOverride : AutoFakerGeneratorOverride +{ + public override bool CanOverride(AutoFakerContext context) + { + return context.GenerateType.IsAssignableTo(typeof(IObjectWithId)); + } + + public override void Generate(AutoFakerOverrideContext context) + { + if (context.Instance is IObjectWithId obj) + { + obj.DeletedAt = null; + } + } +} diff --git a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs index 3371818a9..8921108dc 100644 --- a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs @@ -1,8 +1,10 @@ using MiniLcm.Models; +using MiniLcm.Tests.AutoFakerHelpers; +using Soenneker.Utils.AutoBogus; namespace MiniLcm.Tests; -public abstract class CreateEntryTestsBase: MiniLcmTestBase +public abstract class CreateEntryTestsBase : MiniLcmTestBase { [Fact] public async Task CanCreateEntry() @@ -13,6 +15,14 @@ public async Task CanCreateEntry() entry.LexemeForm.Values["en"].Should().Be("test"); } + [Fact] + public async Task CanCreateEntry_AutoFaker() + { + var entry = await AutoFaker.EntryReadyForCreation(Api); + var createdEntry = await Api.CreateEntry(entry); + createdEntry.Should().BeEquivalentTo(entry); + } + [Fact] public async Task CanCreate_WithComponentsProperty() { @@ -96,7 +106,9 @@ await Api.CreateEntry(new() entry = await Api.GetEntry(complexFormEntryId); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComplexFormEntryId == complexFormEntryId && c.ComponentEntryId == component.Id && c.ComponentSenseId == componentSenseId); + entry!.Components.Should().ContainSingle(c => + c.ComplexFormEntryId == complexFormEntryId && c.ComponentEntryId == component.Id && + c.ComponentSenseId == componentSenseId); } [Fact] @@ -109,8 +121,7 @@ public async Task CanCreate_WithComplexFormTypesProperty() var entry = await Api.CreateEntry(new() { - LexemeForm = { { "en", "test" } }, - ComplexFormTypes = [complexFormType] + LexemeForm = { { "en", "test" } }, ComplexFormTypes = [complexFormType] }); entry = await Api.GetEntry(entry.Id); entry.Should().NotBeNull(); diff --git a/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs b/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs index ffcd982d9..71d03563f 100644 --- a/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs +++ b/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs @@ -1,8 +1,15 @@ -namespace MiniLcm.Tests; +using MiniLcm.Tests.AutoFakerHelpers; +using Soenneker.Utils.AutoBogus; + +namespace MiniLcm.Tests; public abstract class MiniLcmTestBase : IAsyncLifetime { + protected readonly AutoFaker AutoFaker = new(builder => + builder.WithOverride(new MultiStringOverride()) + .WithOverride(new ObjectWithIdOverride()) + ); protected IMiniLcmApi Api = null!; protected abstract Task NewApi(); From f04794e38f293185ddf482def3e204ef02f861c0 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 13:02:33 +0700 Subject: [PATCH 19/38] write sync tests for a generated entry, fix bugs --- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 14 ++++++++++++++ .../FwLiteProjectSync.Tests/UpdateDiffTests.cs | 1 + backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs | 4 ++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index a076fa01f..6db88e53b 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -1,11 +1,14 @@ using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; using MiniLcm.SyncHelpers; +using MiniLcm.Tests.AutoFakerHelpers; +using Soenneker.Utils.AutoBogus; namespace FwLiteProjectSync.Tests; public class EntrySyncTests : IClassFixture { + private readonly AutoFaker _autoFaker = new(builder => builder.WithOverride(new MultiStringOverride()).WithOverride(new ObjectWithIdOverride())); public EntrySyncTests(SyncFixture fixture) { _fixture = fixture; @@ -13,6 +16,17 @@ public EntrySyncTests(SyncFixture fixture) private readonly SyncFixture _fixture; + [Fact] + public async Task CanSyncRandomEntries() + { + var createdEntry = await _fixture.CrdtApi.CreateEntry(await _autoFaker.EntryReadyForCreation(_fixture.CrdtApi)); + var after = await _autoFaker.EntryReadyForCreation(_fixture.CrdtApi, entryId: createdEntry.Id); + await EntrySync.Sync(after, createdEntry, _fixture.CrdtApi); + var actual = await _fixture.CrdtApi.GetEntry(after.Id); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(after); + } + [Fact] public async Task CanChangeComplexFormVisSync_Components() { diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index a9f13f49c..c3ed8aa1c 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -1,6 +1,7 @@ using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; using MiniLcm.SyncHelpers; +using MiniLcm.Tests.AutoFakerHelpers; using Soenneker.Utils.AutoBogus; using Soenneker.Utils.AutoBogus.Config; diff --git a/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs b/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs index bba12f5c9..e0068168f 100644 --- a/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs @@ -45,9 +45,9 @@ public static void ValidatePatchDocument(IJsonPatchDocument patchDocument) { foreach (var operation in patchDocument.GetOperations()) { - if (operation.OperationType == OperationType.Remove) + if (operation.OperationType == OperationType.Remove && char.IsDigit(operation.Path?[^1] ?? default)) { - throw new NotSupportedException("remove at index not supported"); + throw new NotSupportedException("remove at index not supported, op " + JsonSerializer.Serialize(operation)); } // we want to make sure that the path is not an index, as a shortcut we just check the first character is not a digit, because it's invalid for fields to start with a digit. From fb05ae7f21048f85eb55d11c630756a21bf0b67b Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 13:32:19 +0700 Subject: [PATCH 20/38] update harmony to include the new `BeforeSaveObject` option --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 16 ++++++++-------- backend/harmony | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 85b171750..3ce622165 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -17,14 +17,14 @@ public class CrdtMiniLcmApi(DataModel dataModel, CurrentProjectService projectSe private Guid ClientId { get; } = projectService.ProjectData.ClientId; public ProjectData ProjectData => projectService.ProjectData; - private IQueryable Entries => dataModel.GetLatestObjects(); - private IQueryable ComplexFormComponents => dataModel.GetLatestObjects(); - private IQueryable ComplexFormTypes => dataModel.GetLatestObjects(); - private IQueryable Senses => dataModel.GetLatestObjects(); - private IQueryable ExampleSentences => dataModel.GetLatestObjects(); - private IQueryable WritingSystems => dataModel.GetLatestObjects(); - private IQueryable SemanticDomains => dataModel.GetLatestObjects(); - private IQueryable PartsOfSpeech => dataModel.GetLatestObjects(); + private IQueryable Entries => dataModel.QueryLatest(); + private IQueryable ComplexFormComponents => dataModel.QueryLatest(); + private IQueryable ComplexFormTypes => dataModel.QueryLatest(); + private IQueryable Senses => dataModel.QueryLatest(); + private IQueryable ExampleSentences => dataModel.QueryLatest(); + private IQueryable WritingSystems => dataModel.QueryLatest(); + private IQueryable SemanticDomains => dataModel.QueryLatest(); + private IQueryable PartsOfSpeech => dataModel.QueryLatest(); public async Task GetWritingSystems() { diff --git a/backend/harmony b/backend/harmony index 4200fa131..c7dea1f15 160000 --- a/backend/harmony +++ b/backend/harmony @@ -1 +1 @@ -Subproject commit 4200fa131004ab509a16b6385d7b6cd0da459578 +Subproject commit c7dea1f1511da52b7c130b8d2e8bc652941e5dd1 From 2a4e139980eddf57d671d17b85f13b827399b468 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 13:46:23 +0700 Subject: [PATCH 21/38] add version to miniLcm models using CrdtConfig.BeforeSaveObject --- .../MiniLcmTests/CreateEntryTests.cs | 7 +++++++ backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 18 ++++++++++++++++++ .../MiniLcm/Models/ComplexFormComponent.cs | 2 ++ .../FwLite/MiniLcm/Models/ComplexFormType.cs | 3 ++- backend/FwLite/MiniLcm/Models/Entry.cs | 3 ++- .../FwLite/MiniLcm/Models/ExampleSentence.cs | 2 ++ backend/FwLite/MiniLcm/Models/IObjectWithId.cs | 1 + backend/FwLite/MiniLcm/Models/PartOfSpeech.cs | 10 +++++++++- .../FwLite/MiniLcm/Models/SemanticDomain.cs | 2 ++ backend/FwLite/MiniLcm/Models/Sense.cs | 2 ++ backend/FwLite/MiniLcm/Models/WritingSystem.cs | 2 ++ 11 files changed, 49 insertions(+), 3 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs index 00a1985d5..fdbc65e65 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs @@ -17,4 +17,11 @@ public override async Task DisposeAsync() await _fixture.DisposeAsync(); } + [Fact] + public async Task CreateEntry_HasVersion() + { + var entry = await Api.CreateEntry(new() { LexemeForm = { { "en", "test" } } }); + entry.Version.Should().NotBeNullOrWhiteSpace(); + } + } diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 7e4df57c1..32d8f5369 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -66,6 +66,14 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio public static void ConfigureCrdt(CrdtConfig config) { config.EnableProjectedTables = true; + config.BeforeSaveObject = (obj, snapshot) => + { + if (obj is IObjectWithId objWithId) + { + objWithId.SetVersionGuid(snapshot.CommitId); + } + return ValueTask.CompletedTask; + }; config.ObjectTypeListBuilder .CustomAdapter() .Add(builder => @@ -169,4 +177,14 @@ private static async Task LoadMiniLcmApi(IServiceProvider services) await services.GetRequiredService().PopulateProjectDataCache(); return services.GetRequiredService(); } + + public static Guid GetVersionGuid(this IObjectWithId obj) + { + return Guid.Parse(obj.Version ?? throw new NullReferenceException("Version is null")); + } + public static Guid SetVersionGuid(this IObjectWithId obj, Guid version) + { + obj.Version = version.ToString("N"); + return version; + } } diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs b/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs index cbea10589..78d8d2c3d 100644 --- a/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs +++ b/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs @@ -21,6 +21,7 @@ public static ComplexFormComponent FromEntries(Entry complexFormEntry, public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public virtual required Guid ComplexFormEntryId { get; set; } public string? ComplexFormHeadword { get; set; } public virtual required Guid ComponentEntryId { get; set; } @@ -56,6 +57,7 @@ public IObjectWithId Copy() ComponentHeadword = ComponentHeadword, ComponentSenseId = ComponentSenseId, DeletedAt = DeletedAt, + Version = Version, }; } } diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs index 1a0cd095b..2b02b0b3e 100644 --- a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs +++ b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs @@ -7,6 +7,7 @@ public record ComplexFormType : IObjectWithId public required MultiString Name { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public Guid[] GetReferences() { @@ -19,6 +20,6 @@ public void RemoveReference(Guid id, DateTimeOffset time) public IObjectWithId Copy() { - return new ComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt, }; + return new ComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt, Version = Version }; } } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 2112a4550..36aba3362 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -4,6 +4,7 @@ public class Entry : IObjectWithId { public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public virtual MultiString LexemeForm { get; set; } = new(); @@ -40,7 +41,7 @@ public IObjectWithId Copy() { Id = Id, DeletedAt = DeletedAt, - // Version = Version, + Version = Version, LexemeForm = LexemeForm.Copy(), CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), diff --git a/backend/FwLite/MiniLcm/Models/ExampleSentence.cs b/backend/FwLite/MiniLcm/Models/ExampleSentence.cs index dde441994..63d3b1658 100644 --- a/backend/FwLite/MiniLcm/Models/ExampleSentence.cs +++ b/backend/FwLite/MiniLcm/Models/ExampleSentence.cs @@ -9,6 +9,7 @@ public class ExampleSentence : IObjectWithId public Guid SenseId { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public Guid[] GetReferences() { @@ -27,6 +28,7 @@ public IObjectWithId Copy() { Id = Id, DeletedAt = DeletedAt, + Version = Version, SenseId = SenseId, Sentence = Sentence.Copy(), Translation = Translation.Copy(), diff --git a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs index dd0cf269f..475aaafe6 100644 --- a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs +++ b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs @@ -15,6 +15,7 @@ public interface IObjectWithId { public Guid Id { get; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public Guid[] GetReferences(); diff --git a/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs b/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs index 0c0ccd014..97dced8cc 100644 --- a/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs +++ b/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs @@ -6,6 +6,7 @@ public class PartOfSpeech : IObjectWithId public MultiString Name { get; set; } = new(); public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public bool Predefined { get; set; } public Guid[] GetReferences() @@ -19,6 +20,13 @@ public void RemoveReference(Guid id, DateTimeOffset time) public IObjectWithId Copy() { - return new PartOfSpeech { Id = Id, Name = Name, DeletedAt = DeletedAt, Predefined = Predefined }; + return new PartOfSpeech + { + Id = Id, + Name = Name, + DeletedAt = DeletedAt, + Version = Version, + Predefined = Predefined + }; } } diff --git a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs index 616af0adb..9151d258d 100644 --- a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs +++ b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs @@ -6,6 +6,7 @@ public class SemanticDomain : IObjectWithId public virtual required MultiString Name { get; set; } public virtual required string Code { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public bool Predefined { get; set; } public Guid[] GetReferences() @@ -25,6 +26,7 @@ public IObjectWithId Copy() Code = Code, Name = Name, DeletedAt = DeletedAt, + Version = Version, Predefined = Predefined }; } diff --git a/backend/FwLite/MiniLcm/Models/Sense.cs b/backend/FwLite/MiniLcm/Models/Sense.cs index 2117e2738..8fad1fbfd 100644 --- a/backend/FwLite/MiniLcm/Models/Sense.cs +++ b/backend/FwLite/MiniLcm/Models/Sense.cs @@ -4,6 +4,7 @@ public class Sense : IObjectWithId { public virtual Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public Guid EntryId { get; set; } public virtual MultiString Definition { get; set; } = new(); public virtual MultiString Gloss { get; set; } = new(); @@ -34,6 +35,7 @@ public IObjectWithId Copy() Id = Id, EntryId = EntryId, DeletedAt = DeletedAt, + Version = Version, Definition = Definition.Copy(), Gloss = Gloss.Copy(), PartOfSpeech = PartOfSpeech, diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index ed96bcddc..7600bfee0 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -9,6 +9,7 @@ public record WritingSystem: IObjectWithId public required string Font { get; set; } public DateTimeOffset? DeletedAt { get; set; } + public string? Version { get; set; } public required WritingSystemType Type { get; set; } public string[] Exemplars { get; set; } = []; //todo probably need more stuff here, see wesay for ideas @@ -37,6 +38,7 @@ public IObjectWithId Copy() Font = Font, Exemplars = Exemplars, DeletedAt = DeletedAt, + Version = Version, Type = Type, Order = Order }; From 2c4119dae4c43c89889675cd2769befff7138a81 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 15:51:51 +0700 Subject: [PATCH 22/38] fix bug trying to use Commit.DateTime in a query --- .../MiniLcmTests/BasicApiTests.cs | 22 ++++++++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 41 +++++++++++-------- .../FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 31 +++++++------- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 2 +- backend/harmony | 2 +- 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs index 47ec1b108..3963e54f1 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs @@ -22,4 +22,26 @@ public override async Task DisposeAsync() await base.DisposeAsync(); await _fixture.DisposeAsync(); } + + [Fact] + public async Task UpdateEntry_CanUseSameVersionMultipleTimes() + { + var original = await Api.GetEntry(Entry1Id); + await Task.Delay(1000); + ArgumentNullException.ThrowIfNull(original); + var update1 = (Entry) original.Copy(); + var update2 = (Entry)original.Copy(); + + update1.LexemeForm["en"] = "updated"; + var updatedEntry = await Api.UpdateEntry(update1); + updatedEntry.LexemeForm["en"].Should().Be("updated"); + updatedEntry.Should().BeEquivalentTo(update1, options => options.Excluding(e => e.Version)); + + + update2.LexemeForm["es"] = "updated again"; + var updatedEntry2 = await Api.UpdateEntry(update2); + updatedEntry2.LexemeForm["en"].Should().Be("updated"); + updatedEntry2.LexemeForm["es"].Should().Be("updated again"); + updatedEntry2.Should().BeEquivalentTo(update2, options => options.Excluding(e => e.Version).Excluding(e => e.LexemeForm)); + } } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 3ce622165..4f3f11adc 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -350,24 +350,29 @@ public async Task UpdateEntry(Guid id, if (entry is null) throw new NullReferenceException($"unable to find entry with id {id}"); await dataModel.AddChanges(ClientId, [..entry.ToChanges(update.Patch)]); - return await GetEntry(id) ?? throw new NullReferenceException(); - } - - public async Task UpdateEntry(Entry before, Entry after) - { - //todo version stuff isn't working yet. - // if (!Guid.TryParse(entry.Version, out var snapshotId)) - // { - // throw new InvalidOperationException($"Unable to parse snapshot id '{entry.Version}'"); - // } - // - // //todo this will not work for properties not in the snapshot, but we don't have a way to get the full snapshot yet - // var beforeChanges = await dataModel.GetBySnapshotId(snapshotId); - // //workaround to avoid syncing senses, which are not in the snapshot - // beforeChanges.Senses = []; - // entry.Senses = []; - await EntrySync.Sync(after, before, this); - return await GetEntry(after.Id) ?? throw new NullReferenceException(); + return await GetEntry(id) ?? throw new NullReferenceException("unable to find entry with id " + id); + } + + public async Task UpdateEntry(Entry entry) + { + var commitId = entry.GetVersionGuid(); + + var commitHybridDate = await dbContext.Set() + .Where(c => c.Id == commitId) + .Select(c => c.HybridDateTime) + .SingleAsyncEF(); + //todo this will not work for properties not in the snapshot, but we don't have a way to get the full snapshot yet + //todo also, add api for getting an entity at a specific commit + var snapshot = await dataModel.GetEntitySnapshotAtTime(commitHybridDate.DateTime.AddSeconds(1), entry.Id); + var before = snapshot?.Entity.DbObject as Entry; + ArgumentNullException.ThrowIfNull(before); + //workaround to avoid syncing senses, which are not in the snapshot + before.Senses = []; + //don't want to modify what was passed in, so make a copy + entry = (Entry)entry.Copy(); + entry.Senses = []; + await EntrySync.Sync(entry, before, this); + return await GetEntry(entry.Id) ?? throw new NullReferenceException("unable to find entry with id " + entry.Id); } public async Task DeleteEntry(Guid id) diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index 9e6119e1b..83c071592 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -4,8 +4,8 @@ namespace MiniLcm.Tests; public abstract class BasicApiTestsBase : MiniLcmTestBase { - private readonly Guid _entry1Id = new Guid("a3f5aa5a-578f-4181-8f38-eaaf27f01f1c"); - private readonly Guid _entry2Id = new Guid("2de6c334-58fa-4844-b0fd-0bc2ce4ef835"); + protected readonly Guid Entry1Id = new Guid("a3f5aa5a-578f-4181-8f38-eaaf27f01f1c"); + protected readonly Guid Entry2Id = new Guid("2de6c334-58fa-4844-b0fd-0bc2ce4ef835"); public override async Task InitializeAsync() { @@ -34,7 +34,7 @@ await Api.CreateWritingSystem(WritingSystemType.Vernacular, }); await Api.CreateEntry(new Entry { - Id = _entry1Id, + Id = Entry1Id, LexemeForm = { Values = @@ -99,7 +99,7 @@ await Api.CreateEntry(new Entry }); await Api.CreateEntry(new() { - Id = _entry2Id, + Id = Entry2Id, LexemeForm = { Values = { { "en", "apple" } } @@ -172,12 +172,12 @@ public async Task GetEntries() { var entries = await Api.GetEntries().ToArrayAsync(); entries.Should().NotBeEmpty(); - var entry1 = entries.First(e => e.Id == _entry1Id); + var entry1 = entries.First(e => e.Id == Entry1Id); entry1.LexemeForm.Values.Should().NotBeEmpty(); var sense1 = entry1.Senses.Should().NotBeEmpty().And.Subject.First(); sense1.ExampleSentences.Should().NotBeEmpty(); - var entry2 = entries.First(e => e.Id == _entry2Id); + var entry2 = entries.First(e => e.Id == Entry2Id); entry2.LexemeForm.Values.Should().NotBeEmpty(); entry2.Senses.Should().NotBeEmpty(); } @@ -202,7 +202,7 @@ public async Task SearchEntries_MatchesGloss() [Fact] public async Task GetEntry() { - var entry = await Api.GetEntry(_entry1Id); + var entry = await Api.GetEntry(Entry1Id); entry.Should().NotBeNull(); entry!.LexemeForm.Values.Should().NotBeEmpty(); var sense = entry.Senses.Should() @@ -292,7 +292,7 @@ public async Task CreateEntry() [Fact] public async Task UpdateEntry() { - var updatedEntry = await Api.UpdateEntry(_entry1Id, + var updatedEntry = await Api.UpdateEntry(Entry1Id, new UpdateObjectInput() .Set(e => e.LexemeForm["en"], "updated")); updatedEntry.LexemeForm.Values["en"].Should().Be("updated"); @@ -301,13 +301,12 @@ public async Task UpdateEntry() [Fact] public async Task UpdateEntry_SimpleApi() { - var entry = await Api.GetEntry(_entry1Id); + var entry = await Api.GetEntry(Entry1Id); ArgumentNullException.ThrowIfNull(entry); - var before = (Entry) entry.Copy(); entry.LexemeForm["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(before, entry); + var updatedEntry = await Api.UpdateEntry(entry); updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry); + updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); } [Fact] @@ -366,7 +365,7 @@ public async Task UpdateSense() public async Task CreateSense_WontCreateMissingDomains() { var senseId = Guid.NewGuid(); - var createdSense = await Api.CreateSense(_entry1Id, new Sense() + var createdSense = await Api.CreateSense(Entry1Id, new Sense() { Id = senseId, SemanticDomains = [new SemanticDomain() { Id = Guid.NewGuid(), Code = "test", Name = new MultiString() }], @@ -384,7 +383,7 @@ public async Task CreateSense_WillCreateWithExistingDomains() await Api.CreateSemanticDomain(new SemanticDomain() { Id = semanticDomainId, Code = "test", Name = new MultiString() { { "en", "test" } } }); var semanticDomain = await Api.GetSemanticDomains().SingleOrDefaultAsync(sd => sd.Id == semanticDomainId); ArgumentNullException.ThrowIfNull(semanticDomain); - var createdSense = await Api.CreateSense(_entry1Id, new Sense() + var createdSense = await Api.CreateSense(Entry1Id, new Sense() { Id = senseId, SemanticDomains = [semanticDomain], @@ -397,7 +396,7 @@ public async Task CreateSense_WillCreateWithExistingDomains() public async Task CreateSense_WontCreateMissingPartOfSpeech() { var senseId = Guid.NewGuid(); - var createdSense = await Api.CreateSense(_entry1Id, + var createdSense = await Api.CreateSense(Entry1Id, new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = Guid.NewGuid(), }); createdSense.Id.Should().Be(senseId); createdSense.PartOfSpeechId.Should().BeNull("because the part of speech does not exist (or was deleted)"); @@ -411,7 +410,7 @@ public async Task CreateSense_WillCreateWthExistingPartOfSpeech() await Api.CreatePartOfSpeech(new PartOfSpeech() { Id = partOfSpeechId, Name = new MultiString() { { "en", "test" } } }); var partOfSpeech = await Api.GetPartsOfSpeech().SingleOrDefaultAsync(pos => pos.Id == partOfSpeechId); ArgumentNullException.ThrowIfNull(partOfSpeech); - var createdSense = await Api.CreateSense(_entry1Id, + var createdSense = await Api.CreateSense(Entry1Id, new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = partOfSpeechId, }); createdSense.Id.Should().Be(senseId); createdSense.PartOfSpeechId.Should().Be(partOfSpeechId, "because the part of speech does exist"); diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index d21398e08..5bd85b759 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -21,7 +21,7 @@ Task UpdateWritingSystem(WritingSystemId id, Task CreateEntry(Entry entry); Task UpdateEntry(Guid id, UpdateObjectInput update); - Task UpdateEntry(Entry before, Entry after) + Task UpdateEntry(Entry entry) { throw new NotImplementedException(); } diff --git a/backend/harmony b/backend/harmony index c7dea1f15..8749ac516 160000 --- a/backend/harmony +++ b/backend/harmony @@ -1 +1 @@ -Subproject commit c7dea1f1511da52b7c130b8d2e8bc652941e5dd1 +Subproject commit 8749ac516ce09c4c36da81e32831ef2a6aefc290 From 8d7f5a68d3f332bddc3dd049b09b5cb97aea3844 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 16:44:58 +0700 Subject: [PATCH 23/38] implement UpdateEntry(Entry entry) on FwDataMiniLcmApi, then fix bugs --- .../MiniLcmTests/BasicApiTests.cs | 24 +++++++++++- .../Api/FwDataMiniLcmApi.cs | 38 ++++++++++++++++++- .../Api/VersionInvalidException.cs | 5 +++ .../LcmUtils/ActionHandlerHelpers.cs | 26 +++++++++++++ .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 3 +- .../AutoFakerHelpers/EntryFakerHelper.cs | 9 ++++- .../AutoFakerHelpers/MultiStringOverride.cs | 7 +++- .../AutoFakerHelpers/ObjectWithIdOverride.cs | 1 + .../MiniLcm.Tests/CreateEntryTestsBase.cs | 16 ++++++-- .../Helpers/FluentAssertHelpers.cs | 12 ++++++ .../FwLite/MiniLcm.Tests/MiniLcmTestBase.cs | 2 +- 11 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge/Api/VersionInvalidException.cs create mode 100644 backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ActionHandlerHelpers.cs create mode 100644 backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs index 6b01bf088..e73da734e 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs @@ -1,4 +1,6 @@ -using FwDataMiniLcmBridge.Tests.Fixtures; +using FwDataMiniLcmBridge.Api; +using FwDataMiniLcmBridge.Tests.Fixtures; +using MiniLcm.Models; namespace FwDataMiniLcmBridge.Tests.MiniLcmTests; @@ -9,4 +11,24 @@ protected override Task NewApi() { return Task.FromResult(fixture.NewProjectApi("create-entry-test", "en", "en")); } + + [Fact] + public async Task UpdateEntry_WillInvalidateAndPreventMultipleUpdates() + { + var original = await Api.GetEntry(Entry1Id); + await Task.Delay(1000); + ArgumentNullException.ThrowIfNull(original); + var update1 = (Entry)original.Copy(); + var update2 = (Entry)original.Copy(); + + update1.LexemeForm["en"] = "updated"; + var updatedEntry = await Api.UpdateEntry(update1); + updatedEntry.LexemeForm["en"].Should().Be("updated"); + updatedEntry.Should().BeEquivalentTo(update1, options => options.Excluding(e => e.Version)); + + + update2.LexemeForm["es"] = "updated again"; + Func action = async () => await Api.UpdateEntry(update2); + await action.Should().ThrowAsync(); + } } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 2f3fbfaa7..86868af12 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -2,9 +2,11 @@ using System.Reflection; using System.Text; using FwDataMiniLcmBridge.Api.UpdateProxy; +using FwDataMiniLcmBridge.LcmUtils; using Microsoft.Extensions.Logging; using MiniLcm; using MiniLcm.Models; +using MiniLcm.SyncHelpers; using SIL.LCModel; using SIL.LCModel.Core.KernelInterfaces; using SIL.LCModel.Core.Text; @@ -293,6 +295,7 @@ private Entry FromLexEntry(ILexEntry entry) return new Entry { Id = entry.Guid, + Version = entry.DateModified.ToString("O"), Note = FromLcmMultiString(entry.Comment), LexemeForm = FromLcmMultiString(entry.LexemeFormOA.Form), CitationForm = FromLcmMultiString(entry.CitationForm), @@ -652,7 +655,7 @@ private void UpdateLcmMultiString(ITsMultiString multiString, MultiString newMul public Task UpdateEntry(Guid id, UpdateObjectInput update) { var lexEntry = EntriesRepository.GetObject(id); - UndoableUnitOfWorkHelper.Do("Update Entry", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Entry", "Revert entry", Cache.ServiceLocator.ActionHandler, () => @@ -663,6 +666,23 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) return Task.FromResult(FromLexEntry(lexEntry)); } + + + public async Task UpdateEntry(Entry entry) + { + ValidateVersion(entry); + InvalidateVersion(entry); + var before = await GetEntry(entry.Id); + ArgumentNullException.ThrowIfNull(before); + await Cache.DoUsingNewOrCurrentUOW("Update Entry", + "Revert entry", + async () => + { + await EntrySync.Sync(entry, before, this); + }); + return await GetEntry(entry.Id) ?? throw new NullReferenceException("unable to find entry with id " + entry.Id); + } + public Task DeleteEntry(Guid id) { UndoableUnitOfWorkHelper.Do("Delete Entry", @@ -843,4 +863,20 @@ private static void ValidateOwnership(ILexExampleSentence lexExampleSentence, Gu lexExampleSentence.Owner.ClassName); } } + + private readonly HashSet _invalidatedVersions = []; + private void ValidateVersion(IObjectWithId obj) + { + if (obj.Version is null) return; + if (_invalidatedVersions.Contains(obj.Version)) + { + throw new VersionInvalidException(obj.GetType().Name); + } + } + + private void InvalidateVersion(IObjectWithId obj) + { + if (obj.Version is null) return; + _invalidatedVersions.Add(obj.Version); + } } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/VersionInvalidException.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/VersionInvalidException.cs new file mode 100644 index 000000000..b05ead07e --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/VersionInvalidException.cs @@ -0,0 +1,5 @@ +namespace FwDataMiniLcmBridge.Api; + +public class VersionInvalidException(string type, Exception? innerException = null) : Exception( + $"version of {type} is invalid, it has been changed since this version was fetched", + innerException); diff --git a/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ActionHandlerHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ActionHandlerHelpers.cs new file mode 100644 index 000000000..70ef7f830 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge/LcmUtils/ActionHandlerHelpers.cs @@ -0,0 +1,26 @@ +using SIL.LCModel; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Infrastructure; + +namespace FwDataMiniLcmBridge.LcmUtils; + +public static class ActionHandlerHelpers +{ + public static async ValueTask DoUsingNewOrCurrentUOW( + this LcmCache cache, + string description, + string revertDescription, + Func action) + { + var actionHandler = cache.ServiceLocator.ActionHandler; + if (actionHandler.CurrentDepth > 0) + { + await action(); + return; + } + + using var undoHelper = new UndoableUnitOfWorkHelper(actionHandler, description, revertDescription); + await action(); + undoHelper.RollBack = false; // task ran successfully, don't roll back. + } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 6db88e53b..9c57efda0 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -2,6 +2,7 @@ using MiniLcm.Models; using MiniLcm.SyncHelpers; using MiniLcm.Tests.AutoFakerHelpers; +using MiniLcm.Tests.Helpers; using Soenneker.Utils.AutoBogus; namespace FwLiteProjectSync.Tests; @@ -24,7 +25,7 @@ public async Task CanSyncRandomEntries() await EntrySync.Sync(after, createdEntry, _fixture.CrdtApi); var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after); + actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); } [Fact] diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs index 15daa6501..aa1373e8d 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs @@ -46,7 +46,7 @@ static async Task CreateComplexFormComponentEntry(Guid entryId, } var name = $"test {(isComponent ? "component" : "complex form")} {i}"; - await api.CreateEntry(new() + var createdEntry = await api.CreateEntry(new() { Id = isComponent ? complexFormComponent.ComponentEntryId @@ -66,6 +66,13 @@ await api.CreateEntry(new() : (ReadOnlySpan) [] ] }); + if (isComponent) + { + complexFormComponent.ComplexFormHeadword = createdEntry.Headword(); + } else + { + complexFormComponent.ComponentHeadword = createdEntry.Headword(); + } i++; } } diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs index b73b77b47..b6818dd2f 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/MultiStringOverride.cs @@ -4,7 +4,7 @@ namespace MiniLcm.Tests.AutoFakerHelpers; -public class MultiStringOverride: AutoFakerOverride +public class MultiStringOverride(string[]? validWs = null): AutoFakerOverride { public override void Generate(AutoFakerOverrideContext context) { @@ -16,7 +16,10 @@ public override void Generate(AutoFakerOverrideContext context) var wordsArray = context.Faker.Random.WordsArray(1, 4); foreach (var word in wordsArray) { - target[context.Faker.Random.String(2, 'a', 'z')] = word; + var writingSystemId = validWs is not null + ? context.Faker.Random.ArrayElement(validWs) + : context.Faker.Random.String(2, 'a', 'z'); + target[writingSystemId] = word; } } } diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs index 5125236e6..f38e4ddd1 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs @@ -16,6 +16,7 @@ public override void Generate(AutoFakerOverrideContext context) if (context.Instance is IObjectWithId obj) { obj.DeletedAt = null; + obj.Version = null; } } } diff --git a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs index 8921108dc..31b285a6c 100644 --- a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs @@ -1,5 +1,9 @@ -using MiniLcm.Models; +using System.Linq.Expressions; +using System.Reflection; +using FluentAssertions.Equivalency; +using MiniLcm.Models; using MiniLcm.Tests.AutoFakerHelpers; +using MiniLcm.Tests.Helpers; using Soenneker.Utils.AutoBogus; namespace MiniLcm.Tests; @@ -18,9 +22,15 @@ public async Task CanCreateEntry() [Fact] public async Task CanCreateEntry_AutoFaker() { - var entry = await AutoFaker.EntryReadyForCreation(Api); + var entry = await AutoFaker.EntryReadyForCreation(Api, createComplexFormTypes:false); + //todo limitation of fwdata prevents us from specifying the complex form type ahead of time + foreach (var entryComplexFormType in entry.ComplexFormTypes) + { + entryComplexFormType.Id = Guid.Empty; + await Api.CreateComplexFormType(entryComplexFormType); + } var createdEntry = await Api.CreateEntry(entry); - createdEntry.Should().BeEquivalentTo(entry); + createdEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); } [Fact] diff --git a/backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs b/backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs new file mode 100644 index 000000000..09e24cac2 --- /dev/null +++ b/backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs @@ -0,0 +1,12 @@ +using FluentAssertions.Equivalency; +using MiniLcm.Models; + +namespace MiniLcm.Tests.Helpers; + +public static class FluentAssertHelpers +{ + public static EquivalencyAssertionOptions ExcludingVersion(this EquivalencyAssertionOptions options) where T: IObjectWithId + { + return options.Excluding(bool (info) => info.Name == "Version"); + } +} diff --git a/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs b/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs index 71d03563f..f4ee8de99 100644 --- a/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs +++ b/backend/FwLite/MiniLcm.Tests/MiniLcmTestBase.cs @@ -7,7 +7,7 @@ public abstract class MiniLcmTestBase : IAsyncLifetime { protected readonly AutoFaker AutoFaker = new(builder => - builder.WithOverride(new MultiStringOverride()) + builder.WithOverride(new MultiStringOverride(["en"])) .WithOverride(new ObjectWithIdOverride()) ); protected IMiniLcmApi Api = null!; From 356679d3d291685a2edd37702bbddd099b9e785e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 17:00:01 +0700 Subject: [PATCH 24/38] remove unused fields --- backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index 881ab4c07..912bdabf4 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -20,8 +20,6 @@ public class SyncFixture : IAsyncLifetime _services.ServiceProvider.GetRequiredService(); public IServiceProvider Services => _services.ServiceProvider; private readonly string _projectName; - private bool _crdtEnabled = true; - private bool _fwDataEnabled = true; private readonly MockProjectContext _projectContext = new(null); public static SyncFixture Create([CallerMemberName] string projectName = "") => new(projectName); From e4022949277b4394c959d48c76e21bdb4209556c Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 30 Oct 2024 17:17:39 +0700 Subject: [PATCH 25/38] use the version to check for changes having been invalidated rather than using a HashSet of versions --- .../Api/FwDataMiniLcmApi.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 86868af12..990a6e8a5 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -670,10 +670,9 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) public async Task UpdateEntry(Entry entry) { - ValidateVersion(entry); - InvalidateVersion(entry); var before = await GetEntry(entry.Id); ArgumentNullException.ThrowIfNull(before); + ValidateVersion(entry, before); await Cache.DoUsingNewOrCurrentUOW("Update Entry", "Revert entry", async () => @@ -864,19 +863,13 @@ private static void ValidateOwnership(ILexExampleSentence lexExampleSentence, Gu } } - private readonly HashSet _invalidatedVersions = []; - private void ValidateVersion(IObjectWithId obj) + private void ValidateVersion(IObjectWithId after, IObjectWithId before) { - if (obj.Version is null) return; - if (_invalidatedVersions.Contains(obj.Version)) + if (after.GetType() != before.GetType()) throw new InvalidOperationException( + $"Invalidating a different type of object {after.GetType().Name} with {before.GetType().Name}"); + if (after.Version is null || after.Version != before.Version) { - throw new VersionInvalidException(obj.GetType().Name); + throw new VersionInvalidException(after.GetType().Name); } } - - private void InvalidateVersion(IObjectWithId obj) - { - if (obj.Version is null) return; - _invalidatedVersions.Add(obj.Version); - } } From ce07b24b4436ea487872632d53be2436f201c56a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 31 Oct 2024 10:14:42 +0700 Subject: [PATCH 26/38] minor fix suggestions --- .../FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs | 3 +-- backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs index 32b532f8c..ea4703301 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs @@ -65,8 +65,7 @@ public override IList SemanticDomains semanticDomain => { #pragma warning disable VSTHRD002 - if (semanticDomain.Id != default && lexboxLcmApi.GetLcmSemanticDomain(semanticDomain.Id) is { } lcmSemanticDomain) - sense.SemanticDomainsRC.Add(lcmSemanticDomain); + lexboxLcmApi.AddSemanticDomainToSense(sense.Guid, semanticDomain).Wait(); }, semanticDomain => { diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index 912bdabf4..66b29d003 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -49,7 +49,7 @@ public async Task InitializeAsync() .ProjectsFolder; if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true); Directory.CreateDirectory(projectsFolder); - var lcmCache = _services.ServiceProvider.GetRequiredService() + _services.ServiceProvider.GetRequiredService() .NewProject(new FwDataProject(_projectName, projectsFolder), "en", "fr"); FwDataApi = _services.ServiceProvider.GetRequiredService().GetFwDataMiniLcmApi(_projectName, false); From b900c5e7463fb7c31990f615ed928b8ebda6a841 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 31 Oct 2024 10:27:23 +0700 Subject: [PATCH 27/38] test CreateComplexFormComponent, fix a bug when there's more than one component with the same entry --- .../MiniLcmTests/ComplexFormComponentTests.cs | 12 +++ .../Api/FwDataMiniLcmApi.cs | 10 +-- .../ComplexFormComponentTestsBase.cs | 77 +++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/ComplexFormComponentTests.cs create mode 100644 backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/ComplexFormComponentTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/ComplexFormComponentTests.cs new file mode 100644 index 000000000..96f978ca2 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/ComplexFormComponentTests.cs @@ -0,0 +1,12 @@ +using FwDataMiniLcmBridge.Tests.Fixtures; + +namespace FwDataMiniLcmBridge.Tests.MiniLcmTests; + +[Collection(ProjectLoaderFixture.Name)] +public class ComplexFormComponentTests(ProjectLoaderFixture fixture): ComplexFormComponentTestsBase +{ + protected override Task NewApi() + { + return Task.FromResult(fixture.NewProjectApi("complex-form-component-test", "en", "en")); + } +} diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 990a6e8a5..f4d0bd5b1 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -302,7 +302,7 @@ private Entry FromLexEntry(ILexEntry entry) LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), Senses = entry.AllSenses.Select(FromLexSense).ToList(), ComplexFormTypes = ToComplexFormTypes(entry), - Components = ToComplexFormComponents(entry), + Components = ToComplexFormComponents(entry).ToList(), ComplexForms = [ ..entry.ComplexFormEntries.Select(complexEntry => ToEntryReference(entry, complexEntry)), ..entry.AllSenses.SelectMany(sense => sense.ComplexFormEntries.Select(complexEntry => ToSenseReference(sense, complexEntry))) @@ -317,7 +317,7 @@ private IList ToComplexFormTypes(ILexEntry entry) .Select(ToComplexFormType) .ToList() ?? []; } - private IList ToComplexFormComponents(ILexEntry entry) + private IEnumerable ToComplexFormComponents(ILexEntry entry) { return entry.ComplexFormEntryRefs.SingleOrDefault() ?.ComponentLexemesRS @@ -326,8 +326,7 @@ private IList ToComplexFormComponents(ILexEntry entry) ILexEntry component => ToEntryReference(component, entry), ILexSense s => ToSenseReference(s, entry), _ => throw new NotSupportedException($"object type {o.ClassName} not supported") - }) - .ToList() ?? []; + }) ?? []; } private Variants? ToVariants(ILexEntry entry) @@ -531,7 +530,8 @@ public Task CreateComplexFormComponent(ComplexFormComponen var lexEntry = EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId); AddComplexFormComponent(lexEntry, complexFormComponent); }); - return Task.FromResult(ToComplexFormComponents(EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId)).Single(c => c.ComponentEntryId == complexFormComponent.ComponentEntryId)); + return Task.FromResult(ToComplexFormComponents(EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId)) + .Single(c => c.ComponentEntryId == complexFormComponent.ComponentEntryId && c.ComponentSenseId == complexFormComponent.ComponentSenseId)); } public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) diff --git a/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs b/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs new file mode 100644 index 000000000..80dac29d2 --- /dev/null +++ b/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs @@ -0,0 +1,77 @@ +using MiniLcm.Models; + +namespace MiniLcm.Tests; + +public abstract class ComplexFormComponentTestsBase : MiniLcmTestBase +{ + private readonly Guid _complexFormEntryId = Guid.NewGuid(); + private readonly Guid _componentEntryId = Guid.NewGuid(); + private readonly Guid _componentSenseId1 = Guid.NewGuid(); + private readonly Guid _componentSenseId2 = Guid.NewGuid(); + private Entry _complexFormEntry = null!; + private Entry _componentEntry = null!; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + _complexFormEntry = await Api.CreateEntry(new() + { + Id = _complexFormEntryId, + LexemeForm = { { "en", "complex form" } } + }); + _componentEntry = await Api.CreateEntry(new() + { + Id = _componentEntryId, + LexemeForm = { { "en", "component" } }, + Senses = + [ + new Sense + { + Id = _componentSenseId1, + Gloss = { { "en", "component sense 1" } } + }, + new Sense + { + Id = _componentSenseId2, + Gloss = { { "en", "component sense 2" } } + } + ] + }); + } + + [Fact] + public async Task CreateComplexFormComponent_Works() + { + var component = await Api.CreateComplexFormComponent(ComplexFormComponent.FromEntries(_complexFormEntry, _componentEntry)); + component.ComplexFormEntryId.Should().Be(_complexFormEntryId); + component.ComponentEntryId.Should().Be(_componentEntryId); + component.ComponentSenseId.Should().BeNull(); + component.ComplexFormHeadword.Should().Be("complex form"); + component.ComponentHeadword.Should().Be("component"); + } + + [Fact] + public async Task CreateComplexFormComponent_WorksWithSense() + { + var component = await Api.CreateComplexFormComponent(ComplexFormComponent.FromEntries(_complexFormEntry, _componentEntry, _componentSenseId1)); + component.ComplexFormEntryId.Should().Be(_complexFormEntryId); + component.ComponentEntryId.Should().Be(_componentEntryId); + component.ComponentSenseId.Should().Be(_componentSenseId1); + component.ComplexFormHeadword.Should().Be("complex form"); + component.ComponentHeadword.Should().Be("component"); + } + + [Fact] + public async Task CreateComplexFormComponent_CanCreateMultipleComponentSenses() + { + var component1 = await Api.CreateComplexFormComponent(ComplexFormComponent.FromEntries(_complexFormEntry, _componentEntry, _componentSenseId1)); + component1.ComplexFormEntryId.Should().Be(_complexFormEntryId); + component1.ComponentEntryId.Should().Be(_componentEntryId); + component1.ComponentSenseId.Should().Be(_componentSenseId1); + + var component2 = await Api.CreateComplexFormComponent(ComplexFormComponent.FromEntries(_complexFormEntry, _componentEntry, _componentSenseId2)); + component2.ComplexFormEntryId.Should().Be(_complexFormEntryId); + component2.ComponentEntryId.Should().Be(_componentEntryId); + component2.ComponentSenseId.Should().Be(_componentSenseId2); + } +} From df91f0ec5dd1ca7555215d07ad48558354786589 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 31 Oct 2024 10:37:21 +0700 Subject: [PATCH 28/38] write test demonstrating update entry not supporting sense changes --- .../MiniLcmTests/UpdateEntryTests.cs | 18 ++++ .../FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 14 +-- .../FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj | 1 + .../MiniLcm.Tests/UpdateEntryTestsBase.cs | 89 +++++++++++++++++++ 4 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 backend/FwLite/LcmCrdt.Tests/MiniLcmTests/UpdateEntryTests.cs create mode 100644 backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/UpdateEntryTests.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/UpdateEntryTests.cs new file mode 100644 index 000000000..cff6f819f --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/UpdateEntryTests.cs @@ -0,0 +1,18 @@ +namespace LcmCrdt.Tests.MiniLcmTests; + +public class UpdateEntryTests : UpdateEntryTestsBase +{ + private readonly MiniLcmApiFixture _fixture = new(); + protected override async Task NewApi() + { + await _fixture.InitializeAsync(); + var api = _fixture.Api; + return api; + } + + public override async Task DisposeAsync() + { + await base.DisposeAsync(); + await _fixture.DisposeAsync(); + } +} diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index 83c071592..da63a7f4d 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -1,6 +1,4 @@ -using MiniLcm.Models; - -namespace MiniLcm.Tests; +namespace MiniLcm.Tests; public abstract class BasicApiTestsBase : MiniLcmTestBase { @@ -298,16 +296,6 @@ public async Task UpdateEntry() updatedEntry.LexemeForm.Values["en"].Should().Be("updated"); } - [Fact] - public async Task UpdateEntry_SimpleApi() - { - var entry = await Api.GetEntry(Entry1Id); - ArgumentNullException.ThrowIfNull(entry); - entry.LexemeForm["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(entry); - updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); - } [Fact] public async Task UpdateEntryNote() diff --git a/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj b/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj index 6243d7eb5..86bcc41df 100644 --- a/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj +++ b/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs new file mode 100644 index 000000000..bb8ad6097 --- /dev/null +++ b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs @@ -0,0 +1,89 @@ +namespace MiniLcm.Tests; + +public abstract class UpdateEntryTestsBase : MiniLcmTestBase +{ + protected readonly Guid Entry1Id = new Guid("a3f5aa5a-578f-4181-8f38-eaaf27f01f1c"); + protected readonly Guid Entry2Id = new Guid("2de6c334-58fa-4844-b0fd-0bc2ce4ef835"); + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await Api.CreateWritingSystem(WritingSystemType.Analysis, + new WritingSystem() + { + Id = Guid.NewGuid(), + Type = WritingSystemType.Analysis, + WsId = "en", + Name = "English", + Abbreviation = "En", + Font = "Arial", + Exemplars = [] + }); + await Api.CreateWritingSystem(WritingSystemType.Vernacular, + new WritingSystem() + { + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = "en", + Name = "English", + Abbreviation = "En", + Font = "Arial", + Exemplars = [] + }); + await Api.CreateEntry(new Entry + { + Id = Entry1Id, + LexemeForm = { Values = { { "en", "Kevin" } } }, + Note = { Values = { { "en", "this is a test note from Kevin" } } }, + CitationForm = { Values = { { "en", "Kevin" } } }, + LiteralMeaning = { Values = { { "en", "Kevin" } } }, + Senses = + [ + new Sense + { + Gloss = { Values = { { "en", "Kevin" } } }, + Definition = { Values = { { "en", "Kevin" } } }, + ExampleSentences = + [ + new ExampleSentence { Sentence = { Values = { { "en", "Kevin is a good guy" } } } } + ] + } + ] + }); + await Api.CreateEntry(new() + { + Id = Entry2Id, + LexemeForm = { Values = { { "en", "apple" } } }, + Senses = + [ + new Sense + { + Gloss = { Values = { { "en", "fruit" } } }, + Definition = { Values = { { "en", "a round fruit, red or yellow" } } }, + } + ], + }); + } + + [Fact] + public async Task UpdateEntry_Works() + { + var entry = await Api.GetEntry(Entry1Id); + ArgumentNullException.ThrowIfNull(entry); + entry.LexemeForm["en"] = "updated"; + var updatedEntry = await Api.UpdateEntry(entry); + updatedEntry.LexemeForm["en"].Should().Be("updated"); + updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); + } + + [Fact] + public async Task UpdateEntry_SupportsSenseChanges() + { + var entry = await Api.GetEntry(Entry1Id); + ArgumentNullException.ThrowIfNull(entry); + entry.Senses[0].Gloss["en"] = "updated"; + var updatedEntry = await Api.UpdateEntry(entry); + updatedEntry.Senses[0].Gloss["en"].Should().Be("updated"); + updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); + } +} From 883984e95d7241937ba9e06669bc6ea152dc3af4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 1 Nov 2024 09:36:18 +0700 Subject: [PATCH 29/38] attempt to use get at commit api when updating an entry --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 31 ++++++++++--------- .../MiniLcm.Tests/UpdateEntryTestsBase.cs | 20 ++++++++++-- backend/harmony | 2 +- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 4f3f11adc..26e23723d 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -356,21 +356,24 @@ public async Task UpdateEntry(Guid id, public async Task UpdateEntry(Entry entry) { var commitId = entry.GetVersionGuid(); - - var commitHybridDate = await dbContext.Set() - .Where(c => c.Id == commitId) - .Select(c => c.HybridDateTime) - .SingleAsyncEF(); - //todo this will not work for properties not in the snapshot, but we don't have a way to get the full snapshot yet - //todo also, add api for getting an entity at a specific commit - var snapshot = await dataModel.GetEntitySnapshotAtTime(commitHybridDate.DateTime.AddSeconds(1), entry.Id); - var before = snapshot?.Entity.DbObject as Entry; + //todo consider using GetEntry and validate the versions, this could let us update senses + var before = await dataModel.GetAtCommit(commitId, entry.Id); ArgumentNullException.ThrowIfNull(before); - //workaround to avoid syncing senses, which are not in the snapshot - before.Senses = []; - //don't want to modify what was passed in, so make a copy - entry = (Entry)entry.Copy(); - entry.Senses = []; + //workaround to sync senses, which are not in the snapshot, however this will not work for senses that have been removed + before.Senses = await entry.Senses.ToAsyncEnumerable() + .SelectAwait(async s => + { + var beforeSense = await dataModel.GetAtCommit(s.GetVersionGuid(), s.Id); + beforeSense.ExampleSentences = await s.ExampleSentences.ToAsyncEnumerable() + .SelectAwait(async es => + { + var beforeExampleSentence = await dataModel.GetAtCommit(es.GetVersionGuid(), es.Id); + return beforeExampleSentence; + }) + .ToListAsync(); + return beforeSense; + }) + .ToListAsync(); await EntrySync.Sync(entry, before, this); return await GetEntry(entry.Id) ?? throw new NullReferenceException("unable to find entry with id " + entry.Id); } diff --git a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs index bb8ad6097..70037d836 100644 --- a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs @@ -1,4 +1,6 @@ -namespace MiniLcm.Tests; +using MiniLcm.Tests.Helpers; + +namespace MiniLcm.Tests; public abstract class UpdateEntryTestsBase : MiniLcmTestBase { @@ -73,7 +75,7 @@ public async Task UpdateEntry_Works() entry.LexemeForm["en"] = "updated"; var updatedEntry = await Api.UpdateEntry(entry); updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); + updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); } [Fact] @@ -84,6 +86,18 @@ public async Task UpdateEntry_SupportsSenseChanges() entry.Senses[0].Gloss["en"] = "updated"; var updatedEntry = await Api.UpdateEntry(entry); updatedEntry.Senses[0].Gloss["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry, options => options.Excluding(e => e.Version)); + updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); + } + + [Fact] + public async Task UpdateEntry_SupportsRemovingSenseChanges() + { + var entry = await Api.GetEntry(Entry1Id); + ArgumentNullException.ThrowIfNull(entry); + var senseCount = entry.Senses.Count; + entry.Senses.RemoveAt(0); + var updatedEntry = await Api.UpdateEntry(entry); + updatedEntry.Senses.Should().HaveCount(senseCount - 1); + updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); } } diff --git a/backend/harmony b/backend/harmony index 8749ac516..33b1aba76 160000 --- a/backend/harmony +++ b/backend/harmony @@ -1 +1 @@ -Subproject commit 8749ac516ce09c4c36da81e32831ef2a6aefc290 +Subproject commit 33b1aba763633e8fc63f97b1d02332d1e1739c5a From a90266c6ee859c400714a4df7226528b55477e6a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 5 Nov 2024 16:04:14 +0700 Subject: [PATCH 30/38] always using DoUsingNewOrCurrentUOW as we've got a lot of recursive calls now --- .../Api/FwDataMiniLcmApi.cs | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index f4d0bd5b1..cc80012ba 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -153,7 +153,7 @@ internal void CompleteExemplars(WritingSystems writingSystems) public Task CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem) { CoreWritingSystemDefinition? ws = null; - UndoableUnitOfWorkHelper.Do("Create Writing System", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Writing System", "Remove writing system", Cache.ServiceLocator.ActionHandler, () => @@ -203,7 +203,7 @@ public IAsyncEnumerable GetPartsOfSpeech() public Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) { if (partOfSpeech.Id == default) partOfSpeech.Id = Guid.NewGuid(); - UndoableUnitOfWorkHelper.Do("Create Part of Speech", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Part of Speech", "Remove part of speech", Cache.ServiceLocator.ActionHandler, () => @@ -233,7 +233,7 @@ public IAsyncEnumerable GetSemanticDomains() public Task CreateSemanticDomain(SemanticDomain semanticDomain) { if (semanticDomain.Id == Guid.Empty) semanticDomain.Id = Guid.NewGuid(); - UndoableUnitOfWorkHelper.Do("Create Semantic Domain", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Semantic Domain", "Remove semantic domain", Cache.ActionHandlerAccessor, () => @@ -268,7 +268,7 @@ private ComplexFormType ToComplexFormType(ICmPossibility t) public Task CreateComplexFormType(ComplexFormType complexFormType) { if (complexFormType.Id != default) throw new InvalidOperationException("Complex form type id must be empty"); - UndoableUnitOfWorkHelper.Do("Create complex form type", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create complex form type", "Remove complex form type", Cache.ActionHandlerAccessor, () => @@ -483,7 +483,7 @@ public IAsyncEnumerable SearchEntries(string query, QueryOptions? options public async Task CreateEntry(Entry entry) { entry.Id = entry.Id == default ? Guid.NewGuid() : entry.Id; - UndoableUnitOfWorkHelper.Do("Create Entry", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Entry", "Remove entry", Cache.ServiceLocator.ActionHandler, () => @@ -522,7 +522,7 @@ public async Task CreateEntry(Entry entry) public Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) { - UndoableUnitOfWorkHelper.Do("Create Complex Form Component", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Complex Form Component", "Remove Complex Form Component", Cache.ServiceLocator.ActionHandler, () => @@ -536,7 +536,7 @@ public Task CreateComplexFormComponent(ComplexFormComponen public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) { - UndoableUnitOfWorkHelper.Do("Delete Complex Form Component", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Complex Form Component", "Add Complex Form Component", Cache.ServiceLocator.ActionHandler, () => @@ -549,7 +549,7 @@ public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormComponent @new) { - UndoableUnitOfWorkHelper.Do("Replace Complex Form Component", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Replace Complex Form Component", "Replace Complex Form Component", Cache.ServiceLocator.ActionHandler, () => @@ -563,7 +563,7 @@ public Task ReplaceComplexFormComponent(ComplexFormComponent old, ComplexFormCom public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) { - UndoableUnitOfWorkHelper.Do("Add Complex Form Type", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Add Complex Form Type", "Remove Complex Form Type", Cache.ServiceLocator.ActionHandler, () => @@ -575,7 +575,7 @@ public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) public Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) { - UndoableUnitOfWorkHelper.Do("Remove Complex Form Type", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Remove Complex Form Type", "Add Complex Form Type", Cache.ServiceLocator.ActionHandler, () => @@ -666,25 +666,20 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) return Task.FromResult(FromLexEntry(lexEntry)); } - - - public async Task UpdateEntry(Entry entry) + public async Task UpdateEntry(Entry before, Entry after) { - var before = await GetEntry(entry.Id); - ArgumentNullException.ThrowIfNull(before); - ValidateVersion(entry, before); await Cache.DoUsingNewOrCurrentUOW("Update Entry", "Revert entry", async () => { - await EntrySync.Sync(entry, before, this); + await EntrySync.Sync(after, before, this); }); - return await GetEntry(entry.Id) ?? throw new NullReferenceException("unable to find entry with id " + entry.Id); + return await GetEntry(after.Id) ?? throw new NullReferenceException("unable to find entry with id " + after.Id); } public Task DeleteEntry(Guid id) { - UndoableUnitOfWorkHelper.Do("Delete Entry", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Entry", "Revert delete", Cache.ServiceLocator.ActionHandler, () => @@ -736,7 +731,7 @@ public Task CreateSense(Guid entryId, Sense sense) if (sense.Id == default) sense.Id = Guid.NewGuid(); if (!EntriesRepository.TryGetObject(entryId, out var lexEntry)) throw new InvalidOperationException("Entry not found"); - UndoableUnitOfWorkHelper.Do("Create Sense", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Sense", "Remove sense", Cache.ServiceLocator.ActionHandler, () => CreateSense(lexEntry, sense)); @@ -747,7 +742,7 @@ public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput @@ -788,7 +783,7 @@ public Task DeleteSense(Guid entryId, Guid senseId) { var lexSense = SenseRepository.GetObject(senseId); if (lexSense.Entry.Guid != entryId) throw new InvalidOperationException("Sense does not belong to entry"); - UndoableUnitOfWorkHelper.Do("Delete Sense", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Sense", "Revert delete", Cache.ServiceLocator.ActionHandler, () => lexSense.Delete()); @@ -811,7 +806,7 @@ public Task CreateExampleSentence(Guid entryId, Guid senseId, E if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid(); if (!SenseRepository.TryGetObject(senseId, out var lexSense)) throw new InvalidOperationException("Sense not found"); - UndoableUnitOfWorkHelper.Do("Create Example Sentence", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Example Sentence", "Remove example sentence", Cache.ServiceLocator.ActionHandler, () => CreateExampleSentence(lexSense, exampleSentence)); @@ -825,7 +820,7 @@ public Task UpdateExampleSentence(Guid entryId, { var lexExampleSentence = ExampleSentenceRepository.GetObject(exampleSentenceId); ValidateOwnership(lexExampleSentence, entryId, senseId); - UndoableUnitOfWorkHelper.Do("Update Example Sentence", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Example Sentence", "Revert example sentence", Cache.ServiceLocator.ActionHandler, () => @@ -840,7 +835,7 @@ public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSenten { var lexExampleSentence = ExampleSentenceRepository.GetObject(exampleSentenceId); ValidateOwnership(lexExampleSentence, entryId, senseId); - UndoableUnitOfWorkHelper.Do("Delete Example Sentence", + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Example Sentence", "Revert delete", Cache.ServiceLocator.ActionHandler, () => lexExampleSentence.Delete()); From 0a8fbef214641c80223433298eeb57f12b266cc4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 5 Nov 2024 16:07:20 +0700 Subject: [PATCH 31/38] simplify basic api tests to not special case different handling of entry updates --- .../MiniLcmTests/BasicApiTests.cs | 20 ----------------- .../MiniLcmTests/UpdateEntryTests.cs | 12 ++++++++++ ...elSnapshotTests.VerifyDbModel.verified.txt | 8 +++++++ .../MiniLcmTests/BasicApiTests.cs | 22 ------------------- 4 files changed, 20 insertions(+), 42 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/UpdateEntryTests.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs index e73da734e..824e3fda2 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/BasicApiTests.cs @@ -11,24 +11,4 @@ protected override Task NewApi() { return Task.FromResult(fixture.NewProjectApi("create-entry-test", "en", "en")); } - - [Fact] - public async Task UpdateEntry_WillInvalidateAndPreventMultipleUpdates() - { - var original = await Api.GetEntry(Entry1Id); - await Task.Delay(1000); - ArgumentNullException.ThrowIfNull(original); - var update1 = (Entry)original.Copy(); - var update2 = (Entry)original.Copy(); - - update1.LexemeForm["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(update1); - updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(update1, options => options.Excluding(e => e.Version)); - - - update2.LexemeForm["es"] = "updated again"; - Func action = async () => await Api.UpdateEntry(update2); - await action.Should().ThrowAsync(); - } } diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/UpdateEntryTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/UpdateEntryTests.cs new file mode 100644 index 000000000..a3efbc833 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/UpdateEntryTests.cs @@ -0,0 +1,12 @@ +using FwDataMiniLcmBridge.Tests.Fixtures; + +namespace FwDataMiniLcmBridge.Tests.MiniLcmTests; + +[Collection(ProjectLoaderFixture.Name)] +public class UpdateEntryTests(ProjectLoaderFixture fixture) : UpdateEntryTestsBase +{ + protected override Task NewApi() + { + return Task.FromResult(fixture.NewProjectApi("update-entry-test", "en", "en")); + } +} diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt index a7b2cc8c5..c5ad630be 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt @@ -26,6 +26,7 @@ ComponentSenseId (Guid?) FK Index DeletedAt (DateTimeOffset?) SnapshotId (no field, Guid?) Shadow FK Index + Version (string) Keys: Id PK Foreign keys: @@ -54,6 +55,7 @@ Annotations: Relational:ColumnType: jsonb SnapshotId (no field, Guid?) Shadow FK Index + Version (string) Keys: Id PK Foreign keys: @@ -88,6 +90,7 @@ Annotations: Relational:ColumnType: jsonb SnapshotId (no field, Guid?) Shadow FK Index + Version (string) Navigations: ComplexForms (IList) Collection ToDependent ComplexFormComponent Components (IList) Collection ToDependent ComplexFormComponent @@ -119,6 +122,7 @@ Translation (MultiString) Required Annotations: Relational:ColumnType: jsonb + Version (string) Keys: Id PK Foreign keys: @@ -144,6 +148,7 @@ Relational:ColumnType: jsonb Predefined (bool) Required SnapshotId (no field, Guid?) Shadow FK Index + Version (string) Keys: Id PK Foreign keys: @@ -168,6 +173,7 @@ Relational:ColumnType: jsonb Predefined (bool) Required SnapshotId (no field, Guid?) Shadow FK Index + Version (string) Keys: Id PK Foreign keys: @@ -199,6 +205,7 @@ Annotations: Relational:ColumnType: jsonb SnapshotId (no field, Guid?) Shadow FK Index + Version (string) Navigations: ExampleSentences (IList) Collection ToDependent ExampleSentence Keys: @@ -230,6 +237,7 @@ Order (double) Required SnapshotId (no field, Guid?) Shadow FK Index Type (WritingSystemType) Required + Version (string) WsId (WritingSystemId) Required Keys: Id PK diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs index 3963e54f1..47ec1b108 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/BasicApiTests.cs @@ -22,26 +22,4 @@ public override async Task DisposeAsync() await base.DisposeAsync(); await _fixture.DisposeAsync(); } - - [Fact] - public async Task UpdateEntry_CanUseSameVersionMultipleTimes() - { - var original = await Api.GetEntry(Entry1Id); - await Task.Delay(1000); - ArgumentNullException.ThrowIfNull(original); - var update1 = (Entry) original.Copy(); - var update2 = (Entry)original.Copy(); - - update1.LexemeForm["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(update1); - updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(update1, options => options.Excluding(e => e.Version)); - - - update2.LexemeForm["es"] = "updated again"; - var updatedEntry2 = await Api.UpdateEntry(update2); - updatedEntry2.LexemeForm["en"].Should().Be("updated"); - updatedEntry2.LexemeForm["es"].Should().Be("updated again"); - updatedEntry2.Should().BeEquivalentTo(update2, options => options.Excluding(e => e.Version).Excluding(e => e.LexemeForm)); - } } From 9650ad561abeef9c51d285708e76fdf56489ce6f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 5 Nov 2024 16:07:58 +0700 Subject: [PATCH 32/38] allow defining copy which returns the correct type instead of IObjectWithId --- backend/FwLite/MiniLcm/Models/Entry.cs | 4 ++-- backend/FwLite/MiniLcm/Models/IObjectWithId.cs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 36aba3362..837ce1907 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -1,6 +1,6 @@ namespace MiniLcm.Models; -public class Entry : IObjectWithId +public class Entry : IObjectWithId { public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } @@ -35,7 +35,7 @@ public string Headword() } - public IObjectWithId Copy() + public Entry Copy() { return new Entry { diff --git a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs index 475aaafe6..338d90e3e 100644 --- a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs +++ b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs @@ -23,3 +23,9 @@ public interface IObjectWithId public IObjectWithId Copy(); } + +public interface IObjectWithId : IObjectWithId where TSelf : IObjectWithId +{ + IObjectWithId IObjectWithId.Copy() => Copy(); + new TSelf Copy(); +} From 11054f91baf8e1fa1c04ce4b084d6771f6f3dc2a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 5 Nov 2024 16:08:19 +0700 Subject: [PATCH 33/38] change use of GetEntitySnapshotAtTime to GetAtTime --- backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs b/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs index f1fdacd23..4ea387061 100644 --- a/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs @@ -32,7 +32,7 @@ public static IEndpointConventionBuilder MapHistoryRoutes(this WebApplication ap { //todo requires the timestamp to be exact, otherwise the change made on that timestamp will not be included //consider using a commitId and looking up the timestamp, but then we should be exact to the commit which we aren't right now. - return await dataModel.GetEntitySnapshotAtTime(new DateTimeOffset(timestamp), entityId); + return await dataModel.GetAtTime(new DateTimeOffset(timestamp), entityId); }); group.MapGet("/{entityId}", (Guid entityId, ICrdtDbContext dbcontext) => From 18cb5ee24e5a55b52b137828b14d6dec18a98002 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 5 Nov 2024 16:08:46 +0700 Subject: [PATCH 34/38] exclude version from more tests --- backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs | 6 +++--- backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs | 8 +++++--- backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 9c57efda0..15d0c8610 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -57,7 +57,7 @@ public async Task CanChangeComplexFormVisSync_Components() var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after); + actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); } [Fact] @@ -89,7 +89,7 @@ public async Task CanChangeComplexFormViaSync_ComplexForms() var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after); + actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); } [Fact] @@ -103,6 +103,6 @@ public async Task CanChangeComplexFormTypeViaSync() var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after); + actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 6aefac3e8..793103513 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using MiniLcm; using MiniLcm.Models; +using MiniLcm.Tests.Helpers; using SystemTextJsonPatch; namespace FwLiteProjectSync.Tests; @@ -82,7 +83,7 @@ public async Task FirstSyncJustDoesAnImport() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, - options => options.For(e => e.Components).Exclude(c => c.Id) + options => options.ExcludingVersion().For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); } @@ -133,7 +134,7 @@ await crdtApi.CreateEntry(new Entry() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, - options => options.For(e => e.Components).Exclude(c => c.Id) + options => options.ExcludingVersion().For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); } @@ -157,6 +158,7 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options + .ExcludingVersion() .For(e => e.Components).Exclude(c => c.Id) //todo the headword should be changed .For(e => e.Components).Exclude(c => c.ComponentHeadword) @@ -187,7 +189,7 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, - options => options.For(e => e.Components).Exclude(c => c.Id) + options => options.ExcludingVersion().For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index c3ed8aa1c..261677a13 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -2,6 +2,7 @@ using MiniLcm.Models; using MiniLcm.SyncHelpers; using MiniLcm.Tests.AutoFakerHelpers; +using MiniLcm.Tests.Helpers; using Soenneker.Utils.AutoBogus; using Soenneker.Utils.AutoBogus.Config; @@ -22,7 +23,7 @@ public void EntryDiffShouldUpdateAllFields() var entryDiffToUpdate = EntrySync.EntryDiffToUpdate(before, after); ArgumentNullException.ThrowIfNull(entryDiffToUpdate); entryDiffToUpdate.Apply(before); - before.Should().BeEquivalentTo(after, options => options.Excluding(x => x.Id) + before.Should().BeEquivalentTo(after, options => options.ExcludingVersion().Excluding(x => x.Id) .Excluding(x => x.DeletedAt).Excluding(x => x.Senses) .Excluding(x => x.Components) .Excluding(x => x.ComplexForms) @@ -37,7 +38,7 @@ public async Task SenseDiffShouldUpdateAllFields() var senseDiffToUpdate = await SenseSync.SenseDiffToUpdate(before, after); ArgumentNullException.ThrowIfNull(senseDiffToUpdate); senseDiffToUpdate.Apply(before); - before.Should().BeEquivalentTo(after, options => options.Excluding(x => x.Id).Excluding(x => x.EntryId).Excluding(x => x.DeletedAt).Excluding(x => x.ExampleSentences)); + before.Should().BeEquivalentTo(after, options => options.ExcludingVersion().Excluding(x => x.Id).Excluding(x => x.EntryId).Excluding(x => x.DeletedAt).Excluding(x => x.ExampleSentences)); } [Fact] @@ -48,6 +49,6 @@ public void ExampleSentenceDiffShouldUpdateAllFields() var exampleSentenceDiffToUpdate = ExampleSentenceSync.DiffToUpdate(before, after); ArgumentNullException.ThrowIfNull(exampleSentenceDiffToUpdate); exampleSentenceDiffToUpdate.Apply(before); - before.Should().BeEquivalentTo(after, options => options.Excluding(x => x.Id).Excluding(x => x.SenseId).Excluding(x => x.DeletedAt)); + before.Should().BeEquivalentTo(after, options => options.ExcludingVersion().Excluding(x => x.Id).Excluding(x => x.SenseId).Excluding(x => x.DeletedAt)); } } From 57fe76c0bb5b6382acb1e3bb972a0f53d54b871b Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 5 Nov 2024 16:09:20 +0700 Subject: [PATCH 35/38] change UpdateEntry to use before and after instead of depending on Version --- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 6 ++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 27 +++------------- .../AutoFakerHelpers/EntryFakerHelper.cs | 16 ++++++---- .../MiniLcm.Tests/CreateEntryTestsBase.cs | 4 ++- .../MiniLcm.Tests/UpdateEntryTestsBase.cs | 32 +++++++++++++++++-- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 5 +-- backend/FwLite/MiniLcm/InMemoryApi.cs | 5 +++ 7 files changed, 57 insertions(+), 38 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 11fff8575..8f39f6a45 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -96,6 +96,12 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) return GetEntry(id)!; } + public Task UpdateEntry(Entry before, Entry after) + { + DryRunRecords.Add(new DryRunRecord(nameof(UpdateEntry), $"Update entry {after.Id}")); + return Task.FromResult(after); + } + public Task DeleteEntry(Guid id) { DryRunRecords.Add(new DryRunRecord(nameof(DeleteEntry), $"Delete entry {id}")); diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 26e23723d..1f04d6d0e 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -353,29 +353,10 @@ public async Task UpdateEntry(Guid id, return await GetEntry(id) ?? throw new NullReferenceException("unable to find entry with id " + id); } - public async Task UpdateEntry(Entry entry) - { - var commitId = entry.GetVersionGuid(); - //todo consider using GetEntry and validate the versions, this could let us update senses - var before = await dataModel.GetAtCommit(commitId, entry.Id); - ArgumentNullException.ThrowIfNull(before); - //workaround to sync senses, which are not in the snapshot, however this will not work for senses that have been removed - before.Senses = await entry.Senses.ToAsyncEnumerable() - .SelectAwait(async s => - { - var beforeSense = await dataModel.GetAtCommit(s.GetVersionGuid(), s.Id); - beforeSense.ExampleSentences = await s.ExampleSentences.ToAsyncEnumerable() - .SelectAwait(async es => - { - var beforeExampleSentence = await dataModel.GetAtCommit(es.GetVersionGuid(), es.Id); - return beforeExampleSentence; - }) - .ToListAsync(); - return beforeSense; - }) - .ToListAsync(); - await EntrySync.Sync(entry, before, this); - return await GetEntry(entry.Id) ?? throw new NullReferenceException("unable to find entry with id " + entry.Id); + public async Task UpdateEntry(Entry before, Entry after) + { + await EntrySync.Sync(after, before, this); + return await GetEntry(after.Id) ?? throw new NullReferenceException("unable to find entry with id " + after.Id); } public async Task DeleteEntry(Guid id) diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs index aa1373e8d..7e29e9f1f 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs @@ -14,8 +14,8 @@ public static async Task EntryReadyForCreation(this AutoFaker autoFaker, { var entry = autoFaker.Generate(); if (entryId.HasValue) entry.Id = entryId.Value; - if (createComponents) await CreateComplexFormComponentEntry(entry.Id, true, entry.Components, api); - if (createComplexForms) await CreateComplexFormComponentEntry(entry.Id, false, entry.ComplexForms, api); + if (createComponents) await CreateComplexFormComponentEntry(entry, true, entry.Components, api); + if (createComplexForms) await CreateComplexFormComponentEntry(entry, false, entry.ComplexForms, api); if (createComplexFormTypes) await CreateComplexFormTypes(entry.ComplexFormTypes, api); foreach (var sense in entry.Senses) { @@ -26,7 +26,7 @@ public static async Task EntryReadyForCreation(this AutoFaker autoFaker, } } return entry; - static async Task CreateComplexFormComponentEntry(Guid entryId, + static async Task CreateComplexFormComponentEntry(Entry entry, bool isComponent, IList complexFormComponents, IMiniLcmApi api) @@ -37,11 +37,11 @@ static async Task CreateComplexFormComponentEntry(Guid entryId, //generated entries won't have the expected ids, so fix them up here if (isComponent) { - complexFormComponent.ComplexFormEntryId = entryId; + complexFormComponent.ComplexFormEntryId = entry.Id; } else { - complexFormComponent.ComponentEntryId = entryId; + complexFormComponent.ComponentEntryId = entry.Id; complexFormComponent.ComponentSenseId = null; } @@ -68,10 +68,12 @@ static async Task CreateComplexFormComponentEntry(Guid entryId, }); if (isComponent) { - complexFormComponent.ComplexFormHeadword = createdEntry.Headword(); + complexFormComponent.ComponentHeadword = createdEntry.Headword(); + complexFormComponent.ComplexFormHeadword = entry.Headword(); } else { - complexFormComponent.ComponentHeadword = createdEntry.Headword(); + complexFormComponent.ComplexFormHeadword = createdEntry.Headword(); + complexFormComponent.ComponentHeadword = entry.Headword(); } i++; } diff --git a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs index 31b285a6c..539622e4b 100644 --- a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs @@ -30,7 +30,9 @@ public async Task CanCreateEntry_AutoFaker() await Api.CreateComplexFormType(entryComplexFormType); } var createdEntry = await Api.CreateEntry(entry); - createdEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); + createdEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion() + .For(e => e.Components).Exclude(e => e.Id) + .For(e => e.ComplexForms).Exclude(e => e.Id)); } [Fact] diff --git a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs index 70037d836..424ded5f6 100644 --- a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs @@ -72,8 +72,9 @@ public async Task UpdateEntry_Works() { var entry = await Api.GetEntry(Entry1Id); ArgumentNullException.ThrowIfNull(entry); + var before = entry.Copy(); entry.LexemeForm["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(entry); + var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.LexemeForm["en"].Should().Be("updated"); updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); } @@ -83,8 +84,9 @@ public async Task UpdateEntry_SupportsSenseChanges() { var entry = await Api.GetEntry(Entry1Id); ArgumentNullException.ThrowIfNull(entry); + var before = entry.Copy(); entry.Senses[0].Gloss["en"] = "updated"; - var updatedEntry = await Api.UpdateEntry(entry); + var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.Senses[0].Gloss["en"].Should().Be("updated"); updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); } @@ -94,10 +96,34 @@ public async Task UpdateEntry_SupportsRemovingSenseChanges() { var entry = await Api.GetEntry(Entry1Id); ArgumentNullException.ThrowIfNull(entry); + var before = entry.Copy(); var senseCount = entry.Senses.Count; entry.Senses.RemoveAt(0); - var updatedEntry = await Api.UpdateEntry(entry); + var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.Senses.Should().HaveCount(senseCount - 1); updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); } + + [Fact] + public async Task UpdateEntry_CanUseSameVersionMultipleTimes() + { + var original = await Api.GetEntry(Entry1Id); + await Task.Delay(1000); + ArgumentNullException.ThrowIfNull(original); + var update1 = (Entry)original.Copy(); + var update2 = (Entry)original.Copy(); + + update1.LexemeForm["en"] = "updated"; + var updatedEntry = await Api.UpdateEntry(original, update1); + updatedEntry.LexemeForm["en"].Should().Be("updated"); + updatedEntry.Should().BeEquivalentTo(update1, options => options.Excluding(e => e.Version)); + + + update2.LexemeForm["es"] = "updated again"; + var updatedEntry2 = await Api.UpdateEntry(original, update2); + updatedEntry2.LexemeForm["en"].Should().Be("updated"); + updatedEntry2.LexemeForm["es"].Should().Be("updated again"); + updatedEntry2.Should().BeEquivalentTo(update2, + options => options.Excluding(e => e.Version).Excluding(e => e.LexemeForm)); + } } diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 5bd85b759..50616934f 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -21,10 +21,7 @@ Task UpdateWritingSystem(WritingSystemId id, Task CreateEntry(Entry entry); Task UpdateEntry(Guid id, UpdateObjectInput update); - Task UpdateEntry(Entry entry) - { - throw new NotImplementedException(); - } + Task UpdateEntry(Entry before, Entry after); Task DeleteEntry(Guid id); Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent); Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent); diff --git a/backend/FwLite/MiniLcm/InMemoryApi.cs b/backend/FwLite/MiniLcm/InMemoryApi.cs index 4894650b8..c5c7a5a24 100644 --- a/backend/FwLite/MiniLcm/InMemoryApi.cs +++ b/backend/FwLite/MiniLcm/InMemoryApi.cs @@ -284,6 +284,11 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) return Task.FromResult(entry as Entry); } + public Task UpdateEntry(Entry before, Entry after) + { + throw new NotImplementedException(); + } + public Task UpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, From bb428cbd3450cea024bcf5fdaf6761e2ad594e01 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 11 Nov 2024 16:13:48 +0700 Subject: [PATCH 36/38] remove version property --- .../Api/FwDataMiniLcmApi.cs | 11 ----------- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 12 ++++++------ .../FwLiteProjectSync.Tests/SyncTests.cs | 11 +++++------ .../FwLiteProjectSync.Tests/UpdateDiffTests.cs | 1 + .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 12 ------------ ...delSnapshotTests.VerifyDbModel.verified.txt | 8 -------- .../MiniLcmTests/CreateEntryTests.cs | 8 -------- .../AutoFakerHelpers/ObjectWithIdOverride.cs | 1 - .../MiniLcm.Tests/CreateEntryTestsBase.cs | 10 ++-------- .../Helpers/FluentAssertHelpers.cs | 12 ------------ .../MiniLcm.Tests/UpdateEntryTestsBase.cs | 18 ++++++++---------- .../MiniLcm/Models/ComplexFormComponent.cs | 2 -- .../FwLite/MiniLcm/Models/ComplexFormType.cs | 3 +-- backend/FwLite/MiniLcm/Models/Entry.cs | 2 -- .../FwLite/MiniLcm/Models/ExampleSentence.cs | 2 -- backend/FwLite/MiniLcm/Models/IObjectWithId.cs | 1 - backend/FwLite/MiniLcm/Models/PartOfSpeech.cs | 2 -- .../FwLite/MiniLcm/Models/SemanticDomain.cs | 2 -- backend/FwLite/MiniLcm/Models/Sense.cs | 2 -- backend/FwLite/MiniLcm/Models/WritingSystem.cs | 2 -- 20 files changed, 23 insertions(+), 99 deletions(-) delete mode 100644 backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 7aee40533..6d90311ee 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -337,7 +337,6 @@ private Entry FromLexEntry(ILexEntry entry) return new Entry { Id = entry.Guid, - Version = entry.DateModified.ToString("O"), Note = FromLcmMultiString(entry.Comment), LexemeForm = FromLcmMultiString(entry.LexemeFormOA.Form), CitationForm = FromLcmMultiString(entry.CitationForm), @@ -896,14 +895,4 @@ private static void ValidateOwnership(ILexExampleSentence lexExampleSentence, Gu lexExampleSentence.Owner.ClassName); } } - - private void ValidateVersion(IObjectWithId after, IObjectWithId before) - { - if (after.GetType() != before.GetType()) throw new InvalidOperationException( - $"Invalidating a different type of object {after.GetType().Name} with {before.GetType().Name}"); - if (after.Version is null || after.Version != before.Version) - { - throw new VersionInvalidException(after.GetType().Name); - } - } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 15d0c8610..523f73526 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -1,8 +1,8 @@ -using FwLiteProjectSync.Tests.Fixtures; +using FluentAssertions.Equivalency; +using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; using MiniLcm.SyncHelpers; using MiniLcm.Tests.AutoFakerHelpers; -using MiniLcm.Tests.Helpers; using Soenneker.Utils.AutoBogus; namespace FwLiteProjectSync.Tests; @@ -25,7 +25,7 @@ public async Task CanSyncRandomEntries() await EntrySync.Sync(after, createdEntry, _fixture.CrdtApi); var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); + actual.Should().BeEquivalentTo(after, options => options); } [Fact] @@ -57,7 +57,7 @@ public async Task CanChangeComplexFormVisSync_Components() var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); + actual.Should().BeEquivalentTo(after, options => options); } [Fact] @@ -89,7 +89,7 @@ public async Task CanChangeComplexFormViaSync_ComplexForms() var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); + actual.Should().BeEquivalentTo(after, options => options); } [Fact] @@ -103,6 +103,6 @@ public async Task CanChangeComplexFormTypeViaSync() var actual = await _fixture.CrdtApi.GetEntry(after.Id); actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(after, options => options.ExcludingVersion()); + actual.Should().BeEquivalentTo(after, options => options); } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index a01130ba3..82caf21c3 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -1,10 +1,10 @@ -using FwLiteProjectSync.Tests.Fixtures; +using FluentAssertions.Equivalency; +using FwLiteProjectSync.Tests.Fixtures; using LcmCrdt; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using MiniLcm; using MiniLcm.Models; -using MiniLcm.Tests.Helpers; using SystemTextJsonPatch; namespace FwLiteProjectSync.Tests; @@ -83,7 +83,7 @@ public async Task FirstSyncJustDoesAnImport() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, - options => options.ExcludingVersion().For(e => e.Components).Exclude(c => c.Id) + options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); } @@ -134,7 +134,7 @@ await crdtApi.CreateEntry(new Entry() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, - options => options.ExcludingVersion().For(e => e.Components).Exclude(c => c.Id) + options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); } @@ -234,7 +234,6 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options - .ExcludingVersion() .For(e => e.Components).Exclude(c => c.Id) //todo the headword should be changed .For(e => e.Components).Exclude(c => c.ComponentHeadword) @@ -302,7 +301,7 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, - options => options.ExcludingVersion().For(e => e.Components).Exclude(c => c.Id) + options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index a9f13f49c..c3ed8aa1c 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -1,6 +1,7 @@ using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; using MiniLcm.SyncHelpers; +using MiniLcm.Tests.AutoFakerHelpers; using Soenneker.Utils.AutoBogus; using Soenneker.Utils.AutoBogus.Config; diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 031f26a73..e6099590c 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -164,18 +164,6 @@ public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) return Task.CompletedTask; } - public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain) - { - DryRunRecords.Add(new DryRunRecord(nameof(AddSemanticDomainToSense), $"Add semantic domain {semanticDomain.Name}")); - return Task.CompletedTask; - } - - public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) - { - DryRunRecords.Add(new DryRunRecord(nameof(RemoveSemanticDomainFromSense), $"Remove semantic domain {semanticDomainId}")); - return Task.CompletedTask; - } - public Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence) { DryRunRecords.Add(new DryRunRecord(nameof(CreateExampleSentence), $"Create example sentence {exampleSentence.Sentence}")); diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt index c5ad630be..a7b2cc8c5 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt @@ -26,7 +26,6 @@ ComponentSenseId (Guid?) FK Index DeletedAt (DateTimeOffset?) SnapshotId (no field, Guid?) Shadow FK Index - Version (string) Keys: Id PK Foreign keys: @@ -55,7 +54,6 @@ Annotations: Relational:ColumnType: jsonb SnapshotId (no field, Guid?) Shadow FK Index - Version (string) Keys: Id PK Foreign keys: @@ -90,7 +88,6 @@ Annotations: Relational:ColumnType: jsonb SnapshotId (no field, Guid?) Shadow FK Index - Version (string) Navigations: ComplexForms (IList) Collection ToDependent ComplexFormComponent Components (IList) Collection ToDependent ComplexFormComponent @@ -122,7 +119,6 @@ Translation (MultiString) Required Annotations: Relational:ColumnType: jsonb - Version (string) Keys: Id PK Foreign keys: @@ -148,7 +144,6 @@ Relational:ColumnType: jsonb Predefined (bool) Required SnapshotId (no field, Guid?) Shadow FK Index - Version (string) Keys: Id PK Foreign keys: @@ -173,7 +168,6 @@ Relational:ColumnType: jsonb Predefined (bool) Required SnapshotId (no field, Guid?) Shadow FK Index - Version (string) Keys: Id PK Foreign keys: @@ -205,7 +199,6 @@ Annotations: Relational:ColumnType: jsonb SnapshotId (no field, Guid?) Shadow FK Index - Version (string) Navigations: ExampleSentences (IList) Collection ToDependent ExampleSentence Keys: @@ -237,7 +230,6 @@ Order (double) Required SnapshotId (no field, Guid?) Shadow FK Index Type (WritingSystemType) Required - Version (string) WsId (WritingSystemId) Required Keys: Id PK diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs index fdbc65e65..4b089ed1d 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmTests/CreateEntryTests.cs @@ -16,12 +16,4 @@ public override async Task DisposeAsync() await base.DisposeAsync(); await _fixture.DisposeAsync(); } - - [Fact] - public async Task CreateEntry_HasVersion() - { - var entry = await Api.CreateEntry(new() { LexemeForm = { { "en", "test" } } }); - entry.Version.Should().NotBeNullOrWhiteSpace(); - } - } diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs index f38e4ddd1..5125236e6 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/ObjectWithIdOverride.cs @@ -16,7 +16,6 @@ public override void Generate(AutoFakerOverrideContext context) if (context.Instance is IObjectWithId obj) { obj.DeletedAt = null; - obj.Version = null; } } } diff --git a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs index 539622e4b..1f5196bc8 100644 --- a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs @@ -1,10 +1,4 @@ -using System.Linq.Expressions; -using System.Reflection; -using FluentAssertions.Equivalency; -using MiniLcm.Models; -using MiniLcm.Tests.AutoFakerHelpers; -using MiniLcm.Tests.Helpers; -using Soenneker.Utils.AutoBogus; +using MiniLcm.Tests.AutoFakerHelpers; namespace MiniLcm.Tests; @@ -30,7 +24,7 @@ public async Task CanCreateEntry_AutoFaker() await Api.CreateComplexFormType(entryComplexFormType); } var createdEntry = await Api.CreateEntry(entry); - createdEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion() + createdEntry.Should().BeEquivalentTo(entry, options => options .For(e => e.Components).Exclude(e => e.Id) .For(e => e.ComplexForms).Exclude(e => e.Id)); } diff --git a/backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs b/backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs deleted file mode 100644 index 09e24cac2..000000000 --- a/backend/FwLite/MiniLcm.Tests/Helpers/FluentAssertHelpers.cs +++ /dev/null @@ -1,12 +0,0 @@ -using FluentAssertions.Equivalency; -using MiniLcm.Models; - -namespace MiniLcm.Tests.Helpers; - -public static class FluentAssertHelpers -{ - public static EquivalencyAssertionOptions ExcludingVersion(this EquivalencyAssertionOptions options) where T: IObjectWithId - { - return options.Excluding(bool (info) => info.Name == "Version"); - } -} diff --git a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs index 424ded5f6..65913827b 100644 --- a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs @@ -1,6 +1,4 @@ -using MiniLcm.Tests.Helpers; - -namespace MiniLcm.Tests; +namespace MiniLcm.Tests; public abstract class UpdateEntryTestsBase : MiniLcmTestBase { @@ -76,7 +74,7 @@ public async Task UpdateEntry_Works() entry.LexemeForm["en"] = "updated"; var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); + updatedEntry.Should().BeEquivalentTo(entry, options => options); } [Fact] @@ -88,7 +86,7 @@ public async Task UpdateEntry_SupportsSenseChanges() entry.Senses[0].Gloss["en"] = "updated"; var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.Senses[0].Gloss["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); + updatedEntry.Should().BeEquivalentTo(entry, options => options); } [Fact] @@ -101,7 +99,7 @@ public async Task UpdateEntry_SupportsRemovingSenseChanges() entry.Senses.RemoveAt(0); var updatedEntry = await Api.UpdateEntry(before, entry); updatedEntry.Senses.Should().HaveCount(senseCount - 1); - updatedEntry.Should().BeEquivalentTo(entry, options => options.ExcludingVersion()); + updatedEntry.Should().BeEquivalentTo(entry, options => options); } [Fact] @@ -110,13 +108,13 @@ public async Task UpdateEntry_CanUseSameVersionMultipleTimes() var original = await Api.GetEntry(Entry1Id); await Task.Delay(1000); ArgumentNullException.ThrowIfNull(original); - var update1 = (Entry)original.Copy(); - var update2 = (Entry)original.Copy(); + var update1 = original.Copy(); + var update2 = original.Copy(); update1.LexemeForm["en"] = "updated"; var updatedEntry = await Api.UpdateEntry(original, update1); updatedEntry.LexemeForm["en"].Should().Be("updated"); - updatedEntry.Should().BeEquivalentTo(update1, options => options.Excluding(e => e.Version)); + updatedEntry.Should().BeEquivalentTo(update1); update2.LexemeForm["es"] = "updated again"; @@ -124,6 +122,6 @@ public async Task UpdateEntry_CanUseSameVersionMultipleTimes() updatedEntry2.LexemeForm["en"].Should().Be("updated"); updatedEntry2.LexemeForm["es"].Should().Be("updated again"); updatedEntry2.Should().BeEquivalentTo(update2, - options => options.Excluding(e => e.Version).Excluding(e => e.LexemeForm)); + options => options.Excluding(e => e.LexemeForm)); } } diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs b/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs index 78d8d2c3d..cbea10589 100644 --- a/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs +++ b/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs @@ -21,7 +21,6 @@ public static ComplexFormComponent FromEntries(Entry complexFormEntry, public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public virtual required Guid ComplexFormEntryId { get; set; } public string? ComplexFormHeadword { get; set; } public virtual required Guid ComponentEntryId { get; set; } @@ -57,7 +56,6 @@ public IObjectWithId Copy() ComponentHeadword = ComponentHeadword, ComponentSenseId = ComponentSenseId, DeletedAt = DeletedAt, - Version = Version, }; } } diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs index 2b02b0b3e..5ba397094 100644 --- a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs +++ b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs @@ -7,7 +7,6 @@ public record ComplexFormType : IObjectWithId public required MultiString Name { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public Guid[] GetReferences() { @@ -20,6 +19,6 @@ public void RemoveReference(Guid id, DateTimeOffset time) public IObjectWithId Copy() { - return new ComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt, Version = Version }; + return new ComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt }; } } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 837ce1907..191db2753 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -4,7 +4,6 @@ public class Entry : IObjectWithId { public Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public virtual MultiString LexemeForm { get; set; } = new(); @@ -41,7 +40,6 @@ public Entry Copy() { Id = Id, DeletedAt = DeletedAt, - Version = Version, LexemeForm = LexemeForm.Copy(), CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), diff --git a/backend/FwLite/MiniLcm/Models/ExampleSentence.cs b/backend/FwLite/MiniLcm/Models/ExampleSentence.cs index 63d3b1658..dde441994 100644 --- a/backend/FwLite/MiniLcm/Models/ExampleSentence.cs +++ b/backend/FwLite/MiniLcm/Models/ExampleSentence.cs @@ -9,7 +9,6 @@ public class ExampleSentence : IObjectWithId public Guid SenseId { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public Guid[] GetReferences() { @@ -28,7 +27,6 @@ public IObjectWithId Copy() { Id = Id, DeletedAt = DeletedAt, - Version = Version, SenseId = SenseId, Sentence = Sentence.Copy(), Translation = Translation.Copy(), diff --git a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs index 338d90e3e..b9f0dfa0b 100644 --- a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs +++ b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs @@ -15,7 +15,6 @@ public interface IObjectWithId { public Guid Id { get; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public Guid[] GetReferences(); diff --git a/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs b/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs index 514c9dcb6..5595065cb 100644 --- a/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs +++ b/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs @@ -7,7 +7,6 @@ public class PartOfSpeech : IObjectWithId // TODO: Probably need Abbreviation in order to match LCM data model public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public bool Predefined { get; set; } public Guid[] GetReferences() @@ -26,7 +25,6 @@ public IObjectWithId Copy() Id = Id, Name = Name, DeletedAt = DeletedAt, - Version = Version, Predefined = Predefined }; } diff --git a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs index 9151d258d..616af0adb 100644 --- a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs +++ b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs @@ -6,7 +6,6 @@ public class SemanticDomain : IObjectWithId public virtual required MultiString Name { get; set; } public virtual required string Code { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public bool Predefined { get; set; } public Guid[] GetReferences() @@ -26,7 +25,6 @@ public IObjectWithId Copy() Code = Code, Name = Name, DeletedAt = DeletedAt, - Version = Version, Predefined = Predefined }; } diff --git a/backend/FwLite/MiniLcm/Models/Sense.cs b/backend/FwLite/MiniLcm/Models/Sense.cs index 8fad1fbfd..2117e2738 100644 --- a/backend/FwLite/MiniLcm/Models/Sense.cs +++ b/backend/FwLite/MiniLcm/Models/Sense.cs @@ -4,7 +4,6 @@ public class Sense : IObjectWithId { public virtual Guid Id { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public Guid EntryId { get; set; } public virtual MultiString Definition { get; set; } = new(); public virtual MultiString Gloss { get; set; } = new(); @@ -35,7 +34,6 @@ public IObjectWithId Copy() Id = Id, EntryId = EntryId, DeletedAt = DeletedAt, - Version = Version, Definition = Definition.Copy(), Gloss = Gloss.Copy(), PartOfSpeech = PartOfSpeech, diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index 7600bfee0..ed96bcddc 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -9,7 +9,6 @@ public record WritingSystem: IObjectWithId public required string Font { get; set; } public DateTimeOffset? DeletedAt { get; set; } - public string? Version { get; set; } public required WritingSystemType Type { get; set; } public string[] Exemplars { get; set; } = []; //todo probably need more stuff here, see wesay for ideas @@ -38,7 +37,6 @@ public IObjectWithId Copy() Font = Font, Exemplars = Exemplars, DeletedAt = DeletedAt, - Version = Version, Type = Type, Order = Order }; From c8e042f7217232418d1cf94a327bdecc06036d22 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 11 Nov 2024 16:23:05 +0700 Subject: [PATCH 37/38] fix test issue due to parts of speech and domains not being created when an entry is created, fix issue with ordering of adding complex forms to an entry --- .../FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 11 ++++++----- .../AutoFakerHelpers/EntryFakerHelper.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 6d90311ee..0de152930 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -541,6 +541,12 @@ public async Task CreateEntry(Entry entry) CreateSense(lexEntry, sense); } + //form types should be created before components, otherwise the form type "unspecified" will be added + foreach (var complexFormType in entry.ComplexFormTypes) + { + AddComplexFormType(lexEntry, complexFormType.Id); + } + foreach (var component in entry.Components) { AddComplexFormComponent(lexEntry, component); @@ -551,11 +557,6 @@ public async Task CreateEntry(Entry entry) var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId); AddComplexFormComponent(complexLexEntry, complexForm); } - - foreach (var complexFormType in entry.ComplexFormTypes) - { - AddComplexFormType(lexEntry, complexFormType.Id); - } }); return await GetEntry(entry.Id) ?? throw new InvalidOperationException("Entry was not created"); diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs index 7e29e9f1f..ae84a8c21 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs @@ -20,6 +20,19 @@ public static async Task EntryReadyForCreation(this AutoFaker autoFaker, foreach (var sense in entry.Senses) { sense.EntryId = entry.Id; + if (sense.PartOfSpeechId.HasValue) + { + await api.CreatePartOfSpeech(new PartOfSpeech() + { + Id = sense.PartOfSpeechId.Value, + Name = { { "en", sense.PartOfSpeech } } + }); + } + foreach (var senseSemanticDomain in sense.SemanticDomains) + { + senseSemanticDomain.Predefined = false; + await api.CreateSemanticDomain(senseSemanticDomain); + } foreach (var exampleSentence in sense.ExampleSentences) { exampleSentence.SenseId = sense.Id; From 9a315be5809e0ecb4170213597762112f76c39a0 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 12 Nov 2024 09:57:27 +0700 Subject: [PATCH 38/38] exclude Semantic domain predefined due to it always being true in fwdata --- backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs index 1f5196bc8..477362c03 100644 --- a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs @@ -26,7 +26,9 @@ public async Task CanCreateEntry_AutoFaker() var createdEntry = await Api.CreateEntry(entry); createdEntry.Should().BeEquivalentTo(entry, options => options .For(e => e.Components).Exclude(e => e.Id) - .For(e => e.ComplexForms).Exclude(e => e.Id)); + .For(e => e.ComplexForms).Exclude(e => e.Id) + //predefined is always true in fwdata bridge, so we need to exclude it for now + .For(e => e.Senses).For(s => s.SemanticDomains).Exclude(s => s.Predefined)); } [Fact]