From 19ef8fe41717e45c1587054ab377e6c69bbfb3c8 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 23 Sep 2024 16:07:40 +0700 Subject: [PATCH 01/20] add LcmDebugger CLI to be able open projects and inspect LCM stuff in the debugger --- LexBox.sln | 7 +++++++ backend/LfNext/LcmDebugger/LcmDebugger.csproj | 18 ++++++++++++++++++ backend/LfNext/LcmDebugger/Program.cs | 15 +++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 backend/LfNext/LcmDebugger/LcmDebugger.csproj create mode 100644 backend/LfNext/LcmDebugger/Program.cs diff --git a/LexBox.sln b/LexBox.sln index 414ea3a51..cc8b44bd2 100644 --- a/LexBox.sln +++ b/LexBox.sln @@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwLiteProjectSync", "backen EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwLiteProjectSync.Tests", "backend\FwLite\FwLiteProjectSync.Tests\FwLiteProjectSync.Tests.csproj", "{5352D1CC-14C5-4589-9389-731F55E4FFDF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LcmDebugger", "backend\LfNext\LcmDebugger\LcmDebugger.csproj", "{5A9011D8-6EC1-4550-BDD7-AFF00DB2B921}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -127,6 +129,10 @@ Global {5352D1CC-14C5-4589-9389-731F55E4FFDF}.Debug|Any CPU.Build.0 = Debug|Any CPU {5352D1CC-14C5-4589-9389-731F55E4FFDF}.Release|Any CPU.ActiveCfg = Release|Any CPU {5352D1CC-14C5-4589-9389-731F55E4FFDF}.Release|Any CPU.Build.0 = Release|Any CPU + {5A9011D8-6EC1-4550-BDD7-AFF00DB2B921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A9011D8-6EC1-4550-BDD7-AFF00DB2B921}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A9011D8-6EC1-4550-BDD7-AFF00DB2B921}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A9011D8-6EC1-4550-BDD7-AFF00DB2B921}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,6 +150,7 @@ Global {9001FE0F-DBBF-4A78-9EB9-9B5042CF8A78} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} {2245BAB6-753A-48AE-B929-6D8C35CB9804} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} {5352D1CC-14C5-4589-9389-731F55E4FFDF} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} + {5A9011D8-6EC1-4550-BDD7-AFF00DB2B921} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {440AE83C-6DB0-4F18-B2C1-BCD33F0645B6} diff --git a/backend/LfNext/LcmDebugger/LcmDebugger.csproj b/backend/LfNext/LcmDebugger/LcmDebugger.csproj new file mode 100644 index 000000000..e47e15988 --- /dev/null +++ b/backend/LfNext/LcmDebugger/LcmDebugger.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/backend/LfNext/LcmDebugger/Program.cs b/backend/LfNext/LcmDebugger/Program.cs new file mode 100644 index 000000000..490f8b2ea --- /dev/null +++ b/backend/LfNext/LcmDebugger/Program.cs @@ -0,0 +1,15 @@ +// See https://aka.ms/new-console-template for more information + +using FwDataMiniLcmBridge; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = Host.CreateApplicationBuilder(); +builder.Services.AddFwDataBridge(); + +var app = builder.Build(); + +var fwDataFactory = app.Services.GetRequiredService(); +var miniLcmApi = fwDataFactory.GetFwDataMiniLcmApi("fruit", false); +await miniLcmApi.GetEntries().ToArrayAsync(); +var complexEntryTypesOa = miniLcmApi.Cache.LangProject.LexDbOA.ComplexEntryTypesOA; From dfcf5bbb2f9714e54977c12afab0e56e29912b45 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 23 Sep 2024 16:09:37 +0700 Subject: [PATCH 02/20] first pass at what complex forms might look like in miniLcm --- .../Api/FwDataMiniLcmApi.cs | 50 ++++++++++++++++++- backend/FwLite/MiniLcm/Entry.cs | 21 ++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 22f0529af..a68213664 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -9,12 +9,13 @@ using SIL.LCModel.Core.WritingSystems; using SIL.LCModel.DomainServices; using SIL.LCModel.Infrastructure; +using EntryType = MiniLcm.EntryType; namespace FwDataMiniLcmBridge.Api; public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogger logger, FwDataProject project) : ILexboxApi, IDisposable { - private LcmCache Cache => cacheLazy.Value; + public LcmCache Cache => cacheLazy.Value; public FwDataProject Project { get; } = project; public Guid ProjectId => Cache.LangProject.Guid; @@ -27,6 +28,7 @@ public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogge private ILexExampleSentenceFactory LexExampleSentenceFactory => Cache.ServiceLocator.GetInstance(); private IMoMorphTypeRepository MorphTypeRepository => Cache.ServiceLocator.GetInstance(); private IPartOfSpeechRepository PartOfSpeechRepository => Cache.ServiceLocator.GetInstance(); + private ILexEntryTypeRepository LexEntryTypeRepository => Cache.ServiceLocator.GetInstance(); private ICmSemanticDomainRepository SemanticDomainRepository => Cache.ServiceLocator.GetInstance(); private ICmTranslationFactory CmTranslationFactory => Cache.ServiceLocator.GetInstance(); private ICmPossibilityRepository CmPossibilityRepository => Cache.ServiceLocator.GetInstance(); @@ -231,7 +233,51 @@ private Entry FromLexEntry(ILexEntry entry) LexemeForm = FromLcmMultiString(entry.LexemeFormOA.Form), CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), - Senses = entry.AllSenses.Select(FromLexSense).ToList() + Senses = entry.AllSenses.Select(FromLexSense).ToList(), + ComplexForm = ToComplexForm(entry) + }; + } + + private ComplexForm? ToComplexForm(ILexEntry entry) + { + var complexEntryRef = entry.ComplexFormEntryRefs.SingleOrDefault(); + if (complexEntryRef is null) return null; + return new ComplexForm + { + Id = complexEntryRef.Guid, + Components = + [ + ..complexEntryRef.ComponentLexemesRS.Select(o => o switch + { + ILexEntry e => ToEntryReference(e), + ILexSense s => ToSenseReference(s), + _ => throw new NotSupportedException($"object type {o.ClassName} not supported") + }) + ], + Types = + [ + ..complexEntryRef.ComplexEntryTypesRS.Select(t => + new EntryType() { Id = t.Guid, Name = FromLcmMultiString(t.Name), }) + ] + }; + } + + private EntryReference ToEntryReference(ILexEntry entry) + { + return new EntryReference + { + EntryId = entry.Guid, + Headword = entry.HeadWord.Text + }; + } + + private EntryReference ToSenseReference(ILexSense sense) + { + return new EntryReference + { + EntryId = sense.Entry.Guid, + SenseId = sense.Guid, + Headword = sense.Entry.HeadWord.Text, }; } diff --git a/backend/FwLite/MiniLcm/Entry.cs b/backend/FwLite/MiniLcm/Entry.cs index 930d38a62..a494cf67c 100644 --- a/backend/FwLite/MiniLcm/Entry.cs +++ b/backend/FwLite/MiniLcm/Entry.cs @@ -12,6 +12,7 @@ public class Entry : IObjectWithId public virtual IList Senses { get; set; } = []; public virtual MultiString Note { get; set; } = new(); + public virtual ComplexForm? ComplexForm { get; set; } public bool MatchesQuery(string query) => LexemeForm.SearchValue(query) @@ -25,3 +26,23 @@ public string Headword() return word?.Trim() ?? "(Unknown)"; } } + +public class EntryReference +{ + public required Guid EntryId { get; set; } + public Guid? SenseId { get; set; } = null; + public required string Headword { get; set; } +} + +public class ComplexForm +{ + public IList Components { get; set; } = []; + public IList Types { get; set; } = []; + public Guid Id { get; set; } +} + +public class EntryType +{ + public required Guid Id { get; set; } + public required MultiString Name { get; set; } +} From abad0cfdace81617248d78432a2984f66adaa250 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 24 Sep 2024 15:22:57 +0700 Subject: [PATCH 03/20] rename EntryType to ComplexEntryType and expose it via a method on the api --- .../FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 11 +++++++++-- backend/FwLite/MiniLcm/Entry.cs | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index a68213664..8cf0bf5a1 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -9,7 +9,6 @@ using SIL.LCModel.Core.WritingSystems; using SIL.LCModel.DomainServices; using SIL.LCModel.Infrastructure; -using EntryType = MiniLcm.EntryType; namespace FwDataMiniLcmBridge.Api; @@ -32,6 +31,7 @@ public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogge private ICmSemanticDomainRepository SemanticDomainRepository => Cache.ServiceLocator.GetInstance(); private ICmTranslationFactory CmTranslationFactory => Cache.ServiceLocator.GetInstance(); private ICmPossibilityRepository CmPossibilityRepository => Cache.ServiceLocator.GetInstance(); + private ICmPossibilityList ComplexFormTypes => Cache.LangProject.LexDbOA.ComplexEntryTypesOA; public void Dispose() { @@ -224,6 +224,13 @@ internal ICmSemanticDomain GetLcmSemanticDomain(Guid semanticDomainId) return SemanticDomainRepository.GetObject(semanticDomainId); } + public IAsyncEnumerable GetComplexFormTypes() + { + return ComplexFormTypes.PossibilitiesOS + .Select(t => new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) }) + .ToAsyncEnumerable(); + } + private Entry FromLexEntry(ILexEntry entry) { return new Entry @@ -257,7 +264,7 @@ private Entry FromLexEntry(ILexEntry entry) Types = [ ..complexEntryRef.ComplexEntryTypesRS.Select(t => - new EntryType() { Id = t.Guid, Name = FromLcmMultiString(t.Name), }) + new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name), }) ] }; } diff --git a/backend/FwLite/MiniLcm/Entry.cs b/backend/FwLite/MiniLcm/Entry.cs index a494cf67c..9da6cdbbc 100644 --- a/backend/FwLite/MiniLcm/Entry.cs +++ b/backend/FwLite/MiniLcm/Entry.cs @@ -37,11 +37,11 @@ public class EntryReference public class ComplexForm { public IList Components { get; set; } = []; - public IList Types { get; set; } = []; + public IList Types { get; set; } = []; public Guid Id { get; set; } } -public class EntryType +public class ComplexFormType { public required Guid Id { get; set; } public required MultiString Name { get; set; } From 41bd955c3f93f267cd3ccabfca93bd0b5a2dbeb0 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 24 Sep 2024 15:48:17 +0700 Subject: [PATCH 04/20] add support for variants --- .../Api/FwDataMiniLcmApi.cs | 35 ++++++++++++++++++- backend/FwLite/MiniLcm/Entry.cs | 14 ++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 8cf0bf5a1..65d83335b 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -32,6 +32,7 @@ public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogge private ICmTranslationFactory CmTranslationFactory => Cache.ServiceLocator.GetInstance(); private ICmPossibilityRepository CmPossibilityRepository => Cache.ServiceLocator.GetInstance(); private ICmPossibilityList ComplexFormTypes => Cache.LangProject.LexDbOA.ComplexEntryTypesOA; + private ICmPossibilityList VariantTypes => Cache.LangProject.LexDbOA.VariantEntryTypesOA; public void Dispose() { @@ -231,6 +232,13 @@ public IAsyncEnumerable GetComplexFormTypes() .ToAsyncEnumerable(); } + public IAsyncEnumerable GetVariantTypes() + { + return VariantTypes.PossibilitiesOS + .Select(t => new VariantType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) }) + .ToAsyncEnumerable(); + } + private Entry FromLexEntry(ILexEntry entry) { return new Entry @@ -241,7 +249,8 @@ private Entry FromLexEntry(ILexEntry entry) CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), Senses = entry.AllSenses.Select(FromLexSense).ToList(), - ComplexForm = ToComplexForm(entry) + ComplexForm = ToComplexForm(entry), + Variants = ToVariants(entry) }; } @@ -269,6 +278,30 @@ private Entry FromLexEntry(ILexEntry entry) }; } + private Variants? ToVariants(ILexEntry entry) + { + var variantEntryRef = entry.VariantEntryRefs.SingleOrDefault(); + if (variantEntryRef is null) return null; + return new Variants + { + Id = variantEntryRef.Guid, + VariantsOf = + [ + ..variantEntryRef.ComponentLexemesRS.Select(o => o switch + { + ILexEntry e => ToEntryReference(e), + ILexSense s => ToSenseReference(s), + _ => throw new NotSupportedException($"object type {o.ClassName} not supported") + }) + ], + Types = + [ + ..variantEntryRef.VariantEntryTypesRS.Select(t => + new VariantType() { Id = t.Guid, Name = FromLcmMultiString(t.Name), }) + ] + }; + } + private EntryReference ToEntryReference(ILexEntry entry) { return new EntryReference diff --git a/backend/FwLite/MiniLcm/Entry.cs b/backend/FwLite/MiniLcm/Entry.cs index 9da6cdbbc..b9ec1b831 100644 --- a/backend/FwLite/MiniLcm/Entry.cs +++ b/backend/FwLite/MiniLcm/Entry.cs @@ -13,6 +13,7 @@ public class Entry : IObjectWithId public virtual MultiString Note { get; set; } = new(); public virtual ComplexForm? ComplexForm { get; set; } + public virtual Variants? Variants { get; set; } public bool MatchesQuery(string query) => LexemeForm.SearchValue(query) @@ -36,9 +37,16 @@ public class EntryReference public class ComplexForm { + public Guid Id { get; set; } public IList Components { get; set; } = []; public IList Types { get; set; } = []; +} + +public class Variants +{ public Guid Id { get; set; } + public IList VariantsOf { get; set; } = []; + public IList Types { get; set; } = []; } public class ComplexFormType @@ -46,3 +54,9 @@ public class ComplexFormType public required Guid Id { get; set; } public required MultiString Name { get; set; } } + +public class VariantType +{ + public required Guid Id { get; set; } + public required MultiString Name { get; set; } +} From 4b301f0400443d2d3075c516c281b1b694471f8c Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 30 Sep 2024 11:33:36 +0700 Subject: [PATCH 05/20] add crdt support for Complex Forms --- .../Api/FwDataMiniLcmApi.cs | 68 +++++++-------- .../LcmCrdt.Tests/Changes/ComplexFormTests.cs | 84 +++++++++++++++++++ .../FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs | 42 ++++++++++ .../LcmCrdt/Changes/CreateComplexFormType.cs | 20 +++++ .../Entries/AddComplexFormTypeChange.cs | 17 ++++ .../Entries/AddEntryComponentChange.cs | 61 ++++++++++++++ .../Entries/RemoveComplexFormTypeChange.cs | 14 ++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 12 +++ backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 45 ++++++++-- .../Objects/CrdtComplexFormComponent.cs | 45 ++++++++++ .../LcmCrdt/Objects/CrdtComplexFormType.cs | 30 +++++++ backend/FwLite/LcmCrdt/Objects/Entry.cs | 16 +++- backend/FwLite/MiniLcm/Models/Entry.cs | 29 ++++--- 13 files changed, 427 insertions(+), 56 deletions(-) create mode 100644 backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs create mode 100644 backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs create mode 100644 backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs create mode 100644 backend/FwLite/LcmCrdt/Changes/Entries/AddComplexFormTypeChange.cs create mode 100644 backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs create mode 100644 backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs create mode 100644 backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs create mode 100644 backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index fd13e0cb8..05bdf8402 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -259,33 +259,31 @@ private Entry FromLexEntry(ILexEntry entry) CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), Senses = entry.AllSenses.Select(FromLexSense).ToList(), - ComplexForm = ToComplexForm(entry), + ComplexFormTypes = ToComplexFormType(entry), + Components = ToComplexFormComponents(entry), + ComplexForms = [..entry.ComplexFormEntries.Select(complexEntry => ToEntryReference(entry, complexEntry))], Variants = ToVariants(entry) }; } - private ComplexForm? ToComplexForm(ILexEntry entry) + private IList ToComplexFormType(ILexEntry entry) { - var complexEntryRef = entry.ComplexFormEntryRefs.SingleOrDefault(); - if (complexEntryRef is null) return null; - return new ComplexForm - { - Id = complexEntryRef.Guid, - Components = - [ - ..complexEntryRef.ComponentLexemesRS.Select(o => o switch - { - ILexEntry e => ToEntryReference(e), - ILexSense s => ToSenseReference(s), - _ => throw new NotSupportedException($"object type {o.ClassName} not supported") - }) - ], - Types = - [ - ..complexEntryRef.ComplexEntryTypesRS.Select(t => - new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name), }) - ] - }; + return entry.ComplexFormEntryRefs.SingleOrDefault() + ?.ComplexEntryTypesRS + .Select(e => new ComplexFormType { Id = e.Guid, Name = FromLcmMultiString(e.Name) }) + .ToList() ?? []; + } + private IList ToComplexFormComponents(ILexEntry entry) + { + return entry.ComplexFormEntryRefs.SingleOrDefault() + ?.ComponentLexemesRS + .Select(o => o switch + { + 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) @@ -299,8 +297,8 @@ private Entry FromLexEntry(ILexEntry entry) [ ..variantEntryRef.ComponentLexemesRS.Select(o => o switch { - ILexEntry e => ToEntryReference(e), - ILexSense s => ToSenseReference(s), + ILexEntry component => ToEntryReference(component, entry), + ILexSense s => ToSenseReference(s, entry), _ => throw new NotSupportedException($"object type {o.ClassName} not supported") }) ], @@ -312,22 +310,26 @@ private Entry FromLexEntry(ILexEntry entry) }; } - private EntryReference ToEntryReference(ILexEntry entry) + private ComplexFormComponent ToEntryReference(ILexEntry component, ILexEntry complexEntry) { - return new EntryReference + return new ComplexFormComponent { - EntryId = entry.Guid, - Headword = entry.HeadWord.Text + ComponentEntryId = component.Guid, + ComponentHeadword = component.HeadWord.Text, + ComplexFormEntryId = complexEntry.Guid, + ComplexFormHeadword = complexEntry.HeadWord.Text }; } - private EntryReference ToSenseReference(ILexSense sense) + private ComplexFormComponent ToSenseReference(ILexSense componentSense, ILexEntry complexEntry) { - return new EntryReference + return new ComplexFormComponent { - EntryId = sense.Entry.Guid, - SenseId = sense.Guid, - Headword = sense.Entry.HeadWord.Text, + ComponentEntryId = componentSense.Entry.Guid, + ComponentSenseId = componentSense.Guid, + ComponentHeadword = componentSense.Entry.HeadWord.Text, + ComplexFormEntryId = complexEntry.Guid, + ComplexFormHeadword = complexEntry.HeadWord.Text }; } diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs new file mode 100644 index 000000000..5ced23456 --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs @@ -0,0 +1,84 @@ +using LcmCrdt.Changes.Entries; +using LcmCrdt.Objects; +using MiniLcm.Models; +using SIL.Harmony.Changes; +using ComplexFormType = MiniLcm.Models.ComplexFormType; + +namespace LcmCrdt.Tests.Changes; + +public class ComplexFormTests(MiniLcmApiFixture fixture) : IClassFixture +{ + [Fact] + public async Task AddComplexFormType() + { + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; + await fixture.Api.CreateComplexFormType(complexFormType); + var complexEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat rack" } }, }); + var change = new AddComplexFormTypeChange(complexEntry.Id,complexFormType); + await fixture.DataModel.AddChange(Guid.NewGuid(), change); + complexEntry = await fixture.Api.GetEntry(complexEntry.Id); + complexEntry.Should().NotBeNull(); + complexEntry!.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(change.ComplexFormType.Id); + } + + [Fact] + public async Task RemoveComplexFormType() + { + var complexEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat rack" } }, }); + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; + await fixture.Api.CreateComplexFormType(complexFormType); + await fixture.DataModel.AddChange( + Guid.NewGuid(), + new AddComplexFormTypeChange(complexEntry.Id, complexFormType) + ); + complexEntry = await fixture.Api.GetEntry(complexEntry.Id); + complexEntry.Should().NotBeNull(); + complexEntry!.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(complexFormType.Id); + await fixture.DataModel.AddChange( + Guid.NewGuid(), + new RemoveComplexFormTypeChange(complexEntry.Id, complexFormType.Id) + ); + complexEntry = await fixture.Api.GetEntry(complexEntry.Id); + complexEntry.Should().NotBeNull(); + complexEntry!.ComplexFormTypes.Should().BeEmpty(); + } + + [Fact] + public async Task AddEntryComponent() + { + var complexEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat rack" } }, }); + + var coatEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat" } }, }); + var rackEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Rack" } }, }); + + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, coatEntry)); + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, rackEntry)); + complexEntry = await fixture.Api.GetEntry(complexEntry.Id); + complexEntry.Should().NotBeNull(); + complexEntry!.Components.Should().ContainSingle(e => e.ComponentEntryId == coatEntry.Id); + complexEntry.Components.Should().ContainSingle(e => e.ComponentEntryId == rackEntry.Id); + + coatEntry = await fixture.Api.GetEntry(coatEntry.Id); + coatEntry.Should().NotBeNull(); + coatEntry!.ComplexForms.Should().ContainSingle(e => e.ComplexFormEntryId == complexEntry.Id); + } + + [Fact] + public async Task DeleteEntryComponent() + { + var complexEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat rack" } }, }); + var coatEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat" } }, }); + var rackEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Rack" } }, }); + + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, coatEntry)); + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, rackEntry)); + complexEntry = await fixture.Api.GetEntry(complexEntry.Id); + complexEntry.Should().NotBeNull(); + var component = complexEntry!.Components.First(); + + await fixture.DataModel.AddChange(Guid.NewGuid(), new DeleteChange(component.Id)); + complexEntry = await fixture.Api.GetEntry(complexEntry.Id); + complexEntry.Should().NotBeNull(); + complexEntry!.Components.Should().NotContain(c => c.Id == component.Id); + } +} diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs new file mode 100644 index 000000000..17dac1a34 --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -0,0 +1,42 @@ +using LcmCrdt.Tests.Mocks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using MiniLcm; + +namespace LcmCrdt.Tests; + +public class MiniLcmApiFixture : IAsyncLifetime +{ + private readonly AsyncServiceScope _services; + private readonly LcmCrdtDbContext _crdtDbContext; + public CrdtMiniLcmApi Api => (CrdtMiniLcmApi)_services.ServiceProvider.GetRequiredService(); + public DataModel DataModel => _services.ServiceProvider.GetRequiredService(); + + public MiniLcmApiFixture() + { + var services = new ServiceCollection() + .AddLcmCrdtClient() + .AddLogging(builder => builder.AddDebug()) + .RemoveAll(typeof(ProjectContext)) + .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", ":memory:"))) + .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, + new ProjectData("Sena 3", Guid.NewGuid(), null, Guid.NewGuid())); + await _services.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); + } + + public async Task DisposeAsync() + { + await _services.DisposeAsync(); + } +} diff --git a/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs b/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs new file mode 100644 index 000000000..d097f02b2 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs @@ -0,0 +1,20 @@ +using LcmCrdt.Objects; +using MiniLcm.Models; +using SIL.Harmony; +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Changes; + +public class CreateComplexFormType(Guid entityId, MultiString name) : CreateChange(entityId), ISelfNamedType +{ + public MultiString Name { get; } = name; + public override ValueTask NewEntity(Commit commit, ChangeContext context) + { + return ValueTask.FromResult(new CrdtComplexFormType + { + Id = EntityId, + Name = Name + }); + } +} diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/AddComplexFormTypeChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/AddComplexFormTypeChange.cs new file mode 100644 index 000000000..4889737fd --- /dev/null +++ b/backend/FwLite/LcmCrdt/Changes/Entries/AddComplexFormTypeChange.cs @@ -0,0 +1,17 @@ +using MiniLcm.Models; +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Changes.Entries; + +public class AddComplexFormTypeChange(Guid entityId, ComplexFormType complexFormType) + : EditChange(entityId), ISelfNamedType +{ + public ComplexFormType ComplexFormType { get; } = complexFormType; + + public override async ValueTask ApplyChange(Entry entity, ChangeContext context) + { + if (await context.IsObjectDeleted(ComplexFormType.Id)) return; + entity.ComplexFormTypes.Add(ComplexFormType); + } +} diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs new file mode 100644 index 000000000..f12319502 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Serialization; +using LcmCrdt.Objects; +using SIL.Harmony; +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Changes.Entries; + +public class AddEntryComponentChange : CreateChange, ISelfNamedType +{ + public Guid ComplexFormEntryId { get; } + public string ComplexFormHeadword { get; } + public Guid ComponentEntryId { get; } + public Guid? ComponentSenseId { get; } + public string ComponentHeadword { get; } + + [JsonConstructor] + protected AddEntryComponentChange(Guid entityId, + Guid complexFormEntryId, + string complexFormHeadword, + Guid componentEntryId, + string componentHeadword, + Guid? componentSenseId = null) : base(entityId) + { + ComplexFormEntryId = complexFormEntryId; + ComplexFormHeadword = complexFormHeadword; + ComponentEntryId = componentEntryId; + ComponentHeadword = componentHeadword; + ComponentSenseId = componentSenseId; + } + + public AddEntryComponentChange( + MiniLcm.Models.Entry complexEntry, + MiniLcm.Models.Entry componentEntry, + Guid? componentSenseId = null) : this(Guid.NewGuid(), + complexEntry.Id, + complexEntry.Headword(), + componentEntry.Id, + componentEntry.Headword(), + componentSenseId) + { + } + + public override async ValueTask NewEntity(Commit commit, ChangeContext context) + { + return new CrdtComplexFormComponent + { + Id = EntityId, + ComplexFormEntryId = ComplexFormEntryId, + ComplexFormHeadword = ComplexFormHeadword, + ComponentEntryId = ComponentEntryId, + ComponentHeadword = ComponentHeadword, + ComponentSenseId = ComponentSenseId, + DeletedAt = (await context.IsObjectDeleted(ComponentEntryId) || + await context.IsObjectDeleted(ComplexFormEntryId) || + ComponentSenseId.HasValue && await context.IsObjectDeleted(ComponentSenseId.Value)) + ? commit.DateTime + : (DateTime?)null, + }; + } +} diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs new file mode 100644 index 000000000..848956e16 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs @@ -0,0 +1,14 @@ +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Changes.Entries; + +public class RemoveComplexFormTypeChange(Guid entityId, Guid complexFormId) : EditChange(entityId), ISelfNamedType +{ + public Guid ComplexFormId { get; } = complexFormId; + public override ValueTask ApplyChange(Entry entity, ChangeContext context) + { + entity.ComplexFormTypes = entity.ComplexFormTypes.Where(t => t.Id != ComplexFormId).ToList(); + return ValueTask.CompletedTask; + } +} diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index a74fd2d46..bf171299b 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -4,10 +4,12 @@ using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; +using LcmCrdt.Objects; using MiniLcm; using LinqToDB; using LinqToDB.EntityFrameworkCore; using MiniLcm.Models; +using PartOfSpeech = MiniLcm.Models.PartOfSpeech; using SemanticDomain = LcmCrdt.Objects.SemanticDomain; namespace LcmCrdt; @@ -18,6 +20,7 @@ public class CrdtMiniLcmApi(DataModel dataModel, JsonSerializerOptions jsonOptio private IQueryable Entries => dataModel.GetLatestObjects(); + private IQueryable ComplexFormComponents => dataModel.GetLatestObjects(); private IQueryable Senses => dataModel.GetLatestObjects(); private IQueryable ExampleSentences => dataModel.GetLatestObjects(); private IQueryable WritingSystems => dataModel.GetLatestObjects(); @@ -94,6 +97,11 @@ public async Task BulkImportSemanticDomains(IEnumerable new CreateSemanticDomainChange(sd.Id, sd.Name, sd.Code))); } + public async Task CreateComplexFormType(MiniLcm.Models.ComplexFormType complexFormType) + { + await dataModel.AddChange(ClientId, new CreateComplexFormType(complexFormType.Id, complexFormType.Name)); + } + public IAsyncEnumerable GetEntries(QueryOptions? options = null) { return GetEntriesAsyncEnum(predicate: null, options); @@ -186,6 +194,10 @@ private async Task LoadSenses(Entry[] entries) .ToLookup(e => e.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); entry.Senses = senses; + + //could optimize this by doing a single query, but this is easier to read + entry.Components = [..await ComplexFormComponents.Where(c => c.ComplexFormEntryId == id).ToListAsyncEF()]; + entry.ComplexForms = [..await ComplexFormComponents.Where(c => c.ComponentEntryId == id).ToListAsyncEF()]; foreach (var sense in senses) { sense.ExampleSentences = exampleSentences.TryGetValue(sense.Id, out var sentences) ? sentences.ToArray() : []; diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 9f8a0792d..555aa6a6b 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -1,16 +1,16 @@ using System.Text.Json; using SIL.Harmony; using SIL.Harmony.Core; -using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; +using LcmCrdt.Changes.Entries; +using LcmCrdt.Objects; using LinqToDB; using LinqToDB.AspNet.Logging; using LinqToDB.Data; using LinqToDB.EntityFrameworkCore; using LinqToDB.Mapping; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MiniLcm.Models; @@ -72,14 +72,30 @@ public static void ConfigureCrdt(CrdtConfig config) .Add(builder => { builder.Ignore(e => e.Senses); - // builder.OwnsOne(e => e.Note, n => n.ToJson()); - // builder.OwnsOne(e => e.LexemeForm, n => n.ToJson()); - // builder.OwnsOne(e => e.CitationForm, n => n.ToJson()); - // builder.OwnsOne(e => e.LiteralMeaning, n => n.ToJson()); + builder.HasMany(e => e.Components) + .WithOne() + .HasPrincipalKey(entry => entry.Id) + .HasForeignKey(c => c.ComplexFormEntryId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasMany(e => e.ComplexForms) + .WithOne() + .HasPrincipalKey(entry => entry.Id) + .HasForeignKey(c => c.ComponentEntryId) + .OnDelete(DeleteBehavior.Cascade); + builder + .Property(e => e.ComplexFormTypes) + .HasColumnType("jsonb") + .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null), + json => JsonSerializer.Deserialize>(json, + (JsonSerializerOptions?)null) ?? new()); }) .Add(builder => { builder.Ignore(s => s.ExampleSentences); + builder.HasMany() + .WithOne() + .HasForeignKey(c => c.ComponentSenseId) + .OnDelete(DeleteBehavior.Cascade); builder.HasOne() .WithMany() .HasForeignKey(sense => sense.EntryId); @@ -101,7 +117,14 @@ public static void ConfigureCrdt(CrdtConfig config) .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null), json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null) ?? Array.Empty()); - }).Add().Add(); + }) + .Add() + .Add() + .Add() + .Add(builder => + { + builder.ToTable("ComplexFormComponents"); + }); config.ChangeTypeListBuilder.Add>() .Add>() @@ -115,6 +138,8 @@ public static void ConfigureCrdt(CrdtConfig config) .Add>() .Add>() .Add>() + .Add>() + .Add>() .Add() .Add() .Add() @@ -122,6 +147,10 @@ public static void ConfigureCrdt(CrdtConfig config) .Add() .Add() .Add() - .Add(); + .Add() + .Add() + .Add() + .Add() + .Add(); } } diff --git a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs new file mode 100644 index 000000000..27619f966 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs @@ -0,0 +1,45 @@ +using MiniLcm.Models; +using SIL.Harmony; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Objects; + +public class CrdtComplexFormComponent : ComplexFormComponent, IObjectBase +{ + Guid IObjectBase.Id + { + get => Id; + init => Id = value; + } + + public DateTimeOffset? DeletedAt { get; set; } + + public Guid[] GetReferences() + { + Span senseId = (ComponentSenseId.HasValue ? [ComponentSenseId.Value] : []); + return [ + ComplexFormEntryId, + ComponentEntryId, + ..senseId + ]; + } + + public void RemoveReference(Guid id, Commit commit) + { + if (ComponentEntryId == id || ComplexFormEntryId == id || ComponentSenseId == id) + DeletedAt = commit.DateTime; + } + + public IObjectBase Copy() + { + return new CrdtComplexFormComponent + { + Id = Id, + ComplexFormEntryId = ComplexFormEntryId, + ComponentEntryId = ComponentEntryId, + ComponentHeadword = ComponentHeadword, + ComponentSenseId = ComponentSenseId, + DeletedAt = DeletedAt, + }; + } +} diff --git a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs new file mode 100644 index 000000000..07507d449 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs @@ -0,0 +1,30 @@ +using MiniLcm.Models; +using SIL.Harmony; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Objects; + +public class CrdtComplexFormType : ComplexFormType, IObjectBase +{ + Guid IObjectBase.Id + { + get => Id; + init => Id = value; + } + + public DateTimeOffset? DeletedAt { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, Commit commit) + { + } + + public IObjectBase Copy() + { + return new CrdtComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt, }; + } +} diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index 8f62627a1..a4467e8d6 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -32,7 +32,6 @@ Guid IObjectBase.Id set { base.Senses = [..value]; } } - [ExpressionMethod(nameof(HeadwordExpression))] public string Headword(WritingSystemId ws) { @@ -46,11 +45,17 @@ public string Headword(WritingSystemId ws) public Guid[] GetReferences() { - return []; + return + [ + ..Components.SelectMany(c => c.ComponentSenseId is null ? [c.ComponentEntryId] : new [] {c.ComponentEntryId, c.ComponentSenseId.Value}), + ..ComplexForms.Select(c => c.ComplexFormEntryId) + ]; } public void RemoveReference(Guid id, Commit commit) { + Components = Components.Where(c => c.ComponentEntryId != id && c.ComponentSenseId != id).ToList(); + ComplexForms = ComplexForms.Where(c => c.ComplexFormEntryId != id).ToList(); } public IObjectBase Copy() @@ -63,7 +68,12 @@ public IObjectBase Copy() CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), Note = Note.Copy(), - Senses = [..Senses.Select(s => (Sense) s.Copy())] + Senses = [..Senses.Select(s => (Sense) s.Copy())], + //todo should copy? + Components = [..Components.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c))], + ComplexForms = [..ComplexForms.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c))], + ComplexFormTypes = [..ComplexFormTypes.Select(cft => (cft is CrdtComplexFormType ct ? (ComplexFormType) ct.Copy() : cft))], + Variants = Variants }; } } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 38349b2e0..58390a69e 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -12,7 +12,16 @@ public class Entry : IObjectWithId public virtual IList Senses { get; set; } = []; public virtual MultiString Note { get; set; } = new(); - public virtual ComplexForm? ComplexForm { get; set; } + + /// + /// Components making up this complex entry + /// + public virtual IList Components { get; set; } = []; + /// + /// This entry is a part of these complex forms + /// + public virtual IList ComplexForms { get; set; } = []; + public virtual IList ComplexFormTypes { get; set; } = []; public virtual Variants? Variants { get; set; } public bool MatchesQuery(string query) => @@ -28,24 +37,20 @@ public string Headword() } } -public class EntryReference -{ - public required Guid EntryId { get; set; } - public Guid? SenseId { get; set; } = null; - public required string Headword { get; set; } -} - -public class ComplexForm +public class ComplexFormComponent { public Guid Id { get; set; } - public IList Components { get; set; } = []; - public IList Types { get; set; } = []; + public required Guid ComplexFormEntryId { get; set; } + public string? ComplexFormHeadword { get; set; } + public required Guid ComponentEntryId { get; set; } + public Guid? ComponentSenseId { get; set; } = null; + public string? ComponentHeadword { get; set; } } public class Variants { public Guid Id { get; set; } - public IList VariantsOf { get; set; } = []; + public IList VariantsOf { get; set; } = []; public IList Types { get; set; } = []; } From 66f99f7f2b2100d5e42dfb29571c3d5e429dfa97 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 30 Sep 2024 11:46:14 +0700 Subject: [PATCH 06/20] undo ugly hack of putting Sense list on Crdt Entry --- .../FwLite/LcmCrdt.Tests/LexboxApiTests.cs | 32 +++++++++++++++++-- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 3 +- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 4 +-- backend/FwLite/LcmCrdt/Objects/Entry.cs | 18 +---------- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs b/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs index c51ca691b..b31682a44 100644 --- a/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs @@ -133,7 +133,27 @@ await _api.CreateEntry(new() LexemeForm = { Values = { { "en", "apple" } } - } + }, + Senses = + [ + new Sense + { + Gloss = + { + Values = + { + { "en", "fruit" } + } + }, + Definition = + { + Values = + { + { "en", "a round fruit, red or yellow" } + } + }, + } + ], }); } @@ -182,7 +202,7 @@ public async Task GetEntries() var entry2 = entries.First(e => e.Id == _entry2Id); entry2.LexemeForm.Values.Should().NotBeEmpty(); - entry2.Senses.Should().BeEmpty(); + entry2.Senses.Should().NotBeEmpty(); } [Fact] @@ -193,6 +213,14 @@ public async Task SearchEntries() entries.Should().NotContain(e => e.Id == default); entries.Should().NotContain(e => e.LexemeForm.Values["en"] == "Kevin"); } + [Fact] + public async Task SearchEntries_MatchesGloss() + { + var entries = await _api.SearchEntries("fruit").ToArrayAsync(); + entries.Should().NotBeEmpty(); + entries.Should().NotContain(e => e.Id == default); + entries.Should().Contain(e => e.LexemeForm.Values["en"] == "apple"); + } [Fact] public async Task GetEntry() diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index bf171299b..4fd8a9d87 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -113,8 +113,7 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexFormType complexFo return GetEntriesAsyncEnum(e => e.LexemeForm.SearchValue(query) || e.CitationForm.SearchValue(query) - || e.Senses.Any(s => s.Gloss.SearchValue(query)) - + || Senses.Any(s => s.EntryId == e.Id && s.Gloss.SearchValue(query)) , options); } diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 555aa6a6b..1e7823657 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -54,7 +54,6 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio .HasAttribute(new ColumnAttribute(nameof(HybridDateTime.Counter), nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter))) .Entity().Property(e => e.Id) - .Association(e => (e.Senses as IEnumerable)!, e => e.Id, s => s.EntryId) .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -71,7 +70,6 @@ public static void ConfigureCrdt(CrdtConfig config) config.ObjectTypeListBuilder .Add(builder => { - builder.Ignore(e => e.Senses); builder.HasMany(e => e.Components) .WithOne() .HasPrincipalKey(entry => entry.Id) @@ -86,7 +84,7 @@ public static void ConfigureCrdt(CrdtConfig config) .Property(e => e.ComplexFormTypes) .HasColumnType("jsonb") .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null), - json => JsonSerializer.Deserialize>(json, + json => JsonSerializer.Deserialize>(json, (JsonSerializerOptions?)null) ?? new()); }) .Add(builder => diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index a4467e8d6..f8f971f9d 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -17,21 +17,6 @@ Guid IObjectBase.Id public DateTimeOffset? DeletedAt { get; set; } - /// - /// This is a bit of a hack, we want to be able to reference senses when running a query, and they must be CrdtSenses - /// however we only want to store the senses in the entry as MiniLcmSenses, so we need to convert them back to CrdtSenses - /// Note, even though this is JsonIgnored, the Senses property in the base class is still serialized - /// - [JsonIgnore] - public new IReadOnlyList Senses - { - get - { - return [..base.Senses.Select(s => s as Sense ?? Sense.FromMiniLcm(s, Id))]; - } - set { base.Senses = [..value]; } - } - [ExpressionMethod(nameof(HeadwordExpression))] public string Headword(WritingSystemId ws) { @@ -68,8 +53,7 @@ public IObjectBase Copy() CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), Note = Note.Copy(), - Senses = [..Senses.Select(s => (Sense) s.Copy())], - //todo should copy? + Senses = [..Senses.Select(s => (s is Sense cs ? (Sense) cs.Copy() : s))], Components = [..Components.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c))], ComplexForms = [..ComplexForms.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c))], ComplexFormTypes = [..ComplexFormTypes.Select(cft => (cft is CrdtComplexFormType ct ? (ComplexFormType) ct.Copy() : cft))], From af357b4b1c0996d57eb22c74698766db014e1776 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 30 Sep 2024 12:00:58 +0700 Subject: [PATCH 07/20] add a note when populating the complex forms of an entry --- backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 1 + backend/FwLite/LcmCrdt/Objects/Entry.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 05bdf8402..c1c350807 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -261,6 +261,7 @@ private Entry FromLexEntry(ILexEntry entry) Senses = entry.AllSenses.Select(FromLexSense).ToList(), ComplexFormTypes = ToComplexFormType(entry), Components = ToComplexFormComponents(entry), + //todo, this does not include complex forms which reference a sense ComplexForms = [..entry.ComplexFormEntries.Select(complexEntry => ToEntryReference(entry, complexEntry))], Variants = ToVariants(entry) }; diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index f8f971f9d..172d3087b 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -1,5 +1,4 @@ using System.Linq.Expressions; -using System.Text.Json.Serialization; using SIL.Harmony; using SIL.Harmony.Entities; using LinqToDB; From f1555a2682c749a6e8ab32699ea87de47f8fac6e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 30 Sep 2024 15:20:09 +0700 Subject: [PATCH 08/20] write tests and support creating entries with complex forms or components --- .../CreateEntryTests.cs | 102 ++++++++++++++++++ .../Api/FwDataMiniLcmApi.cs | 60 ++++++++++- .../FwLiteProjectSync.Tests/SyncTests.cs | 28 ++++- backend/FwLite/MiniLcm/Models/Entry.cs | 2 +- backend/LfNext/LcmDebugger/Program.cs | 2 +- 5 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs new file mode 100644 index 000000000..7ed435e27 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs @@ -0,0 +1,102 @@ +using FwDataMiniLcmBridge.Api; +using FwDataMiniLcmBridge.Tests.Fixtures; +using MiniLcm.Models; + +namespace FwDataMiniLcmBridge.Tests; + +[Collection(ProjectLoaderFixture.Name)] +public class CreateEntryTests(ProjectLoaderFixture fixture) : IAsyncLifetime +{ + private FwDataMiniLcmApi _api = null!; + + public Task InitializeAsync() + { + var projectName = "create-entry-test_" + Guid.NewGuid(); + fixture.MockFwProjectLoader.NewProject(projectName, "en", "en"); + _api = fixture.CreateApi(projectName); + return Task.CompletedTask; + } + + public Task DisposeAsync() + { + _api.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task CanCreateEntry() + { + var entry = await _api.CreateEntry(new() { LexemeForm = { { "en", "test" } } }); + entry.Should().NotBeNull(); + entry!.LexemeForm.Values.Should().ContainKey("en"); + entry.LexemeForm.Values["en"].Should().Be("test"); + } + + [Fact] + public async Task CanCreate_WithComponentsProperty() + { + var component = await _api.CreateEntry(new() { LexemeForm = { { "en", "test component" } } }); + var entryId = Guid.NewGuid(); + var entry = await _api.CreateEntry(new() + { + Id = entryId, + LexemeForm = { { "en", "test" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component.Id, + ComponentHeadword = component.Headword(), + ComplexFormEntryId = entryId, + ComplexFormHeadword = "test" + } + ] + }); + entry = await _api.GetEntry(entry.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id); + } + + [Fact] + public async Task CanCreate_WithComplexFormsProperty() + { + var complexForm = await _api.CreateEntry(new() { LexemeForm = { { "en", "test complex form" } } }); + var entryId = Guid.NewGuid(); + var entry = await _api.CreateEntry(new() + { + Id = entryId, + LexemeForm = { { "en", "test" } }, + ComplexForms = + [ + new ComplexFormComponent() + { + ComponentEntryId = entryId, + ComponentHeadword = "test", + ComplexFormEntryId = complexForm.Id, + ComplexFormHeadword = "test complex form" + } + ] + }); + entry = await _api.GetEntry(entry.Id); + entry.Should().NotBeNull(); + entry!.ComplexForms.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm.Id); + } + + [Fact] + public async Task CanCreate_WithComplexFormTypesProperty() + { + var complexFormType = await _api.CreateComplexFormType(new() + { + Name = new MultiString() { { "en", "test complex form type" } } + }); + + var entry = await _api.CreateEntry(new() + { + LexemeForm = { { "en", "test" } }, + ComplexFormTypes = [complexFormType] + }); + entry = await _api.GetEntry(entry.Id); + entry.Should().NotBeNull(); + entry!.ComplexFormTypes.Should().ContainSingle(c => c.Id == complexFormType.Id); + } +} diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index c1c350807..141f7fbf1 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using System.Reflection; using System.Text; using FwDataMiniLcmBridge.Api.UpdateProxy; using Microsoft.Extensions.Logging; @@ -238,10 +239,33 @@ internal ICmSemanticDomain GetLcmSemanticDomain(Guid semanticDomainId) public IAsyncEnumerable GetComplexFormTypes() { return ComplexFormTypes.PossibilitiesOS - .Select(t => new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) }) + .Select(ToComplexFormType) .ToAsyncEnumerable(); } + private ComplexFormType ToComplexFormType(ICmPossibility t) + { + return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) }; + } + + 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", + "Remove complex form type", + Cache.ActionHandlerAccessor, + () => + { + var lexComplexFormType = Cache.ServiceLocator + .GetInstance() + .Create(); + ComplexFormTypes.PossibilitiesOS.Add(lexComplexFormType); + UpdateLcmMultiString(lexComplexFormType.Name, complexFormType.Name); + complexFormType.Id = lexComplexFormType.Guid; + }); + return Task.FromResult(ToComplexFormType(ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormType.Id))); + } + public IAsyncEnumerable GetVariantTypes() { return VariantTypes.PossibilitiesOS @@ -271,7 +295,7 @@ private IList ToComplexFormType(ILexEntry entry) { return entry.ComplexFormEntryRefs.SingleOrDefault() ?.ComplexEntryTypesRS - .Select(e => new ComplexFormType { Id = e.Guid, Name = FromLcmMultiString(e.Name) }) + .Select(ToComplexFormType) .ToList() ?? []; } private IList ToComplexFormComponents(ILexEntry entry) @@ -456,6 +480,38 @@ public async Task CreateEntry(Entry entry) CreateSense(lexEntry, sense); } + foreach (var component in entry.Components) + { + ICmObject lexComponent = component.ComponentSenseId is not null + ? SenseRepository.GetObject(component.ComponentSenseId.Value) + : EntriesRepository.GetObject(component.ComponentEntryId); + lexEntry.AddComponent(lexComponent); + } + + foreach (var complexForm in entry.ComplexForms) + { + ICmObject lexComponent = complexForm.ComponentSenseId is not null + ? SenseRepository.GetObject(complexForm.ComponentSenseId.Value) + : lexEntry; + + var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId); + complexLexEntry.AddComponent(lexComponent); + } + + ILexEntryRef? entryRef = lexEntry.ComplexFormEntryRefs.SingleOrDefault(); + foreach (var complexFormType in entry.ComplexFormTypes) + { + if (entryRef is null) + { + entryRef = Cache.ServiceLocator.GetInstance().Create(); + lexEntry.EntryRefsOS.Add(entryRef); + entryRef.RefType = LexEntryRefTags.krtComplexForm; + entryRef.HideMinorEntry = 0; + } + + var lexEntryType = (ILexEntryType) ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormType.Id); + entryRef.ComplexEntryTypesRS.Add(lexEntryType); + } }); return await GetEntry(entry.Id) ?? throw new InvalidOperationException("Entry was not created"); diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 857cca5a5..3b27a0e1d 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -5,11 +5,12 @@ namespace FwLiteProjectSync.Tests; -public class SyncTests : IClassFixture +public class SyncTests : IClassFixture, IAsyncLifetime { private readonly SyncFixture _fixture; private readonly CrdtFwdataProjectSyncService _syncService; + private readonly Guid _complexEntryId = Guid.NewGuid(); private Entry _testEntry = new Entry { Id = Guid.NewGuid(), @@ -29,6 +30,27 @@ public class SyncTests : IClassFixture ] }; + public async Task InitializeAsync() + { + await _fixture.FwDataApi.CreateEntry(_testEntry); + await _fixture.FwDataApi.CreateEntry(new Entry() + { + Id = _complexEntryId, LexemeForm = { { "en", "Pineapple" } }, + ComplexForms = [new ComplexFormComponent() { + Id = Guid.NewGuid(), + ComponentEntryId = _complexEntryId, + ComponentHeadword = "Pineapple", + ComplexFormEntryId = _testEntry.Id, + ComplexFormHeadword = "Apple" + }] + }); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + public SyncTests(SyncFixture fixture) { _fixture = fixture; @@ -40,7 +62,6 @@ public async Task FirstSyncJustDoesAnImport() { var crdtApi = _fixture.CrdtApi; var fwdataApi = _fixture.FwDataApi; - await fwdataApi.CreateEntry(_testEntry); await _syncService.Sync(crdtApi, fwdataApi); var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); @@ -53,7 +74,6 @@ public async Task CreatingAnEntryInEachProjectSyncsAcrossBoth() { var crdtApi = _fixture.CrdtApi; var fwdataApi = _fixture.FwDataApi; - await fwdataApi.CreateEntry(_testEntry); await _syncService.Sync(crdtApi, fwdataApi); await fwdataApi.CreateEntry(new Entry() @@ -84,7 +104,6 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() { var crdtApi = _fixture.CrdtApi; var fwdataApi = _fixture.FwDataApi; - await fwdataApi.CreateEntry(_testEntry); await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = new WritingSystemId("es"), Name = "Spanish", Abbreviation = "es", Font = "Arial" }); await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = new WritingSystemId("fr"), Name = "French", Abbreviation = "fr", Font = "Arial" }); await _syncService.Sync(crdtApi, fwdataApi); @@ -106,7 +125,6 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth() { var crdtApi = _fixture.CrdtApi; var fwdataApi = _fixture.FwDataApi; - await fwdataApi.CreateEntry(_testEntry); await _syncService.Sync(crdtApi, fwdataApi); await fwdataApi.CreateSense(_testEntry.Id, new Sense() diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 58390a69e..7c980acc3 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -56,7 +56,7 @@ public class Variants public class ComplexFormType { - public required Guid Id { get; set; } + public Guid Id { get; set; } public required MultiString Name { get; set; } } diff --git a/backend/LfNext/LcmDebugger/Program.cs b/backend/LfNext/LcmDebugger/Program.cs index 490f8b2ea..5a2ea43ed 100644 --- a/backend/LfNext/LcmDebugger/Program.cs +++ b/backend/LfNext/LcmDebugger/Program.cs @@ -11,5 +11,5 @@ var fwDataFactory = app.Services.GetRequiredService(); var miniLcmApi = fwDataFactory.GetFwDataMiniLcmApi("fruit", false); -await miniLcmApi.GetEntries().ToArrayAsync(); +var entries = await miniLcmApi.GetEntries().ToArrayAsync(); var complexEntryTypesOa = miniLcmApi.Cache.LangProject.LexDbOA.ComplexEntryTypesOA; From 3d9f45c6e784eab89186450d12e27dbbbf182d1a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 30 Sep 2024 16:58:29 +0700 Subject: [PATCH 09/20] add complex form type to miniLcm API, ensure complex forms get imported --- .../FwLiteProjectSync.Tests/SyncTests.cs | 37 +++++++++++------ .../CrdtFwdataProjectSyncService.cs | 2 + .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 12 ++++++ .../FwLite/FwLiteProjectSync/MiniLcmImport.cs | 6 +++ .../LcmCrdt.Tests/Changes/ComplexFormTests.cs | 8 ++-- .../Entries/AddEntryComponentChange.cs | 26 ++++++------ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 40 ++++++++++++++++++- .../Objects/CrdtComplexFormComponent.cs | 1 + backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 1 + backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 1 + backend/FwLite/MiniLcm/InMemoryApi.cs | 10 +++++ backend/FwLite/MiniLcm/Models/Entry.cs | 12 ++++++ backend/LfClassicData/LfClassicMiniLcmApi.cs | 4 ++ 13 files changed, 128 insertions(+), 32 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 3b27a0e1d..960b12803 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -35,14 +35,19 @@ public async Task InitializeAsync() await _fixture.FwDataApi.CreateEntry(_testEntry); await _fixture.FwDataApi.CreateEntry(new Entry() { - Id = _complexEntryId, LexemeForm = { { "en", "Pineapple" } }, - ComplexForms = [new ComplexFormComponent() { - Id = Guid.NewGuid(), - ComponentEntryId = _complexEntryId, - ComponentHeadword = "Pineapple", - ComplexFormEntryId = _testEntry.Id, - ComplexFormHeadword = "Apple" - }] + Id = _complexEntryId, + LexemeForm = { { "en", "Pineapple" } }, + Components = + [ + new ComplexFormComponent() + { + Id = Guid.NewGuid(), + ComplexFormEntryId = _complexEntryId, + ComplexFormHeadword = "Pineapple", + ComponentEntryId = _testEntry.Id, + ComponentHeadword = "Apple" + } + ] }); } @@ -66,7 +71,9 @@ public async Task FirstSyncJustDoesAnImport() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); - crdtEntries.Should().BeEquivalentTo(fwdataEntries); + crdtEntries.Should().BeEquivalentTo(fwdataEntries, + options => options.For(e => e.Components).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.Id)); } [Fact] @@ -96,7 +103,9 @@ await crdtApi.CreateEntry(new Entry() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); - crdtEntries.Should().BeEquivalentTo(fwdataEntries); + crdtEntries.Should().BeEquivalentTo(fwdataEntries, + options => options.For(e => e.Components).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.Id)); } [Fact] @@ -117,7 +126,9 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); - crdtEntries.Should().BeEquivalentTo(fwdataEntries); + crdtEntries.Should().BeEquivalentTo(fwdataEntries, + options => options.For(e => e.Components).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.Id)); } [Fact] @@ -142,6 +153,8 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth() var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); - crdtEntries.Should().BeEquivalentTo(fwdataEntries); + crdtEntries.Should().BeEquivalentTo(fwdataEntries, + options => options.For(e => e.Components).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.Id)); } } diff --git a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs index 5f7d8fa92..4ccff1d34 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -42,6 +42,8 @@ private async Task Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, return new SyncResult(entryCount, 0); } + //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); LogDryRun(crdtApi, "crdt"); diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 6f8eb35ab..30e1b3f8b 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -57,6 +57,18 @@ public Task CreateSemanticDomain(SemanticDomain semanticDomain) return Task.CompletedTask; } + public IAsyncEnumerable GetComplexFormTypes() + { + return api.GetComplexFormTypes(); + } + + public Task CreateComplexFormType(ComplexFormType complexFormType) + { + DryRunRecords.Add(new DryRunRecord(nameof(CreateComplexFormType), + $"Create complex form type {complexFormType.Name}")); + return Task.FromResult(complexFormType); + } + public IAsyncEnumerable GetEntries(QueryOptions? options = null) { return api.GetEntries(options); diff --git a/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs b/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs index bd77d5ccd..ea289e6ad 100644 --- a/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs +++ b/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs @@ -28,6 +28,12 @@ public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, in logger.LogInformation("Imported part of speech {Id}", partOfSpeech.Id); } + await foreach (var complexFormType in importFrom.GetComplexFormTypes()) + { + await importTo.CreateComplexFormType(complexFormType); + logger.LogInformation("Imported complex form type {Id}", complexFormType.Id); + } + var semanticDomains = importFrom.GetSemanticDomains(); var entries = importFrom.GetEntries(new QueryOptions(Count: 100_000, Offset: 0)); diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs index 5ced23456..dcf895209 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs @@ -51,8 +51,8 @@ public async Task AddEntryComponent() var coatEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat" } }, }); var rackEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Rack" } }, }); - await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, coatEntry)); - await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, rackEntry)); + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(ComplexFormComponent.FromEntries(complexEntry, coatEntry))); + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(ComplexFormComponent.FromEntries(complexEntry, rackEntry))); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); complexEntry!.Components.Should().ContainSingle(e => e.ComponentEntryId == coatEntry.Id); @@ -70,8 +70,8 @@ public async Task DeleteEntryComponent() var coatEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Coat" } }, }); var rackEntry = await fixture.Api.CreateEntry(new() { LexemeForm = { { "en", "Rack" } }, }); - await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, coatEntry)); - await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(complexEntry, rackEntry)); + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(ComplexFormComponent.FromEntries(complexEntry, coatEntry))); + await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(ComplexFormComponent.FromEntries(complexEntry, rackEntry))); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); var component = complexEntry!.Components.First(); diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs index f12319502..b8188f9e4 100644 --- a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using LcmCrdt.Objects; +using MiniLcm.Models; using SIL.Harmony; using SIL.Harmony.Changes; using SIL.Harmony.Entities; @@ -9,17 +10,17 @@ namespace LcmCrdt.Changes.Entries; public class AddEntryComponentChange : CreateChange, ISelfNamedType { public Guid ComplexFormEntryId { get; } - public string ComplexFormHeadword { get; } + public string? ComplexFormHeadword { get; } public Guid ComponentEntryId { get; } public Guid? ComponentSenseId { get; } - public string ComponentHeadword { get; } + public string? ComponentHeadword { get; } [JsonConstructor] - protected AddEntryComponentChange(Guid entityId, + public AddEntryComponentChange(Guid entityId, Guid complexFormEntryId, - string complexFormHeadword, + string? complexFormHeadword, Guid componentEntryId, - string componentHeadword, + string? componentHeadword, Guid? componentSenseId = null) : base(entityId) { ComplexFormEntryId = complexFormEntryId; @@ -29,15 +30,12 @@ protected AddEntryComponentChange(Guid entityId, ComponentSenseId = componentSenseId; } - public AddEntryComponentChange( - MiniLcm.Models.Entry complexEntry, - MiniLcm.Models.Entry componentEntry, - Guid? componentSenseId = null) : this(Guid.NewGuid(), - complexEntry.Id, - complexEntry.Headword(), - componentEntry.Id, - componentEntry.Headword(), - componentSenseId) + public AddEntryComponentChange(ComplexFormComponent component) : this(component.Id == default ? Guid.NewGuid() : component.Id, + component.ComplexFormEntryId, + component.ComplexFormHeadword, + component.ComponentEntryId, + component.ComponentHeadword, + component.ComponentSenseId) { } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 4fd8a9d87..79b81e4dd 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -4,6 +4,7 @@ using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; +using LcmCrdt.Changes.Entries; using LcmCrdt.Objects; using MiniLcm; using LinqToDB; @@ -21,6 +22,7 @@ public class CrdtMiniLcmApi(DataModel dataModel, JsonSerializerOptions jsonOptio 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(); @@ -97,9 +99,15 @@ public async Task BulkImportSemanticDomains(IEnumerable new CreateSemanticDomainChange(sd.Id, sd.Name, sd.Code))); } - public async Task CreateComplexFormType(MiniLcm.Models.ComplexFormType complexFormType) + public IAsyncEnumerable GetComplexFormTypes() + { + return ComplexFormTypes.AsAsyncEnumerable(); + } + + public async Task CreateComplexFormType(MiniLcm.Models.ComplexFormType complexFormType) { await dataModel.AddChange(ClientId, new CreateComplexFormType(complexFormType.Id, complexFormType.Name)); + return await ComplexFormTypes.SingleAsync(c => c.Id == complexFormType.Id); } public IAsyncEnumerable GetEntries(QueryOptions? options = null) @@ -153,6 +161,7 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexFormType complexFo .Take(options.Count); var entries = await queryable.ToArrayAsyncLinqToDB(); await LoadSenses(entries); + await LoadComplexFormData(entries); return entries; } @@ -182,6 +191,20 @@ private async Task LoadSenses(Entry[] entries) } } + private async Task LoadComplexFormData(Entry[] entries) + { + var allComponents = await ComplexFormComponents + .Where(c => entries.Select(e => e.Id).Contains(c.ComplexFormEntryId) || entries.Select(e => e.Id).Contains(c.ComponentEntryId)) + .ToArrayAsyncEF(); + var componentLookup = allComponents.ToLookup(c => c.ComplexFormEntryId).ToDictionary(c => c.Key, c => c.ToArray()); + var complexFormLookup = allComponents.ToLookup(c => c.ComponentEntryId).ToDictionary(c => c.Key, c => c.ToArray()); + foreach (var entry in entries) + { + entry.Components = componentLookup.TryGetValue(entry.Id, out var components) ? components.ToArray() : []; + entry.ComplexForms = complexFormLookup.TryGetValue(entry.Id, out var complexForms) ? complexForms.ToArray() : []; + } + } + public async Task GetEntry(Guid id) { var entry = await Entries.SingleOrDefaultAsync(e => e.Id == id); @@ -230,6 +253,16 @@ public async Task BulkCreateEntries(IAsyncEnumerable entri private IEnumerable CreateEntryChanges(MiniLcm.Models.Entry entry, Dictionary semanticDomains, Dictionary partsOfSpeech) { yield return new CreateEntryChange(entry); + + //only add components, if we add both components and complex forms we'll get duplicates + foreach (var addEntryComponentChange in entry.Components.Select(c => new AddEntryComponentChange(c))) + { + yield return addEntryComponentChange; + } + foreach (var addComplexFormTypeChange in entry.ComplexFormTypes.Select(c => new AddComplexFormTypeChange(entry.Id, c))) + { + yield return addComplexFormTypeChange; + } foreach (var sense in entry.Senses) { sense.SemanticDomains = sense.SemanticDomains @@ -256,7 +289,10 @@ await dataModel.AddChanges(ClientId, new CreateEntryChange(entry), ..await entry.Senses.ToAsyncEnumerable() .SelectMany(s => CreateSenseChanges(entry.Id, s)) - .ToArrayAsync() + .ToArrayAsync(), + ..entry.Components.Select(c => new AddEntryComponentChange(c)), + ..entry.ComplexForms.Select(c => new AddEntryComponentChange(c)), + ..entry.ComplexFormTypes.Select(c => new AddComplexFormTypeChange(entry.Id, c)) ]); return await GetEntry(entry.Id) ?? throw new NullReferenceException(); } diff --git a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs index 27619f966..d83a9a48f 100644 --- a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs +++ b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs @@ -36,6 +36,7 @@ public IObjectBase Copy() { Id = Id, ComplexFormEntryId = ComplexFormEntryId, + ComplexFormHeadword = ComplexFormHeadword, ComponentEntryId = ComponentEntryId, ComponentHeadword = ComponentHeadword, ComponentSenseId = ComponentSenseId, diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index 90acaf41d..08c260f06 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs @@ -8,6 +8,7 @@ public interface IMiniLcmReadApi Task GetWritingSystems(); IAsyncEnumerable GetPartsOfSpeech(); IAsyncEnumerable GetSemanticDomains(); + IAsyncEnumerable GetComplexFormTypes(); IAsyncEnumerable GetEntries(QueryOptions? options = null); IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null); Task GetEntry(Guid id); diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 938461b93..f7ebc2209 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -14,6 +14,7 @@ Task UpdateWritingSystem(WritingSystemId id, Task CreatePartOfSpeech(PartOfSpeech partOfSpeech); Task CreateSemanticDomain(SemanticDomain semanticDomain); + Task CreateComplexFormType(ComplexFormType complexFormType); Task CreateEntry(Entry entry); Task UpdateEntry(Guid id, UpdateObjectInput update); Task DeleteEntry(Guid id); diff --git a/backend/FwLite/MiniLcm/InMemoryApi.cs b/backend/FwLite/MiniLcm/InMemoryApi.cs index f0b2304cc..fc6bfe0e1 100644 --- a/backend/FwLite/MiniLcm/InMemoryApi.cs +++ b/backend/FwLite/MiniLcm/InMemoryApi.cs @@ -150,6 +150,16 @@ public IAsyncEnumerable GetSemanticDomains() throw new NotImplementedException(); } + public IAsyncEnumerable GetComplexFormTypes() + { + throw new NotImplementedException(); + } + + public Task CreateComplexFormType(ComplexFormType complexFormType) + { + throw new NotImplementedException(); + } + private readonly string[] _exemplars = Enumerable.Range('a', 'z').Select(c => ((char)c).ToString()).ToArray(); public Task CreateEntry(Entry entry) diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 7c980acc3..c0f2cd33e 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -39,6 +39,18 @@ public string Headword() public class ComplexFormComponent { + public static ComplexFormComponent FromEntries(Entry complexFormEntry, Entry componentEntry, Guid? componentSenseId = null) + { + return new ComplexFormComponent + { + Id = Guid.NewGuid(), + ComplexFormEntryId = complexFormEntry.Id, + ComplexFormHeadword = complexFormEntry.Headword(), + ComponentEntryId = componentEntry.Id, + ComponentHeadword = componentEntry.Headword(), + ComponentSenseId = componentSenseId, + }; + } public Guid Id { get; set; } public required Guid ComplexFormEntryId { get; set; } public string? ComplexFormHeadword { get; set; } diff --git a/backend/LfClassicData/LfClassicMiniLcmApi.cs b/backend/LfClassicData/LfClassicMiniLcmApi.cs index b74d2cb5f..4eaa57c43 100644 --- a/backend/LfClassicData/LfClassicMiniLcmApi.cs +++ b/backend/LfClassicData/LfClassicMiniLcmApi.cs @@ -11,6 +11,10 @@ namespace LfClassicData; public class LfClassicMiniLcmApi(string projectCode, ProjectDbContext dbContext, SystemDbContext systemDbContext) : IMiniLcmReadApi { private IMongoCollection Entries => dbContext.Entries(projectCode); + public IAsyncEnumerable GetComplexFormTypes() + { + return AsyncEnumerable.Empty(); + } public async Task GetWritingSystems() { From 786e20bc3de34be3088ff696dca77baaa374621f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 1 Oct 2024 15:01:05 +0700 Subject: [PATCH 10/20] support updating LCM entry components via the components property --- .../UpdateComplexFormsTests.cs | 180 ++++++++++++++++++ .../Api/FwDataMiniLcmApi.cs | 38 ++-- .../UpdateComplexFormComponentProxy.cs | 69 +++++++ .../Api/UpdateProxy/UpdateEntryProxy.cs | 17 +- backend/FwLite/MiniLcm/Models/Entry.cs | 6 +- 5 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs create mode 100644 backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs new file mode 100644 index 000000000..9d2552859 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs @@ -0,0 +1,180 @@ +using FwDataMiniLcmBridge.Api; +using FwDataMiniLcmBridge.Tests.Fixtures; +using MiniLcm; +using MiniLcm.Models; + +namespace FwDataMiniLcmBridge.Tests; + +[Collection(ProjectLoaderFixture.Name)] +public class UpdateComplexFormsTests(ProjectLoaderFixture fixture) : IAsyncLifetime +{ + private FwDataMiniLcmApi _api = null!; + + public Task InitializeAsync() + { + var projectName = "update-complex-forms-test_" + Guid.NewGuid(); + fixture.MockFwProjectLoader.NewProject(projectName, "en", "en"); + _api = fixture.CreateApi(projectName); + return Task.CompletedTask; + } + + public Task DisposeAsync() + { + _api.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task CanAddComponentToExistingEntry() + { + var complexForm = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form" } } }); + var component = await _api.CreateEntry(new() { LexemeForm = { { "en", "component" } } }); + + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Add(e => e.Components, + ComplexFormComponent.FromEntries(complexForm, component))); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id); + entry!.Components.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm.Id); + } + + [Fact] + public async Task CanRemoveComponentFromExistingEntry() + { + var component = await _api.CreateEntry(new() { LexemeForm = { { "en", "component" } } }); + var complexFormId = Guid.NewGuid(); + var complexForm = await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = [new ComplexFormComponent() + { + ComponentEntryId = component.Id, + ComponentHeadword = component.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + }] + }); + + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Remove(e => e.Components, 0)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().BeEmpty(); + } + + [Fact] + public async Task CanChangeComponentId() + { + var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } } }); + var complexFormId = Guid.NewGuid(); + var complexForm = await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + + await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentEntryId, component2.Id)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component2.Id); + } + + [Fact] + public async Task CanChangeComponentSenseId() + { + var component2SenseId = Guid.NewGuid(); + var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } }, Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] }); + var complexFormId = Guid.NewGuid(); + var complexForm = await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + + await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, component2SenseId)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComponentSenseId == component2SenseId); + } + [Fact] + public async Task CanChangeComponentSenseIdToNull() + { + var component2SenseId = Guid.NewGuid(); + var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } }, Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] }); + var complexFormId = Guid.NewGuid(); + var complexForm = await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentSenseId = component2SenseId, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + + await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, null)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComponentSenseId == null); + } + + [Fact] + public async Task CanChangeComplexFormId() + { + var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var complexForm2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form 2" } }}); + var complexFormId = Guid.NewGuid(); + var complexForm = await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + + await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComplexFormEntryId, complexForm2.Id)); + var entry = await _api.GetEntry(complexForm2.Id); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component1.Id); + } +} diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 141f7fbf1..e7392b621 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -21,8 +21,8 @@ public class FwDataMiniLcmApi(Lazy cacheLazy, bool onCloseSave, ILogge public Guid ProjectId => Cache.LangProject.Guid; private IWritingSystemContainer WritingSystemContainer => Cache.ServiceLocator.WritingSystems; - private ILexEntryRepository EntriesRepository => Cache.ServiceLocator.GetInstance(); - private IRepository SenseRepository => Cache.ServiceLocator.GetInstance>(); + internal ILexEntryRepository EntriesRepository => Cache.ServiceLocator.GetInstance(); + internal IRepository SenseRepository => Cache.ServiceLocator.GetInstance>(); private IRepository ExampleSentenceRepository => Cache.ServiceLocator.GetInstance>(); private ILexEntryFactory LexEntryFactory => Cache.ServiceLocator.GetInstance(); private ILexSenseFactory LexSenseFactory => Cache.ServiceLocator.GetInstance(); @@ -482,20 +482,13 @@ public async Task CreateEntry(Entry entry) foreach (var component in entry.Components) { - ICmObject lexComponent = component.ComponentSenseId is not null - ? SenseRepository.GetObject(component.ComponentSenseId.Value) - : EntriesRepository.GetObject(component.ComponentEntryId); - lexEntry.AddComponent(lexComponent); + AddComplexFormComponent(lexEntry, component); } foreach (var complexForm in entry.ComplexForms) { - ICmObject lexComponent = complexForm.ComponentSenseId is not null - ? SenseRepository.GetObject(complexForm.ComponentSenseId.Value) - : lexEntry; - var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId); - complexLexEntry.AddComponent(lexComponent); + AddComplexFormComponent(complexLexEntry, complexForm); } ILexEntryRef? entryRef = lexEntry.ComplexFormEntryRefs.SingleOrDefault(); @@ -517,6 +510,29 @@ public async Task CreateEntry(Entry entry) return await GetEntry(entry.Id) ?? throw new InvalidOperationException("Entry was not created"); } + /// + /// must be called as part of an lcm action + /// + internal void AddComplexFormComponent(ILexEntry lexEntry, ComplexFormComponent component) + { + ICmObject lexComponent = component.ComponentSenseId is not null + ? SenseRepository.GetObject(component.ComponentSenseId.Value) + : EntriesRepository.GetObject(component.ComponentEntryId); + lexEntry.AddComponent(lexComponent); + } + + internal void RemoveComplexFormComponent(ILexEntry lexEntry, ComplexFormComponent component) + { + ICmObject lexComponent = component.ComponentSenseId is not null + ? SenseRepository.GetObject(component.ComponentSenseId.Value) + : EntriesRepository.GetObject(component.ComponentEntryId); + var entryRef = lexEntry.ComplexFormEntryRefs.Single(); + if (!entryRef.ComponentLexemesRS.Remove(lexComponent)) + { + throw new InvalidOperationException("Complex form component not found, searched for " + lexComponent.ObjectIdName.Text); + } + } + private IList MultiStringToTsStrings(MultiString? multiString) { if (multiString is null) return []; diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs new file mode 100644 index 000000000..c3b672e6f --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.CodeAnalysis; +using MiniLcm.Models; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateComplexFormComponentProxy : ComplexFormComponent +{ + private readonly ILexEntry _lexComplexForm; + private readonly FwDataMiniLcmApi _lexboxLcmApi; + private readonly ILexEntry _lexComponentEntry; + private readonly ILexSense? _lexSense; + + [SetsRequiredMembers] + public UpdateComplexFormComponentProxy(ILexEntry lexComplexForm, ICmObject o, FwDataMiniLcmApi lexboxLcmApi) + { + _lexComplexForm = lexComplexForm; + _lexboxLcmApi = lexboxLcmApi; + _lexSense = o as ILexSense; + _lexComponentEntry = _lexSense?.Entry ?? (ILexEntry)o; + } + + public override required Guid ComplexFormEntryId + { + get => _lexComplexForm.Guid; + set + { + _lexboxLcmApi.RemoveComplexFormComponent(_lexComplexForm, this); + _lexboxLcmApi.EntriesRepository.GetObject(value).AddComponent((_lexSense as ICmObject ?? _lexComponentEntry)); + } + } + + public override required Guid ComponentEntryId + { + get => _lexComponentEntry.Guid; + set + { + _lexboxLcmApi.RemoveComplexFormComponent(_lexComplexForm, this); + _lexboxLcmApi.AddComplexFormComponent(_lexComplexForm, + new ComplexFormComponent { ComplexFormEntryId = _lexComplexForm.Guid, ComponentEntryId = value, }); + } + } + + public override Guid? ComponentSenseId + { + get => _lexSense?.Guid; + set + { + _lexboxLcmApi.RemoveComplexFormComponent(_lexComplexForm, this); + if (value is null) + { + _lexboxLcmApi.AddComplexFormComponent(_lexComplexForm, + new ComplexFormComponent + { + ComplexFormEntryId = _lexComplexForm.Guid, + ComponentEntryId = _lexComponentEntry.Guid + }); + return; + } + _lexboxLcmApi.AddComplexFormComponent(_lexComplexForm, + new ComplexFormComponent + { + ComplexFormEntryId = _lexComplexForm.Guid, + ComponentSenseId = value, + ComponentEntryId = _lexboxLcmApi.SenseRepository.GetObject(value.Value).Entry.Guid + }); + } + } +} diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index c2bec6f96..5948c2ef0 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -42,6 +42,20 @@ public override IList Senses set => throw new NotImplementedException(); } + public override IList Components + { + get => + new UpdateListProxy( + component => lexboxLcmApi.AddComplexFormComponent(lcmEntry, component), + component => lexboxLcmApi.RemoveComplexFormComponent(lcmEntry, component), + i => new UpdateComplexFormComponentProxy(lcmEntry, + lcmEntry.ComplexFormEntryRefs.Single().ComponentLexemesRS[i], + lexboxLcmApi), + lcmEntry.ComplexFormEntryRefs.SingleOrDefault()?.ComponentLexemesRS.Count ?? 0 + ); + set => throw new NotImplementedException(); + } + public override MultiString Note { get => new UpdateMultiStringProxy(lcmEntry.Comment, lexboxLcmApi); @@ -51,7 +65,8 @@ public override MultiString Note public class UpdateMultiStringProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi) : MultiString { - public override IDictionary Values { get; } = new UpdateDictionaryProxy(multiString, lexboxLcmApi); + public override IDictionary Values { get; } = + new UpdateDictionaryProxy(multiString, lexboxLcmApi); public override MultiString Copy() { diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index c0f2cd33e..c99c50b38 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -52,10 +52,10 @@ public static ComplexFormComponent FromEntries(Entry complexFormEntry, Entry com }; } public Guid Id { get; set; } - public required Guid ComplexFormEntryId { get; set; } + public virtual required Guid ComplexFormEntryId { get; set; } public string? ComplexFormHeadword { get; set; } - public required Guid ComponentEntryId { get; set; } - public Guid? ComponentSenseId { get; set; } = null; + public virtual required Guid ComponentEntryId { get; set; } + public virtual Guid? ComponentSenseId { get; set; } = null; public string? ComponentHeadword { get; set; } } From 0fc946e1256da30628b7900c01237187943f78e9 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Tue, 1 Oct 2024 16:36:25 +0700 Subject: [PATCH 11/20] add support for changing ComplexForms collection directly --- .../CreateEntryTests.cs | 36 ++++++ .../UpdateComplexFormsTests.cs | 103 +++++++++++++++++- .../Api/UpdateProxy/UpdateEntryProxy.cs | 15 +++ 3 files changed, 149 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs index 7ed435e27..a699a8a36 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/CreateEntryTests.cs @@ -82,6 +82,42 @@ public async Task CanCreate_WithComplexFormsProperty() entry!.ComplexForms.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm.Id); } + [Fact] + public async Task CreateEntry_WithComponentSenseDoesNotShowOnComplexFormsList() + { + var componentSenseId = Guid.NewGuid(); + var component = await _api.CreateEntry(new() + { + LexemeForm = { { "en", "test component" } }, + Senses = [new Sense() { Id = componentSenseId, Gloss = { { "en", "test component sense" } } }] + }); + var complexFormEntryId = Guid.NewGuid(); + await _api.CreateEntry(new() + { + Id = complexFormEntryId, + LexemeForm = { { "en", "test" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component.Id, + ComponentHeadword = component.Headword(), + ComponentSenseId = componentSenseId, + ComplexFormEntryId = complexFormEntryId, + ComplexFormHeadword = "test" + } + ] + }); + + var entry = await _api.GetEntry(component.Id); + entry.Should().NotBeNull(); + entry!.ComplexForms.Should().BeEmpty(); + + entry = await _api.GetEntry(complexFormEntryId); + entry.Should().NotBeNull(); + entry!.Components.Should().ContainSingle(c => c.ComplexFormEntryId == complexFormEntryId && c.ComponentEntryId == component.Id && c.ComponentSenseId == componentSenseId); + } + [Fact] public async Task CanCreate_WithComplexFormTypesProperty() { diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs index 9d2552859..286b9f0ff 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs @@ -35,8 +35,7 @@ await _api.UpdateEntry(complexForm.Id, ComplexFormComponent.FromEntries(complexForm, component))); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id); - entry!.Components.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm.Id); + entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); } [Fact] @@ -124,7 +123,6 @@ public async Task CanChangeComponentSenseId() public async Task CanChangeComponentSenseIdToNull() { var component2SenseId = Guid.NewGuid(); - var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } }, Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] }); var complexFormId = Guid.NewGuid(); var complexForm = await _api.CreateEntry(new() @@ -135,9 +133,9 @@ public async Task CanChangeComponentSenseIdToNull() [ new ComplexFormComponent() { - ComponentEntryId = component1.Id, + ComponentEntryId = component2.Id, ComponentSenseId = component2SenseId, - ComponentHeadword = component1.Headword(), + ComponentHeadword = component2.Headword(), ComplexFormEntryId = complexFormId, ComplexFormHeadword = "complex form" } @@ -177,4 +175,99 @@ public async Task CanChangeComplexFormId() entry.Should().NotBeNull(); entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component1.Id); } + + [Fact] + public async Task CanAddComplexFormToExistingEntry() + { + var complexForm = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form" } } }); + var component = await _api.CreateEntry(new() { LexemeForm = { { "en", "component" } } }); + + await _api.UpdateEntry(component.Id, + new UpdateObjectInput().Add(e => e.ComplexForms, + ComplexFormComponent.FromEntries(complexForm, component))); + var entry = await _api.GetEntry(component.Id); + entry.Should().NotBeNull(); + entry!.ComplexForms.Should().ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); + } + + [Fact] + public async Task CanRemoveComplexFormFromExistingEntry() + { + var component = await _api.CreateEntry(new() { LexemeForm = { { "en", "component" } } }); + var complexFormId = Guid.NewGuid(); + await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = [new ComplexFormComponent() + { + ComponentEntryId = component.Id, + ComponentHeadword = component.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + }] + }); + + await _api.UpdateEntry(component.Id, + new UpdateObjectInput().Remove(e => e.ComplexForms, 0)); + var entry = await _api.GetEntry(component.Id); + entry.Should().NotBeNull(); + entry!.ComplexForms.Should().BeEmpty(); + } + + [Fact] + public async Task CanChangeComplexFormIdOnComplexFormsList() + { + var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var complexForm2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form 2" } } }); + var complexFormId = Guid.NewGuid(); + await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + + await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComplexFormEntryId, complexForm2.Id)); + var entry = await _api.GetEntry(component1.Id); + entry.Should().NotBeNull(); + entry!.ComplexForms.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm2.Id); + } + + [Fact] + public async Task CanChangeComponentIdOnComplexFormsList() + { + var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); + var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } } }); + var complexFormId = Guid.NewGuid(); + await _api.CreateEntry(new() + { + Id = complexFormId, + LexemeForm = { { "en", "complex form" } }, + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component1.Id, + ComponentHeadword = component1.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] + }); + + await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComponentEntryId, component2.Id)); + var entry = await _api.GetEntry(component2.Id); + entry.Should().NotBeNull(); + entry!.ComplexForms.Should().ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComplexFormEntryId == complexFormId); + } } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index 5948c2ef0..b49b0a363 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -56,6 +56,21 @@ public override IList Components set => throw new NotImplementedException(); } + public override IList ComplexForms + { + get => + new UpdateListProxy( + component => lexboxLcmApi.AddComplexFormComponent(lexboxLcmApi.EntriesRepository.GetObject(component.ComplexFormEntryId), component), + component => lexboxLcmApi.RemoveComplexFormComponent(lexboxLcmApi.EntriesRepository.GetObject(component.ComplexFormEntryId), component), + //todo this does not handle complex forms which reference a sense + i => new UpdateComplexFormComponentProxy(lcmEntry.ComplexFormEntries.ElementAt(i), + lcmEntry, + lexboxLcmApi), + lcmEntry.ComplexFormEntries.Count() + ); + set => throw new NotImplementedException(); + } + public override MultiString Note { get => new UpdateMultiStringProxy(lcmEntry.Comment, lexboxLcmApi); From 67939135850497552855ac1332713e751cfaa0ed Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 2 Oct 2024 11:43:52 +0700 Subject: [PATCH 12/20] add support for changing complex form types --- .../UpdateComplexFormsTests.cs | 135 ++++++++++++++---- .../Api/FwDataMiniLcmApi.cs | 35 +++-- .../UpdateProxy/UpdateComplexFormTypeProxy.cs | 31 ++++ .../Api/UpdateProxy/UpdateEntryProxy.cs | 13 ++ backend/FwLite/MiniLcm/Models/Entry.cs | 2 +- 5 files changed, 173 insertions(+), 43 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs index 286b9f0ff..653d5f144 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs @@ -35,7 +35,8 @@ await _api.UpdateEntry(complexForm.Id, ComplexFormComponent.FromEntries(complexForm, component))); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); + entry!.Components.Should() + .ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); } [Fact] @@ -47,13 +48,16 @@ public async Task CanRemoveComponentFromExistingEntry() { Id = complexFormId, LexemeForm = { { "en", "complex form" } }, - Components = [new ComplexFormComponent() - { - ComponentEntryId = component.Id, - ComponentHeadword = component.Headword(), - ComplexFormEntryId = complexFormId, - ComplexFormHeadword = "complex form" - }] + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component.Id, + ComponentHeadword = component.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] }); await _api.UpdateEntry(complexForm.Id, @@ -85,10 +89,12 @@ public async Task CanChangeComponentId() ] }); - await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentEntryId, component2.Id)); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Set(e => e.Components[0].ComponentEntryId, component2.Id)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component2.Id); + var complexFormComponent = entry!.Components.Should().ContainSingle().Subject; + complexFormComponent.ComponentEntryId.Should().Be(component2.Id); } [Fact] @@ -96,7 +102,11 @@ public async Task CanChangeComponentSenseId() { var component2SenseId = Guid.NewGuid(); var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); - var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } }, Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] }); + var component2 = await _api.CreateEntry(new() + { + LexemeForm = { { "en", "component2" } }, + Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] + }); var complexFormId = Guid.NewGuid(); var complexForm = await _api.CreateEntry(new() { @@ -114,16 +124,24 @@ public async Task CanChangeComponentSenseId() ] }); - await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, component2SenseId)); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, component2SenseId)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComponentSenseId == component2SenseId); + var complexFormComponent = entry!.Components.Should().ContainSingle().Subject; + complexFormComponent.ComponentEntryId.Should().Be(component2.Id); + complexFormComponent.ComponentSenseId.Should().Be(component2SenseId); } + [Fact] public async Task CanChangeComponentSenseIdToNull() { var component2SenseId = Guid.NewGuid(); - var component2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component2" } }, Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] }); + var component2 = await _api.CreateEntry(new() + { + LexemeForm = { { "en", "component2" } }, + Senses = [new Sense() { Id = component2SenseId, Gloss = { { "en", "component2" } } }] + }); var complexFormId = Guid.NewGuid(); var complexForm = await _api.CreateEntry(new() { @@ -142,17 +160,19 @@ public async Task CanChangeComponentSenseIdToNull() ] }); - await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, null)); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, null)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComponentSenseId == null); + entry!.Components.Should() + .ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComponentSenseId == null); } [Fact] public async Task CanChangeComplexFormId() { var component1 = await _api.CreateEntry(new() { LexemeForm = { { "en", "component1" } } }); - var complexForm2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form 2" } }}); + var complexForm2 = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form 2" } } }); var complexFormId = Guid.NewGuid(); var complexForm = await _api.CreateEntry(new() { @@ -170,10 +190,12 @@ public async Task CanChangeComplexFormId() ] }); - await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComplexFormEntryId, complexForm2.Id)); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Set(e => e.Components[0].ComplexFormEntryId, complexForm2.Id)); var entry = await _api.GetEntry(complexForm2.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component1.Id); + var complexFormComponent = entry!.Components.Should().ContainSingle().Subject; + complexFormComponent.ComponentEntryId.Should().Be(component1.Id); } [Fact] @@ -187,7 +209,8 @@ await _api.UpdateEntry(component.Id, ComplexFormComponent.FromEntries(complexForm, component))); var entry = await _api.GetEntry(component.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should().ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); + entry!.ComplexForms.Should() + .ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); } [Fact] @@ -199,13 +222,16 @@ await _api.CreateEntry(new() { Id = complexFormId, LexemeForm = { { "en", "complex form" } }, - Components = [new ComplexFormComponent() - { - ComponentEntryId = component.Id, - ComponentHeadword = component.Headword(), - ComplexFormEntryId = complexFormId, - ComplexFormHeadword = "complex form" - }] + Components = + [ + new ComplexFormComponent() + { + ComponentEntryId = component.Id, + ComponentHeadword = component.Headword(), + ComplexFormEntryId = complexFormId, + ComplexFormHeadword = "complex form" + } + ] }); await _api.UpdateEntry(component.Id, @@ -237,10 +263,12 @@ await _api.CreateEntry(new() ] }); - await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComplexFormEntryId, complexForm2.Id)); + await _api.UpdateEntry(component1.Id, + new UpdateObjectInput().Set(e => e.ComplexForms[0].ComplexFormEntryId, complexForm2.Id)); var entry = await _api.GetEntry(component1.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm2.Id); + var complexFormComponent = entry!.ComplexForms.Should().ContainSingle().Subject; + complexFormComponent.ComplexFormEntryId.Should().Be(complexForm2.Id); } [Fact] @@ -265,9 +293,54 @@ await _api.CreateEntry(new() ] }); - await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComponentEntryId, component2.Id)); + await _api.UpdateEntry(component1.Id, + new UpdateObjectInput().Set(e => e.ComplexForms[0].ComponentEntryId, component2.Id)); var entry = await _api.GetEntry(component2.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should().ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComplexFormEntryId == complexFormId); + var complexFormComponent = entry!.ComplexForms.Should().ContainSingle().Subject; + complexFormComponent.ComponentEntryId.Should().Be(component2.Id); + complexFormComponent.ComplexFormEntryId.Should().Be(complexFormId); + } + + [Fact] + public async Task CanAddComplexFormType() + { + var complexFormType = + await _api.CreateComplexFormType(new ComplexFormType() { Name = new() { { "en", "Add" } } }); + var complexForm = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form" } } }); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Add(e => e.ComplexFormTypes, complexFormType)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.ComplexFormTypes.Should().ContainSingle(c => c.Id == complexFormType.Id); + } + + [Fact] + public async Task CanRemoveComplexFormType() + { + var complexFormType = + await _api.CreateComplexFormType(new ComplexFormType() { Name = new() { { "en", "Remove" } } }); + var complexForm = await _api.CreateEntry(new() + { + LexemeForm = { { "en", "complex form" } }, ComplexFormTypes = [complexFormType] + }); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Remove(e => e.ComplexFormTypes, 0)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.ComplexFormTypes.Should().BeEmpty(); + } + + [Fact] + public async Task CanChangeComplexFormType() + { + var complexFormType1 = await _api.CreateComplexFormType(new ComplexFormType() { Name = new() { { "en", "Change from" } } }); + var complexFormType2 = await _api.CreateComplexFormType(new ComplexFormType() { Name = new() { { "en", "Change to" } } }); + var complexForm = await _api.CreateEntry(new() { LexemeForm = { { "en", "complex form" } }, ComplexFormTypes = [complexFormType1] }); + await _api.UpdateEntry(complexForm.Id, + new UpdateObjectInput().Set(e => e.ComplexFormTypes[0].Id, complexFormType2.Id)); + var entry = await _api.GetEntry(complexForm.Id); + entry.Should().NotBeNull(); + entry!.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(complexFormType2.Id); } } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index e7392b621..9dbbdc615 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -491,19 +491,9 @@ public async Task CreateEntry(Entry entry) AddComplexFormComponent(complexLexEntry, complexForm); } - ILexEntryRef? entryRef = lexEntry.ComplexFormEntryRefs.SingleOrDefault(); foreach (var complexFormType in entry.ComplexFormTypes) { - if (entryRef is null) - { - entryRef = Cache.ServiceLocator.GetInstance().Create(); - lexEntry.EntryRefsOS.Add(entryRef); - entryRef.RefType = LexEntryRefTags.krtComplexForm; - entryRef.HideMinorEntry = 0; - } - - var lexEntryType = (ILexEntryType) ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormType.Id); - entryRef.ComplexEntryTypesRS.Add(lexEntryType); + AddComplexFormType(lexEntry, complexFormType.Id); } }); @@ -533,6 +523,29 @@ internal void RemoveComplexFormComponent(ILexEntry lexEntry, ComplexFormComponen } } + internal void AddComplexFormType(ILexEntry lexEntry, Guid complexFormTypeId) + { + ILexEntryRef? entryRef = lexEntry.ComplexFormEntryRefs.SingleOrDefault(); + if (entryRef is null) + { + entryRef = Cache.ServiceLocator.GetInstance().Create(); + lexEntry.EntryRefsOS.Add(entryRef); + entryRef.RefType = LexEntryRefTags.krtComplexForm; + entryRef.HideMinorEntry = 0; + } + + var lexEntryType = (ILexEntryType)ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormTypeId); + entryRef.ComplexEntryTypesRS.Add(lexEntryType); + } + + internal void RemoveComplexFormType(ILexEntry lexEntry, Guid complexFormTypeId) + { + ILexEntryRef? entryRef = lexEntry.ComplexFormEntryRefs.SingleOrDefault(); + if (entryRef is null) return; + var lexEntryType = (ILexEntryType)ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormTypeId); + entryRef.ComplexEntryTypesRS.Remove(lexEntryType); + } + private IList MultiStringToTsStrings(MultiString? multiString) { if (multiString is null) return []; diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs new file mode 100644 index 000000000..3dad6ad23 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using MiniLcm.Models; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateComplexFormTypeProxy : ComplexFormType +{ + private readonly ILexEntryType _lexEntryType; + private readonly ILexEntry _lcmEntry; + private readonly FwDataMiniLcmApi _lexboxLcmApi; + + [SetsRequiredMembers] + public UpdateComplexFormTypeProxy(ILexEntryType lexEntryType, ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi) + { + _lexEntryType = lexEntryType; + _lcmEntry = lcmEntry; + _lexboxLcmApi = lexboxLcmApi; + Name = new(); + } + + public override Guid Id + { + get => _lexEntryType.Guid; + set + { + _lexboxLcmApi.RemoveComplexFormType(_lcmEntry, _lexEntryType.Guid); + _lexboxLcmApi.AddComplexFormType(_lcmEntry, value); + } + } +} diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index b49b0a363..f96d149a6 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -71,6 +71,19 @@ public override IList ComplexForms set => throw new NotImplementedException(); } + public override IList ComplexFormTypes + { + get => + new UpdateListProxy( + complexFormType => lexboxLcmApi.AddComplexFormType(lcmEntry, complexFormType.Id), + complexFormType => lexboxLcmApi.RemoveComplexFormType(lcmEntry, complexFormType.Id), + i => new UpdateComplexFormTypeProxy(lcmEntry.ComplexFormEntryRefs.Single().ComplexEntryTypesRS[i], lcmEntry, lexboxLcmApi), + lcmEntry.ComplexFormEntryRefs.SingleOrDefault() + ?.ComplexEntryTypesRS.Count ?? 0 + ); + set => throw new NotImplementedException(); + } + public override MultiString Note { get => new UpdateMultiStringProxy(lcmEntry.Comment, lexboxLcmApi); diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index c99c50b38..f8fde425d 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -68,7 +68,7 @@ public class Variants public class ComplexFormType { - public Guid Id { get; set; } + public virtual Guid Id { get; set; } public required MultiString Name { get; set; } } From 95762f9ad41cc8ab5694606a84f4c39f58dd9b53 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 2 Oct 2024 12:53:01 +0700 Subject: [PATCH 13/20] add support for changing Components of a complex form --- .../UpdateComplexFormComponentProxy.cs | 2 +- .../JsonPatchEntryRewriteTests.cs | 77 +++++++++++++++++ ...Tests.cs => JsonPatchSenseRewriteTests.cs} | 2 +- .../Entries/SetComplexFormComponentChange.cs | 42 +++++++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 6 +- .../Objects/CrdtComplexFormComponent.cs | 2 +- backend/FwLite/LcmCrdt/Objects/Entry.cs | 85 +++++++++++++++++-- backend/FwLite/MiniLcm/Models/Entry.cs | 4 +- 8 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs rename backend/FwLite/LcmCrdt.Tests/{JsonPatchRewriteTests.cs => JsonPatchSenseRewriteTests.cs} (99%) create mode 100644 backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs index c3b672e6f..2dda07e71 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormComponentProxy.cs @@ -4,7 +4,7 @@ namespace FwDataMiniLcmBridge.Api.UpdateProxy; -public class UpdateComplexFormComponentProxy : ComplexFormComponent +public record UpdateComplexFormComponentProxy : ComplexFormComponent { private readonly ILexEntry _lexComplexForm; private readonly FwDataMiniLcmApi _lexboxLcmApi; diff --git a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs new file mode 100644 index 000000000..65756228c --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs @@ -0,0 +1,77 @@ +using LcmCrdt.Changes.Entries; +using LcmCrdt.Objects; +using MiniLcm.Models; +using SIL.Harmony.Changes; +using SystemTextJsonPatch; +using Entry = LcmCrdt.Objects.Entry; + +namespace LcmCrdt.Tests; + +public class JsonPatchEntryRewriteTests +{ + private Entry _entry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "test" } } }; + + [Fact] + public void ChangesFromJsonPatch_AddComponentMakesAddEntryComponentChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + patch.Add(entry => entry.Components, ComplexFormComponent.FromEntries(_entry, componentEntry)); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var addEntryComponentChange = + changes.Should().ContainSingle().Which.Should().BeOfType().Subject; + addEntryComponentChange.ComplexFormEntryId.Should().Be(_entry.Id); + addEntryComponentChange.ComponentEntryId.Should().Be(componentEntry.Id); + addEntryComponentChange.ComponentHeadword.Should().Be(componentEntry.Headword()); + } + + [Fact] + public void ChangesFromJsonPatch_RemoveComponentMakesDeleteChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); + _entry.Components.Add(complexFormComponent); + patch.Remove(entry => entry.Components, 0); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var removeEntryComponentChange = changes.Should().ContainSingle().Which.Should() + .BeOfType>().Subject; + removeEntryComponentChange.EntityId.Should().Be(complexFormComponent.Id); + } + + [Fact] + public void ChangesFromJsonPatch_ReplaceComponentMakesReplaceEntryComponentChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); + _entry.Components.Add(complexFormComponent); + var newComponentId = Guid.NewGuid(); + patch.Replace(entry => entry.Components, complexFormComponent with { ComponentEntryId = newComponentId, ComponentHeadword = "new" }, 0); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() + .BeOfType().Subject; + setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); + setComplexFormComponentChange.ComplexFormEntryId.Should().BeNull(); + setComplexFormComponentChange.ComponentEntryId.Should().Be(newComponentId); + setComplexFormComponentChange.ComponentSenseId.Should().BeNull(); + } + + [Fact] + public void ChangesFromJsonPatch_ReplaceComponentWithNewComplexFormIdMakesReplaceEntryComponentChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); + _entry.Components.Add(complexFormComponent); + var newComplexFormId = Guid.NewGuid(); + patch.Replace(entry => entry.Components, complexFormComponent with { ComplexFormEntryId = newComplexFormId, ComplexFormHeadword = "new" }, 0); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() + .BeOfType().Subject; + setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); + setComplexFormComponentChange.ComponentEntryId.Should().BeNull(); + setComplexFormComponentChange.ComplexFormEntryId.Should().Be(newComplexFormId); + setComplexFormComponentChange.ComponentSenseId.Should().BeNull(); + } +} diff --git a/backend/FwLite/LcmCrdt.Tests/JsonPatchRewriteTests.cs b/backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs similarity index 99% rename from backend/FwLite/LcmCrdt.Tests/JsonPatchRewriteTests.cs rename to backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs index 7ea679835..f82f50b84 100644 --- a/backend/FwLite/LcmCrdt.Tests/JsonPatchRewriteTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs @@ -6,7 +6,7 @@ namespace LcmCrdt.Tests; -public class JsonPatchRewriteTests +public class JsonPatchSenseRewriteTests { private Sense _sense = new Sense() { diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs new file mode 100644 index 000000000..36fa0eba6 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; +using LcmCrdt.Objects; +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Changes.Entries; + +public class SetComplexFormComponentChange : EditChange, ISelfNamedType +{ + [JsonConstructor] + protected SetComplexFormComponentChange(Guid entityId, Guid? complexFormEntryId, Guid? componentEntryId, Guid? componentSenseId) : base(entityId) + { + ComplexFormEntryId = complexFormEntryId; + ComponentEntryId = componentEntryId; + ComponentSenseId = componentSenseId; + } + + public static SetComplexFormComponentChange NewComplexForm(Guid id, Guid complexFormEntryId) => new(id, complexFormEntryId, null, null); + public static SetComplexFormComponentChange NewComponent(Guid id, Guid componentEntryId) => new(id, null, componentEntryId, null); + public static SetComplexFormComponentChange NewComponentSense(Guid id, Guid componentEntryId, Guid? componentSenseId) => new(id, null, componentEntryId, componentSenseId); + public Guid? ComplexFormEntryId { get; } + public Guid? ComponentEntryId { get; } + public Guid? ComponentSenseId { get; } + public override async ValueTask ApplyChange(CrdtComplexFormComponent entity, ChangeContext context) + { + if (ComplexFormEntryId.HasValue) + { + entity.ComplexFormEntryId = ComplexFormEntryId.Value; + entity.DeletedAt = await context.IsObjectDeleted(ComplexFormEntryId.Value) ? context.Commit.DateTime : (DateTime?)null; + } + if (ComponentEntryId.HasValue) + { + entity.ComponentEntryId = ComponentEntryId.Value; + entity.DeletedAt = await context.IsObjectDeleted(ComponentEntryId.Value) ? context.Commit.DateTime : (DateTime?)null; + } + entity.ComponentSenseId = ComponentSenseId; + if (ComponentSenseId.HasValue) + { + entity.DeletedAt = await context.IsObjectDeleted(ComponentSenseId.Value) ? context.Commit.DateTime : (DateTime?)null; + } + } +} diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 79b81e4dd..3e46ff933 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -300,8 +300,10 @@ ..await entry.Senses.ToAsyncEnumerable() public async Task UpdateEntry(Guid id, UpdateObjectInput update) { - var patchChange = new JsonPatchChange(id, update.Patch, jsonOptions); - await dataModel.AddChange(ClientId, patchChange); + var entry = await GetEntry(id); + if (entry is null) throw new NullReferenceException($"unable to find entry with id {id}"); + + await dataModel.AddChanges(ClientId, [..Entry.ChangesFromJsonPatch((Entry)entry, update.Patch)]); return await GetEntry(id) ?? throw new NullReferenceException(); } diff --git a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs index d83a9a48f..a28aa1a70 100644 --- a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs +++ b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs @@ -4,7 +4,7 @@ namespace LcmCrdt.Objects; -public class CrdtComplexFormComponent : ComplexFormComponent, IObjectBase +public record CrdtComplexFormComponent : ComplexFormComponent, IObjectBase { Guid IObjectBase.Id { diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index 172d3087b..d93d5cbd3 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -1,8 +1,14 @@ using System.Linq.Expressions; +using LcmCrdt.Changes; +using LcmCrdt.Changes.Entries; +using LcmCrdt.Utils; using SIL.Harmony; using SIL.Harmony.Entities; using LinqToDB; using MiniLcm.Models; +using SIL.Harmony.Changes; +using SystemTextJsonPatch; +using SystemTextJsonPatch.Operations; namespace LcmCrdt.Objects; @@ -25,13 +31,18 @@ public string Headword(WritingSystemId ws) } protected static Expression> HeadwordExpression() => - (e, ws) => (string.IsNullOrEmpty(Json.Value(e.CitationForm, ms => ms[ws])) ? Json.Value(e.LexemeForm, ms => ms[ws]) : Json.Value(e.CitationForm, ms => ms[ws]))!.Trim(); + (e, ws) => (string.IsNullOrEmpty(Json.Value(e.CitationForm, ms => ms[ws])) + ? Json.Value(e.LexemeForm, ms => ms[ws]) + : Json.Value(e.CitationForm, ms => ms[ws]))!.Trim(); public Guid[] GetReferences() { return [ - ..Components.SelectMany(c => c.ComponentSenseId is null ? [c.ComponentEntryId] : new [] {c.ComponentEntryId, c.ComponentSenseId.Value}), + ..Components.SelectMany(c => + c.ComponentSenseId is null + ? [c.ComponentEntryId] + : new[] { c.ComponentEntryId, c.ComponentSenseId.Value }), ..ComplexForms.Select(c => c.ComplexFormEntryId) ]; } @@ -52,11 +63,73 @@ public IObjectBase Copy() CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), Note = Note.Copy(), - Senses = [..Senses.Select(s => (s is Sense cs ? (Sense) cs.Copy() : s))], - Components = [..Components.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c))], - ComplexForms = [..ComplexForms.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c))], - ComplexFormTypes = [..ComplexFormTypes.Select(cft => (cft is CrdtComplexFormType ct ? (ComplexFormType) ct.Copy() : cft))], + Senses = [..Senses.Select(s => (s is Sense cs ? (Sense)cs.Copy() : s))], + Components = + [ + ..Components.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c)) + ], + ComplexForms = + [ + ..ComplexForms.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c)) + ], + ComplexFormTypes = + [ + ..ComplexFormTypes.Select(cft => (cft is CrdtComplexFormType ct ? (ComplexFormType)ct.Copy() : cft)) + ], Variants = Variants }; } + + public static IEnumerable ChangesFromJsonPatch(Entry entry, JsonPatchDocument patch) + { + foreach (var rewriteChange in patch.RewriteChanges(s => s.Components, + (component, index, operationType) => + { + if (operationType == OperationType.Add) + { + ArgumentNullException.ThrowIfNull(component); + return new AddEntryComponentChange(component); + } + + if (operationType == OperationType.Replace) + { + ArgumentNullException.ThrowIfNull(component); + var currentComponent = entry.Components[index]; + if (currentComponent.ComponentEntryId != component.ComponentEntryId && + currentComponent.ComplexFormEntryId != component.ComplexFormEntryId + ) + { + throw new InvalidOperationException("both component id and complex form id have changed"); + } + if (currentComponent.Id != component.Id) throw new InvalidOperationException( + $"complexFormComponent id mismatch at index {index}, expected {currentComponent.Id}, actual {component.Id}"); + if (currentComponent.ComponentEntryId != component.ComponentEntryId) + { + return SetComplexFormComponentChange.NewComponent(currentComponent.Id, component.ComponentEntryId); + } + if (currentComponent.ComponentSenseId != component.ComponentSenseId) + { + return SetComplexFormComponentChange.NewComponentSense(currentComponent.Id, component.ComponentEntryId, component.ComponentSenseId); + } + if (currentComponent.ComplexFormEntryId != component.ComplexFormEntryId) + { + return SetComplexFormComponentChange.NewComplexForm(currentComponent.Id, component.ComplexFormEntryId); + } + } + + if (operationType == OperationType.Remove) + { + component ??= entry.Components[index]; + return new DeleteChange(component.Id); + } + + throw new NotSupportedException($"operation {operationType} not supported for components"); + })) + { + yield return rewriteChange; + } + + if (patch.Operations.Count > 0) + yield return new JsonPatchChange(entry.Id, patch, patch.Options); + } } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index f8fde425d..46772f733 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -37,10 +37,12 @@ public string Headword() } } -public class ComplexFormComponent +public record ComplexFormComponent { public static ComplexFormComponent FromEntries(Entry complexFormEntry, Entry componentEntry, Guid? componentSenseId = null) { + if (componentEntry.Id == default) throw new ArgumentException("componentEntry.Id is empty"); + if (complexFormEntry.Id == default) throw new ArgumentException("complexFormEntry.Id is empty"); return new ComplexFormComponent { Id = Guid.NewGuid(), From 1c4951db9f15ddbded70bdec7833a75aaa7c09e6 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 2 Oct 2024 13:08:02 +0700 Subject: [PATCH 14/20] add support for changing ComplexForms of a component --- .../JsonPatchEntryRewriteTests.cs | 64 +++++++++++ backend/FwLite/LcmCrdt/Objects/Entry.cs | 100 ++++++++++-------- 2 files changed, 121 insertions(+), 43 deletions(-) diff --git a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs index 65756228c..503711999 100644 --- a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs @@ -74,4 +74,68 @@ public void ChangesFromJsonPatch_ReplaceComponentWithNewComplexFormIdMakesReplac setComplexFormComponentChange.ComplexFormEntryId.Should().Be(newComplexFormId); setComplexFormComponentChange.ComponentSenseId.Should().BeNull(); } + + [Fact] + public void ChangesFromJsonPatch_AddComplexFormMakesAddEntryComponentChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "complex form" } } }; + patch.Add(entry => entry.ComplexForms, ComplexFormComponent.FromEntries(_entry, componentEntry)); + var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var addEntryComponentChange = + changes.Should().ContainSingle().Which.Should().BeOfType().Subject; + addEntryComponentChange.ComplexFormEntryId.Should().Be(_entry.Id); + addEntryComponentChange.ComponentEntryId.Should().Be(componentEntry.Id); + addEntryComponentChange.ComponentHeadword.Should().Be(componentEntry.Headword()); + } + + [Fact] + public void ChangesFromJsonPatch_RemoveComplexFormMakesDeleteChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); + componentEntry.ComplexForms.Add(complexFormComponent); + patch.Remove(entry => entry.ComplexForms, 0); + var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var removeEntryComponentChange = changes.Should().ContainSingle().Which.Should() + .BeOfType>().Subject; + removeEntryComponentChange.EntityId.Should().Be(complexFormComponent.Id); + } + + [Fact] + public void ChangesFromJsonPatch_ReplaceComplexFormMakesReplaceEntryComponentChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); + componentEntry.ComplexForms.Add(complexFormComponent); + var newComponentId = Guid.NewGuid(); + patch.Replace(entry => entry.ComplexForms, complexFormComponent with { ComponentEntryId = newComponentId, ComponentHeadword = "new" }, 0); + var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() + .BeOfType().Subject; + setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); + setComplexFormComponentChange.ComplexFormEntryId.Should().BeNull(); + setComplexFormComponentChange.ComponentEntryId.Should().Be(newComponentId); + setComplexFormComponentChange.ComponentSenseId.Should().BeNull(); + } + + [Fact] + public void ChangesFromJsonPatch_ReplaceComplexFormWithNewComplexFormIdMakesReplaceEntryComponentChange() + { + var patch = new JsonPatchDocument(); + var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; + var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); + componentEntry.ComplexForms.Add(complexFormComponent); + var newComplexFormId = Guid.NewGuid(); + patch.Replace(entry => entry.ComplexForms, complexFormComponent with { ComplexFormEntryId = newComplexFormId, ComplexFormHeadword = "new" }, 0); + var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() + .BeOfType().Subject; + setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); + setComplexFormComponentChange.ComponentEntryId.Should().BeNull(); + setComplexFormComponentChange.ComplexFormEntryId.Should().Be(newComplexFormId); + setComplexFormComponentChange.ComponentSenseId.Should().BeNull(); + } } diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index d93d5cbd3..5137d7b9b 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -82,53 +82,67 @@ public IObjectBase Copy() public static IEnumerable ChangesFromJsonPatch(Entry entry, JsonPatchDocument patch) { - foreach (var rewriteChange in patch.RewriteChanges(s => s.Components, - (component, index, operationType) => - { - if (operationType == OperationType.Add) - { - ArgumentNullException.ThrowIfNull(component); - return new AddEntryComponentChange(component); - } - - if (operationType == OperationType.Replace) - { - ArgumentNullException.ThrowIfNull(component); - var currentComponent = entry.Components[index]; - if (currentComponent.ComponentEntryId != component.ComponentEntryId && - currentComponent.ComplexFormEntryId != component.ComplexFormEntryId - ) - { - throw new InvalidOperationException("both component id and complex form id have changed"); - } - if (currentComponent.Id != component.Id) throw new InvalidOperationException( - $"complexFormComponent id mismatch at index {index}, expected {currentComponent.Id}, actual {component.Id}"); - if (currentComponent.ComponentEntryId != component.ComponentEntryId) - { - return SetComplexFormComponentChange.NewComponent(currentComponent.Id, component.ComponentEntryId); - } - if (currentComponent.ComponentSenseId != component.ComponentSenseId) - { - return SetComplexFormComponentChange.NewComponentSense(currentComponent.Id, component.ComponentEntryId, component.ComponentSenseId); - } - if (currentComponent.ComplexFormEntryId != component.ComplexFormEntryId) - { - return SetComplexFormComponentChange.NewComplexForm(currentComponent.Id, component.ComplexFormEntryId); - } - } - - if (operationType == OperationType.Remove) - { - component ??= entry.Components[index]; - return new DeleteChange(component.Id); - } - - throw new NotSupportedException($"operation {operationType} not supported for components"); - })) + IChange RewriteComplexFormComponents(IList components, ComplexFormComponent? component, Index index, OperationType operationType) + { + if (operationType == OperationType.Add) + { + ArgumentNullException.ThrowIfNull(component); + return new AddEntryComponentChange(component); + } + + if (operationType == OperationType.Replace) + { + ArgumentNullException.ThrowIfNull(component); + var currentComponent = components[index]; + if (currentComponent.ComponentEntryId != component.ComponentEntryId && currentComponent.ComplexFormEntryId != component.ComplexFormEntryId) + { + throw new InvalidOperationException("both component id and complex form id have changed"); + } + + if (currentComponent.Id != component.Id) throw new InvalidOperationException($"complexFormComponent id mismatch at index {index}, expected {currentComponent.Id}, actual {component.Id}"); + if (currentComponent.ComponentEntryId != component.ComponentEntryId) + { + return SetComplexFormComponentChange.NewComponent(currentComponent.Id, component.ComponentEntryId); + } + + if (currentComponent.ComponentSenseId != component.ComponentSenseId) + { + return SetComplexFormComponentChange.NewComponentSense(currentComponent.Id, component.ComponentEntryId, component.ComponentSenseId); + } + + if (currentComponent.ComplexFormEntryId != component.ComplexFormEntryId) + { + return SetComplexFormComponentChange.NewComplexForm(currentComponent.Id, component.ComplexFormEntryId); + } + } + + if (operationType == OperationType.Remove) + { + component ??= components[index]; + return new DeleteChange(component.Id); + } + + throw new NotSupportedException($"operation {operationType} not supported for components"); + } + + foreach (var rewriteChange in patch.RewriteChanges( + s => s.Components, + (component, index, operationType) => RewriteComplexFormComponents(entry.Components, component, index, operationType) + )) { yield return rewriteChange; } + foreach (var rewriteChange in patch.RewriteChanges( + s => s.ComplexForms, + (component, index, operationType) => RewriteComplexFormComponents(entry.ComplexForms, component, index, operationType) + )) + { + yield return rewriteChange; + } + + + if (patch.Operations.Count > 0) yield return new JsonPatchChange(entry.Id, patch, patch.Options); } From 5f3b81f9478f2ad847272d99a4e12a984be09719 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 2 Oct 2024 15:08:43 +0700 Subject: [PATCH 15/20] implement complex form type changes via json patch --- .../JsonPatchEntryRewriteTests.cs | 43 +++++++++++++++++++ .../Entries/RemoveComplexFormTypeChange.cs | 6 +-- .../Entries/ReplaceComplexFormTypeChange.cs | 21 +++++++++ backend/FwLite/LcmCrdt/Objects/Entry.cs | 28 ++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 backend/FwLite/LcmCrdt/Changes/Entries/ReplaceComplexFormTypeChange.cs diff --git a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs index 503711999..d09cf5a63 100644 --- a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs @@ -138,4 +138,47 @@ public void ChangesFromJsonPatch_ReplaceComplexFormWithNewComplexFormIdMakesRepl setComplexFormComponentChange.ComplexFormEntryId.Should().Be(newComplexFormId); setComplexFormComponentChange.ComponentSenseId.Should().BeNull(); } + + [Fact] + public void ChangesFromJsonPatch_AddComplexFormTypeMakesAddComplexFormTypeChange() + { + var patch = new JsonPatchDocument(); + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; + patch.Add(entry => entry.ComplexFormTypes, complexFormType); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var addComplexFormTypeChange = + changes.Should().ContainSingle().Which.Should().BeOfType().Subject; + addComplexFormTypeChange.EntityId.Should().Be(_entry.Id); + addComplexFormTypeChange.ComplexFormType.Should().Be(complexFormType); + } + + [Fact] + public void ChangesFromJsonPatch_RemoveComplexFormTypeMakesRemoveComplexFormTypeChange() + { + var patch = new JsonPatchDocument(); + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; + _entry.ComplexFormTypes.Add(complexFormType); + patch.Remove(entry => entry.ComplexFormTypes, 0); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var removeComplexFormTypeChange = changes.Should().ContainSingle().Which.Should() + .BeOfType().Subject; + removeComplexFormTypeChange.EntityId.Should().Be(_entry.Id); + removeComplexFormTypeChange.ComplexFormTypeId.Should().Be(complexFormType.Id); + } + + [Fact] + public void ChangesFromJsonPatch_ReplaceComplexFormTypeMakesReplaceComplexFormTypeChange() + { + var patch = new JsonPatchDocument(); + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; + _entry.ComplexFormTypes.Add(complexFormType); + var newComplexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; + patch.Replace(entry => entry.ComplexFormTypes, newComplexFormType, 0); + var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var replaceComplexFormTypeChange = changes.Should().ContainSingle().Which.Should() + .BeOfType().Subject; + replaceComplexFormTypeChange.EntityId.Should().Be(_entry.Id); + replaceComplexFormTypeChange.OldComplexFormTypeId.Should().Be(complexFormType.Id); + replaceComplexFormTypeChange.NewComplexFormType.Should().Be(newComplexFormType); + } } diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs index 848956e16..6ee80a3ca 100644 --- a/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/Entries/RemoveComplexFormTypeChange.cs @@ -3,12 +3,12 @@ namespace LcmCrdt.Changes.Entries; -public class RemoveComplexFormTypeChange(Guid entityId, Guid complexFormId) : EditChange(entityId), ISelfNamedType +public class RemoveComplexFormTypeChange(Guid entityId, Guid complexFormTypeId) : EditChange(entityId), ISelfNamedType { - public Guid ComplexFormId { get; } = complexFormId; + public Guid ComplexFormTypeId { get; } = complexFormTypeId; public override ValueTask ApplyChange(Entry entity, ChangeContext context) { - entity.ComplexFormTypes = entity.ComplexFormTypes.Where(t => t.Id != ComplexFormId).ToList(); + entity.ComplexFormTypes = entity.ComplexFormTypes.Where(t => t.Id != ComplexFormTypeId).ToList(); return ValueTask.CompletedTask; } } diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/ReplaceComplexFormTypeChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/ReplaceComplexFormTypeChange.cs new file mode 100644 index 000000000..a26b39e6f --- /dev/null +++ b/backend/FwLite/LcmCrdt/Changes/Entries/ReplaceComplexFormTypeChange.cs @@ -0,0 +1,21 @@ +using MiniLcm.Models; +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Changes.Entries; + +public class ReplaceComplexFormTypeChange(Guid entityId, ComplexFormType newComplexFormType, Guid oldComplexFormTypeId) : EditChange(entityId), ISelfNamedType +{ + public ComplexFormType NewComplexFormType { get; } = newComplexFormType; + public Guid OldComplexFormTypeId { get; } = oldComplexFormTypeId; + + public override ValueTask ApplyChange(Entry entity, ChangeContext context) + { + entity.ComplexFormTypes = + [ + ..entity.ComplexFormTypes.Where(t => t.Id != OldComplexFormTypeId), + NewComplexFormType + ]; + return ValueTask.CompletedTask; + } +} diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index 5137d7b9b..99e53d72c 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -141,6 +141,34 @@ IChange RewriteComplexFormComponents(IList components, Com yield return rewriteChange; } + foreach (var rewriteChange in patch.RewriteChanges( + s => s.ComplexFormTypes, + (complexFormType, index, operationType) => + { + if (operationType == OperationType.Add) + { + ArgumentNullException.ThrowIfNull(complexFormType); + return new AddComplexFormTypeChange(entry.Id, complexFormType); + } + + if (operationType == OperationType.Remove) + { + complexFormType ??= entry.ComplexFormTypes[index]; + return new RemoveComplexFormTypeChange(entry.Id, complexFormType.Id); + } + + if (operationType == OperationType.Replace) + { + ArgumentNullException.ThrowIfNull(complexFormType); + var currentComplexFormType = entry.ComplexFormTypes[index]; + return new ReplaceComplexFormTypeChange(entry.Id, complexFormType, currentComplexFormType.Id); + } + throw new NotSupportedException($"operation {operationType} not supported for complex form types"); + })) + { + yield return rewriteChange; + } + if (patch.Operations.Count > 0) From 73dca2f02a03c5226bec15434a3f662e595d855c Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 4 Oct 2024 10:05:16 +0700 Subject: [PATCH 16/20] define custom mapping for MiniLcm.Models.Entry.Senses so that linq2db can translate that into a join/sub query for filtering --- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index f74665a06..82ca1a843 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Linq.Expressions; +using System.Text.Json; using SIL.Harmony; using SIL.Harmony.Core; using SIL.Harmony.Changes; @@ -54,6 +55,11 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio .HasAttribute(new ColumnAttribute(nameof(HybridDateTime.Counter), nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter))) .Entity().Property(e => e.Id) + .Property(e => e.Senses) + .HasAttribute(new ExpressionMethodAttribute(SensesExpression())).IsNotColumn() + .Entity() + .Property(s => s.ExampleSentences) + .HasAttribute(new ExpressionMethodAttribute(ExampleSentencesExpression())).IsNotColumn() .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -64,6 +70,15 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio }); } + private static Expression>> SensesExpression() + { + return (entry, context) => context.GetTable().Where(s => s.EntryId == entry.Id); + } + private static Expression>> ExampleSentencesExpression() + { + return (sense, context) => context.GetTable().Where(e => e.SenseId == sense.Id); + } + public static void ConfigureCrdt(CrdtConfig config) { config.EnableProjectedTables = true; From 0b25746c5fe09ed72dc3dea0c6723f9eba0713a0 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 4 Oct 2024 17:20:52 +0700 Subject: [PATCH 17/20] update code to better handle filtering on senses --- .../Api/UpdateProxy/UpdateEntryProxy.cs | 60 ++++++++++--------- .../FwLite/LcmCrdt.Tests/LexboxApiTests.cs | 4 +- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 40 +++++-------- backend/FwLite/LcmCrdt/Json.cs | 6 +- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 16 +++-- backend/FwLite/LcmCrdt/Objects/Entry.cs | 7 +++ backend/FwLite/LcmCrdt/Objects/Sense.cs | 4 ++ backend/FwLite/MiniLcm/Models/Sense.cs | 3 + 8 files changed, 76 insertions(+), 64 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index f96d149a6..dc9c67dad 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -4,29 +4,33 @@ namespace FwDataMiniLcmBridge.Api.UpdateProxy; -public class UpdateEntryProxy(ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi) : Entry +public class UpdateEntryProxy : Entry { - public override Guid Id + private readonly ILexEntry _lcmEntry; + private readonly FwDataMiniLcmApi _lexboxLcmApi; + + public UpdateEntryProxy(ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi) { - get => lcmEntry.Guid; - set => throw new NotImplementedException(); + _lcmEntry = lcmEntry; + Id = lcmEntry.Guid; + _lexboxLcmApi = lexboxLcmApi; } public override MultiString LexemeForm { - get => new UpdateMultiStringProxy(lcmEntry.LexemeFormOA.Form, lexboxLcmApi); + get => new UpdateMultiStringProxy(_lcmEntry.LexemeFormOA.Form, _lexboxLcmApi); set => throw new NotImplementedException(); } public override MultiString CitationForm { - get => new UpdateMultiStringProxy(lcmEntry.CitationForm, lexboxLcmApi); + get => new UpdateMultiStringProxy(_lcmEntry.CitationForm, _lexboxLcmApi); set => throw new NotImplementedException(); } public override MultiString LiteralMeaning { - get => new UpdateMultiStringProxy(lcmEntry.LiteralMeaning, lexboxLcmApi); + get => new UpdateMultiStringProxy(_lcmEntry.LiteralMeaning, _lexboxLcmApi); set => throw new NotImplementedException(); } @@ -34,10 +38,10 @@ public override IList Senses { get => new UpdateListProxy( - sense => lexboxLcmApi.CreateSense(lcmEntry, sense), - sense => lexboxLcmApi.DeleteSense(Id, sense.Id), - i => new UpdateSenseProxy(lcmEntry.SensesOS[i], lexboxLcmApi), - lcmEntry.SensesOS.Count + sense => _lexboxLcmApi.CreateSense(_lcmEntry, sense), + sense => _lexboxLcmApi.DeleteSense(Id, sense.Id), + i => new UpdateSenseProxy(_lcmEntry.SensesOS[i], _lexboxLcmApi), + _lcmEntry.SensesOS.Count ); set => throw new NotImplementedException(); } @@ -46,12 +50,12 @@ public override IList Components { get => new UpdateListProxy( - component => lexboxLcmApi.AddComplexFormComponent(lcmEntry, component), - component => lexboxLcmApi.RemoveComplexFormComponent(lcmEntry, component), - i => new UpdateComplexFormComponentProxy(lcmEntry, - lcmEntry.ComplexFormEntryRefs.Single().ComponentLexemesRS[i], - lexboxLcmApi), - lcmEntry.ComplexFormEntryRefs.SingleOrDefault()?.ComponentLexemesRS.Count ?? 0 + component => _lexboxLcmApi.AddComplexFormComponent(_lcmEntry, component), + component => _lexboxLcmApi.RemoveComplexFormComponent(_lcmEntry, component), + i => new UpdateComplexFormComponentProxy(_lcmEntry, + _lcmEntry.ComplexFormEntryRefs.Single().ComponentLexemesRS[i], + _lexboxLcmApi), + _lcmEntry.ComplexFormEntryRefs.SingleOrDefault()?.ComponentLexemesRS.Count ?? 0 ); set => throw new NotImplementedException(); } @@ -60,13 +64,13 @@ public override IList ComplexForms { get => new UpdateListProxy( - component => lexboxLcmApi.AddComplexFormComponent(lexboxLcmApi.EntriesRepository.GetObject(component.ComplexFormEntryId), component), - component => lexboxLcmApi.RemoveComplexFormComponent(lexboxLcmApi.EntriesRepository.GetObject(component.ComplexFormEntryId), component), + component => _lexboxLcmApi.AddComplexFormComponent(_lexboxLcmApi.EntriesRepository.GetObject(component.ComplexFormEntryId), component), + component => _lexboxLcmApi.RemoveComplexFormComponent(_lexboxLcmApi.EntriesRepository.GetObject(component.ComplexFormEntryId), component), //todo this does not handle complex forms which reference a sense - i => new UpdateComplexFormComponentProxy(lcmEntry.ComplexFormEntries.ElementAt(i), - lcmEntry, - lexboxLcmApi), - lcmEntry.ComplexFormEntries.Count() + i => new UpdateComplexFormComponentProxy(_lcmEntry.ComplexFormEntries.ElementAt(i), + _lcmEntry, + _lexboxLcmApi), + _lcmEntry.ComplexFormEntries.Count() ); set => throw new NotImplementedException(); } @@ -75,10 +79,10 @@ public override IList ComplexFormTypes { get => new UpdateListProxy( - complexFormType => lexboxLcmApi.AddComplexFormType(lcmEntry, complexFormType.Id), - complexFormType => lexboxLcmApi.RemoveComplexFormType(lcmEntry, complexFormType.Id), - i => new UpdateComplexFormTypeProxy(lcmEntry.ComplexFormEntryRefs.Single().ComplexEntryTypesRS[i], lcmEntry, lexboxLcmApi), - lcmEntry.ComplexFormEntryRefs.SingleOrDefault() + complexFormType => _lexboxLcmApi.AddComplexFormType(_lcmEntry, complexFormType.Id), + complexFormType => _lexboxLcmApi.RemoveComplexFormType(_lcmEntry, complexFormType.Id), + i => new UpdateComplexFormTypeProxy(_lcmEntry.ComplexFormEntryRefs.Single().ComplexEntryTypesRS[i], _lcmEntry, _lexboxLcmApi), + _lcmEntry.ComplexFormEntryRefs.SingleOrDefault() ?.ComplexEntryTypesRS.Count ?? 0 ); set => throw new NotImplementedException(); @@ -86,7 +90,7 @@ public override IList ComplexFormTypes public override MultiString Note { - get => new UpdateMultiStringProxy(lcmEntry.Comment, lexboxLcmApi); + get => new UpdateMultiStringProxy(_lcmEntry.Comment, _lexboxLcmApi); set => throw new NotImplementedException(); } } diff --git a/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs b/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs index b31682a44..d7c26bc4b 100644 --- a/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs @@ -155,6 +155,7 @@ await _api.CreateEntry(new() } ], }); + _crdtDbContext.ChangeTracker.Clear(); } public async Task DisposeAsync() @@ -198,7 +199,8 @@ public async Task GetEntries() entries.Should().NotBeEmpty(); var entry1 = entries.First(e => e.Id == _entry1Id); entry1.LexemeForm.Values.Should().NotBeEmpty(); - entry1.Senses.Should().NotBeEmpty(); + var sense1 = entry1.Senses.Should().NotBeEmpty().And.Subject.First(); + sense1.ExampleSentences.Should().NotBeEmpty(); var entry2 = entries.First(e => e.Id == _entry2Id); entry2.LexemeForm.Values.Should().NotBeEmpty(); diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 29cdcbce9..d7de25883 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -153,39 +153,34 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexF var sortWs = (await GetWritingSystem(options.Order.WritingSystem, WritingSystemType.Vernacular))?.WsId; if (sortWs is null) throw new NullReferenceException($"sort writing system {options.Order.WritingSystem} not found"); - queryable = queryable.OrderBy(e => e.Headword(sortWs.Value)) + queryable = queryable + .OrderBy(e => e.Headword(sortWs.Value)) // .ThenBy(e => e.Id) .Skip(options.Offset) .Take(options.Count); - var entries = await queryable.ToArrayAsyncLinqToDB(); - await LoadSenses(entries); + var entries = await queryable + .LoadWith(e => e.Senses) + .ToArrayAsyncLinqToDB(); + await LoadExamples(entries); await LoadComplexFormData(entries); return entries; } - private async Task LoadSenses(Entry[] entries) + private async Task LoadExamples(Entry[] entries) { - var allSenses = (await Senses - .Where(s => entries.Select(e => e.Id).Contains(s.EntryId)) - .ToArrayAsyncEF()) - .ToLookup(s => s.EntryId) - .ToDictionary(g => g.Key, g => g.ToArray()); - var allSenseIds = allSenses.Values.SelectMany(s => s, (_, sense) => sense.Id); + var allSenses = entries.SelectMany(e => e.Senses).ToArray(); + var allSenseIds = allSenses.Select(s => s.Id); var allExampleSentences = (await ExampleSentences .Where(e => allSenseIds.Contains(e.SenseId)) .ToArrayAsyncEF()) .ToLookup(s => s.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); - foreach (var entry in entries) + foreach (var sense in allSenses) { - entry.Senses = allSenses.TryGetValue(entry.Id, out var senses) ? senses.ToArray() : []; - foreach (var sense in entry.Senses) - { - sense.ExampleSentences = allExampleSentences.TryGetValue(sense.Id, out var sentences) - ? sentences.ToArray() - : []; - } + sense.ExampleSentences = allExampleSentences.TryGetValue(sense.Id, out var sentences) + ? sentences.ToArray() + : []; } } @@ -205,20 +200,17 @@ private async Task LoadComplexFormData(Entry[] entries) public async Task GetEntry(Guid id) { - var entry = await Entries.SingleOrDefaultAsync(e => e.Id == id); + var entry = await Entries.LoadWith(e => e.Senses).SingleOrDefaultAsyncLinqToDB(e => e.Id == id); if (entry is null) return null; - var senses = await Senses - .Where(s => s.EntryId == id).ToArrayAsyncLinqToDB(); var exampleSentences = (await ExampleSentences - .Where(e => senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncEF()) + .Where(e => entry.Senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncEF()) .ToLookup(e => e.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); - entry.Senses = senses; //could optimize this by doing a single query, but this is easier to read entry.Components = [..await ComplexFormComponents.Where(c => c.ComplexFormEntryId == id).ToListAsyncEF()]; entry.ComplexForms = [..await ComplexFormComponents.Where(c => c.ComponentEntryId == id).ToListAsyncEF()]; - foreach (var sense in senses) + foreach (var sense in entry.Senses) { sense.ExampleSentences = exampleSentences.TryGetValue(sense.Id, out var sentences) ? sentences.ToArray() : []; } diff --git a/backend/FwLite/LcmCrdt/Json.cs b/backend/FwLite/LcmCrdt/Json.cs index 16b1a08fa..95d49bd6f 100644 --- a/backend/FwLite/LcmCrdt/Json.cs +++ b/backend/FwLite/LcmCrdt/Json.cs @@ -45,7 +45,7 @@ public void Build(Sql.ISqExtensionBuilder builder) if (returnType != typeof(string)) { - valueExpression = PseudoFunctions.MakeConvert(new SqlDataType(new DbDataType(returnType)), + valueExpression = PseudoFunctions.MakeTryConvert(new SqlDataType(new DbDataType(returnType)), new SqlDataType(new DbDataType(typeof(string), DataType.Text)), valueExpression); } @@ -68,7 +68,9 @@ private static void BuildParameterPath(Expression? pathBody, case MethodCallExpression mce: if (IsIndexerPropertyMethod(mce.Method)) { - parameters.Insert(0, builder.ConvertExpressionToSql(mce.Arguments[0])); + var sql = builder.ConvertExpressionToSql(mce.Arguments[0]); + ArgumentNullException.ThrowIfNull(sql); + parameters.Insert(0, sql); pathBody = mce.Object; } else diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 82ca1a843..d64afa13a 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -54,12 +54,10 @@ 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().Property(e => e.Id) + .Entity() .Property(e => e.Senses) - .HasAttribute(new ExpressionMethodAttribute(SensesExpression())).IsNotColumn() - .Entity() - .Property(s => s.ExampleSentences) - .HasAttribute(new ExpressionMethodAttribute(ExampleSentencesExpression())).IsNotColumn() + //tells linq2db how to translate `e.Senses` into a join when it sees it + .HasAttribute(new AssociationAttribute(){ QueryExpression = SensesExpression() }) .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -70,13 +68,13 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio }); } - private static Expression>> SensesExpression() + private static Expression>> SensesExpression() { - return (entry, context) => context.GetTable().Where(s => s.EntryId == entry.Id); + return (entry, context) => context.GetTable().Where(s => s.EntryId == entry.Id).Cast(); } - private static Expression>> ExampleSentencesExpression() + private static Expression>> ExampleSentencesExpression() { - return (sense, context) => context.GetTable().Where(e => e.SenseId == sense.Id); + return (sense, context) => context.GetTable().Where(e => e.SenseId == sense.Id).Cast(); } public static void ConfigureCrdt(CrdtConfig config) diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index 99e53d72c..ffc2fde4a 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -22,6 +22,13 @@ Guid IObjectBase.Id public DateTimeOffset? DeletedAt { get; set; } + //used to avoid a bug in linq2db where it doesn't recognize the association due it this property being defined in the base class + public override IList Senses + { + get => base.Senses; + set => base.Senses = value; + } + [ExpressionMethod(nameof(HeadwordExpression))] public string Headword(WritingSystemId ws) { diff --git a/backend/FwLite/LcmCrdt/Objects/Sense.cs b/backend/FwLite/LcmCrdt/Objects/Sense.cs index 899865181..beb5594e0 100644 --- a/backend/FwLite/LcmCrdt/Objects/Sense.cs +++ b/backend/FwLite/LcmCrdt/Objects/Sense.cs @@ -9,6 +9,10 @@ namespace LcmCrdt.Objects; +/// +/// Contains a definition for an entry +/// This is a CRDT object +/// public class Sense : MiniLcm.Models.Sense, IObjectBase { public static Sense FromMiniLcm(MiniLcm.Models.Sense sense, Guid entryId) diff --git a/backend/FwLite/MiniLcm/Models/Sense.cs b/backend/FwLite/MiniLcm/Models/Sense.cs index f8fd22d22..5497c8c0d 100644 --- a/backend/FwLite/MiniLcm/Models/Sense.cs +++ b/backend/FwLite/MiniLcm/Models/Sense.cs @@ -1,5 +1,8 @@ namespace MiniLcm.Models; +/// +/// Contains a definition for an entry +/// public class Sense : IObjectWithId { public virtual Guid Id { get; set; } From 58cbdcacc8b23237b45d4e220a0b3a8e27787cdd Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 7 Oct 2024 09:29:08 +0700 Subject: [PATCH 18/20] revert changes to Entry.Senses --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 32 ++++++++++++++++-------- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 16 +++--------- backend/FwLite/LcmCrdt/Objects/Entry.cs | 20 +++++++++++---- backend/FwLite/MiniLcm/Models/Entry.cs | 2 +- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index d7de25883..098b0a385 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -159,28 +159,35 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexF .Skip(options.Offset) .Take(options.Count); var entries = await queryable - .LoadWith(e => e.Senses) .ToArrayAsyncLinqToDB(); - await LoadExamples(entries); + await LoadSenses(entries); await LoadComplexFormData(entries); return entries; } - private async Task LoadExamples(Entry[] entries) + private async Task LoadSenses(Entry[] entries) { - var allSenses = entries.SelectMany(e => e.Senses).ToArray(); - var allSenseIds = allSenses.Select(s => s.Id); + var allSenses = (await Senses + .Where(s => entries.Select(e => e.Id).Contains(s.EntryId)) + .ToArrayAsyncEF()) + .ToLookup(s => s.EntryId) + .ToDictionary(g => g.Key, g => g.ToArray()); + var allSenseIds = allSenses.Values.SelectMany(s => s, (_, sense) => sense.Id); var allExampleSentences = (await ExampleSentences .Where(e => allSenseIds.Contains(e.SenseId)) .ToArrayAsyncEF()) .ToLookup(s => s.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); - foreach (var sense in allSenses) + foreach (var entry in entries) { - sense.ExampleSentences = allExampleSentences.TryGetValue(sense.Id, out var sentences) - ? sentences.ToArray() - : []; + entry.Senses = allSenses.TryGetValue(entry.Id, out var senses) ? senses.ToArray() : []; + foreach (var sense in entry.Senses) + { + sense.ExampleSentences = allExampleSentences.TryGetValue(sense.Id, out var sentences) + ? sentences.ToArray() + : []; + } } } @@ -200,16 +207,19 @@ private async Task LoadComplexFormData(Entry[] entries) public async Task GetEntry(Guid id) { - var entry = await Entries.LoadWith(e => e.Senses).SingleOrDefaultAsyncLinqToDB(e => e.Id == id); + var entry = await Entries.SingleOrDefaultAsync(e => e.Id == id); if (entry is null) return null; + var senses = await Senses + .Where(s => s.EntryId == id).ToArrayAsyncLinqToDB(); var exampleSentences = (await ExampleSentences - .Where(e => entry.Senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncEF()) + .Where(e => senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncEF()) .ToLookup(e => e.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); //could optimize this by doing a single query, but this is easier to read entry.Components = [..await ComplexFormComponents.Where(c => c.ComplexFormEntryId == id).ToListAsyncEF()]; entry.ComplexForms = [..await ComplexFormComponents.Where(c => c.ComponentEntryId == id).ToListAsyncEF()]; + entry.Senses = senses; foreach (var sense in entry.Senses) { sense.ExampleSentences = exampleSentences.TryGetValue(sense.Id, out var sentences) ? sentences.ToArray() : []; diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index d64afa13a..6ec4fa44b 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -54,10 +54,8 @@ 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() - .Property(e => e.Senses) - //tells linq2db how to translate `e.Senses` into a join when it sees it - .HasAttribute(new AssociationAttribute(){ QueryExpression = SensesExpression() }) + .Entity().Property(e => e.Id) + .Association(e => (e.Senses as IEnumerable)!, e => e.Id, s => s.EntryId) .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -68,21 +66,13 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio }); } - private static Expression>> SensesExpression() - { - return (entry, context) => context.GetTable().Where(s => s.EntryId == entry.Id).Cast(); - } - private static Expression>> ExampleSentencesExpression() - { - return (sense, context) => context.GetTable().Where(e => e.SenseId == sense.Id).Cast(); - } - public static void ConfigureCrdt(CrdtConfig config) { config.EnableProjectedTables = true; config.ObjectTypeListBuilder .Add(builder => { + builder.Ignore(e => e.Senses); builder.HasMany(e => e.Components) .WithOne() .HasPrincipalKey(entry => entry.Id) diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index ffc2fde4a..234656444 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using System.Text.Json.Serialization; using LcmCrdt.Changes; using LcmCrdt.Changes.Entries; using LcmCrdt.Utils; @@ -22,13 +23,22 @@ Guid IObjectBase.Id public DateTimeOffset? DeletedAt { get; set; } - //used to avoid a bug in linq2db where it doesn't recognize the association due it this property being defined in the base class - public override IList Senses + /// + /// This is a bit of a hack, we want to be able to reference senses when running a query, and they must be CrdtSenses + /// however we only want to store the senses in the entry as MiniLcmSenses, so we need to convert them back to CrdtSenses + /// Note, even though this is JsonIgnored, the Senses property in the base class is still serialized + /// + [JsonIgnore] + public new IReadOnlyList Senses { - get => base.Senses; - set => base.Senses = value; + get + { + return [..base.Senses.Select(s => s as Sense ?? Sense.FromMiniLcm(s, Id))]; + } + set { base.Senses = [..value]; } } + [ExpressionMethod(nameof(HeadwordExpression))] public string Headword(WritingSystemId ws) { @@ -70,7 +80,7 @@ public IObjectBase Copy() CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), Note = Note.Copy(), - Senses = [..Senses.Select(s => (s is Sense cs ? (Sense)cs.Copy() : s))], + Senses = [..Senses.Select(s => (Sense)s.Copy())], Components = [ ..Components.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c)) diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 46772f733..847614dbc 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -2,7 +2,7 @@ public class Entry : IObjectWithId { - public virtual Guid Id { get; set; } + public Guid Id { get; set; } public virtual MultiString LexemeForm { get; set; } = new(); From 1b05889d0d7fe4632cf921cd94a54cac791b6c30 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 7 Oct 2024 09:31:32 +0700 Subject: [PATCH 19/20] remove variants field from Entry as it's not ready and is out of scope --- backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 3 +-- backend/FwLite/LcmCrdt/Objects/Entry.cs | 3 +-- backend/FwLite/MiniLcm/Models/Entry.cs | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index b20e44ac3..0bba288ab 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -286,8 +286,7 @@ private Entry FromLexEntry(ILexEntry entry) ComplexFormTypes = ToComplexFormType(entry), Components = ToComplexFormComponents(entry), //todo, this does not include complex forms which reference a sense - ComplexForms = [..entry.ComplexFormEntries.Select(complexEntry => ToEntryReference(entry, complexEntry))], - Variants = ToVariants(entry) + ComplexForms = [..entry.ComplexFormEntries.Select(complexEntry => ToEntryReference(entry, complexEntry))] }; } diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/Entry.cs index 234656444..807c89be0 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/Entry.cs @@ -92,8 +92,7 @@ public IObjectBase Copy() ComplexFormTypes = [ ..ComplexFormTypes.Select(cft => (cft is CrdtComplexFormType ct ? (ComplexFormType)ct.Copy() : cft)) - ], - Variants = Variants + ] }; } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index 847614dbc..2457eecc2 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -22,7 +22,6 @@ public class Entry : IObjectWithId /// public virtual IList ComplexForms { get; set; } = []; public virtual IList ComplexFormTypes { get; set; } = []; - public virtual Variants? Variants { get; set; } public bool MatchesQuery(string query) => LexemeForm.SearchValue(query) From 2652efcfc28f36b04b6c6ae2b150041f1259315c Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 7 Oct 2024 11:51:08 +0700 Subject: [PATCH 20/20] correct some failing tests --- .../FwLiteProjectSync.Tests/SyncTests.cs | 18 ++++++++++++++---- .../FwLiteProjectSync.Tests/UpdateDiffTests.cs | 6 +++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 960b12803..c6d74d239 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -51,9 +51,16 @@ await _fixture.FwDataApi.CreateEntry(new Entry() }); } - public Task DisposeAsync() + public async Task DisposeAsync() { - return Task.CompletedTask; + await foreach (var entry in _fixture.FwDataApi.GetEntries()) + { + await _fixture.FwDataApi.DeleteEntry(entry.Id); + } + await foreach (var entry in _fixture.CrdtApi.GetEntries()) + { + await _fixture.CrdtApi.DeleteEntry(entry.Id); + } } public SyncTests(SyncFixture fixture) @@ -127,8 +134,11 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() 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) - .For(e => e.ComplexForms).Exclude(c => c.Id)); + options => options + .For(e => e.Components).Exclude(c => c.Id) + .For(e => e.Components).Exclude(c => c.ComponentHeadword) + .For(e => e.ComplexForms).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.ComponentHeadword)); } [Fact] diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index 4e01e9e68..6bf34b549 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -20,7 +20,11 @@ public void EntryDiffShouldUpdateAllFields() var entryDiffToUpdate = CrdtFwdataProjectSyncService.EntryDiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(entryDiffToUpdate); entryDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id).Excluding(x => x.Senses)); + previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id) + .Excluding(x => x.Senses) + .Excluding(x => x.Components) + .Excluding(x => x.ComplexForms) + .Excluding(x => x.ComplexFormTypes)); } [Fact]