diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 93e4d08c21..00d85140f2 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -488,6 +488,70 @@ 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.FromLcmMorphType(morphType), + Name = FromLcmMultiString(morphType.Name), + Abbreviation = FromLcmMultiString(morphType.Abbreviation), + Description = FromLcmMultiString(morphType.Description), + LeadingToken = morphType.Prefix, + TrailingToken = morphType.Postfix, + SecondaryOrder = morphType.SecondaryOrder, + }; + } + + 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() { return VariantTypes.PossibilitiesOS @@ -533,6 +597,7 @@ private Entry FromLexEntry(ILexEntry entry) LexemeForm = FromLcmMultiString(entry.LexemeFormOA?.Form), CitationForm = FromLcmMultiString(entry.CitationForm), LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), + 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(), @@ -836,7 +901,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 3812af59e3..9221da8960 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -83,6 +83,66 @@ internal static bool SearchValue(this ITsMultiString multiString, string value) '\u0640', // Arabic Tatweel ]; + internal static MorphType FromLcmMorphType(IMoMorphType? morphType) + { + var lcmMorphTypeId = morphType?.Id.Guid; + + 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 FromLcmMorphType + _ => null, + }; + } + internal static void ContributeExemplars(ITsMultiString multiString, IReadOnlyDictionary> wsExemplars) { for (var i = 0; i < multiString.StringCount; i++) @@ -148,21 +208,51 @@ 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 + IsAffixMorphType(morphType) + ? cache.ServiceLocator.GetInstance().Create() + : cache.ServiceLocator.GetInstance().Create(); } - internal static ILexEntry CreateEntry(this LcmCache cache, Guid id) + internal static bool IsAffixMorphType(MorphType morphType) + { + return morphType switch + { + // 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, + + // Everything else should use the Stem morph type factory + _ => false, + }; + } + + 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(); - //must be done after the IMoForm is set on the LexemeForm property - lexEntry.LexemeFormOA.MorphTypeRA = cache.ServiceLocator.GetInstance().GetObject(MoMorphTypeTags.kguidMorphStem); + 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.LexemeFormOA; + } + internal static string GetSemanticDomainCode(ICmSemanticDomain semanticDomain) { var abbr = semanticDomain.Abbreviation; diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs index 4348f9ab64..c2e6a21648 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(); - return new UpdateMultiStringProxy(_lcmEntry.LexemeFormOA.Form, _lexboxLcmApi); + var form = _lcmEntry.LexemeFormOA ?? LcmHelpers.SetLexemeForm( + _lcmEntry, + LcmHelpers.FromLcmMorphType(_lcmEntry.PrimaryMorphType), + _lexboxLcmApi.Cache); + return new UpdateMultiStringProxy(form.Form, _lexboxLcmApi); } set => throw new NotImplementedException(); } @@ -39,6 +42,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/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateMorphTypeDataProxy.cs new file mode 100644 index 0000000000..873ec41b1c --- /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 => _lcmMorphType.Prefix = value; + } + + public override string TrailingToken + { + get => _lcmMorphType.Postfix; + set => _lcmMorphType.Postfix = value; + } + + public override int SecondaryOrder + { + get => _lcmMorphType.SecondaryOrder; + set => _lcmMorphType.SecondaryOrder = value; + } +} 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/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/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/LcmCrdt.Tests/ConfigRegistrationTests.cs b/backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs index 15a0a78592..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 = []; + private readonly HashSet ExcludedObjectTypes = [typeof(MorphTypeData)]; // Remove from exclude list once CRDT supports morph types private readonly HashSet _excludedChangeTypes = [ 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 diff --git a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs index be14afdf96..d16953cfe0 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 ?? MiniLcm.Models.MorphType.Stem, }); } } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 277adc8457..07f828729a 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -337,6 +337,36 @@ 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 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(); 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"); 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; + } + } } 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); 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/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index e31e51d6d7..6dd736f160 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; } = MorphType.Stem; 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 new file mode 100644 index 0000000000..acc54bba9e --- /dev/null +++ b/backend/FwLite/MiniLcm/Models/MorphType.cs @@ -0,0 +1,68 @@ +using System.Text.Json.Serialization; + +namespace MiniLcm.Models; + +[JsonConverter(typeof(JsonStringEnumConverter))] +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 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; } + + 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, + }; + } +} 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); } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs b/backend/FwLite/MiniLcm/SyncHelpers/IntegerDiff.cs new file mode 100644 index 0000000000..1ca2f6085e --- /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, after); + 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); + } + } +} 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); } } 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; 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/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 */ 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({