From 636cc22e602a10552b22960f7f8b482aa6a3e34a Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 13:16:43 +0700 Subject: [PATCH 01/17] Add MorphType enum and MorphTypeData class --- .../FwDataMiniLcmBridge/Api/LcmHelpers.cs | 58 +++++++++++++++++ backend/FwLite/MiniLcm/Models/MorphType.cs | 65 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 backend/FwLite/MiniLcm/Models/MorphType.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 3812af59e3..54c44f50d0 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -83,6 +83,64 @@ internal static bool SearchValue(this ITsMultiString multiString, string value) '\u0640', // Arabic Tatweel ]; + internal static MorphType FromLcmMorphTypeId(Guid? lcmMorphTypeId) + { + return lcmMorphTypeId switch + { + null => MorphType.Unknown, + // Can't switch on Guids since they're not compile-type constants, but thankfully pattern matching works + Guid g when g == MoMorphTypeTags.kguidMorphBoundRoot => MorphType.BoundRoot, + Guid g when g == MoMorphTypeTags.kguidMorphBoundStem => MorphType.BoundStem, + Guid g when g == MoMorphTypeTags.kguidMorphCircumfix => MorphType.Circumfix, + Guid g when g == MoMorphTypeTags.kguidMorphClitic => MorphType.Clitic, + Guid g when g == MoMorphTypeTags.kguidMorphEnclitic => MorphType.Enclitic, + Guid g when g == MoMorphTypeTags.kguidMorphInfix => MorphType.Infix, + Guid g when g == MoMorphTypeTags.kguidMorphParticle => MorphType.Particle, + Guid g when g == MoMorphTypeTags.kguidMorphPrefix => MorphType.Prefix, + Guid g when g == MoMorphTypeTags.kguidMorphProclitic => MorphType.Proclitic, + Guid g when g == MoMorphTypeTags.kguidMorphRoot => MorphType.Root, + Guid g when g == MoMorphTypeTags.kguidMorphSimulfix => MorphType.Simulfix, + Guid g when g == MoMorphTypeTags.kguidMorphStem => MorphType.Stem, + Guid g when g == MoMorphTypeTags.kguidMorphSuffix => MorphType.Suffix, + Guid g when g == MoMorphTypeTags.kguidMorphSuprafix => MorphType.Suprafix, + Guid g when g == MoMorphTypeTags.kguidMorphInfixingInterfix => MorphType.InfixingInterfix, + Guid g when g == MoMorphTypeTags.kguidMorphPrefixingInterfix => MorphType.PrefixingInterfix, + Guid g when g == MoMorphTypeTags.kguidMorphSuffixingInterfix => MorphType.SuffixingInterfix, + Guid g when g == MoMorphTypeTags.kguidMorphPhrase => MorphType.Phrase, + Guid g when g == MoMorphTypeTags.kguidMorphDiscontiguousPhrase => MorphType.DiscontiguousPhrase, + _ => MorphType.Other, + }; + } + + internal static Guid? ToLcmMorphTypeId(MorphType morphType) + { + return morphType switch + { + MorphType.BoundRoot => MoMorphTypeTags.kguidMorphBoundRoot, + MorphType.BoundStem => MoMorphTypeTags.kguidMorphBoundStem, + MorphType.Circumfix => MoMorphTypeTags.kguidMorphCircumfix, + MorphType.Clitic => MoMorphTypeTags.kguidMorphClitic, + MorphType.Enclitic => MoMorphTypeTags.kguidMorphEnclitic, + MorphType.Infix => MoMorphTypeTags.kguidMorphInfix, + MorphType.Particle => MoMorphTypeTags.kguidMorphParticle, + MorphType.Prefix => MoMorphTypeTags.kguidMorphPrefix, + MorphType.Proclitic => MoMorphTypeTags.kguidMorphProclitic, + MorphType.Root => MoMorphTypeTags.kguidMorphRoot, + MorphType.Simulfix => MoMorphTypeTags.kguidMorphSimulfix, + MorphType.Stem => MoMorphTypeTags.kguidMorphStem, + MorphType.Suffix => MoMorphTypeTags.kguidMorphSuffix, + MorphType.Suprafix => MoMorphTypeTags.kguidMorphSuprafix, + MorphType.InfixingInterfix => MoMorphTypeTags.kguidMorphInfixingInterfix, + MorphType.PrefixingInterfix => MoMorphTypeTags.kguidMorphPrefixingInterfix, + MorphType.SuffixingInterfix => MoMorphTypeTags.kguidMorphSuffixingInterfix, + MorphType.Phrase => MoMorphTypeTags.kguidMorphPhrase, + MorphType.DiscontiguousPhrase => MoMorphTypeTags.kguidMorphDiscontiguousPhrase, + MorphType.Unknown => null, + MorphType.Other => null, // Note that this will not round-trip with FromLcmMorphTypeId + _ => null, + }; + } + internal static void ContributeExemplars(ITsMultiString multiString, IReadOnlyDictionary> wsExemplars) { for (var i = 0; i < multiString.StringCount; i++) diff --git a/backend/FwLite/MiniLcm/Models/MorphType.cs b/backend/FwLite/MiniLcm/Models/MorphType.cs new file mode 100644 index 0000000000..0c467b9d1a --- /dev/null +++ b/backend/FwLite/MiniLcm/Models/MorphType.cs @@ -0,0 +1,65 @@ +namespace MiniLcm.Models; + +public enum MorphType +{ + Unknown, + BoundRoot, + BoundStem, + Circumfix, + Clitic, + Enclitic, + Infix, + Particle, + Prefix, + Proclitic, + Root, + Simulfix, + Stem, + Suffix, + Suprafix, + InfixingInterfix, + PrefixingInterfix, + SuffixingInterfix, + Phrase, + DiscontiguousPhrase, + Other, +} + +public class MorphTypeData : IObjectWithId +{ + public Guid Id { get; set; } + public MorphType MorphType { get; set; } + public MultiString Name { get; set; } = []; + public MultiString Abbreviation { get; set; } = []; + public RichMultiString Description { get; set; } = []; + public string LeadingToken { get; set; } = ""; + public string TrailingToken { get; set; } = ""; + public int SecondaryOrder { get; set; } + + public DateTimeOffset? DeletedAt { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + } + + public MorphTypeData Copy() + { + return new MorphTypeData + { + Id = Id, + MorphType = MorphType, + Name = Name.Copy(), + Abbreviation = Abbreviation.Copy(), + Description = Description.Copy(), + LeadingToken = LeadingToken, + TrailingToken = TrailingToken, + SecondaryOrder = SecondaryOrder, + DeletedAt = DeletedAt, + }; + } +} From 09025383719740b69e832ca185ff5ce997125c60 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 14:29:53 +0700 Subject: [PATCH 02/17] Set morph type when ILexEntry is created --- .../Api/FwDataMiniLcmApi.cs | 3 +- .../FwDataMiniLcmBridge/Api/LcmHelpers.cs | 41 ++++++++++++++++--- .../Api/UpdateProxy/UpdateEntryProxy.cs | 8 +++- .../TypeGen/ReinforcedFwLiteTypingConfig.cs | 1 + backend/FwLite/MiniLcm/Models/Entry.cs | 2 + backend/FwLite/MiniLcm/Models/MorphType.cs | 5 ++- .../generated-types/MiniLcm/Models/IEntry.ts | 2 + .../MiniLcm/Models/MorphType.ts | 29 +++++++++++++ 8 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/MorphType.ts diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 93e4d08c21..2a6116cbfb 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -533,6 +533,7 @@ private Entry FromLexEntry(ILexEntry entry) LexemeForm = FromLcmMultiString(entry.LexemeFormOA?.Form), CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), + MorphType = LcmHelpers.FromLcmMorphTypeId(entry.PrimaryMorphType.Id.Guid), // TODO: Decide what to do about entries with *mixed* morph types Senses = entry.AllSenses.Select(FromLexSense).ToList(), ComplexFormTypes = ToComplexFormTypes(entry), Components = ToComplexFormComponents(entry).ToList(), @@ -836,7 +837,7 @@ public async Task CreateEntry(Entry entry) Cache.ServiceLocator.ActionHandler, () => { - var lexEntry = Cache.CreateEntry(entry.Id); + var lexEntry = Cache.CreateEntry(entry.Id, entry.MorphType); UpdateLcmMultiString(lexEntry.LexemeFormOA.Form, entry.LexemeForm); UpdateLcmMultiString(lexEntry.CitationForm, entry.CitationForm); UpdateLcmMultiString(lexEntry.LiteralMeaning, entry.LiteralMeaning); diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 54c44f50d0..5a892f317b 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -206,18 +206,49 @@ internal static int GetWritingSystemHandle(this LcmCache cache, WritingSystemId return multiString.get_String(wsHandle)?.Text ?? null; } - internal static IMoStemAllomorph CreateLexemeForm(this LcmCache cache) + internal static IMoForm CreateLexemeForm(this LcmCache cache, MorphType morphType) { - return cache.ServiceLocator.GetInstance().Create(); + return morphType switch + { + // Stems, roots, particles, clitics, and phrases use the Stem allomorph factory + MorphType.Stem => cache.ServiceLocator.GetInstance().Create(), + MorphType.Root => cache.ServiceLocator.GetInstance().Create(), + MorphType.BoundStem => cache.ServiceLocator.GetInstance().Create(), + MorphType.BoundRoot => cache.ServiceLocator.GetInstance().Create(), + MorphType.Particle => cache.ServiceLocator.GetInstance().Create(), + MorphType.Clitic => cache.ServiceLocator.GetInstance().Create(), + MorphType.Enclitic => cache.ServiceLocator.GetInstance().Create(), + MorphType.Proclitic => cache.ServiceLocator.GetInstance().Create(), + + MorphType.Phrase => cache.ServiceLocator.GetInstance().Create(), + MorphType.DiscontiguousPhrase => cache.ServiceLocator.GetInstance().Create(), + + // Affixes (of all kinds) use the Affix allomorph factory + MorphType.Circumfix => cache.ServiceLocator.GetInstance().Create(), + MorphType.Infix => cache.ServiceLocator.GetInstance().Create(), + MorphType.Prefix => cache.ServiceLocator.GetInstance().Create(), + MorphType.Simulfix => cache.ServiceLocator.GetInstance().Create(), + MorphType.Suffix => cache.ServiceLocator.GetInstance().Create(), + MorphType.Suprafix => cache.ServiceLocator.GetInstance().Create(), + MorphType.InfixingInterfix => cache.ServiceLocator.GetInstance().Create(), + MorphType.PrefixingInterfix => cache.ServiceLocator.GetInstance().Create(), + MorphType.SuffixingInterfix => cache.ServiceLocator.GetInstance().Create(), + + // Default will be stem if we don't know what else to do + MorphType.Unknown => cache.ServiceLocator.GetInstance().Create(), + MorphType.Other => cache.ServiceLocator.GetInstance().Create(), + _ => cache.ServiceLocator.GetInstance().Create(), + }; } - internal static ILexEntry CreateEntry(this LcmCache cache, Guid id) + internal static ILexEntry CreateEntry(this LcmCache cache, Guid id, MorphType morphType) { var lexEntry = cache.ServiceLocator.GetInstance().Create(id, cache.ServiceLocator.GetInstance().Singleton.LexDbOA); - lexEntry.LexemeFormOA = cache.CreateLexemeForm(); + lexEntry.LexemeFormOA = cache.CreateLexemeForm(morphType); //must be done after the IMoForm is set on the LexemeForm property - lexEntry.LexemeFormOA.MorphTypeRA = cache.ServiceLocator.GetInstance().GetObject(MoMorphTypeTags.kguidMorphStem); + var lcmMorphType = ToLcmMorphTypeId(morphType) ?? ToLcmMorphTypeId(MorphType.Stem); + lexEntry.LexemeFormOA.MorphTypeRA = cache.ServiceLocator.GetInstance().GetObject(lcmMorphType!.Value); return lexEntry; } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index 4348f9ab64..b183c160a2 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -21,7 +21,7 @@ public override MultiString LexemeForm { get { - _lcmEntry.LexemeFormOA ??= _lexboxLcmApi.Cache.CreateLexemeForm(); + _lcmEntry.LexemeFormOA ??= _lexboxLcmApi.Cache.CreateLexemeForm(LcmHelpers.FromLcmMorphTypeId(_lcmEntry.PrimaryMorphType.Id.Guid)); return new UpdateMultiStringProxy(_lcmEntry.LexemeFormOA.Form, _lexboxLcmApi); } set => throw new NotImplementedException(); @@ -39,6 +39,12 @@ public override RichMultiString LiteralMeaning set => throw new NotImplementedException(); } + public override MorphType MorphType + { + get => throw new NotImplementedException(); + set => Console.WriteLine("setting MorphType not implemented"); // Not throwing, for now + } + public override List Senses { get => throw new NotImplementedException(); diff --git a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs index 236492eb6c..f72b41f7ee 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs @@ -130,6 +130,7 @@ private static void ConfigureFwLiteSharedTypes(ConfigurationBuilder builder) builder.ExportAsEnum(); builder.ExportAsEnum().UseString(); builder.ExportAsEnum().UseString(); + builder.ExportAsEnum().UseString(); builder.ExportAsEnum().UseString(); builder.ExportAsEnum().UseString(); var serviceTypes = Enum.GetValues() diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index e31e51d6d7..ed34519da4 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -10,6 +10,7 @@ public record Entry : IObjectWithId public virtual MultiString CitationForm { get; set; } = new(); public virtual RichMultiString LiteralMeaning { get; set; } = new(); + public virtual MorphType MorphType { get; set; } public virtual List Senses { get; set; } = []; public virtual RichMultiString Note { get; set; } = new(); @@ -49,6 +50,7 @@ public Entry Copy() CitationForm = CitationForm.Copy(), LiteralMeaning = LiteralMeaning.Copy(), Note = Note.Copy(), + MorphType = MorphType, Senses = [..Senses.Select(s => s.Copy())], Components = [ diff --git a/backend/FwLite/MiniLcm/Models/MorphType.cs b/backend/FwLite/MiniLcm/Models/MorphType.cs index 0c467b9d1a..a0ea79358f 100644 --- a/backend/FwLite/MiniLcm/Models/MorphType.cs +++ b/backend/FwLite/MiniLcm/Models/MorphType.cs @@ -1,5 +1,8 @@ -namespace MiniLcm.Models; +using System.Text.Json.Serialization; +namespace MiniLcm.Models; + +[JsonConverter(typeof(JsonStringEnumConverter))] public enum MorphType { Unknown, diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IEntry.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IEntry.ts index 1c3fa01e74..51a54d595b 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IEntry.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IEntry.ts @@ -6,6 +6,7 @@ import type {IObjectWithId} from './IObjectWithId'; import type {IMultiString} from '$lib/dotnet-types/i-multi-string'; import type {IRichMultiString} from '$lib/dotnet-types/i-multi-string'; +import type {MorphType} from './MorphType'; import type {ISense} from './ISense'; import type {IComplexFormComponent} from './IComplexFormComponent'; import type {IComplexFormType} from './IComplexFormType'; @@ -18,6 +19,7 @@ export interface IEntry extends IObjectWithId lexemeForm: IMultiString; citationForm: IMultiString; literalMeaning: IRichMultiString; + morphType: MorphType; senses: ISense[]; note: IRichMultiString; components: IComplexFormComponent[]; diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/MorphType.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/MorphType.ts new file mode 100644 index 0000000000..1762db249b --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/MorphType.ts @@ -0,0 +1,29 @@ +/* eslint-disable */ +// This code was generated by a Reinforced.Typings tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. + +export enum MorphType { + Unknown = "Unknown", + BoundRoot = "BoundRoot", + BoundStem = "BoundStem", + Circumfix = "Circumfix", + Clitic = "Clitic", + Enclitic = "Enclitic", + Infix = "Infix", + Particle = "Particle", + Prefix = "Prefix", + Proclitic = "Proclitic", + Root = "Root", + Simulfix = "Simulfix", + Stem = "Stem", + Suffix = "Suffix", + Suprafix = "Suprafix", + InfixingInterfix = "InfixingInterfix", + PrefixingInterfix = "PrefixingInterfix", + SuffixingInterfix = "SuffixingInterfix", + Phrase = "Phrase", + DiscontiguousPhrase = "DiscontiguousPhrase", + Other = "Other" +} +/* eslint-enable */ From 2163a6712a48bd200c7ba3a303ed948916b837d1 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 15:01:08 +0700 Subject: [PATCH 03/17] Add get methods for morph types to read API --- .../Api/FwDataMiniLcmApi.cs | 33 +++++++++++++++++++ backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 10 ++++++ backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 2 ++ 3 files changed, 45 insertions(+) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 2a6116cbfb..278cb3e3fc 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -488,6 +488,39 @@ await Cache.DoUsingNewOrCurrentUOW("Delete Complex Form Type", }); } + public IAsyncEnumerable GetAllMorphTypeData() + { + return + MorphTypeRepository + .AllInstances() + .ToAsyncEnumerable() + .Select(FromLcmMorphType); + + } + + public Task GetMorphTypeData(Guid id) + { + MorphTypeRepository.TryGetObject(id, out var lcmMorphType); + if (lcmMorphType is null) return Task.FromResult(null); + return Task.FromResult(FromLcmMorphType(lcmMorphType)); + } + + internal MorphTypeData FromLcmMorphType(IMoMorphType morphType) + { + return new MorphTypeData + { + Id = morphType.Guid, + MorphType = LcmHelpers.FromLcmMorphTypeId(morphType.Guid), + Name = FromLcmMultiString(morphType.Name), + Abbreviation = FromLcmMultiString(morphType.Abbreviation), + Description = FromLcmMultiString(morphType.Description), + LeadingToken = morphType.Prefix, + TrailingToken = morphType.Postfix, + SecondaryOrder = morphType.SecondaryOrder, + }; + } + + public IAsyncEnumerable GetVariantTypes() { return VariantTypes.PossibilitiesOS diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 277adc8457..5813a62df9 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -337,6 +337,16 @@ public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) await AddChange(new RemoveComplexFormTypeChange(entryId, complexFormTypeId)); } + public IAsyncEnumerable GetAllMorphTypeData() + { + throw new NotImplementedException(); + } + + public Task GetMorphTypeData(Guid id) + { + throw new NotImplementedException(); + } + public async Task CountEntries(string? query = null, FilterQueryOptions? options = null) { await using var repo = await repoFactory.CreateRepoAsync(); diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index c7c895ec1d..5f283c3aca 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs @@ -12,7 +12,9 @@ public interface IMiniLcmReadApi IAsyncEnumerable GetPublications(); IAsyncEnumerable GetSemanticDomains(); IAsyncEnumerable GetComplexFormTypes(); + IAsyncEnumerable GetAllMorphTypeData(); Task GetComplexFormType(Guid id); + Task GetMorphTypeData(Guid id); Task CountEntries(string? query = null, FilterQueryOptions? options = null); IAsyncEnumerable GetEntries(QueryOptions? options = null); IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null); From 3f342bdaf39184ea7c998e0cd6276961f340a22b Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 15:22:58 +0700 Subject: [PATCH 04/17] Add write API methods for MorphTypeData --- .../Api/FwDataMiniLcmApi.cs | 33 ++++++++- .../FwDataMiniLcmBridge/Api/LcmHelpers.cs | 47 ++++++------ .../UpdateProxy/UpdateMorphTypeDataProxy.cs | 53 ++++++++++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 9 +++ backend/FwLite/MiniLcm/Models/MorphType.cs | 16 ++--- .../FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs | 16 +++++ .../MiniLcm/SyncHelpers/MorphTypeDataSync.cs | 71 +++++++++++++++++++ 7 files changed, 209 insertions(+), 36 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs create mode 100644 backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs create mode 100644 backend/FwLite/MiniLcm/SyncHelpers/MorphTypeDataSync.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 278cb3e3fc..dbb8aecd97 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -495,7 +495,6 @@ public IAsyncEnumerable GetAllMorphTypeData() .AllInstances() .ToAsyncEnumerable() .Select(FromLcmMorphType); - } public Task GetMorphTypeData(Guid id) @@ -520,6 +519,38 @@ internal MorphTypeData FromLcmMorphType(IMoMorphType morphType) }; } + public Task CreateMorphTypeData(MorphTypeData morphTypeData) + { + // Creating new morph types not allowed in FwData projects, so silently ignore operation + return Task.FromResult(morphTypeData); + } + + public Task UpdateMorphTypeData(Guid id, UpdateObjectInput update) + { + var lcmMorphType = MorphTypeRepository.GetObject(id); + if (lcmMorphType is null) throw new NullReferenceException($"unable to find morph type with id {id}"); + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Morph Type", + "Revert Morph Type", + Cache.ServiceLocator.ActionHandler, + () => + { + var updateProxy = new UpdateMorphTypeDataProxy(lcmMorphType, this); + update.Apply(updateProxy); + }); + return Task.FromResult(FromLcmMorphType(lcmMorphType)); + } + + public async Task UpdateMorphTypeData(MorphTypeData before, MorphTypeData after, IMiniLcmApi? api = null) + { + await MorphTypeDataSync.Sync(before, after, api ?? this); + return await GetMorphTypeData(after.Id) ?? throw new NullReferenceException("unable to find morph type with id " + after.Id); + } + + public Task DeleteMorphTypeData(Guid id) + { + // Deleting morph types not allowed in FwData projects, so silently ignore operation + return Task.CompletedTask; + } public IAsyncEnumerable GetVariantTypes() { diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 5a892f317b..f6d8fdcb43 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -207,37 +207,30 @@ internal static int GetWritingSystemHandle(this LcmCache cache, WritingSystemId } internal static IMoForm CreateLexemeForm(this LcmCache cache, MorphType morphType) + { + return + IsAffixMorphType(morphType) + ? cache.ServiceLocator.GetInstance().Create() + : cache.ServiceLocator.GetInstance().Create(); + } + + internal static bool IsAffixMorphType(MorphType morphType) { return morphType switch { - // Stems, roots, particles, clitics, and phrases use the Stem allomorph factory - MorphType.Stem => cache.ServiceLocator.GetInstance().Create(), - MorphType.Root => cache.ServiceLocator.GetInstance().Create(), - MorphType.BoundStem => cache.ServiceLocator.GetInstance().Create(), - MorphType.BoundRoot => cache.ServiceLocator.GetInstance().Create(), - MorphType.Particle => cache.ServiceLocator.GetInstance().Create(), - MorphType.Clitic => cache.ServiceLocator.GetInstance().Create(), - MorphType.Enclitic => cache.ServiceLocator.GetInstance().Create(), - MorphType.Proclitic => cache.ServiceLocator.GetInstance().Create(), - - MorphType.Phrase => cache.ServiceLocator.GetInstance().Create(), - MorphType.DiscontiguousPhrase => cache.ServiceLocator.GetInstance().Create(), - - // Affixes (of all kinds) use the Affix allomorph factory - MorphType.Circumfix => cache.ServiceLocator.GetInstance().Create(), - MorphType.Infix => cache.ServiceLocator.GetInstance().Create(), - MorphType.Prefix => cache.ServiceLocator.GetInstance().Create(), - MorphType.Simulfix => cache.ServiceLocator.GetInstance().Create(), - MorphType.Suffix => cache.ServiceLocator.GetInstance().Create(), - MorphType.Suprafix => cache.ServiceLocator.GetInstance().Create(), - MorphType.InfixingInterfix => cache.ServiceLocator.GetInstance().Create(), - MorphType.PrefixingInterfix => cache.ServiceLocator.GetInstance().Create(), - MorphType.SuffixingInterfix => cache.ServiceLocator.GetInstance().Create(), + // Affixes of all types should use the Affix morph type factory + MorphType.Circumfix => true, + MorphType.Infix => true, + MorphType.Prefix => true, + MorphType.Simulfix => true, + MorphType.Suffix => true, + MorphType.Suprafix => true, + MorphType.InfixingInterfix => true, + MorphType.PrefixingInterfix => true, + MorphType.SuffixingInterfix => true, - // Default will be stem if we don't know what else to do - MorphType.Unknown => cache.ServiceLocator.GetInstance().Create(), - MorphType.Other => cache.ServiceLocator.GetInstance().Create(), - _ => cache.ServiceLocator.GetInstance().Create(), + // Everything else should use the Stem morph type factory + _ => false, }; } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs new file mode 100644 index 0000000000..e28c4d6d3a --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs @@ -0,0 +1,53 @@ +using MiniLcm.Models; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateMorphTypeDataProxy : MorphTypeData +{ + private readonly IMoMorphType _lcmMorphType; + private readonly FwDataMiniLcmApi _lexboxLcmApi; + + public UpdateMorphTypeDataProxy(IMoMorphType lcmMorphType, FwDataMiniLcmApi lexboxLcmApi) + { + _lcmMorphType = lcmMorphType; + Id = lcmMorphType.Guid; + _lexboxLcmApi = lexboxLcmApi; + } + + public override MultiString Name + { + get => new UpdateMultiStringProxy(_lcmMorphType.Name, _lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override MultiString Abbreviation + { + get => new UpdateMultiStringProxy(_lcmMorphType.Abbreviation, _lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override RichMultiString Description + { + get => new UpdateRichMultiStringProxy(_lcmMorphType.Description, _lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override string LeadingToken + { + get => _lcmMorphType.Prefix; + set => throw new NotImplementedException(); + } + + public override string TrailingToken + { + get => _lcmMorphType.Postfix; + set => throw new NotImplementedException(); + } + + public override int SecondaryOrder + { + get => _lcmMorphType.SecondaryOrder; + set => throw new NotImplementedException(); + } +} diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 05688d706e..5b9f4edefc 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -35,10 +35,19 @@ Task UpdateWritingSystem(WritingSystemId id, Task DeleteSemanticDomain(Guid id); #endregion + #region ComplexFormType Task CreateComplexFormType(ComplexFormType complexFormType); Task UpdateComplexFormType(Guid id, UpdateObjectInput update); Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after, IMiniLcmApi? api = null); Task DeleteComplexFormType(Guid id); + #endregion + + #region MorphType + Task CreateMorphTypeData(MorphTypeData morphType); + Task UpdateMorphTypeData(Guid id, UpdateObjectInput update); + Task UpdateMorphTypeData(MorphTypeData before, MorphTypeData after, IMiniLcmApi? api = null); + Task DeleteMorphTypeData(Guid id); + #endregion #region Entry Task CreateEntry(Entry entry); diff --git a/backend/FwLite/MiniLcm/Models/MorphType.cs b/backend/FwLite/MiniLcm/Models/MorphType.cs index a0ea79358f..acc54bba9e 100644 --- a/backend/FwLite/MiniLcm/Models/MorphType.cs +++ b/backend/FwLite/MiniLcm/Models/MorphType.cs @@ -30,14 +30,14 @@ public enum MorphType public class MorphTypeData : IObjectWithId { - public Guid Id { get; set; } - public MorphType MorphType { get; set; } - public MultiString Name { get; set; } = []; - public MultiString Abbreviation { get; set; } = []; - public RichMultiString Description { get; set; } = []; - public string LeadingToken { get; set; } = ""; - public string TrailingToken { get; set; } = ""; - public int SecondaryOrder { get; set; } + public virtual Guid Id { get; set; } + public virtual MorphType MorphType { get; set; } + public virtual MultiString Name { get; set; } = []; + public virtual MultiString Abbreviation { get; set; } = []; + public virtual RichMultiString Description { get; set; } = []; + public virtual string LeadingToken { get; set; } = ""; + public virtual string TrailingToken { get; set; } = ""; + public virtual int SecondaryOrder { get; set; } public DateTimeOffset? DeletedAt { get; set; } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs b/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs new file mode 100644 index 0000000000..dcd2c4b027 --- /dev/null +++ b/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs @@ -0,0 +1,16 @@ +using SystemTextJsonPatch.Operations; + +namespace MiniLcm.SyncHelpers; + +public static class IntegerDiff +{ + public static IEnumerable> GetIntegerDiff(string path, + int? before, + int? after) where T : class + { + if (before == after) yield break; + if (after is null) yield return new Operation("remove", $"/{path}", null); + else if (before is null) yield return new Operation("add", $"/{path}", null); + else yield return new Operation("replace", $"/{path}", null, after); + } +} diff --git a/backend/FwLite/MiniLcm/SyncHelpers/MorphTypeDataSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/MorphTypeDataSync.cs new file mode 100644 index 0000000000..88ab96d88e --- /dev/null +++ b/backend/FwLite/MiniLcm/SyncHelpers/MorphTypeDataSync.cs @@ -0,0 +1,71 @@ +using MiniLcm.Models; +using SystemTextJsonPatch; + +namespace MiniLcm.SyncHelpers; + +public static class MorphTypeDataSync +{ + public static async Task Sync(MorphTypeData[] beforeMorphTypes, + MorphTypeData[] afterMorphTypes, + IMiniLcmApi api) + { + return await DiffCollection.Diff( + beforeMorphTypes, + afterMorphTypes, + new MorphTypeDataDiffApi(api)); + } + + public static async Task Sync(MorphTypeData before, + MorphTypeData after, + IMiniLcmApi api) + { + var updateObjectInput = MorphTypeDataDiffToUpdate(before, after); + if (updateObjectInput is not null) await api.UpdateMorphTypeData(after.Id, updateObjectInput); + return updateObjectInput is null ? 0 : 1; + } + + public static UpdateObjectInput? MorphTypeDataDiffToUpdate(MorphTypeData beforeMorphTypeData, MorphTypeData afterMorphTypeData) + { + JsonPatchDocument patchDocument = new(); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(MorphTypeData.Name), + beforeMorphTypeData.Name, + afterMorphTypeData.Name)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(MorphTypeData.Abbreviation), + beforeMorphTypeData.Abbreviation, + afterMorphTypeData.Abbreviation)); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(MorphTypeData.Description), + beforeMorphTypeData.Description, + afterMorphTypeData.Description)); + patchDocument.Operations.AddRange(SimpleStringDiff.GetStringDiff(nameof(MorphTypeData.LeadingToken), + beforeMorphTypeData.LeadingToken, + afterMorphTypeData.LeadingToken)); + patchDocument.Operations.AddRange(SimpleStringDiff.GetStringDiff(nameof(MorphTypeData.TrailingToken), + beforeMorphTypeData.TrailingToken, + afterMorphTypeData.TrailingToken)); + patchDocument.Operations.AddRange(IntegerDiff.GetIntegerDiff(nameof(MorphTypeData.SecondaryOrder), + beforeMorphTypeData.SecondaryOrder, + afterMorphTypeData.SecondaryOrder)); + if (patchDocument.Operations.Count == 0) return null; + return new UpdateObjectInput(patchDocument); + } + + private class MorphTypeDataDiffApi(IMiniLcmApi api) : ObjectWithIdCollectionDiffApi + { + public override async Task Add(MorphTypeData currentMorphType) + { + await api.CreateMorphTypeData(currentMorphType); + return 1; + } + + public override async Task Remove(MorphTypeData beforeMorphType) + { + await api.DeleteMorphTypeData(beforeMorphType.Id); + return 1; + } + + public override Task Replace(MorphTypeData beforeMorphType, MorphTypeData afterMorphType) + { + return Sync(beforeMorphType, afterMorphType, api); + } + } +} From e4847c0f7bebf420260d745fb37a9812fbfc091d Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 16:01:32 +0700 Subject: [PATCH 05/17] CrdtMiniLcmApi methods for morph types should throw --- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 5813a62df9..07f828729a 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -347,6 +347,26 @@ public IAsyncEnumerable GetAllMorphTypeData() throw new NotImplementedException(); } + public Task CreateMorphTypeData(MorphTypeData morphTypeData) + { + throw new NotImplementedException(); + } + + public Task UpdateMorphTypeData(Guid id, UpdateObjectInput update) + { + throw new NotImplementedException(); + } + + public Task UpdateMorphTypeData(MorphTypeData before, MorphTypeData after, IMiniLcmApi? api = null) + { + throw new NotImplementedException(); + } + + public Task DeleteMorphTypeData(Guid id) + { + throw new NotImplementedException(); + } + public async Task CountEntries(string? query = null, FilterQueryOptions? options = null) { await using var repo = await repoFactory.CreateRepoAsync(); From 06c3d0d74a6f0377a4c098c87948ba4ac00b368b Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 16:38:45 +0700 Subject: [PATCH 06/17] Fix bug in SimpleStringDiff and IntegerDiff The "add" operation, if we had ever used it, would have added nothing because the value to be added wasn't being passed in. Fixed now. --- .../IntegerDiffTests.cs | 51 +++++++++++++++++++ .../SimpleDiffTests.cs | 51 +++++++++++++++++++ .../FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs | 2 +- .../MiniLcm/SyncHelpers/SimpleStringDiff.cs | 2 +- 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/IntegerDiffTests.cs create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/SimpleDiffTests.cs diff --git a/backend/FwLite/FwLiteProjectSync.Tests/IntegerDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/IntegerDiffTests.cs new file mode 100644 index 0000000000..c13cb93edd --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/IntegerDiffTests.cs @@ -0,0 +1,51 @@ +using MiniLcm.SyncHelpers; +using SystemTextJsonPatch.Operations; + +namespace FwLiteProjectSync.Tests; + +public class IntegerDiffTests +{ + private record Placeholder(); + + [Fact] + public void DiffEmptyIntegersDoNothing() + { + int? before = null; + int? after = null; + var result = IntegerDiff.GetIntegerDiff("test", before, after); + result.Should().BeEmpty(); + } + + [Fact] + public void DiffOneToEmptyAddsOne() + { + int? before = null; + var after = 1; + var result = IntegerDiff.GetIntegerDiff("test", before, after); + result.Should().BeEquivalentTo([ + new Operation("add", "/test", null, 1) + ]); + } + + [Fact] + public void DiffOneToTwoReplacesOne() + { + var before = 1; + var after = 2; + var result = IntegerDiff.GetIntegerDiff("test", before, after); + result.Should().BeEquivalentTo([ + new Operation("replace", "/test", null, 2) + ]); + } + + [Fact] + public void DiffNoneToOneRemovesOne() + { + var before = 1; + int? after = null; + var result = IntegerDiff.GetIntegerDiff("test", before, after); + result.Should().BeEquivalentTo([ + new Operation("remove", "/test", null) + ]); + } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SimpleDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SimpleDiffTests.cs new file mode 100644 index 0000000000..15b4451b41 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/SimpleDiffTests.cs @@ -0,0 +1,51 @@ +using MiniLcm.SyncHelpers; +using SystemTextJsonPatch.Operations; + +namespace FwLiteProjectSync.Tests; + +public class SimpleStringDiffTests +{ + private record Placeholder(); + + [Fact] + public void DiffEmptyStringsDoNothing() + { + string? before = null; + string? after = null; + var result = SimpleStringDiff.GetStringDiff("test", before, after); + result.Should().BeEmpty(); + } + + [Fact] + public void DiffOneToEmptyAddsOne() + { + string? before = null; + var after = "hello"; + var result = SimpleStringDiff.GetStringDiff("test", before, after); + result.Should().BeEquivalentTo([ + new Operation("add", "/test", null, "hello") + ]); + } + + [Fact] + public void DiffOneToOneReplacesOne() + { + var before = "hello"; + var after = "world"; + var result = SimpleStringDiff.GetStringDiff("test", before, after); + result.Should().BeEquivalentTo([ + new Operation("replace", "/test", null, "world") + ]); + } + + [Fact] + public void DiffNoneToOneRemovesOne() + { + var before = "hello"; + string? after = null; + var result = SimpleStringDiff.GetStringDiff("test", before, after); + result.Should().BeEquivalentTo([ + new Operation("remove", "/test", null) + ]); + } +} diff --git a/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs b/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs index dcd2c4b027..1ca2f6085e 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs @@ -10,7 +10,7 @@ public static IEnumerable> GetIntegerDiff(string path, { if (before == after) yield break; if (after is null) yield return new Operation("remove", $"/{path}", null); - else if (before is null) yield return new Operation("add", $"/{path}", null); + else if (before is null) yield return new Operation("add", $"/{path}", null, after); else yield return new Operation("replace", $"/{path}", null, after); } } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/SimpleStringDiff.cs b/backend/FwLite/MiniLcm/SyncHelpers/SimpleStringDiff.cs index 37f9e78708..b5668c4c65 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/SimpleStringDiff.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/SimpleStringDiff.cs @@ -10,7 +10,7 @@ public static IEnumerable> GetStringDiff(string path, { if (before == after) yield break; if (after is null) yield return new Operation("remove", $"/{path}", null); - else if (before is null) yield return new Operation("add", $"/{path}", null); + else if (before is null) yield return new Operation("add", $"/{path}", null, after); else yield return new Operation("replace", $"/{path}", null, after); } } From 52f9007b9f9da7d83024c9c3ac96f776e7cbf5bc Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 29 Jul 2025 16:40:27 +0700 Subject: [PATCH 07/17] Implement necessary methods in dry run and legacy --- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 25 +++++++++++++++++++ backend/LfClassicData/LfClassicMiniLcmApi.cs | 10 ++++++++ 2 files changed, 35 insertions(+) diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 87c5d28549..5fb57de316 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -119,6 +119,31 @@ public Task DeleteComplexFormType(Guid id) return Task.CompletedTask; } + public Task CreateMorphTypeData(MorphTypeData morphType) + { + DryRunRecords.Add(new DryRunRecord(nameof(CreateMorphTypeData), + $"Create morph type {morphType.Name}")); + return Task.FromResult(morphType); + } + + public async Task UpdateMorphTypeData(Guid id, UpdateObjectInput update) + { + DryRunRecords.Add(new DryRunRecord(nameof(UpdateMorphTypeData), $"Update morph type {id}")); + return await _api.GetMorphTypeData(id) ?? throw new NullReferenceException($"unable to find morph type with id {id}"); + } + + public Task UpdateMorphTypeData(MorphTypeData before, MorphTypeData after, IMiniLcmApi? api) + { + DryRunRecords.Add(new DryRunRecord(nameof(UpdateMorphTypeData), $"Update morph type {after.Id}")); + return Task.FromResult(after); + } + + public Task DeleteMorphTypeData(Guid id) + { + DryRunRecords.Add(new DryRunRecord(nameof(DeleteMorphTypeData), $"Delete morph type {id}")); + return Task.CompletedTask; + } + public Task CreateEntry(Entry entry) { DryRunRecords.Add(new DryRunRecord(nameof(CreateEntry), $"Create entry {entry.Headword()}")); diff --git a/backend/LfClassicData/LfClassicMiniLcmApi.cs b/backend/LfClassicData/LfClassicMiniLcmApi.cs index 23b59e5191..4b8f149055 100644 --- a/backend/LfClassicData/LfClassicMiniLcmApi.cs +++ b/backend/LfClassicData/LfClassicMiniLcmApi.cs @@ -25,6 +25,16 @@ public IAsyncEnumerable GetComplexFormTypes() return Task.FromResult(null); } + public IAsyncEnumerable GetAllMorphTypeData() + { + return AsyncEnumerable.Empty(); + } + + public Task GetMorphTypeData(Guid id) + { + return Task.FromResult(null); + } + private Dictionary? _partsOfSpeechCacheByGuid = null; private Dictionary? _partsOfSpeechCacheByStringKey = null; From 6b537efdd744241c79c8a088dfd22db6e1914c54 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 1 Aug 2025 10:29:07 +0700 Subject: [PATCH 08/17] Address review comments --- backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs | 8 +++++++- .../Api/UpdateProxy/UpdateEntryProxy.cs | 7 +++++-- .../Api/UpdateProxy/UpdateMorphTypeDataProxy.cs | 6 +++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index f6d8fdcb43..1d8819ebcf 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -238,11 +238,17 @@ internal static ILexEntry CreateEntry(this LcmCache cache, Guid id, MorphType mo { var lexEntry = cache.ServiceLocator.GetInstance().Create(id, cache.ServiceLocator.GetInstance().Singleton.LexDbOA); + SetLexemeForm(lexEntry, morphType, cache); + return lexEntry; + } + + internal static IMoForm SetLexemeForm(ILexEntry lexEntry, MorphType morphType, LcmCache cache) + { lexEntry.LexemeFormOA = cache.CreateLexemeForm(morphType); //must be done after the IMoForm is set on the LexemeForm property var lcmMorphType = ToLcmMorphTypeId(morphType) ?? ToLcmMorphTypeId(MorphType.Stem); lexEntry.LexemeFormOA.MorphTypeRA = cache.ServiceLocator.GetInstance().GetObject(lcmMorphType!.Value); - return lexEntry; + return lexEntry.LexemeFormOA; } internal static string GetSemanticDomainCode(ICmSemanticDomain semanticDomain) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index b183c160a2..a6ad6786bb 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -21,8 +21,11 @@ public override MultiString LexemeForm { get { - _lcmEntry.LexemeFormOA ??= _lexboxLcmApi.Cache.CreateLexemeForm(LcmHelpers.FromLcmMorphTypeId(_lcmEntry.PrimaryMorphType.Id.Guid)); - return new UpdateMultiStringProxy(_lcmEntry.LexemeFormOA.Form, _lexboxLcmApi); + var form = _lcmEntry.LexemeFormOA ?? LcmHelpers.SetLexemeForm( + _lcmEntry, + LcmHelpers.FromLcmMorphTypeId(_lcmEntry.PrimaryMorphType.Id.Guid), + _lexboxLcmApi.Cache); + return new UpdateMultiStringProxy(form.Form, _lexboxLcmApi); } set => throw new NotImplementedException(); } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs index e28c4d6d3a..873ec41b1c 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs @@ -36,18 +36,18 @@ public override RichMultiString Description public override string LeadingToken { get => _lcmMorphType.Prefix; - set => throw new NotImplementedException(); + set => _lcmMorphType.Prefix = value; } public override string TrailingToken { get => _lcmMorphType.Postfix; - set => throw new NotImplementedException(); + set => _lcmMorphType.Postfix = value; } public override int SecondaryOrder { get => _lcmMorphType.SecondaryOrder; - set => throw new NotImplementedException(); + set => _lcmMorphType.SecondaryOrder = value; } } From 92a0d4ef58fc763e5be73befb01949fc1156d825 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 6 Aug 2025 15:47:33 +0700 Subject: [PATCH 09/17] Add DB migration for CRDT database --- .../20250806084710_AddMorphTypes.Designer.cs | 729 ++++++++++++++++++ .../20250806084710_AddMorphTypes.cs | 29 + .../LcmCrdtDbContextModelSnapshot.cs | 3 + 3 files changed, 761 insertions(+) create mode 100644 backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.Designer.cs create mode 100644 backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.cs diff --git a/backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.Designer.cs b/backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.Designer.cs new file mode 100644 index 0000000000..93c978dfa7 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.Designer.cs @@ -0,0 +1,729 @@ +// +using System; +using System.Collections.Generic; +using LcmCrdt; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LcmCrdt.Migrations +{ + [DbContext(typeof(LcmCrdtDbContext))] + [Migration("20250806084710_AddMorphTypes")] + partial class AddMorphTypes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + + modelBuilder.Entity("LcmCrdt.FullTextSearch.EntrySearchRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CitationForm") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Gloss") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LexemeForm") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EntrySearchRecord", null, t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("LcmCrdt.ProjectData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FwProjectId") + .HasColumnType("TEXT"); + + b.Property("LastUserId") + .HasColumnType("TEXT"); + + b.Property("LastUserName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginDomain") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Editor"); + + b.HasKey("Id"); + + b.ToTable("ProjectData"); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ComplexFormEntryId") + .HasColumnType("TEXT"); + + b.Property("ComplexFormHeadword") + .HasColumnType("TEXT"); + + b.Property("ComponentEntryId") + .HasColumnType("TEXT"); + + b.Property("ComponentHeadword") + .HasColumnType("TEXT"); + + b.Property("ComponentSenseId") + .HasColumnType("TEXT") + .HasColumnName("ComponentSenseId"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ComponentEntryId"); + + b.HasIndex("ComponentSenseId"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.HasIndex("ComplexFormEntryId", "ComponentEntryId") + .IsUnique() + .HasFilter("ComponentSenseId IS NULL"); + + b.HasIndex("ComplexFormEntryId", "ComponentEntryId", "ComponentSenseId") + .IsUnique() + .HasFilter("ComponentSenseId IS NOT NULL"); + + b.ToTable("ComplexFormComponents", (string)null); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("ComplexFormType"); + }); + + modelBuilder.Entity("MiniLcm.Models.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CitationForm") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ComplexFormTypes") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("LexemeForm") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("LiteralMeaning") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MorphType") + .HasColumnType("INTEGER"); + + b.Property("Note") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PublishIn") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("Entry"); + }); + + modelBuilder.Entity("MiniLcm.Models.ExampleSentence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("Reference") + .HasColumnType("jsonb"); + + b.Property("SenseId") + .HasColumnType("TEXT"); + + b.Property("Sentence") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.Property("Translation") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SenseId"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("ExampleSentence"); + }); + + modelBuilder.Entity("MiniLcm.Models.PartOfSpeech", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Predefined") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("PartOfSpeech"); + }); + + modelBuilder.Entity("MiniLcm.Models.Publication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("Publication"); + }); + + modelBuilder.Entity("MiniLcm.Models.SemanticDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Predefined") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("SemanticDomain"); + }); + + modelBuilder.Entity("MiniLcm.Models.Sense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("EntryId") + .HasColumnType("TEXT"); + + b.Property("Gloss") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("PartOfSpeechId") + .HasColumnType("TEXT"); + + b.Property("SemanticDomains") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EntryId"); + + b.HasIndex("PartOfSpeechId"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("Sense"); + }); + + modelBuilder.Entity("MiniLcm.Models.WritingSystem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Abbreviation") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Exemplars") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Font") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("REAL"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WsId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.HasIndex("WsId", "Type") + .IsUnique(); + + b.ToTable("WritingSystem"); + }); + + modelBuilder.Entity("SIL.Harmony.Commit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ParentHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.ComplexProperty>("HybridDateTime", "SIL.Harmony.Commit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("INTEGER") + .HasColumnName("Counter"); + + b1.Property("DateTime") + .HasColumnType("TEXT") + .HasColumnName("DateTime"); + }); + + b.HasKey("Id"); + + b.ToTable("Commits", (string)null); + }); + + modelBuilder.Entity("SIL.Harmony.Core.ChangeEntity", b => + { + b.Property("CommitId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Change") + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.HasKey("CommitId", "Index"); + + b.ToTable("ChangeEntities", (string)null); + }); + + modelBuilder.Entity("SIL.Harmony.Db.ObjectSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommitId") + .HasColumnType("TEXT"); + + b.Property("Entity") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityIsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRoot") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("References") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EntityId"); + + b.HasIndex("CommitId", "EntityId") + .IsUnique(); + + b.ToTable("Snapshots", (string)null); + }); + + modelBuilder.Entity("SIL.Harmony.Resource.LocalResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LocalResource"); + }); + + modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteId") + .HasColumnType("TEXT"); + + b.Property("SnapshotId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SnapshotId") + .IsUnique(); + + b.ToTable("RemoteResource"); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormComponent", b => + { + b.HasOne("MiniLcm.Models.Entry", null) + .WithMany("Components") + .HasForeignKey("ComplexFormEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MiniLcm.Models.Entry", null) + .WithMany("ComplexForms") + .HasForeignKey("ComponentEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MiniLcm.Models.Sense", null) + .WithMany() + .HasForeignKey("ComponentSenseId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.ComplexFormComponent", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.ComplexFormType", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.ComplexFormType", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Entry", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.Entry", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.ExampleSentence", b => + { + b.HasOne("MiniLcm.Models.Sense", null) + .WithMany("ExampleSentences") + .HasForeignKey("SenseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.ExampleSentence", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.PartOfSpeech", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.PartOfSpeech", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Publication", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.Publication", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.SemanticDomain", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.SemanticDomain", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Sense", b => + { + b.HasOne("MiniLcm.Models.Entry", null) + .WithMany("Senses") + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MiniLcm.Models.PartOfSpeech", "PartOfSpeech") + .WithMany() + .HasForeignKey("PartOfSpeechId"); + + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.Sense", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("PartOfSpeech"); + }); + + modelBuilder.Entity("MiniLcm.Models.WritingSystem", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("MiniLcm.Models.WritingSystem", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("SIL.Harmony.Core.ChangeEntity", b => + { + b.HasOne("SIL.Harmony.Commit", null) + .WithMany("ChangeEntities") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SIL.Harmony.Db.ObjectSnapshot", b => + { + b.HasOne("SIL.Harmony.Commit", "Commit") + .WithMany("Snapshots") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Commit"); + }); + + modelBuilder.Entity("SIL.Harmony.Resource.RemoteResource", b => + { + b.HasOne("SIL.Harmony.Db.ObjectSnapshot", null) + .WithOne() + .HasForeignKey("SIL.Harmony.Resource.RemoteResource", "SnapshotId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MiniLcm.Models.Entry", b => + { + b.Navigation("ComplexForms"); + + b.Navigation("Components"); + + b.Navigation("Senses"); + }); + + modelBuilder.Entity("MiniLcm.Models.Sense", b => + { + b.Navigation("ExampleSentences"); + }); + + modelBuilder.Entity("SIL.Harmony.Commit", b => + { + b.Navigation("ChangeEntities"); + + b.Navigation("Snapshots"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.cs b/backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.cs new file mode 100644 index 0000000000..67b723ff8b --- /dev/null +++ b/backend/FwLite/LcmCrdt/Migrations/20250806084710_AddMorphTypes.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LcmCrdt.Migrations +{ + /// + public partial class AddMorphTypes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MorphType", + table: "Entry", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MorphType", + table: "Entry"); + } + } +} diff --git a/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs b/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs index c05a7c64ac..6e8babbc38 100644 --- a/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs +++ b/backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs @@ -192,6 +192,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("jsonb"); + b.Property("MorphType") + .HasColumnType("INTEGER"); + b.Property("Note") .IsRequired() .HasColumnType("jsonb"); From 5a3fdd302b15a7d9c02c854aae1b38e603e3e088 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 7 Aug 2025 12:44:41 +0700 Subject: [PATCH 10/17] Update VerifyDbModel --- .../DataModelSnapshotTests.VerifyDbModel.verified.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt index 0ed0880b02..0c649d9c4a 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt @@ -116,6 +116,7 @@ LiteralMeaning (RichMultiString) Required Annotations: Relational:ColumnType: jsonb + MorphType (MorphType) Required Note (RichMultiString) Required Annotations: Relational:ColumnType: jsonb From 7b21ee4668f41e4bdf26646217f6adc45b7d9b56 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 8 Aug 2025 17:00:57 +0200 Subject: [PATCH 11/17] Persist and update morph type in CRDT --- backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs | 6 +++++- backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs index be14afdf96..eb8f1fabca 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs @@ -16,6 +16,7 @@ public CreateEntryChange(Entry entry) : base(entry.Id == Guid.Empty ? Guid.NewGu CitationForm = entry.CitationForm; LiteralMeaning = entry.LiteralMeaning; Note = entry.Note; + MorphType = entry.MorphType; } [JsonConstructor] @@ -31,6 +32,8 @@ private CreateEntryChange(Guid entityId) : base(entityId) public RichMultiString? Note { get; set; } + public MorphType? MorphType { get; set; } + public override ValueTask NewEntity(Commit commit, IChangeContext context) { return new(new Entry @@ -39,7 +42,8 @@ public override ValueTask NewEntity(Commit commit, IChangeContext context LexemeForm = LexemeForm ?? new MultiString(), CitationForm = CitationForm ?? new MultiString(), LiteralMeaning = LiteralMeaning ?? new(), - Note = Note ?? new() + Note = Note ?? new(), + MorphType = MorphType ?? default }); } } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs index b4e577f263..6bfd17a3a4 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs @@ -1,6 +1,7 @@ using MiniLcm.Exceptions; using MiniLcm.Models; using SystemTextJsonPatch; +using SystemTextJsonPatch.Operations; namespace MiniLcm.SyncHelpers; @@ -88,6 +89,8 @@ private static async Task SensesSync(Guid entryId, patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.CitationForm), beforeEntry.CitationForm, afterEntry.CitationForm)); patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.Note), beforeEntry.Note, afterEntry.Note)); patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(Entry.LiteralMeaning), beforeEntry.LiteralMeaning, afterEntry.LiteralMeaning)); + if (beforeEntry.MorphType != afterEntry.MorphType) + patchDocument.Operations.Add(new Operation("replace", $"/{nameof(Entry.MorphType)}", null, afterEntry.MorphType)); if (patchDocument.Operations.Count == 0) return null; return new UpdateObjectInput(patchDocument); } From 10788749beecb1e6c1982e3811df4419cfd7425e Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 8 Aug 2025 17:44:56 +0200 Subject: [PATCH 12/17] Fix linting errors --- frontend/viewer/src/lib/demo-entry-data.ts | 4 +++- frontend/viewer/src/lib/dotnet-types/index.ts | 1 + frontend/viewer/src/lib/utils.ts | 3 ++- .../entity-primitives/entry-editor-primitive.stories.svelte | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/viewer/src/lib/demo-entry-data.ts b/frontend/viewer/src/lib/demo-entry-data.ts index 08ce32835b..68c6dc2a79 100644 --- a/frontend/viewer/src/lib/demo-entry-data.ts +++ b/frontend/viewer/src/lib/demo-entry-data.ts @@ -1,4 +1,4 @@ -import {type IEntry, type IWritingSystems, WritingSystemType} from '$lib/dotnet-types'; +import {type IEntry, type IWritingSystems, MorphType, WritingSystemType} from '$lib/dotnet-types'; export const projectName = 'Sena 3'; @@ -88,6 +88,7 @@ export const _entries: IEntry[] = [ 'lexemeForm': { 'seh': 'a' }, 'citationForm': {}, 'literalMeaning': {}, + morphType: MorphType.Stem, 'senses': [ { 'id': 'f53f0f28-3ec1-4051-b9a3-fafdca6209ce', @@ -146,6 +147,7 @@ export const _entries: IEntry[] = [ 'lexemeForm': { 'seh': 'dance' }, 'citationForm': {}, 'literalMeaning': {}, + morphType: MorphType.Stem, 'senses': [ { 'id': 'f53f0f29-3ec1-4051-b9a3-fafdca6209ce', diff --git a/frontend/viewer/src/lib/dotnet-types/index.ts b/frontend/viewer/src/lib/dotnet-types/index.ts index 12c8252e04..a8ec4aa7bf 100644 --- a/frontend/viewer/src/lib/dotnet-types/index.ts +++ b/frontend/viewer/src/lib/dotnet-types/index.ts @@ -23,6 +23,7 @@ export * from './generated-types/MiniLcm/Models/IWritingSystem'; export * from './generated-types/MiniLcm/Models/IWritingSystems'; export * from './generated-types/MiniLcm/Models/ProjectDataFormat'; export * from './generated-types/MiniLcm/Models/WritingSystemType'; +export * from './generated-types/MiniLcm/Models/MorphType'; export * from './generated-types/MiniLcm/ICountQueryOptions'; export * from './generated-types/MiniLcm/IExemplarOptions'; export * from './generated-types/MiniLcm/IFilterQueryOptions'; diff --git a/frontend/viewer/src/lib/utils.ts b/frontend/viewer/src/lib/utils.ts index b18b94982e..3fdb0036db 100644 --- a/frontend/viewer/src/lib/utils.ts +++ b/frontend/viewer/src/lib/utils.ts @@ -1,4 +1,4 @@ -import type {IEntry, IExampleSentence, ISense, IWritingSystem, WritingSystemType} from '$lib/dotnet-types'; +import {MorphType, type IEntry, type IExampleSentence, type ISense, type IWritingSystem, type WritingSystemType} from '$lib/dotnet-types'; import {get, writable, type Readable} from 'svelte/store'; import {type ClassValue, clsx} from 'clsx'; import {twMerge} from 'tailwind-merge'; @@ -38,6 +38,7 @@ export function defaultEntry(): IEntry { complexFormTypes: [], components: [], publishIn: [], + morphType: MorphType.Stem, }; } diff --git a/frontend/viewer/src/stories/editor/entity-primitives/entry-editor-primitive.stories.svelte b/frontend/viewer/src/stories/editor/entity-primitives/entry-editor-primitive.stories.svelte index 69280e1622..9256432894 100644 --- a/frontend/viewer/src/stories/editor/entity-primitives/entry-editor-primitive.stories.svelte +++ b/frontend/viewer/src/stories/editor/entity-primitives/entry-editor-primitive.stories.svelte @@ -3,7 +3,7 @@ import { expect, fn, userEvent, within } from 'storybook/test'; import EntityEditorPrimitiveDecorator from './EntityEditorPrimitiveDecorator.svelte'; import EntryEditorPrimitive from '$lib/entry-editor/object-editors/EntryEditorPrimitive.svelte'; - import type {IEntry} from '$lib/dotnet-types'; + import {type IEntry, MorphType} from '$lib/dotnet-types'; import {fwliteStoryParameters} from '../../fwl-parameters'; import {tick} from 'svelte'; @@ -39,6 +39,7 @@ components: [], publishIn: [], senses: [], + morphType: MorphType.Stem }); const { Story } = defineMeta({ From 2f5d426ae115ec07baeb876814a9c1d9c7015966 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 8 Aug 2025 17:45:16 +0200 Subject: [PATCH 13/17] Make AutoFaker exclude Unknown and Other, because they don't round trip --- .../AutoFakerHelpers/AutoFakerDefault.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/AutoFakerDefault.cs b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/AutoFakerDefault.cs index 1d1f24233a..db85553c09 100644 --- a/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/AutoFakerDefault.cs +++ b/backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/AutoFakerDefault.cs @@ -36,7 +36,12 @@ public static AutoFakerConfig MakeConfig(string[]? validWs = null, int repeatCou { domain.Predefined = false; } - }, true) + }, true), + new PredicateOverride(morph => + { + // these values map to null and get replaced with MorphType.Stem so they're no round-tripped + return morph is not MorphType.Unknown and not MorphType.Other; + }, true), ] }; } @@ -50,4 +55,19 @@ public override void Generate(AutoFakerOverrideContext context) execute(context); } } + + private class PredicateOverride(Func predicate, bool preInit = false) : AutoFakerOverride + { + public override bool Preinitialize { get; } = preInit; + + public override void Generate(AutoFakerOverrideContext context) + { + var value = context.Instance; + while (value is not T instance || !predicate(instance)) + { + value = context.AutoFaker.Generate(); + } + context.Instance = value; + } + } } From c95af0c8e3773360e6f2777da827ea7a21dc9c1c Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 8 Aug 2025 17:59:01 +0200 Subject: [PATCH 14/17] Explicitly default to MorphType.Stem in several places --- backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 5 ++++- backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs | 2 +- backend/FwLite/MiniLcm/Models/Entry.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index dbb8aecd97..b97523a9f7 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -597,7 +597,10 @@ private Entry FromLexEntry(ILexEntry entry) LexemeForm = FromLcmMultiString(entry.LexemeFormOA?.Form), CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), - MorphType = LcmHelpers.FromLcmMorphTypeId(entry.PrimaryMorphType.Id.Guid), // TODO: Decide what to do about entries with *mixed* morph types + // PrimaryMorphType is null if LexemeFormOA is null + MorphType = entry.PrimaryMorphType is null + ? MorphType.Stem + : LcmHelpers.FromLcmMorphTypeId(entry.PrimaryMorphType.Id.Guid), // TODO: Decide what to do about entries with *mixed* morph types Senses = entry.AllSenses.Select(FromLexSense).ToList(), ComplexFormTypes = ToComplexFormTypes(entry), Components = ToComplexFormComponents(entry).ToList(), diff --git a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs index eb8f1fabca..d16953cfe0 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs @@ -43,7 +43,7 @@ public override ValueTask NewEntity(Commit commit, IChangeContext context CitationForm = CitationForm ?? new MultiString(), LiteralMeaning = LiteralMeaning ?? new(), Note = Note ?? new(), - MorphType = MorphType ?? default + MorphType = MorphType ?? MiniLcm.Models.MorphType.Stem, }); } } diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index ed34519da4..6dd736f160 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -10,7 +10,7 @@ public record Entry : IObjectWithId public virtual MultiString CitationForm { get; set; } = new(); public virtual RichMultiString LiteralMeaning { get; set; } = new(); - public virtual MorphType MorphType { get; set; } + public virtual MorphType MorphType { get; set; } = MorphType.Stem; public virtual List Senses { get; set; } = []; public virtual RichMultiString Note { get; set; } = new(); From ba7a6478a087ce81cba47d6487e438be604a0888 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 11 Aug 2025 09:55:48 +0200 Subject: [PATCH 15/17] Handle undocumented nullability of ILexEntry.PrimaryMorphType --- backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 7 ++----- backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs | 6 ++++-- .../Api/UpdateProxy/UpdateEntryProxy.cs | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index b97523a9f7..00d85140f2 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -509,7 +509,7 @@ internal MorphTypeData FromLcmMorphType(IMoMorphType morphType) return new MorphTypeData { Id = morphType.Guid, - MorphType = LcmHelpers.FromLcmMorphTypeId(morphType.Guid), + MorphType = LcmHelpers.FromLcmMorphType(morphType), Name = FromLcmMultiString(morphType.Name), Abbreviation = FromLcmMultiString(morphType.Abbreviation), Description = FromLcmMultiString(morphType.Description), @@ -597,10 +597,7 @@ private Entry FromLexEntry(ILexEntry entry) LexemeForm = FromLcmMultiString(entry.LexemeFormOA?.Form), CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), - // PrimaryMorphType is null if LexemeFormOA is null - MorphType = entry.PrimaryMorphType is null - ? MorphType.Stem - : LcmHelpers.FromLcmMorphTypeId(entry.PrimaryMorphType.Id.Guid), // TODO: Decide what to do about entries with *mixed* morph types + MorphType = LcmHelpers.FromLcmMorphType(entry.PrimaryMorphType), // TODO: Decide what to do about entries with *mixed* morph types Senses = entry.AllSenses.Select(FromLexSense).ToList(), ComplexFormTypes = ToComplexFormTypes(entry), Components = ToComplexFormComponents(entry).ToList(), diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 1d8819ebcf..9221da8960 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -83,8 +83,10 @@ internal static bool SearchValue(this ITsMultiString multiString, string value) '\u0640', // Arabic Tatweel ]; - internal static MorphType FromLcmMorphTypeId(Guid? lcmMorphTypeId) + internal static MorphType FromLcmMorphType(IMoMorphType? morphType) { + var lcmMorphTypeId = morphType?.Id.Guid; + return lcmMorphTypeId switch { null => MorphType.Unknown, @@ -136,7 +138,7 @@ internal static MorphType FromLcmMorphTypeId(Guid? lcmMorphTypeId) MorphType.Phrase => MoMorphTypeTags.kguidMorphPhrase, MorphType.DiscontiguousPhrase => MoMorphTypeTags.kguidMorphDiscontiguousPhrase, MorphType.Unknown => null, - MorphType.Other => null, // Note that this will not round-trip with FromLcmMorphTypeId + MorphType.Other => null, // Note that this will not round-trip with FromLcmMorphType _ => null, }; } diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index a6ad6786bb..c2e6a21648 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -23,7 +23,7 @@ public override MultiString LexemeForm { var form = _lcmEntry.LexemeFormOA ?? LcmHelpers.SetLexemeForm( _lcmEntry, - LcmHelpers.FromLcmMorphTypeId(_lcmEntry.PrimaryMorphType.Id.Guid), + LcmHelpers.FromLcmMorphType(_lcmEntry.PrimaryMorphType), _lexboxLcmApi.Cache); return new UpdateMultiStringProxy(form.Form, _lexboxLcmApi); } From 31218de891085038c42cba6ade36451cacbdba52 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 14 Aug 2025 10:49:44 +0700 Subject: [PATCH 16/17] Exclude MorphType from AllObjectsAreRegistered test --- backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index 15a0a78592..e0e7135b18 100644 --- a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs @@ -8,7 +8,7 @@ namespace LcmCrdt.Tests; public class ConfigRegistrationTests { - private readonly HashSet ExcludedObjectTypes = []; + private readonly HashSet ExcludedObjectTypes = [typeof(MorphType)]; // Remove from exclude list once CRDT supports morph types private readonly HashSet _excludedChangeTypes = [ From fc34c257cecc076956817f147b82392c05d2310b Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 14 Aug 2025 11:05:16 +0700 Subject: [PATCH 17/17] Exclude correct type --- backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index e0e7135b18..e839c6f16d 100644 --- a/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs @@ -8,7 +8,7 @@ namespace LcmCrdt.Tests; public class ConfigRegistrationTests { - private readonly HashSet ExcludedObjectTypes = [typeof(MorphType)]; // Remove from exclude list once CRDT supports morph types + private readonly HashSet ExcludedObjectTypes = [typeof(MorphTypeData)]; // Remove from exclude list once CRDT supports morph types private readonly HashSet _excludedChangeTypes = [