diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 26e415466..a06b92257 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -104,19 +104,23 @@ public Task GetWritingSystems() .Select(ws => ws.Id).ToHashSet(); var writingSystems = new WritingSystems { - Vernacular = WritingSystemContainer.CurrentVernacularWritingSystems.Select(FromLcmWritingSystem).ToArray(), - Analysis = Cache.ServiceLocator.WritingSystems.CurrentAnalysisWritingSystems.Select(FromLcmWritingSystem).ToArray() + Vernacular = WritingSystemContainer.CurrentVernacularWritingSystems.Select(definition => + FromLcmWritingSystem(definition, WritingSystemType.Vernacular)).ToArray(), + Analysis = Cache.ServiceLocator.WritingSystems.CurrentAnalysisWritingSystems.Select(definition => + FromLcmWritingSystem(definition, WritingSystemType.Analysis)).ToArray() }; CompleteExemplars(writingSystems); return Task.FromResult(writingSystems); } - private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws) + private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws, WritingSystemType type) { return new WritingSystem { + Id = Guid.Empty, + Type = type, //todo determine current and create a property for that. - Id = ws.Id, + WsId = ws.Id, Name = ws.LanguageTag, Abbreviation = ws.Abbreviation, Font = ws.DefaultFontName, @@ -127,9 +131,9 @@ private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws) internal void CompleteExemplars(WritingSystems writingSystems) { var wsExemplars = writingSystems.Vernacular.Concat(writingSystems.Analysis) - .DistinctBy(ws => ws.Id) + .DistinctBy(ws => ws.WsId) .ToDictionary(ws => ws, ws => ws.Exemplars.Select(s => s[0]).ToHashSet()); - var wsExemplarsByHandle = wsExemplars.ToFrozenDictionary(kv => GetWritingSystemHandle(kv.Key.Id), kv => kv.Value); + var wsExemplarsByHandle = wsExemplars.ToFrozenDictionary(kv => GetWritingSystemHandle(kv.Key.WsId), kv => kv.Value); foreach (var entry in EntriesRepository.AllInstances()) { @@ -151,7 +155,7 @@ public Task CreateWritingSystem(WritingSystemType type, WritingSy Cache.ServiceLocator.ActionHandler, () => { - Cache.ServiceLocator.WritingSystemManager.GetOrSet(writingSystem.Id.Code, out ws); + Cache.ServiceLocator.WritingSystemManager.GetOrSet(writingSystem.WsId.Code, out ws); ws.Abbreviation = writingSystem.Abbreviation; switch (type) { @@ -164,7 +168,7 @@ public Task CreateWritingSystem(WritingSystemType type, WritingSy } }); if (ws is null) throw new InvalidOperationException("Writing system not found"); - return Task.FromResult(FromLcmWritingSystem(ws)); + return Task.FromResult(FromLcmWritingSystem(ws, type)); } public Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) @@ -363,6 +367,7 @@ private Sense FromLexSense(ILexSense sense) var s = new Sense { Id = sense.Guid, + EntryId = sense.Entry.Guid, Gloss = FromLcmMultiString(sense.Gloss), Definition = FromLcmMultiString(sense.Definition), PartOfSpeech = sense.MorphoSyntaxAnalysisRA?.GetPartOfSpeech()?.Name.get_String(enWs).Text ?? "", @@ -373,17 +378,18 @@ private Sense FromLexSense(ILexSense sense) Name = FromLcmMultiString(s.Name), Code = s.OcmCodes }).ToList(), - ExampleSentences = sense.ExamplesOS.Select(FromLexExampleSentence).ToList() + ExampleSentences = sense.ExamplesOS.Select(sentence => FromLexExampleSentence(sense.Guid, sentence)).ToList() }; return s; } - private ExampleSentence FromLexExampleSentence(ILexExampleSentence sentence) + private ExampleSentence FromLexExampleSentence(Guid senseGuid, ILexExampleSentence sentence) { var translation = sentence.TranslationsOC.FirstOrDefault()?.Translation; return new ExampleSentence { Id = sentence.Guid, + SenseId = senseGuid, Sentence = FromLcmMultiString(sentence.Example), Reference = sentence.Reference.Text, Translation = translation is null ? new MultiString() : FromLcmMultiString(translation), @@ -686,7 +692,7 @@ public Task CreateExampleSentence(Guid entryId, Guid senseId, E "Remove example sentence", Cache.ServiceLocator.ActionHandler, () => CreateExampleSentence(lexSense, exampleSentence)); - return Task.FromResult(FromLexExampleSentence(ExampleSentenceRepository.GetObject(exampleSentence.Id))); + return Task.FromResult(FromLexExampleSentence(senseId, ExampleSentenceRepository.GetObject(exampleSentence.Id))); } public Task UpdateExampleSentence(Guid entryId, @@ -704,7 +710,7 @@ public Task UpdateExampleSentence(Guid entryId, var updateProxy = new UpdateExampleSentenceProxy(lexExampleSentence, this); update.Apply(updateProxy); }); - return Task.FromResult(FromLexExampleSentence(lexExampleSentence)); + return Task.FromResult(FromLexExampleSentence(senseId, lexExampleSentence)); } public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 9174e6ebd..c51f1022d 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -120,8 +120,8 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() { var crdtApi = _fixture.CrdtApi; var fwdataApi = _fixture.FwDataApi; - await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = new WritingSystemId("es"), Name = "Spanish", Abbreviation = "es", Font = "Arial" }); - await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = new WritingSystemId("fr"), Name = "French", Abbreviation = "fr", Font = "Arial" }); + await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = Guid.NewGuid(), Type = WritingSystemType.Vernacular, WsId = new WritingSystemId("es"), Name = "Spanish", Abbreviation = "es", Font = "Arial" }); + await fwdataApi.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = Guid.NewGuid(), Type = WritingSystemType.Vernacular, WsId = new WritingSystemId("fr"), Name = "French", Abbreviation = "fr", Font = "Arial" }); await _syncService.Sync(crdtApi, fwdataApi); await crdtApi.UpdateEntry(_testEntry.Id, new UpdateObjectInput().Set(entry => entry.LexemeForm["es"],"Manzana")); diff --git a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs index 6bf34b549..be158c8e7 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs @@ -20,8 +20,8 @@ public void EntryDiffShouldUpdateAllFields() var entryDiffToUpdate = CrdtFwdataProjectSyncService.EntryDiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(entryDiffToUpdate); entryDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id) - .Excluding(x => x.Senses) + previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id) + .Excluding(x => x.DeletedAt).Excluding(x => x.Senses) .Excluding(x => x.Components) .Excluding(x => x.ComplexForms) .Excluding(x => x.ComplexFormTypes)); @@ -35,7 +35,7 @@ public async Task SenseDiffShouldUpdateAllFields() var senseDiffToUpdate = await CrdtFwdataProjectSyncService.SenseDiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(senseDiffToUpdate); senseDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id).Excluding(x => x.ExampleSentences)); + previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id).Excluding(x => x.EntryId).Excluding(x => x.DeletedAt).Excluding(x => x.ExampleSentences)); } [Fact] @@ -46,6 +46,6 @@ public void ExampleSentenceDiffShouldUpdateAllFields() var exampleSentenceDiffToUpdate = CrdtFwdataProjectSyncService.ExampleDiffToUpdate(previous, current); ArgumentNullException.ThrowIfNull(exampleSentenceDiffToUpdate); exampleSentenceDiffToUpdate.Apply(previous); - previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id)); + previous.Should().BeEquivalentTo(current, options => options.Excluding(x => x.Id).Excluding(x => x.SenseId).Excluding(x => x.DeletedAt)); } } diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index 30e1b3f8b..e30ba7cdf 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -31,7 +31,7 @@ public async Task UpdateWritingSystem(WritingSystemId id, { WritingSystemType.Vernacular => ws.Vernacular, WritingSystemType.Analysis => ws.Analysis - }).First(w => w.Id == id); + }).First(w => w.WsId == id); } public IAsyncEnumerable GetPartsOfSpeech() diff --git a/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs b/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs index ea289e6ad..40572b47d 100644 --- a/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs +++ b/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs @@ -13,13 +13,13 @@ public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, in foreach (var ws in writingSystems.Analysis) { await importTo.CreateWritingSystem(WritingSystemType.Analysis, ws); - logger.LogInformation("Imported ws {WsId}", ws.Id); + logger.LogInformation("Imported ws {WsId}", ws.WsId); } foreach (var ws in writingSystems.Vernacular) { await importTo.CreateWritingSystem(WritingSystemType.Vernacular, ws); - logger.LogInformation("Imported ws {WsId}", ws.Id); + logger.LogInformation("Imported ws {WsId}", ws.WsId); } await foreach (var partOfSpeech in importFrom.GetPartsOfSpeech()) diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs index dcf895209..5f5be9ed1 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs @@ -76,7 +76,7 @@ public async Task DeleteEntryComponent() complexEntry.Should().NotBeNull(); var component = complexEntry!.Components.First(); - await fixture.DataModel.AddChange(Guid.NewGuid(), new DeleteChange(component.Id)); + await fixture.DataModel.AddChange(Guid.NewGuid(), new DeleteChange(component.Id)); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); complexEntry!.Components.Should().NotContain(c => c.Id == component.Id); diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/JsonPatchChangeTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/JsonPatchChangeTests.cs index 32bca32b1..0bcc627dd 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/JsonPatchChangeTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/JsonPatchChangeTests.cs @@ -34,7 +34,7 @@ public void NewChangeIPatchDoc_ThrowsForRemoveAtIndex() { var patch = new JsonPatchDocument(); patch.Operations.Add(new Operation("remove", "/senses/1", null, null)); - var act = () => new JsonPatchChange(Guid.NewGuid(), patch, JsonSerializerOptions.Default); + var act = () => new JsonPatchChange(Guid.NewGuid(), patch); act.Should().Throw(); } diff --git a/backend/FwLite/LcmCrdt.Tests/Data/FilteringTests.cs b/backend/FwLite/LcmCrdt.Tests/Data/FilteringTests.cs index 6429701f7..4807d40f7 100644 --- a/backend/FwLite/LcmCrdt.Tests/Data/FilteringTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Data/FilteringTests.cs @@ -1,6 +1,5 @@ using LcmCrdt.Data; using MiniLcm.Models; -using Entry = LcmCrdt.Objects.Entry; namespace LcmCrdt.Tests.Data; diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt new file mode 100644 index 000000000..b20d42518 --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt @@ -0,0 +1,118 @@ +{ + DerivedTypes: [ + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:Entry + }, + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:Sense + }, + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:ExampleSentence + }, + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:WritingSystem + }, + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:PartOfSpeech + }, + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:SemanticDomain + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:Entry + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:Sense + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:ExampleSentence + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:WritingSystem + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:PartOfSpeech + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:SemanticDomain + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:ComplexFormType + }, + { + DerivedType: DeleteChange, + TypeDiscriminator: delete:ComplexFormComponent + }, + { + DerivedType: SetPartOfSpeechChange, + TypeDiscriminator: SetPartOfSpeechChange + }, + { + DerivedType: AddSemanticDomainChange, + TypeDiscriminator: AddSemanticDomainChange + }, + { + DerivedType: RemoveSemanticDomainChange, + TypeDiscriminator: RemoveSemanticDomainChange + }, + { + DerivedType: ReplaceSemanticDomainChange, + TypeDiscriminator: ReplaceSemanticDomainChange + }, + { + DerivedType: CreateEntryChange, + TypeDiscriminator: CreateEntryChange + }, + { + DerivedType: CreateSenseChange, + TypeDiscriminator: CreateSenseChange + }, + { + DerivedType: CreateExampleSentenceChange, + TypeDiscriminator: CreateExampleSentenceChange + }, + { + DerivedType: CreatePartOfSpeechChange, + TypeDiscriminator: CreatePartOfSpeechChange + }, + { + DerivedType: CreateSemanticDomainChange, + TypeDiscriminator: CreateSemanticDomainChange + }, + { + DerivedType: CreateWritingSystemChange, + TypeDiscriminator: CreateWritingSystemChange + }, + { + DerivedType: AddComplexFormTypeChange, + TypeDiscriminator: AddComplexFormTypeChange + }, + { + DerivedType: AddEntryComponentChange, + TypeDiscriminator: AddEntryComponentChange + }, + { + DerivedType: RemoveComplexFormTypeChange, + TypeDiscriminator: RemoveComplexFormTypeChange + }, + { + DerivedType: CreateComplexFormType, + TypeDiscriminator: CreateComplexFormType + } + ], + IgnoreUnrecognizedTypeDiscriminators: false, + TypeDiscriminatorPropertyName: $type +} \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt new file mode 100644 index 000000000..18a0d6aca --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt @@ -0,0 +1,330 @@ +Model: + EntityType: ProjectData + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + ClientId (Guid) Required + Name (string) Required + OriginDomain (string) + Keys: + Id PK + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: ProjectData + Relational:ViewName: + Relational:ViewSchema: + EntityType: ComplexFormComponent + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + ComplexFormEntryId (Guid) Required FK Index + ComplexFormHeadword (string) + ComponentEntryId (Guid) Required FK Index + ComponentHeadword (string) + ComponentSenseId (Guid?) FK Index + DeletedAt (DateTimeOffset?) + SnapshotId (no field, Guid?) Shadow FK Index + Keys: + Id PK + Foreign keys: + ComplexFormComponent {'ComplexFormEntryId'} -> Entry {'Id'} Required Cascade ToDependent: Components + ComplexFormComponent {'ComponentEntryId'} -> Entry {'Id'} Required Cascade ToDependent: ComplexForms + ComplexFormComponent {'ComponentSenseId'} -> Sense {'Id'} Cascade + ComplexFormComponent {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + ComplexFormEntryId + ComponentEntryId + ComponentSenseId + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: ComplexFormComponents + Relational:ViewName: + Relational:ViewSchema: + EntityType: ComplexFormType + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + DeletedAt (DateTimeOffset?) + Name (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + SnapshotId (no field, Guid?) Shadow FK Index + Keys: + Id PK + Foreign keys: + ComplexFormType {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: ComplexFormType + Relational:ViewName: + Relational:ViewSchema: + EntityType: Entry + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + CitationForm (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + ComplexFormTypes (IList) Required + Annotations: + Relational:ColumnType: jsonb + DeletedAt (DateTimeOffset?) + LexemeForm (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + LiteralMeaning (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + Note (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + SnapshotId (no field, Guid?) Shadow FK Index + Navigations: + ComplexForms (IList) Collection ToDependent ComplexFormComponent + Components (IList) Collection ToDependent ComplexFormComponent + Senses (IList) Collection ToDependent Sense + Keys: + Id PK + Foreign keys: + Entry {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: Entry + Relational:ViewName: + Relational:ViewSchema: + EntityType: ExampleSentence + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + DeletedAt (DateTimeOffset?) + Reference (string) + SenseId (Guid) Required FK Index + Sentence (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + SnapshotId (no field, Guid?) Shadow FK Index + Translation (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + Keys: + Id PK + Foreign keys: + ExampleSentence {'SenseId'} -> Sense {'Id'} Required Cascade ToDependent: ExampleSentences + ExampleSentence {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + SenseId + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: ExampleSentence + Relational:ViewName: + Relational:ViewSchema: + EntityType: PartOfSpeech + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + DeletedAt (DateTimeOffset?) + Name (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + Predefined (bool) Required + SnapshotId (no field, Guid?) Shadow FK Index + Keys: + Id PK + Foreign keys: + PartOfSpeech {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: PartOfSpeech + Relational:ViewName: + Relational:ViewSchema: + EntityType: SemanticDomain + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + Code (string) Required + DeletedAt (DateTimeOffset?) + Name (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + Predefined (bool) Required + SnapshotId (no field, Guid?) Shadow FK Index + Keys: + Id PK + Foreign keys: + SemanticDomain {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: SemanticDomain + Relational:ViewName: + Relational:ViewSchema: + EntityType: Sense + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + Definition (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + DeletedAt (DateTimeOffset?) + EntryId (Guid) Required FK Index + Gloss (MultiString) Required + Annotations: + Relational:ColumnType: jsonb + PartOfSpeech (string) Required + PartOfSpeechId (Guid?) + SemanticDomains (IList) Required + Annotations: + Relational:ColumnType: jsonb + SnapshotId (no field, Guid?) Shadow FK Index + Navigations: + ExampleSentences (IList) Collection ToDependent ExampleSentence + Keys: + Id PK + Foreign keys: + Sense {'EntryId'} -> Entry {'Id'} Required Cascade ToDependent: Senses + Sense {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + EntryId + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: Sense + Relational:ViewName: + Relational:ViewSchema: + EntityType: WritingSystem + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + Abbreviation (string) Required + DeletedAt (DateTimeOffset?) + Exemplars (string[]) Required + Annotations: + Relational:ColumnType: jsonb + Font (string) Required + Name (string) Required + Order (double) Required + SnapshotId (no field, Guid?) Shadow FK Index + Type (WritingSystemType) Required + WsId (WritingSystemId) Required + Keys: + Id PK + Foreign keys: + WritingSystem {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull + Indexes: + SnapshotId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: WritingSystem + Relational:ViewName: + Relational:ViewSchema: + EntityType: Commit + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + ClientId (Guid) Required + Hash (string) Required + Metadata (CommitMetadata) Required + Annotations: + Relational:ColumnType: jsonb + ParentHash (string) Required + Navigations: + ChangeEntities (List>) Collection ToDependent ChangeEntity + Snapshots (List) Collection ToDependent ObjectSnapshot Inverse: Commit + Complex properties: + HybridDateTime (HybridDateTime) Required + ComplexType: Commit.HybridDateTime#HybridDateTime + Properties: + Counter (long) Required + Annotations: + Relational:ColumnName: Counter + DateTime (DateTimeOffset) Required + Annotations: + Relational:ColumnName: DateTime + Keys: + Id PK + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: Commits + Relational:ViewName: + Relational:ViewSchema: + EntityType: ChangeEntity + Properties: + CommitId (Guid) Required PK FK AfterSave:Throw + Index (int) Required PK AfterSave:Throw + Change (IChange) + Annotations: + Relational:ColumnType: jsonb + EntityId (Guid) Required + Keys: + CommitId, Index PK + Foreign keys: + ChangeEntity {'CommitId'} -> Commit {'Id'} Required Cascade ToDependent: ChangeEntities + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: ChangeEntities + Relational:ViewName: + Relational:ViewSchema: + EntityType: ObjectSnapshot + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + CommitId (Guid) Required FK Index + Entity (IObjectBase) Required + Annotations: + Relational:ColumnType: jsonb + EntityId (Guid) Required Index + EntityIsDeleted (bool) Required + IsRoot (bool) Required + References (Guid[]) Required Element type: Guid Required + Annotations: + ElementType: Element type: Guid Required + TypeName (string) Required + Navigations: + Commit (Commit) ToPrincipal Commit Inverse: Snapshots + Keys: + Id PK + Foreign keys: + ObjectSnapshot {'CommitId'} -> Commit {'Id'} Required Cascade ToDependent: Snapshots ToPrincipal: Commit + Indexes: + CommitId, EntityId Unique + Annotations: + DiscriminatorProperty: + Relational:FunctionName: + Relational:Schema: + Relational:SqlQuery: + Relational:TableName: Snapshots + Relational:ViewName: + Relational:ViewSchema: +Annotations: + ProductVersion: 8.0.4 \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt new file mode 100644 index 000000000..e23180b0b --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectBaseModels.verified.txt @@ -0,0 +1,10 @@ +{ + DerivedTypes: [ + { + DerivedType: MiniLcmCrdtAdapter, + TypeDiscriminator: MiniLcmCrdtAdapter + } + ], + IgnoreUnrecognizedTypeDiscriminators: false, + TypeDiscriminatorPropertyName: $type +} \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectWithIdModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectWithIdModels.verified.txt new file mode 100644 index 000000000..109722b5d --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyIObjectWithIdModels.verified.txt @@ -0,0 +1,38 @@ +{ + DerivedTypes: [ + { + DerivedType: Entry, + TypeDiscriminator: Entry + }, + { + DerivedType: Sense, + TypeDiscriminator: Sense + }, + { + DerivedType: ExampleSentence, + TypeDiscriminator: ExampleSentence + }, + { + DerivedType: WritingSystem, + TypeDiscriminator: WritingSystem + }, + { + DerivedType: PartOfSpeech, + TypeDiscriminator: PartOfSpeech + }, + { + DerivedType: SemanticDomain, + TypeDiscriminator: SemanticDomain + }, + { + DerivedType: ComplexFormType, + TypeDiscriminator: ComplexFormType + }, + { + DerivedType: ComplexFormComponent, + TypeDiscriminator: ComplexFormComponent + } + ], + IgnoreUnrecognizedTypeDiscriminators: false, + TypeDiscriminatorPropertyName: $type +} \ No newline at end of file diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs new file mode 100644 index 000000000..7118f61e0 --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs @@ -0,0 +1,92 @@ +using FluentAssertions.Execution; +using LcmCrdt.Objects; +using LcmCrdt.Tests.Mocks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SIL.Harmony.Changes; +using SIL.Harmony.Entities; +using Soenneker.Utils.AutoBogus; + +namespace LcmCrdt.Tests; + +public class DataModelSnapshotTests : IAsyncLifetime +{ + protected readonly AsyncServiceScope _services; + private readonly LcmCrdtDbContext _crdtDbContext; + private CrdtConfig _crdtConfig; + public DataModelSnapshotTests() + { + + var services = new ServiceCollection() + .AddLcmCrdtClient() + .AddLogging(builder => builder.AddDebug()) + .RemoveAll(typeof(ProjectContext)) + .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", ":memory:"))) + .BuildServiceProvider(); + _services = services.CreateAsyncScope(); + _crdtDbContext = _services.ServiceProvider.GetRequiredService(); + _crdtConfig = _services.ServiceProvider.GetRequiredService>().Value; + } + + public async Task InitializeAsync() + { + await _crdtDbContext.Database.OpenConnectionAsync(); + //can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db. + await ProjectsService.InitProjectDb(_crdtDbContext, + new ProjectData("Sena 3", Guid.NewGuid(), null, Guid.NewGuid())); + await _services.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); + } + + public async Task DisposeAsync() + { + await _services.DisposeAsync(); + } + + [Fact] + public async Task VerifyDbModel() + { + await Verify(_crdtDbContext.Model.ToDebugString(MetadataDebugStringOptions.LongDefault)); + } + + [Fact] + public async Task VerifyChangeModels() + { + var jsonSerializerOptions = _crdtConfig.JsonSerializerOptions; + await Verify(jsonSerializerOptions.GetTypeInfo(typeof(IChange)).PolymorphismOptions); + } + + [Fact] + public async Task VerifyIObjectBaseModels() + { + var jsonSerializerOptions = _crdtConfig.JsonSerializerOptions; + await Verify(jsonSerializerOptions.GetTypeInfo(typeof(IObjectBase)).PolymorphismOptions); + } + + [Fact] + public async Task VerifyIObjectWithIdModels() + { + var jsonSerializerOptions = _crdtConfig.JsonSerializerOptions; + await Verify(jsonSerializerOptions.GetTypeInfo(typeof(IObjectWithId)).PolymorphismOptions); + } + + [Fact] + public void VerifyIObjectWithIdsMatchAdapterGetObjectTypeName() + { + var faker = new AutoFaker(); + var jsonSerializerOptions = _crdtConfig.JsonSerializerOptions; + var types = jsonSerializerOptions.GetTypeInfo(typeof(IObjectWithId)).PolymorphismOptions?.DerivedTypes ?? []; + using (new AssertionScope()) + { + foreach (var jsonDerivedType in types) + { + var typeDiscriminator = jsonDerivedType.TypeDiscriminator.Should().BeOfType().Subject; + var obj = faker.Generate(jsonDerivedType.DerivedType); + new MiniLcmCrdtAdapter((IObjectWithId)obj).GetObjectTypeName().Should().Be(typeDiscriminator); + } + } + } +} diff --git a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs index 99786bcdd..22fb0cebf 100644 --- a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs @@ -29,8 +29,8 @@ public static IEnumerable GetEntityTypes() [MemberData(nameof(GetEntityTypes))] public void EntityCopyMethodShouldCopyAllFields(Type type) { - type.IsAssignableTo(typeof(IObjectBase)).Should().BeTrue(); - var entity = (IObjectBase) _autoFaker.Generate(type); + type.IsAssignableTo(typeof(IObjectWithId)).Should().BeTrue(); + var entity = (IObjectWithId) _autoFaker.Generate(type); var copy = entity.Copy(); copy.Should().BeEquivalentTo(entity, options => options.IncludingAllRuntimeProperties()); } diff --git a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs index d09cf5a63..b2370c85e 100644 --- a/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/JsonPatchEntryRewriteTests.cs @@ -1,9 +1,7 @@ using LcmCrdt.Changes.Entries; using LcmCrdt.Objects; -using MiniLcm.Models; using SIL.Harmony.Changes; using SystemTextJsonPatch; -using Entry = LcmCrdt.Objects.Entry; namespace LcmCrdt.Tests; @@ -17,7 +15,7 @@ public void ChangesFromJsonPatch_AddComponentMakesAddEntryComponentChange() var patch = new JsonPatchDocument(); var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "component" } } }; patch.Add(entry => entry.Components, ComplexFormComponent.FromEntries(_entry, componentEntry)); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var addEntryComponentChange = changes.Should().ContainSingle().Which.Should().BeOfType().Subject; addEntryComponentChange.ComplexFormEntryId.Should().Be(_entry.Id); @@ -33,9 +31,9 @@ public void ChangesFromJsonPatch_RemoveComponentMakesDeleteChange() var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); _entry.Components.Add(complexFormComponent); patch.Remove(entry => entry.Components, 0); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var removeEntryComponentChange = changes.Should().ContainSingle().Which.Should() - .BeOfType>().Subject; + .BeOfType>().Subject; removeEntryComponentChange.EntityId.Should().Be(complexFormComponent.Id); } @@ -48,7 +46,7 @@ public void ChangesFromJsonPatch_ReplaceComponentMakesReplaceEntryComponentChang _entry.Components.Add(complexFormComponent); var newComponentId = Guid.NewGuid(); patch.Replace(entry => entry.Components, complexFormComponent with { ComponentEntryId = newComponentId, ComponentHeadword = "new" }, 0); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() .BeOfType().Subject; setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); @@ -66,7 +64,7 @@ public void ChangesFromJsonPatch_ReplaceComponentWithNewComplexFormIdMakesReplac _entry.Components.Add(complexFormComponent); var newComplexFormId = Guid.NewGuid(); patch.Replace(entry => entry.Components, complexFormComponent with { ComplexFormEntryId = newComplexFormId, ComplexFormHeadword = "new" }, 0); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() .BeOfType().Subject; setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); @@ -81,7 +79,7 @@ public void ChangesFromJsonPatch_AddComplexFormMakesAddEntryComponentChange() var patch = new JsonPatchDocument(); var componentEntry = new Entry() { Id = Guid.NewGuid(), LexemeForm = { { "en", "complex form" } } }; patch.Add(entry => entry.ComplexForms, ComplexFormComponent.FromEntries(_entry, componentEntry)); - var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var changes = componentEntry.ToChanges(patch); var addEntryComponentChange = changes.Should().ContainSingle().Which.Should().BeOfType().Subject; addEntryComponentChange.ComplexFormEntryId.Should().Be(_entry.Id); @@ -97,9 +95,9 @@ public void ChangesFromJsonPatch_RemoveComplexFormMakesDeleteChange() var complexFormComponent = ComplexFormComponent.FromEntries(_entry, componentEntry); componentEntry.ComplexForms.Add(complexFormComponent); patch.Remove(entry => entry.ComplexForms, 0); - var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var changes = componentEntry.ToChanges(patch); var removeEntryComponentChange = changes.Should().ContainSingle().Which.Should() - .BeOfType>().Subject; + .BeOfType>().Subject; removeEntryComponentChange.EntityId.Should().Be(complexFormComponent.Id); } @@ -112,7 +110,7 @@ public void ChangesFromJsonPatch_ReplaceComplexFormMakesReplaceEntryComponentCha componentEntry.ComplexForms.Add(complexFormComponent); var newComponentId = Guid.NewGuid(); patch.Replace(entry => entry.ComplexForms, complexFormComponent with { ComponentEntryId = newComponentId, ComponentHeadword = "new" }, 0); - var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var changes = componentEntry.ToChanges(patch); var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() .BeOfType().Subject; setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); @@ -130,7 +128,7 @@ public void ChangesFromJsonPatch_ReplaceComplexFormWithNewComplexFormIdMakesRepl componentEntry.ComplexForms.Add(complexFormComponent); var newComplexFormId = Guid.NewGuid(); patch.Replace(entry => entry.ComplexForms, complexFormComponent with { ComplexFormEntryId = newComplexFormId, ComplexFormHeadword = "new" }, 0); - var changes = Entry.ChangesFromJsonPatch(componentEntry, patch); + var changes = componentEntry.ToChanges(patch); var setComplexFormComponentChange = changes.Should().ContainSingle().Which.Should() .BeOfType().Subject; setComplexFormComponentChange.EntityId.Should().Be(complexFormComponent.Id); @@ -145,7 +143,7 @@ public void ChangesFromJsonPatch_AddComplexFormTypeMakesAddComplexFormTypeChange var patch = new JsonPatchDocument(); var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; patch.Add(entry => entry.ComplexFormTypes, complexFormType); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var addComplexFormTypeChange = changes.Should().ContainSingle().Which.Should().BeOfType().Subject; addComplexFormTypeChange.EntityId.Should().Be(_entry.Id); @@ -159,7 +157,7 @@ public void ChangesFromJsonPatch_RemoveComplexFormTypeMakesRemoveComplexFormType var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; _entry.ComplexFormTypes.Add(complexFormType); patch.Remove(entry => entry.ComplexFormTypes, 0); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var removeComplexFormTypeChange = changes.Should().ContainSingle().Which.Should() .BeOfType().Subject; removeComplexFormTypeChange.EntityId.Should().Be(_entry.Id); @@ -174,7 +172,7 @@ public void ChangesFromJsonPatch_ReplaceComplexFormTypeMakesReplaceComplexFormTy _entry.ComplexFormTypes.Add(complexFormType); var newComplexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new MultiString() }; patch.Replace(entry => entry.ComplexFormTypes, newComplexFormType, 0); - var changes = Entry.ChangesFromJsonPatch(_entry, patch); + var changes = _entry.ToChanges(patch); var replaceComplexFormTypeChange = changes.Should().ContainSingle().Which.Should() .BeOfType().Subject; replaceComplexFormTypeChange.EntityId.Should().Be(_entry.Id); diff --git a/backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs b/backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs index 3b42f66e2..47df869f5 100644 --- a/backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs @@ -1,10 +1,9 @@ using System.Text.Json; using LcmCrdt.Changes; +using LcmCrdt.Objects; using MiniLcm.Models; using SystemTextJsonPatch; using SystemTextJsonPatch.Operations; -using SemanticDomain = LcmCrdt.Objects.SemanticDomain; -using Sense = LcmCrdt.Objects.Sense; namespace LcmCrdt.Tests; @@ -28,7 +27,7 @@ public void RewritePartOfSpeechChangesIntoSetPartOfSpeechChange() _patchDocument.Replace(s => s.PartOfSpeechId, newPartOfSpeechId); _patchDocument.Replace(s => s.Gloss["en"], "new gloss"); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var setPartOfSpeechChange = changes.OfType().Should().ContainSingle().Subject; setPartOfSpeechChange.EntityId.Should().Be(_sense.Id); @@ -45,7 +44,7 @@ public void JsonPatchChangeRewriteDoesNotReturnEmptyPatchChanges() var newPartOfSpeechId = Guid.NewGuid(); _patchDocument.Replace(s => s.PartOfSpeechId, newPartOfSpeechId); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var setPartOfSpeechChange = changes.Should().ContainSingle() .Subject.Should().BeOfType().Subject; @@ -60,7 +59,7 @@ public void RewritesAddSemanticDomainChangesIntoAddSemanticDomainChange() _patchDocument.Add(s => s.SemanticDomains, new SemanticDomain() { Id = newSemanticDomainId, Code = "new code", Name = new MultiString() }); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var addSemanticDomainChange = (AddSemanticDomainChange)changes.Should().AllBeOfType().And.ContainSingle().Subject; addSemanticDomainChange.EntityId.Should().Be(_sense.Id); @@ -74,7 +73,7 @@ public void RewritesAddSemanticDomainChangesIntoAddSemanticDomainChange_JsonElem _patchDocument.Operations.Add(new Operation("add", "/semanticDomains/-", null, JsonSerializer.Deserialize("""{"deletedAt":null,"predefined":true,"id":"46e4fe08-ffa0-4c8b-bf88-2c56138904d1","name":{"en":"Sky"},"code":"1.1"}"""))); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var addSemanticDomainChange = (AddSemanticDomainChange)changes.Should().AllBeOfType().And.ContainSingle().Subject; addSemanticDomainChange.EntityId.Should().Be(_sense.Id); @@ -88,7 +87,7 @@ public void RewritesReplaceSemanticDomainPatchChangesIntoReplaceSemanticDomainCh var newSemanticDomainId = Guid.NewGuid(); _patchDocument.Replace(s => s.SemanticDomains, new SemanticDomain() { Id = newSemanticDomainId, Code = "new code", Name = new MultiString() }, 0); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var replaceSemanticDomainChange = (ReplaceSemanticDomainChange)changes.Should().AllBeOfType().And.ContainSingle().Subject; replaceSemanticDomainChange.EntityId.Should().Be(_sense.Id); @@ -102,7 +101,7 @@ public void RewritesReplaceNoIndexSemanticDomainPatchChangesIntoReplaceSemanticD var newSemanticDomainId = Guid.NewGuid(); _patchDocument.Replace(s => s.SemanticDomains, new SemanticDomain() { Id = newSemanticDomainId, Code = "new code", Name = new MultiString() }); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var replaceSemanticDomainChange = (ReplaceSemanticDomainChange)changes.Should().AllBeOfType().And.ContainSingle().Subject; replaceSemanticDomainChange.EntityId.Should().Be(_sense.Id); @@ -116,7 +115,7 @@ public void RewritesRemoveSemanticDomainPatchChangesIntoReplaceSemanticDomainCha var semanticDomainIdToRemove = _sense.SemanticDomains[0].Id; _patchDocument.Remove(s => s.SemanticDomains, 0); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var removeSemanticDomainChange = (RemoveSemanticDomainChange)changes.Should().AllBeOfType< RemoveSemanticDomainChange>().And.ContainSingle().Subject; @@ -130,7 +129,7 @@ public void RewritesRemoveNoIndexSemanticDomainPatchChangesIntoReplaceSemanticDo var semanticDomainIdToRemove = _sense.SemanticDomains[0].Id; _patchDocument.Remove(s => s.SemanticDomains); - var changes = Sense.ChangesFromJsonPatch(_sense, _patchDocument).ToArray(); + var changes = _sense.ToChanges(_patchDocument).ToArray(); var removeSemanticDomainChange = (RemoveSemanticDomainChange)changes.Should().AllBeOfType< RemoveSemanticDomainChange>().And.ContainSingle().Subject; diff --git a/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj b/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj index d0b0c2ca5..870bf38bb 100644 --- a/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj +++ b/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj @@ -18,7 +18,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -32,6 +33,7 @@ + diff --git a/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs b/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs index 22ac41ac8..27b867130 100644 --- a/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/LexboxApiTests.cs @@ -1,16 +1,11 @@ -using SIL.Harmony; -using SIL.Harmony.Db; -using LcmCrdt.Changes; +using LcmCrdt.Changes; using LcmCrdt.Tests.Mocks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using MiniLcm; -using MiniLcm.Models; using Entry = MiniLcm.Models.Entry; -using ExampleSentence = MiniLcm.Models.ExampleSentence; -using Sense = MiniLcm.Models.Sense; namespace LcmCrdt.Tests; @@ -47,7 +42,9 @@ public virtual async Task InitializeAsync() await _api.CreateWritingSystem(WritingSystemType.Analysis, new WritingSystem() { - Id = "en", + Id = Guid.NewGuid(), + Type = WritingSystemType.Analysis, + WsId = "en", Name = "English", Abbreviation = "En", Font = "Arial", @@ -56,7 +53,9 @@ await _api.CreateWritingSystem(WritingSystemType.Analysis, await _api.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { - Id = "en", + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = "en", Name = "English", Abbreviation = "En", Font = "Arial", @@ -173,8 +172,8 @@ public async Task GetWritingSystems() [Fact] public async Task CreatingMultipleWritingSystems_DoesNotHaveDuplicateOrders() { - await _api.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = "test-2", Name = "test", Abbreviation = "test", Font = "Arial", Exemplars = new[] { "test" } }); - var writingSystems = await DataModel.GetLatestObjects().Where(ws => ws.Type == WritingSystemType.Vernacular).ToArrayAsync(); + await _api.CreateWritingSystem(WritingSystemType.Vernacular, new WritingSystem() { Id = Guid.NewGuid(), Type = WritingSystemType.Vernacular, WsId = "test-2", Name = "test", Abbreviation = "test", Font = "Arial", Exemplars = new[] { "test" } }); + var writingSystems = await DataModel.GetLatestObjects().Where(ws => ws.Type == WritingSystemType.Vernacular).ToArrayAsync(); writingSystems.GroupBy(ws => ws.Order).Should().NotContain(g => g.Count() > 1); } @@ -395,7 +394,7 @@ public async Task CreateSense_WillCreateWithExistingDomains() var senseId = Guid.NewGuid(); var semanticDomainId = Guid.NewGuid(); await DataModel.AddChange(Guid.NewGuid(), new CreateSemanticDomainChange(semanticDomainId, new MultiString() { { "en", "test" } }, "test")); - var semanticDomain = await DataModel.GetLatest(semanticDomainId); + var semanticDomain = await DataModel.GetLatest(semanticDomainId); ArgumentNullException.ThrowIfNull(semanticDomain); var createdSense = await _api.CreateSense(_entry1Id, new Sense() { @@ -422,7 +421,7 @@ public async Task CreateSense_WillCreateWthExistingPartOfSpeech() var senseId = Guid.NewGuid(); var partOfSpeechId = Guid.NewGuid(); await DataModel.AddChange(Guid.NewGuid(), new CreatePartOfSpeechChange(partOfSpeechId, new MultiString() { { "en", "test" } })); - var partOfSpeech = await DataModel.GetLatest(partOfSpeechId); + var partOfSpeech = await DataModel.GetLatest(partOfSpeechId); ArgumentNullException.ThrowIfNull(partOfSpeech); var createdSense = await _api.CreateSense(_entry1Id, new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = partOfSpeechId, }); @@ -473,7 +472,7 @@ public async Task UpdateSenseSemanticDomain() { var newDomainId = Guid.NewGuid(); await DataModel.AddChange(Guid.NewGuid(), new CreateSemanticDomainChange(newDomainId, new MultiString() { { "en", "test" } }, "updated")); - var newSemanticDomain = await DataModel.GetLatest(newDomainId); + var newSemanticDomain = await DataModel.GetLatest(newDomainId); ArgumentNullException.ThrowIfNull(newSemanticDomain); var entry = await _api.CreateEntry(new Entry { @@ -513,7 +512,7 @@ public async Task RemoveSenseSemanticDomain() { var newDomainId = Guid.NewGuid(); await DataModel.AddChange(Guid.NewGuid(), new CreateSemanticDomainChange(newDomainId, new MultiString() { { "en", "test" } }, "updated")); - var newSemanticDomain = await DataModel.GetLatest(newDomainId); + var newSemanticDomain = await DataModel.GetLatest(newDomainId); ArgumentNullException.ThrowIfNull(newSemanticDomain); var entry = await _api.CreateEntry(new Entry { diff --git a/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs b/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs index 04ef90fcf..268255141 100644 --- a/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs @@ -1,9 +1,6 @@ using System.Text.Json; using MiniLcm.Models; using Xunit.Abstractions; -using Entry = LcmCrdt.Objects.Entry; -using Sense = LcmCrdt.Objects.Sense; -using ExampleSentence = LcmCrdt.Objects.ExampleSentence; namespace LcmCrdt.Tests; diff --git a/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs b/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs index acc118deb..42a8c3a7c 100644 --- a/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs @@ -1,6 +1,5 @@ using SIL.Harmony.Changes; using SIL.Harmony.Entities; -using MiniLcm.Models; namespace LcmCrdt.Changes; diff --git a/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs b/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs index d097f02b2..dffec6378 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateComplexFormType.cs @@ -6,12 +6,12 @@ namespace LcmCrdt.Changes; -public class CreateComplexFormType(Guid entityId, MultiString name) : CreateChange(entityId), ISelfNamedType +public class CreateComplexFormType(Guid entityId, MultiString name) : CreateChange(entityId), ISelfNamedType { public MultiString Name { get; } = name; - public override ValueTask NewEntity(Commit commit, ChangeContext context) + public override ValueTask NewEntity(Commit commit, ChangeContext context) { - return ValueTask.FromResult(new CrdtComplexFormType + return ValueTask.FromResult(new ComplexFormType { Id = EntityId, Name = Name diff --git a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs index 1badac7d2..041601b2a 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateEntryChange.cs @@ -2,14 +2,13 @@ using SIL.Harmony; using SIL.Harmony.Changes; using SIL.Harmony.Entities; -using MiniLcm.Models; namespace LcmCrdt.Changes; public class CreateEntryChange : CreateChange, ISelfNamedType { - public CreateEntryChange(MiniLcm.Models.Entry entry) : base(entry.Id == Guid.Empty ? Guid.NewGuid() : entry.Id) + public CreateEntryChange(Entry entry) : base(entry.Id == Guid.Empty ? Guid.NewGuid() : entry.Id) { entry.Id = EntityId; LexemeForm = entry.LexemeForm; @@ -31,7 +30,7 @@ private CreateEntryChange(Guid entityId) : base(entityId) public MultiString? Note { get; set; } - public override ValueTask NewEntity(Commit commit, ChangeContext context) + public override ValueTask NewEntity(Commit commit, ChangeContext context) { return new(new Entry { diff --git a/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs index 0515f1873..d99dbe6c4 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateExampleSentenceChange.cs @@ -1,15 +1,13 @@ using System.Text.Json.Serialization; using SIL.Harmony; using SIL.Harmony.Changes; -using SIL.Harmony.Db; using SIL.Harmony.Entities; -using MiniLcm.Models; namespace LcmCrdt.Changes; public class CreateExampleSentenceChange: CreateChange, ISelfNamedType { - public CreateExampleSentenceChange(MiniLcm.Models.ExampleSentence exampleSentence, Guid senseId) + public CreateExampleSentenceChange(ExampleSentence exampleSentence, Guid senseId) : base(exampleSentence.Id == Guid.Empty ? Guid.NewGuid() : exampleSentence.Id) { exampleSentence.Id = EntityId; @@ -30,7 +28,7 @@ private CreateExampleSentenceChange(Guid entityId, Guid senseId) : base(entityId public MultiString? Translation { get; set; } public string? Reference { get; set; } - public override async ValueTask NewEntity(Commit commit, ChangeContext context) + public override async ValueTask NewEntity(Commit commit, ChangeContext context) { return new ExampleSentence { diff --git a/backend/FwLite/LcmCrdt/Changes/CreatePartOfSpeechChange.cs b/backend/FwLite/LcmCrdt/Changes/CreatePartOfSpeechChange.cs index 079503054..b169f2441 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreatePartOfSpeechChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreatePartOfSpeechChange.cs @@ -1,8 +1,6 @@ using SIL.Harmony; using SIL.Harmony.Changes; using SIL.Harmony.Entities; -using MiniLcm.Models; -using PartOfSpeech = LcmCrdt.Objects.PartOfSpeech; namespace LcmCrdt.Changes; @@ -12,8 +10,8 @@ public class CreatePartOfSpeechChange(Guid entityId, MultiString name, bool pred public MultiString Name { get; } = name; public bool Predefined { get; } = predefined; - public override async ValueTask NewEntity(Commit commit, ChangeContext context) + public override ValueTask NewEntity(Commit commit, ChangeContext context) { - return new PartOfSpeech { Id = EntityId, Name = Name, Predefined = Predefined }; + return ValueTask.FromResult(new PartOfSpeech { Id = EntityId, Name = Name, Predefined = Predefined }); } } diff --git a/backend/FwLite/LcmCrdt/Changes/CreateSemanticDomainChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateSemanticDomainChange.cs index 6b686728f..829b405e8 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateSemanticDomainChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateSemanticDomainChange.cs @@ -1,9 +1,6 @@ -using System.Text.Json.Serialization; -using SIL.Harmony; +using SIL.Harmony; using SIL.Harmony.Changes; using SIL.Harmony.Entities; -using MiniLcm.Models; -using SemanticDomain = LcmCrdt.Objects.SemanticDomain; namespace LcmCrdt.Changes; @@ -15,8 +12,8 @@ public class CreateSemanticDomainChange(Guid entityId, MultiString name, string public bool Predefined { get; } = predefined; public string Code { get; } = code; - public override async ValueTask NewEntity(Commit commit, ChangeContext context) + public override ValueTask NewEntity(Commit commit, ChangeContext context) { - return new SemanticDomain { Id = EntityId, Code = Code, Name = Name, Predefined = Predefined }; + return ValueTask.FromResult(new SemanticDomain { Id = EntityId, Code = Code, Name = Name, Predefined = Predefined }); } } diff --git a/backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs index 7b6b970c2..4682ae30d 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs @@ -1,15 +1,13 @@ using System.Text.Json.Serialization; using SIL.Harmony; using SIL.Harmony.Changes; -using SIL.Harmony.Db; using SIL.Harmony.Entities; -using MiniLcm.Models; namespace LcmCrdt.Changes; public class CreateSenseChange: CreateChange, ISelfNamedType { - public CreateSenseChange(MiniLcm.Models.Sense sense, Guid entryId) : base(sense.Id == Guid.Empty ? Guid.NewGuid() : sense.Id) + public CreateSenseChange(Sense sense, Guid entryId) : base(sense.Id == Guid.Empty ? Guid.NewGuid() : sense.Id) { sense.Id = EntityId; EntryId = entryId; @@ -33,7 +31,7 @@ private CreateSenseChange(Guid entityId, Guid entryId) : base(entityId) public Guid? PartOfSpeechId { get; set; } public IList? SemanticDomains { get; set; } - public override async ValueTask NewEntity(Commit commit, ChangeContext context) + public override async ValueTask NewEntity(Commit commit, ChangeContext context) { return new Sense { diff --git a/backend/FwLite/LcmCrdt/Changes/CreateWritingSystemChange.cs b/backend/FwLite/LcmCrdt/Changes/CreateWritingSystemChange.cs index 7fd59deda..ab9d762e1 100644 --- a/backend/FwLite/LcmCrdt/Changes/CreateWritingSystemChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/CreateWritingSystemChange.cs @@ -2,9 +2,7 @@ using System.Text.Json.Serialization; using SIL.Harmony; using SIL.Harmony.Changes; -using SIL.Harmony.Db; using SIL.Harmony.Entities; -using MiniLcm.Models; namespace LcmCrdt.Changes; @@ -19,10 +17,10 @@ public class CreateWritingSystemChange : CreateChange, ISelfNamed public required double Order { get; init; } [SetsRequiredMembers] - public CreateWritingSystemChange(MiniLcm.Models.WritingSystem writingSystem, WritingSystemType type, Guid entityId, double order) : + public CreateWritingSystemChange(WritingSystem writingSystem, WritingSystemType type, Guid entityId, double order) : base(entityId) { - WsId = writingSystem.Id; + WsId = writingSystem.WsId; Name = writingSystem.Name; Abbreviation = writingSystem.Abbreviation; Font = writingSystem.Font; @@ -36,10 +34,11 @@ private CreateWritingSystemChange(Guid entityId) : base(entityId) { } - public override ValueTask NewEntity(Commit commit, ChangeContext context) + public override ValueTask NewEntity(Commit commit, ChangeContext context) { - return new(new WritingSystem(EntityId) + return ValueTask.FromResult(new WritingSystem { + Id = EntityId, WsId = WsId, Name = Name, Abbreviation = Abbreviation, diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs index b8188f9e4..a51313198 100644 --- a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs @@ -7,7 +7,7 @@ namespace LcmCrdt.Changes.Entries; -public class AddEntryComponentChange : CreateChange, ISelfNamedType +public class AddEntryComponentChange : CreateChange, ISelfNamedType { public Guid ComplexFormEntryId { get; } public string? ComplexFormHeadword { get; } @@ -39,9 +39,9 @@ public AddEntryComponentChange(Guid entityId, { } - public override async ValueTask NewEntity(Commit commit, ChangeContext context) + public override async ValueTask NewEntity(Commit commit, ChangeContext context) { - return new CrdtComplexFormComponent + return new ComplexFormComponent { Id = EntityId, ComplexFormEntryId = ComplexFormEntryId, diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs index 563e2c236..0a13b2c73 100644 --- a/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/Entries/SetComplexFormComponentChange.cs @@ -5,7 +5,7 @@ namespace LcmCrdt.Changes.Entries; -public class SetComplexFormComponentChange : EditChange, ISelfNamedType +public class SetComplexFormComponentChange : EditChange, ISelfNamedType { [JsonConstructor] protected SetComplexFormComponentChange(Guid entityId, Guid? complexFormEntryId, Guid? componentEntryId, Guid? componentSenseId) : base(entityId) @@ -21,19 +21,19 @@ protected SetComplexFormComponentChange(Guid entityId, Guid? complexFormEntryId, public Guid? ComplexFormEntryId { get; } public Guid? ComponentEntryId { get; } public Guid? ComponentSenseId { get; } - public override async ValueTask ApplyChange(CrdtComplexFormComponent entity, ChangeContext context) + public override async ValueTask ApplyChange(ComplexFormComponent entity, ChangeContext context) { if (ComplexFormEntryId.HasValue) { entity.ComplexFormEntryId = ComplexFormEntryId.Value; - var complexFormEntry = (await context.GetSnapshot(ComplexFormEntryId.Value))?.Entity.Is(); + var complexFormEntry = await context.GetCurrent(ComplexFormEntryId.Value); entity.ComplexFormHeadword = complexFormEntry?.Headword(); entity.DeletedAt = complexFormEntry?.DeletedAt != null ? context.Commit.DateTime : (DateTime?)null; } if (ComponentEntryId.HasValue) { entity.ComponentEntryId = ComponentEntryId.Value; - var componentEntry = (await context.GetSnapshot(ComponentEntryId.Value))?.Entity.Is(); + var componentEntry = await context.GetCurrent(ComponentEntryId.Value); entity.ComponentHeadword = componentEntry?.Headword(); entity.DeletedAt = componentEntry?.DeletedAt != null ? context.Commit.DateTime : (DateTime?)null; } diff --git a/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs b/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs index 5855b4fbf..bba12f5c9 100644 --- a/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/JsonPatchChange.cs @@ -1,9 +1,6 @@ -using System.Buffers; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; -using SIL.Harmony; using SIL.Harmony.Changes; -using SIL.Harmony.Db; using SIL.Harmony.Entities; using SystemTextJsonPatch; using SystemTextJsonPatch.Internal; @@ -11,9 +8,9 @@ namespace LcmCrdt.Changes; -public class JsonPatchChange : EditChange, IPolyType where T : class, IPolyType, IObjectBase +public class JsonPatchChange : EditChange, IPolyType where T : class { - public static string TypeName => "jsonPatch:" + T.TypeName; + public static string TypeName => "jsonPatch:" + typeof(T).Name; public JsonPatchChange(Guid entityId, Action> action) : base(entityId) { PatchDocument = new(); @@ -27,12 +24,6 @@ public JsonPatchChange(Guid entityId, JsonPatchDocument patchDocument): base( PatchDocument = patchDocument; JsonPatchValidator.ValidatePatchDocument(PatchDocument); } - public JsonPatchChange(Guid entityId, IJsonPatchDocument patchDocument, JsonSerializerOptions options): base(entityId) - { - PatchDocument = new JsonPatchDocument(patchDocument.GetOperations().Select(o => - new Operation(o.Op!, o.Path!, o.From, o.Value)).ToList(), options); - JsonPatchValidator.ValidatePatchDocument(PatchDocument); - } public JsonPatchDocument PatchDocument { get; } diff --git a/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs b/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs index cd9de40db..f87e90afc 100644 --- a/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs @@ -1,6 +1,5 @@ using SIL.Harmony.Changes; using SIL.Harmony.Entities; -using MiniLcm.Models; namespace LcmCrdt.Changes; diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index f10daa8b3..11f3bcc76 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -1,34 +1,28 @@ using System.Linq.Expressions; -using System.Text.Json; -using SIL.Harmony.Core; using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; using LcmCrdt.Changes.Entries; -using LcmCrdt.Objects; using LcmCrdt.Data; -using MiniLcm; +using LcmCrdt.Objects; using LinqToDB; using LinqToDB.EntityFrameworkCore; -using MiniLcm.Models; -using PartOfSpeech = MiniLcm.Models.PartOfSpeech; -using SemanticDomain = LcmCrdt.Objects.SemanticDomain; namespace LcmCrdt; -public class CrdtMiniLcmApi(DataModel dataModel, JsonSerializerOptions jsonOptions, IHybridDateTimeProvider timeProvider, CurrentProjectService projectService) : IMiniLcmApi +public class CrdtMiniLcmApi(DataModel dataModel, CurrentProjectService projectService) : IMiniLcmApi { private Guid ClientId { get; } = projectService.ProjectData.ClientId; private IQueryable Entries => dataModel.GetLatestObjects(); - private IQueryable ComplexFormComponents => dataModel.GetLatestObjects(); - private IQueryable ComplexFormTypes => dataModel.GetLatestObjects(); + private IQueryable ComplexFormComponents => dataModel.GetLatestObjects(); + private IQueryable ComplexFormTypes => dataModel.GetLatestObjects(); private IQueryable Senses => dataModel.GetLatestObjects(); private IQueryable ExampleSentences => dataModel.GetLatestObjects(); private IQueryable WritingSystems => dataModel.GetLatestObjects(); private IQueryable SemanticDomains => dataModel.GetLatestObjects(); - private IQueryable PartsOfSpeech => dataModel.GetLatestObjects(); + private IQueryable PartsOfSpeech => dataModel.GetLatestObjects(); public async Task GetWritingSystems() { @@ -36,13 +30,13 @@ public async Task GetWritingSystems() return new WritingSystems { Analysis = systems.Where(ws => ws.Type == WritingSystemType.Analysis) - .Select(w => ((MiniLcm.Models.WritingSystem)w)).ToArray(), + .Select(w => ((WritingSystem)w)).ToArray(), Vernacular = systems.Where(ws => ws.Type == WritingSystemType.Vernacular) - .Select(w => ((MiniLcm.Models.WritingSystem)w)).ToArray() + .Select(w => ((WritingSystem)w)).ToArray() }; } - public async Task CreateWritingSystem(WritingSystemType type, MiniLcm.Models.WritingSystem writingSystem) + public async Task CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem) { var entityId = Guid.NewGuid(); var wsCount = await WritingSystems.CountAsync(ws => ws.Type == type); @@ -50,11 +44,11 @@ public async Task GetWritingSystems() return await dataModel.GetLatest(entityId) ?? throw new NullReferenceException(); } - public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) + public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) { var ws = await GetWritingSystem(id, type); if (ws is null) throw new NullReferenceException($"unable to find writing system with id {id}"); - var patchChange = new JsonPatchChange(ws.Id, update.Patch, jsonOptions); + var patchChange = new JsonPatchChange(ws.Id, update.Patch); await dataModel.AddChange(ClientId, patchChange); return await dataModel.GetLatest(ws.Id) ?? throw new NullReferenceException(); } @@ -111,19 +105,19 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexF return await ComplexFormTypes.SingleAsync(c => c.Id == complexFormType.Id); } - public IAsyncEnumerable GetEntries(QueryOptions? options = null) + public IAsyncEnumerable GetEntries(QueryOptions? options = null) { return GetEntriesAsyncEnum(predicate: null, options); } - public IAsyncEnumerable SearchEntries(string? query, QueryOptions? options = null) + public IAsyncEnumerable SearchEntries(string? query, QueryOptions? options = null) { if (string.IsNullOrEmpty(query)) return GetEntriesAsyncEnum(null, options); return GetEntriesAsyncEnum(Filtering.SearchFilter(query), options); } - private async IAsyncEnumerable GetEntriesAsyncEnum( + private async IAsyncEnumerable GetEntriesAsyncEnum( Expression>? predicate = null, QueryOptions? options = null) { @@ -134,7 +128,7 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexF } } - private async Task GetEntries( + private async Task GetEntries( Expression>? predicate = null, QueryOptions? options = null) { @@ -154,77 +148,29 @@ public async Task CreateComplexFormType(MiniLcm.Models.ComplexF if (sortWs is null) throw new NullReferenceException($"sort writing system {options.Order.WritingSystem} not found"); queryable = queryable + .LoadWith(e => e.Senses).ThenLoad(s => s.ExampleSentences) + .LoadWith(e => e.ComplexForms) + .LoadWith(e => e.Components) + .AsQueryable() .OrderBy(e => e.Headword(sortWs.Value)) - // .ThenBy(e => e.Id) + .ThenBy(e => e.Id) .Skip(options.Offset) .Take(options.Count); var entries = await queryable .ToArrayAsyncLinqToDB(); - await LoadSenses(entries); - await LoadComplexFormData(entries); return entries; } - private async Task LoadSenses(Entry[] entries) - { - var allSenses = (await Senses - .Where(s => entries.Select(e => e.Id).Contains(s.EntryId)) - .ToArrayAsyncEF()) - .ToLookup(s => s.EntryId) - .ToDictionary(g => g.Key, g => g.ToArray()); - var allSenseIds = allSenses.Values.SelectMany(s => s, (_, sense) => sense.Id); - var allExampleSentences = (await ExampleSentences - .Where(e => allSenseIds.Contains(e.SenseId)) - .ToArrayAsyncEF()) - .ToLookup(s => s.SenseId) - .ToDictionary(g => g.Key, g => g.ToArray()); - foreach (var entry in entries) - { - entry.Senses = allSenses.TryGetValue(entry.Id, out var senses) ? senses.ToArray() : []; - foreach (var sense in entry.Senses) - { - sense.ExampleSentences = allExampleSentences.TryGetValue(sense.Id, out var sentences) - ? sentences.ToArray() - : []; - } - } - } - - private async Task LoadComplexFormData(Entry[] entries) + public async Task GetEntry(Guid id) { - var allComponents = await ComplexFormComponents - .Where(c => entries.Select(e => e.Id).Contains(c.ComplexFormEntryId) || entries.Select(e => e.Id).Contains(c.ComponentEntryId)) - .ToArrayAsyncEF(); - var componentLookup = allComponents.ToLookup(c => c.ComplexFormEntryId).ToDictionary(c => c.Key, c => c.ToArray()); - var complexFormLookup = allComponents.ToLookup(c => c.ComponentEntryId).ToDictionary(c => c.Key, c => c.ToArray()); - foreach (var entry in entries) - { - entry.Components = componentLookup.TryGetValue(entry.Id, out var components) ? components.ToArray() : []; - entry.ComplexForms = complexFormLookup.TryGetValue(entry.Id, out var complexForms) ? complexForms.ToArray() : []; - } - } - - public async Task GetEntry(Guid id) - { - var entry = await Entries.SingleOrDefaultAsync(e => e.Id == id); - if (entry is null) return null; - var senses = await Senses - .Where(s => s.EntryId == id).ToArrayAsyncLinqToDB(); - var exampleSentences = (await ExampleSentences - .Where(e => senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncEF()) - .ToLookup(e => e.SenseId) - .ToDictionary(g => g.Key, g => g.ToArray()); - - var complexFormComponents = await ComplexFormComponents.Where(c => c.ComplexFormEntryId == id || c.ComponentEntryId == id).ToListAsyncEF(); - entry.Components = [..complexFormComponents.Where(c => c.ComplexFormEntryId == id)]; - entry.ComplexForms = [..complexFormComponents .Where(c => c.ComponentEntryId == id)]; - entry.Senses = senses; - foreach (var sense in entry.Senses) - { - sense.ExampleSentences = exampleSentences.TryGetValue(sense.Id, out var sentences) ? sentences.ToArray() : []; - } - + var entry = await Entries + .LoadWith(e => e.Senses) + .ThenLoad(s => s.ExampleSentences) + .LoadWith(e => e.ComplexForms) + .LoadWith(e => e.Components) + .AsQueryable() + .SingleOrDefaultAsync(e => e.Id == id); return entry; } @@ -232,7 +178,7 @@ private async Task LoadComplexFormData(Entry[] entries) /// does not return the newly created entry, used for importing a large amount of data /// /// - public async Task CreateEntryLite(MiniLcm.Models.Entry entry) + public async Task CreateEntryLite(Entry entry) { await dataModel.AddChanges(ClientId, [ @@ -243,14 +189,14 @@ await dataModel.AddChanges(ClientId, ], deferCommit: true); } - public async Task BulkCreateEntries(IAsyncEnumerable entries) + public async Task BulkCreateEntries(IAsyncEnumerable entries) { var semanticDomains = await SemanticDomains.ToDictionaryAsync(sd => sd.Id, sd => sd); var partsOfSpeech = await PartsOfSpeech.ToDictionaryAsync(p => p.Id, p => p); await dataModel.AddChanges(ClientId, entries.ToBlockingEnumerable().SelectMany(entry => CreateEntryChanges(entry, semanticDomains, partsOfSpeech))); } - private IEnumerable CreateEntryChanges(MiniLcm.Models.Entry entry, Dictionary semanticDomains, Dictionary partsOfSpeech) + private IEnumerable CreateEntryChanges(Entry entry, Dictionary semanticDomains, Dictionary partsOfSpeech) { yield return new CreateEntryChange(entry); @@ -282,7 +228,7 @@ private IEnumerable CreateEntryChanges(MiniLcm.Models.Entry entry, Dict } } - public async Task CreateEntry(MiniLcm.Models.Entry entry) + public async Task CreateEntry(Entry entry) { await dataModel.AddChanges(ClientId, [ @@ -297,13 +243,13 @@ ..await entry.Senses.ToAsyncEnumerable() return await GetEntry(entry.Id) ?? throw new NullReferenceException(); } - public async Task UpdateEntry(Guid id, - UpdateObjectInput update) + public async Task UpdateEntry(Guid id, + UpdateObjectInput update) { var entry = await GetEntry(id); if (entry is null) throw new NullReferenceException($"unable to find entry with id {id}"); - await dataModel.AddChanges(ClientId, [..Entry.ChangesFromJsonPatch((Entry)entry, update.Patch)]); + await dataModel.AddChanges(ClientId, [..entry.ToChanges(update.Patch)]); return await GetEntry(id) ?? throw new NullReferenceException(); } @@ -312,11 +258,10 @@ public async Task DeleteEntry(Guid id) await dataModel.AddChange(ClientId, new DeleteChange(id)); } - private async IAsyncEnumerable CreateSenseChanges(Guid entryId, MiniLcm.Models.Sense sense) + private async IAsyncEnumerable CreateSenseChanges(Guid entryId, Sense sense) { sense.SemanticDomains = await SemanticDomains .Where(sd => sense.SemanticDomains.Select(s => s.Id).Contains(sd.Id)) - .OfType() .ToListAsync(); if (sense.PartOfSpeechId is not null) { @@ -333,19 +278,19 @@ private async IAsyncEnumerable CreateSenseChanges(Guid entryId, MiniLcm } } - public async Task CreateSense(Guid entryId, MiniLcm.Models.Sense sense) + public async Task CreateSense(Guid entryId, Sense sense) { await dataModel.AddChanges(ClientId, await CreateSenseChanges(entryId, sense).ToArrayAsync()); return await dataModel.GetLatest(sense.Id) ?? throw new NullReferenceException(); } - public async Task UpdateSense(Guid entryId, + public async Task UpdateSense(Guid entryId, Guid senseId, - UpdateObjectInput update) + UpdateObjectInput update) { var sense = await dataModel.GetLatest(senseId); if (sense is null) throw new NullReferenceException($"unable to find sense with id {senseId}"); - await dataModel.AddChanges(ClientId, [..Sense.ChangesFromJsonPatch(sense, update.Patch)]); + await dataModel.AddChanges(ClientId, [..sense.ToChanges(update.Patch)]); return await dataModel.GetLatest(senseId) ?? throw new NullReferenceException(); } @@ -354,21 +299,21 @@ public async Task DeleteSense(Guid entryId, Guid senseId) await dataModel.AddChange(ClientId, new DeleteChange(senseId)); } - public async Task CreateExampleSentence(Guid entryId, + public async Task CreateExampleSentence(Guid entryId, Guid senseId, - MiniLcm.Models.ExampleSentence exampleSentence) + ExampleSentence exampleSentence) { await dataModel.AddChange(ClientId, new CreateExampleSentenceChange(exampleSentence, senseId)); return await dataModel.GetLatest(exampleSentence.Id) ?? throw new NullReferenceException(); } - public async Task UpdateExampleSentence(Guid entryId, + public async Task UpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, - UpdateObjectInput update) + UpdateObjectInput update) { var jsonPatch = update.Patch; - var patchChange = new JsonPatchChange(exampleSentenceId, jsonPatch, jsonOptions); + var patchChange = new JsonPatchChange(exampleSentenceId, jsonPatch); await dataModel.AddChange(ClientId, patchChange); return await dataModel.GetLatest(exampleSentenceId) ?? throw new NullReferenceException(); } diff --git a/backend/FwLite/LcmCrdt/CrdtProject.cs b/backend/FwLite/LcmCrdt/CrdtProject.cs index cbd62ee60..736c0df5f 100644 --- a/backend/FwLite/LcmCrdt/CrdtProject.cs +++ b/backend/FwLite/LcmCrdt/CrdtProject.cs @@ -1,6 +1,4 @@ -using MiniLcm.Models; - -namespace LcmCrdt; +namespace LcmCrdt; public class CrdtProject(string name, string dbPath) : IProjectIdentifier { diff --git a/backend/FwLite/LcmCrdt/CurrentProjectService.cs b/backend/FwLite/LcmCrdt/CurrentProjectService.cs index da3202174..1a00a5f8d 100644 --- a/backend/FwLite/LcmCrdt/CurrentProjectService.cs +++ b/backend/FwLite/LcmCrdt/CurrentProjectService.cs @@ -1,5 +1,4 @@ -using SIL.Harmony.Db; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; namespace LcmCrdt; diff --git a/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs b/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs new file mode 100644 index 000000000..eb0dcc037 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Data/EntryQueryHelpers.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; +using LinqToDB; + +namespace LcmCrdt.Data; + +public static class EntryQueryHelpers +{ + [ExpressionMethod(nameof(HeadwordExpression))] + public static string Headword(this Entry e, WritingSystemId ws) + { + var word = e.CitationForm[ws]; + if (string.IsNullOrEmpty(word)) word = e.LexemeForm[ws]; + return word.Trim(); + } + + private static Expression> HeadwordExpression() => + (e, ws) => (string.IsNullOrEmpty(Json.Value(e.CitationForm, ms => ms[ws])) + ? Json.Value(e.LexemeForm, ms => ms[ws]) + : Json.Value(e.CitationForm, ms => ms[ws]))!.Trim(); +} diff --git a/backend/FwLite/LcmCrdt/Data/Filtering.cs b/backend/FwLite/LcmCrdt/Data/Filtering.cs index 856241723..10d0a44a8 100644 --- a/backend/FwLite/LcmCrdt/Data/Filtering.cs +++ b/backend/FwLite/LcmCrdt/Data/Filtering.cs @@ -1,5 +1,4 @@ using System.Linq.Expressions; -using MiniLcm.Models; namespace LcmCrdt.Data; diff --git a/backend/FwLite/LcmCrdt/GlobalUsings.cs b/backend/FwLite/LcmCrdt/GlobalUsings.cs index fc8cd9bdd..47bb026ce 100644 --- a/backend/FwLite/LcmCrdt/GlobalUsings.cs +++ b/backend/FwLite/LcmCrdt/GlobalUsings.cs @@ -1,4 +1,2 @@ -global using Entry = LcmCrdt.Objects.Entry; -global using Sense = LcmCrdt.Objects.Sense; -global using ExampleSentence = LcmCrdt.Objects.ExampleSentence; -global using WritingSystem = LcmCrdt.Objects.WritingSystem; +global using MiniLcm; +global using MiniLcm.Models; diff --git a/backend/FwLite/LcmCrdt/Json.cs b/backend/FwLite/LcmCrdt/Json.cs index 95d49bd6f..694d8c0f1 100644 --- a/backend/FwLite/LcmCrdt/Json.cs +++ b/backend/FwLite/LcmCrdt/Json.cs @@ -1,10 +1,7 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -using MiniLcm; using LinqToDB; using LinqToDB.Common; -using LinqToDB.Expressions; using LinqToDB.SqlQuery; namespace LcmCrdt; diff --git a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs index 780796a68..3a303f643 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Options; -using MiniLcm.Models; namespace LcmCrdt; diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index aeb0e4cdb..9a6b41098 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -14,9 +14,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using MiniLcm.Models; -using PartOfSpeech = LcmCrdt.Objects.PartOfSpeech; -using SemanticDomain = LcmCrdt.Objects.SemanticDomain; namespace LcmCrdt; @@ -32,7 +29,7 @@ public static IServiceCollection AddLcmCrdtClient(this IServiceCollection servic services.AddCrdtData( ConfigureCrdt ); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); @@ -54,8 +51,6 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.DateTime))) .HasAttribute(new ColumnAttribute(nameof(HybridDateTime.Counter), nameof(Commit.HybridDateTime) + "." + nameof(HybridDateTime.Counter))) - .Entity().Property(e => e.Id) - .Association(e => (e.Senses as IEnumerable)!, e => e.Id, s => s.EntryId) .Build(); mappingSchema.SetConvertExpression((WritingSystemId id) => new DataParameter { Value = id.Code, DataType = DataType.Text }); @@ -70,6 +65,7 @@ public static void ConfigureCrdt(CrdtConfig config) { config.EnableProjectedTables = true; config.ObjectTypeListBuilder + .CustomAdapter() .Add(builder => { builder.Ignore(e => e.Senses); @@ -92,23 +88,22 @@ public static void ConfigureCrdt(CrdtConfig config) }) .Add(builder => { - builder.Ignore(s => s.ExampleSentences); builder.HasMany() .WithOne() .HasForeignKey(c => c.ComponentSenseId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne() - .WithMany() + .WithMany(e => e.Senses) .HasForeignKey(sense => sense.EntryId); builder.Property(s => s.SemanticDomains) .HasColumnType("jsonb") .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null), - json => JsonSerializer.Deserialize>(json, (JsonSerializerOptions?)null) ?? new()); + json => JsonSerializer.Deserialize>(json, (JsonSerializerOptions?)null) ?? new()); }) .Add(builder => { builder.HasOne() - .WithMany() + .WithMany(s => s.ExampleSentences) .HasForeignKey(e => e.SenseId); }) .Add(builder => @@ -121,8 +116,8 @@ public static void ConfigureCrdt(CrdtConfig config) }) .Add() .Add() - .Add() - .Add(builder => + .Add() + .Add(builder => { builder.ToTable("ComplexFormComponents"); }); @@ -139,8 +134,8 @@ public static void ConfigureCrdt(CrdtConfig config) .Add>() .Add>() .Add>() - .Add>() - .Add>() + .Add>() + .Add>() .Add() .Add() .Add() diff --git a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs deleted file mode 100644 index a28aa1a70..000000000 --- a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormComponent.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MiniLcm.Models; -using SIL.Harmony; -using SIL.Harmony.Entities; - -namespace LcmCrdt.Objects; - -public record CrdtComplexFormComponent : ComplexFormComponent, IObjectBase -{ - Guid IObjectBase.Id - { - get => Id; - init => Id = value; - } - - public DateTimeOffset? DeletedAt { get; set; } - - public Guid[] GetReferences() - { - Span senseId = (ComponentSenseId.HasValue ? [ComponentSenseId.Value] : []); - return [ - ComplexFormEntryId, - ComponentEntryId, - ..senseId - ]; - } - - public void RemoveReference(Guid id, Commit commit) - { - if (ComponentEntryId == id || ComplexFormEntryId == id || ComponentSenseId == id) - DeletedAt = commit.DateTime; - } - - public IObjectBase Copy() - { - return new CrdtComplexFormComponent - { - Id = Id, - ComplexFormEntryId = ComplexFormEntryId, - ComplexFormHeadword = ComplexFormHeadword, - ComponentEntryId = ComponentEntryId, - ComponentHeadword = ComponentHeadword, - ComponentSenseId = ComponentSenseId, - DeletedAt = DeletedAt, - }; - } -} diff --git a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs b/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs deleted file mode 100644 index 07507d449..000000000 --- a/backend/FwLite/LcmCrdt/Objects/CrdtComplexFormType.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MiniLcm.Models; -using SIL.Harmony; -using SIL.Harmony.Entities; - -namespace LcmCrdt.Objects; - -public class CrdtComplexFormType : ComplexFormType, IObjectBase -{ - Guid IObjectBase.Id - { - get => Id; - init => Id = value; - } - - public DateTimeOffset? DeletedAt { get; set; } - - public Guid[] GetReferences() - { - return []; - } - - public void RemoveReference(Guid id, Commit commit) - { - } - - public IObjectBase Copy() - { - return new CrdtComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt, }; - } -} diff --git a/backend/FwLite/LcmCrdt/Objects/ExampleSentence.cs b/backend/FwLite/LcmCrdt/Objects/ExampleSentence.cs deleted file mode 100644 index e0041205b..000000000 --- a/backend/FwLite/LcmCrdt/Objects/ExampleSentence.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json; -using SIL.Harmony; -using SIL.Harmony.Db; -using SIL.Harmony.Entities; - -namespace LcmCrdt.Objects; - -public class ExampleSentence : MiniLcm.Models.ExampleSentence, IObjectBase -{ - Guid IObjectBase.Id - { - get => Id; - init => Id = value; - } - - public required Guid SenseId { get; set; } - public DateTimeOffset? DeletedAt { get; set; } - - public Guid[] GetReferences() - { - return [SenseId]; - } - - public void RemoveReference(Guid id, Commit commit) - { - if (id == SenseId) - DeletedAt = commit.DateTime; - } - - public IObjectBase Copy() - { - return JsonSerializer.Deserialize(JsonSerializer.Serialize(this))!; - } -} diff --git a/backend/FwLite/LcmCrdt/Objects/Entry.cs b/backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs similarity index 58% rename from backend/FwLite/LcmCrdt/Objects/Entry.cs rename to backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs index 807c89be0..692fc6a05 100644 --- a/backend/FwLite/LcmCrdt/Objects/Entry.cs +++ b/backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs @@ -1,102 +1,58 @@ -using System.Linq.Expressions; -using System.Text.Json.Serialization; -using LcmCrdt.Changes; +using LcmCrdt.Changes; using LcmCrdt.Changes.Entries; using LcmCrdt.Utils; -using SIL.Harmony; -using SIL.Harmony.Entities; -using LinqToDB; -using MiniLcm.Models; using SIL.Harmony.Changes; using SystemTextJsonPatch; using SystemTextJsonPatch.Operations; namespace LcmCrdt.Objects; -public class Entry : MiniLcm.Models.Entry, IObjectBase +public static class JsonPatchChangeExtractor { - Guid IObjectBase.Id + public static IEnumerable ToChanges(this Sense sense, JsonPatchDocument patch) { - get => Id; - init => Id = value; - } - - public DateTimeOffset? DeletedAt { get; set; } - - /// - /// This is a bit of a hack, we want to be able to reference senses when running a query, and they must be CrdtSenses - /// however we only want to store the senses in the entry as MiniLcmSenses, so we need to convert them back to CrdtSenses - /// Note, even though this is JsonIgnored, the Senses property in the base class is still serialized - /// - [JsonIgnore] - public new IReadOnlyList Senses - { - get + foreach (var rewriteChange in patch.RewriteChanges(s => s.PartOfSpeechId, + (partOfSpeechId, operationType) => + { + if (operationType == OperationType.Replace) + return new SetPartOfSpeechChange(sense.Id, partOfSpeechId); + throw new NotSupportedException($"operation {operationType} not supported for part of speech"); + })) { - return [..base.Senses.Select(s => s as Sense ?? Sense.FromMiniLcm(s, Id))]; - } - set { base.Senses = [..value]; } - } - + yield return rewriteChange; + } - [ExpressionMethod(nameof(HeadwordExpression))] - public string Headword(WritingSystemId ws) - { - var word = CitationForm[ws]; - if (string.IsNullOrEmpty(word)) word = LexemeForm[ws]; - return word.Trim(); - } + foreach (var rewriteChange in patch.RewriteChanges(s => s.SemanticDomains, + (semanticDomain, index, operationType) => + { + if (operationType is OperationType.Add) + { + ArgumentNullException.ThrowIfNull(semanticDomain); + return new AddSemanticDomainChange(semanticDomain, sense.Id); + } - protected static Expression> HeadwordExpression() => - (e, ws) => (string.IsNullOrEmpty(Json.Value(e.CitationForm, ms => ms[ws])) - ? Json.Value(e.LexemeForm, ms => ms[ws]) - : Json.Value(e.CitationForm, ms => ms[ws]))!.Trim(); + if (operationType is OperationType.Replace) + { + ArgumentNullException.ThrowIfNull(semanticDomain); + return new ReplaceSemanticDomainChange(sense.SemanticDomains[index].Id, semanticDomain, sense.Id); + } + if (operationType is OperationType.Remove) + { + return new RemoveSemanticDomainChange(sense.SemanticDomains[index].Id, sense.Id); + } - public Guid[] GetReferences() - { - return - [ - ..Components.SelectMany(c => - c.ComponentSenseId is null - ? [c.ComponentEntryId] - : new[] { c.ComponentEntryId, c.ComponentSenseId.Value }), - ..ComplexForms.Select(c => c.ComplexFormEntryId) - ]; - } + throw new NotSupportedException($"operation {operationType} not supported for semantic domains"); + })) + { + yield return rewriteChange; + } - public void RemoveReference(Guid id, Commit commit) - { - Components = Components.Where(c => c.ComponentEntryId != id && c.ComponentSenseId != id).ToList(); - ComplexForms = ComplexForms.Where(c => c.ComplexFormEntryId != id).ToList(); + if (patch.Operations.Count > 0) + yield return new JsonPatchChange(sense.Id, patch); } - public IObjectBase Copy() - { - return new Entry - { - Id = Id, - DeletedAt = DeletedAt, - LexemeForm = LexemeForm.Copy(), - CitationForm = CitationForm.Copy(), - LiteralMeaning = LiteralMeaning.Copy(), - Note = Note.Copy(), - Senses = [..Senses.Select(s => (Sense)s.Copy())], - Components = - [ - ..Components.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c)) - ], - ComplexForms = - [ - ..ComplexForms.Select(c => (c is CrdtComplexFormComponent cc ? (ComplexFormComponent)cc.Copy() : c)) - ], - ComplexFormTypes = - [ - ..ComplexFormTypes.Select(cft => (cft is CrdtComplexFormType ct ? (ComplexFormType)ct.Copy() : cft)) - ] - }; - } - public static IEnumerable ChangesFromJsonPatch(Entry entry, JsonPatchDocument patch) + public static IEnumerable ToChanges(this Entry entry, JsonPatchDocument patch) { IChange RewriteComplexFormComponents(IList components, ComplexFormComponent? component, Index index, OperationType operationType) { @@ -135,7 +91,7 @@ IChange RewriteComplexFormComponents(IList components, Com if (operationType == OperationType.Remove) { component ??= components[index]; - return new DeleteChange(component.Id); + return new DeleteChange(component.Id); } throw new NotSupportedException($"operation {operationType} not supported for components"); @@ -188,6 +144,6 @@ IChange RewriteComplexFormComponents(IList components, Com if (patch.Operations.Count > 0) - yield return new JsonPatchChange(entry.Id, patch, patch.Options); + yield return new JsonPatchChange(entry.Id, patch); } } diff --git a/backend/FwLite/LcmCrdt/Objects/MiniLcmCrdtAdapter.cs b/backend/FwLite/LcmCrdt/Objects/MiniLcmCrdtAdapter.cs new file mode 100644 index 000000000..3568d3cc8 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Objects/MiniLcmCrdtAdapter.cs @@ -0,0 +1,52 @@ +using SIL.Harmony; +using SIL.Harmony.Adapters; +using SIL.Harmony.Entities; + +namespace LcmCrdt.Objects; + +public class MiniLcmCrdtAdapter : ICustomAdapter +{ + public static string AdapterTypeName => "MiniLcmCrdtAdapter"; + + public MiniLcmCrdtAdapter(IObjectWithId obj) + { + Obj = obj; + } + + public IObjectWithId Obj { get; set; } + public Guid Id => Obj.Id; + + public DateTimeOffset? DeletedAt + { + get => Obj.DeletedAt; + set => Obj.DeletedAt = value; + } + + public Guid[] GetReferences() + { + return Obj.GetReferences(); + } + + public void RemoveReference(Guid id, Commit commit) + { + Obj.RemoveReference(id, commit.DateTime); + } + + public IObjectBase Copy() + { + return Create(Obj.Copy()); + } + + public string GetObjectTypeName() + { + //todo we might not want to do this as a refactor rename of any of our objects will cause problems + return Obj.GetType().Name; + } + + public object DbObject => Obj; + + public static MiniLcmCrdtAdapter Create(IObjectWithId obj) + { + return new MiniLcmCrdtAdapter(obj); + } +} diff --git a/backend/FwLite/LcmCrdt/Objects/PartOfSpeech.cs b/backend/FwLite/LcmCrdt/Objects/PartOfSpeech.cs deleted file mode 100644 index 9bd491855..000000000 --- a/backend/FwLite/LcmCrdt/Objects/PartOfSpeech.cs +++ /dev/null @@ -1,46 +0,0 @@ -using SIL.Harmony; -using SIL.Harmony.Entities; -using LcmCrdt.Changes; -using MiniLcm.Models; - -namespace LcmCrdt.Objects; - -public class PartOfSpeech : MiniLcm.Models.PartOfSpeech, IObjectBase -{ - Guid IObjectBase.Id - { - get => Id; - init => Id = value; - } - public DateTimeOffset? DeletedAt { get; set; } - public bool Predefined { get; set; } - public Guid[] GetReferences() - { - return []; - } - - public void RemoveReference(Guid id, Commit commit) - { - } - - public IObjectBase Copy() - { - return new PartOfSpeech - { - Id = Id, - Name = Name, - DeletedAt = DeletedAt, - Predefined = Predefined - }; - } - - public static async Task PredefinedPartsOfSpeech(DataModel dataModel, Guid clientId) - { - //todo load from xml instead of hardcoding - await dataModel.AddChanges(clientId, - [ - new CreatePartOfSpeechChange(new Guid("46e4fe08-ffa0-4c8b-bf98-2c56f38904d9"), new MultiString() { { "en", "Adverb" } }, true) - ], - new Guid("023faebb-711b-4d2f-b34f-a15621fc66bb")); - } -} diff --git a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs new file mode 100644 index 000000000..b69837bd0 --- /dev/null +++ b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs @@ -0,0 +1,35 @@ +using LcmCrdt.Changes; +using SIL.Harmony; + +namespace LcmCrdt.Objects; + +public static class PreDefinedData +{ + internal static async Task PredefinedSemanticDomains(DataModel dataModel, Guid clientId) + { + //todo load from xml instead of hardcoding and use real IDs + await dataModel.AddChanges(clientId, + [ + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d0"), new MultiString() { { "en", "Universe, Creation" } }, "1", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d1"), new MultiString() { { "en", "Sky" } }, "1.1", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d2"), new MultiString() { { "en", "World" } }, "1.2", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d3"), new MultiString() { { "en", "Person" } }, "2", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d4"), new MultiString() { { "en", "Body" } }, "2.1", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d5"), new MultiString() { { "en", "Head" } }, "2.1.1", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d6"), new MultiString() { { "en", "Eye" } }, "2.1.1.1", true), + ], + new Guid("023faebb-711b-4d2f-a14f-a15621fc66bc")); + } + + public static async Task PredefinedPartsOfSpeech(DataModel dataModel, Guid clientId) + { + //todo load from xml instead of hardcoding + await dataModel.AddChanges(clientId, + [ + new CreatePartOfSpeechChange(new Guid("46e4fe08-ffa0-4c8b-bf98-2c56f38904d9"), + new MultiString() { { "en", "Adverb" } }, + true) + ], + new Guid("023faebb-711b-4d2f-b34f-a15621fc66bb")); + } +} diff --git a/backend/FwLite/LcmCrdt/Objects/SemanticDomain.cs b/backend/FwLite/LcmCrdt/Objects/SemanticDomain.cs deleted file mode 100644 index 23e44c8f4..000000000 --- a/backend/FwLite/LcmCrdt/Objects/SemanticDomain.cs +++ /dev/null @@ -1,55 +0,0 @@ -using LcmCrdt.Changes; -using MiniLcm.Models; -using SIL.Harmony; -using SIL.Harmony.Entities; - -namespace LcmCrdt.Objects; - -public class SemanticDomain : MiniLcm.Models.SemanticDomain, IObjectBase -{ - Guid IObjectBase.Id - { - get => Id; - init => Id = value; - } - - public DateTimeOffset? DeletedAt { get; set; } - public bool Predefined { get; set; } - - public Guid[] GetReferences() - { - return []; - } - - public void RemoveReference(Guid id, Commit commit) - { - } - - public IObjectBase Copy() - { - return new SemanticDomain - { - Id = Id, - Code = Code, - Name = Name, - DeletedAt = DeletedAt, - Predefined = Predefined - }; - } - - internal static async Task PredefinedSemanticDomains(DataModel dataModel, Guid clientId) - { - //todo load from xml instead of hardcoding and use real IDs - await dataModel.AddChanges(clientId, - [ - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d0"), new MultiString() { { "en", "Universe, Creation" } }, "1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d1"), new MultiString() { { "en", "Sky" } }, "1.1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d2"), new MultiString() { { "en", "World" } }, "1.2", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d3"), new MultiString() { { "en", "Person" } }, "2", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d4"), new MultiString() { { "en", "Body" } }, "2.1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d5"), new MultiString() { { "en", "Head" } }, "2.1.1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d6"), new MultiString() { { "en", "Eye" } }, "2.1.1.1", true), - ], - new Guid("023faebb-711b-4d2f-a14f-a15621fc66bc")); - } -} diff --git a/backend/FwLite/LcmCrdt/Objects/Sense.cs b/backend/FwLite/LcmCrdt/Objects/Sense.cs deleted file mode 100644 index 899865181..000000000 --- a/backend/FwLite/LcmCrdt/Objects/Sense.cs +++ /dev/null @@ -1,109 +0,0 @@ -using SIL.Harmony; -using SIL.Harmony.Changes; -using SIL.Harmony.Db; -using SIL.Harmony.Entities; -using LcmCrdt.Changes; -using LcmCrdt.Utils; -using SystemTextJsonPatch; -using SystemTextJsonPatch.Operations; - -namespace LcmCrdt.Objects; - -public class Sense : MiniLcm.Models.Sense, IObjectBase -{ - public static Sense FromMiniLcm(MiniLcm.Models.Sense sense, Guid entryId) - { - return new Sense - { - Id = sense.Id, - Definition = sense.Definition, - Gloss = sense.Gloss, - PartOfSpeech = sense.PartOfSpeech, - PartOfSpeechId = sense.PartOfSpeechId, - SemanticDomains = sense.SemanticDomains, - ExampleSentences = sense.ExampleSentences, - EntryId = entryId - }; - } - public static IEnumerable ChangesFromJsonPatch(Sense sense, JsonPatchDocument patch) - { - foreach (var rewriteChange in patch.RewriteChanges(s => s.PartOfSpeechId, - (partOfSpeechId, operationType) => - { - if (operationType == OperationType.Replace) - return new SetPartOfSpeechChange(sense.Id, partOfSpeechId); - throw new NotSupportedException($"operation {operationType} not supported for part of speech"); - })) - { - yield return rewriteChange; - } - - foreach (var rewriteChange in patch.RewriteChanges(s => s.SemanticDomains, - (semanticDomain, index, operationType) => - { - if (operationType is OperationType.Add) - { - ArgumentNullException.ThrowIfNull(semanticDomain); - return new AddSemanticDomainChange(semanticDomain, sense.Id); - } - - if (operationType is OperationType.Replace) - { - ArgumentNullException.ThrowIfNull(semanticDomain); - return new ReplaceSemanticDomainChange(sense.SemanticDomains[index].Id, semanticDomain, sense.Id); - } - if (operationType is OperationType.Remove) - { - return new RemoveSemanticDomainChange(sense.SemanticDomains[index].Id, sense.Id); - } - - throw new NotSupportedException($"operation {operationType} not supported for semantic domains"); - })) - { - yield return rewriteChange; - } - - if (patch.Operations.Count > 0) - yield return new JsonPatchChange(sense.Id, patch, patch.Options); - } - - Guid IObjectBase.Id - { - get => Id; - init => Id = value; - } - - public DateTimeOffset? DeletedAt { get; set; } - public required Guid EntryId { get; set; } - - public Guid[] GetReferences() - { - ReadOnlySpan pos = PartOfSpeechId.HasValue ? [PartOfSpeechId.Value] : []; - return [EntryId, ..pos, ..SemanticDomains.Select(sd => sd.Id)]; - } - - public void RemoveReference(Guid id, Commit commit) - { - if (id == EntryId) - DeletedAt = commit.DateTime; - if (id == PartOfSpeechId) - PartOfSpeechId = null; - SemanticDomains = [..SemanticDomains.Where(sd => sd.Id != id)]; - } - - public IObjectBase Copy() - { - return new Sense - { - Id = Id, - EntryId = EntryId, - DeletedAt = DeletedAt, - Definition = Definition.Copy(), - Gloss = Gloss.Copy(), - PartOfSpeech = PartOfSpeech, - PartOfSpeechId = PartOfSpeechId, - SemanticDomains = [..SemanticDomains], - ExampleSentences = [..ExampleSentences] - }; - } -} diff --git a/backend/FwLite/LcmCrdt/Objects/WritingSystem.cs b/backend/FwLite/LcmCrdt/Objects/WritingSystem.cs deleted file mode 100644 index 356a24948..000000000 --- a/backend/FwLite/LcmCrdt/Objects/WritingSystem.cs +++ /dev/null @@ -1,66 +0,0 @@ -using SIL.Harmony; -using SIL.Harmony.Changes; -using SIL.Harmony.Db; -using SIL.Harmony.Entities; -using MiniLcm.Models; - -namespace LcmCrdt.Objects; - -public class WritingSystem : IObjectBase, IOrderableCrdt -{ - public WritingSystem() - { - } - - public WritingSystem(Guid id) - { - Id = id; - } - - public Guid Id { get; init; } - public DateTimeOffset? DeletedAt { get; set; } - public required WritingSystemId WsId { get; init; } - public required WritingSystemType Type { get; set; } - public required string Name { get; set; } - public required string Abbreviation { get; set; } - //todo need to accommodate font features too - public required string Font { get; set; } - - public string[] Exemplars { get; set; } = []; - public double Order { get; set; } - - public Guid[] GetReferences() - { - return []; - } - - public void RemoveReference(Guid id, Commit commit) - { - } - - public IObjectBase Copy() - { - return new WritingSystem(Id) - { - WsId = WsId, - Name = Name, - Abbreviation = Abbreviation, - Font = Font, - Exemplars = Exemplars, - DeletedAt = DeletedAt, - Type = Type, - Order = Order - }; - } - - - public static implicit operator MiniLcm.Models.WritingSystem(WritingSystem ws) => - new() - { - Id = ws.WsId, - Name = ws.Name, - Abbreviation = ws.Abbreviation, - Font = ws.Font, - Exemplars = ws.Exemplars - }; -} diff --git a/backend/FwLite/LcmCrdt/ProjectsService.cs b/backend/FwLite/LcmCrdt/ProjectsService.cs index 59d9002ab..2274712e3 100644 --- a/backend/FwLite/LcmCrdt/ProjectsService.cs +++ b/backend/FwLite/LcmCrdt/ProjectsService.cs @@ -1,12 +1,8 @@ using SIL.Harmony; -using SIL.Harmony.Db; -using LcmCrdt.Utils; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MiniLcm; -using PartOfSpeech = LcmCrdt.Objects.PartOfSpeech; using LcmCrdt.Objects; namespace LcmCrdt; @@ -83,8 +79,8 @@ internal static async Task InitProjectDb(LcmCrdtDbContext db, ProjectData data) internal static async Task SeedSystemData(DataModel dataModel, Guid clientId) { - await PartOfSpeech.PredefinedPartsOfSpeech(dataModel, clientId); - await SemanticDomain.PredefinedSemanticDomains(dataModel, clientId); + await PreDefinedData.PredefinedPartsOfSpeech(dataModel, clientId); + await PreDefinedData.PredefinedSemanticDomains(dataModel, clientId); } public AsyncServiceScope CreateProjectScope(CrdtProject crdtProject) diff --git a/backend/FwLite/LcmCrdt/SqlHelpers.cs b/backend/FwLite/LcmCrdt/SqlHelpers.cs index e1f2de676..b62a5253e 100644 --- a/backend/FwLite/LcmCrdt/SqlHelpers.cs +++ b/backend/FwLite/LcmCrdt/SqlHelpers.cs @@ -1,5 +1,4 @@ using LinqToDB; -using MiniLcm.Models; namespace LcmCrdt; diff --git a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index b243a98dd..dcc92ae36 100644 --- a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs +++ b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -47,14 +47,14 @@ public override async Task OnDisconnectedAsync(Exception? exception) memoryCache.Remove($"CurrentFilter|HubConnectionId={Context.ConnectionId}"); } - private Func CurrentFilter + private Func CurrentFilter { set => memoryCache.Set($"CurrentFilter|HubConnectionId={Context.ConnectionId}", value); } - public static Func CurrentProjectFilter(IMemoryCache memoryCache, string connectionId) + public static Func CurrentProjectFilter(IMemoryCache memoryCache, string connectionId) { - return memoryCache.Get>( + return memoryCache.Get>( $"CurrentFilter|HubConnectionId={connectionId}") ?? (_ => true); } diff --git a/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs b/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs index ebfdf9c6d..f1fdacd23 100644 --- a/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs @@ -48,7 +48,8 @@ from change in dbcontext.Set>().LeftJoin(c => commit.HybridDateTime.DateTime, snapshot.Id, change.Change, - snapshot.Entity); + snapshot.Entity, + snapshot.TypeName); return query.ToLinqToDB().AsAsyncEnumerable(); }); return group; @@ -69,14 +70,15 @@ public HistoryLineItem( DateTimeOffset timestamp, Guid? snapshotId, IChange? change, - IObjectBase? entity) : this(commitId, + IObjectBase? entity, + string typeName) : this(commitId, entityId, new DateTimeOffset(timestamp.Ticks, TimeSpan.Zero), //todo this is a workaround for linq2db bug where it reads a date and assumes it's local when it's UTC snapshotId, change?.GetType().Name, entity, - entity?.TypeName) + typeName) { } } diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 095078c88..1ad474bf1 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -151,7 +151,9 @@ await lexboxApi.CreateEntry(new() await lexboxApi.CreateWritingSystem(WritingSystemType.Vernacular, new() { - Id = "en", + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = "en", Name = "English", Abbreviation = "en", Font = "Arial", @@ -161,7 +163,9 @@ await lexboxApi.CreateWritingSystem(WritingSystemType.Vernacular, await lexboxApi.CreateWritingSystem(WritingSystemType.Analysis, new() { - Id = "en", + Id = Guid.NewGuid(), + Type = WritingSystemType.Analysis, + WsId = "en", Name = "English", Abbreviation = "en", Font = "Arial", diff --git a/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs b/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs index a3352d83b..91cf0ef3d 100644 --- a/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs @@ -2,7 +2,7 @@ using LocalWebApp.Services; using Microsoft.OpenApi.Models; using MiniLcm; -using Entry = LcmCrdt.Objects.Entry; +using MiniLcm.Models; namespace LocalWebApp.Routes; @@ -27,16 +27,14 @@ public static IEndpointConventionBuilder MapTest(this WebApplication app) }); group.MapPost("/set-entry-note", async (IMiniLcmApi api, ChangeEventBus eventBus, Guid entryId, string ws, string note) => { - var entry = await api.UpdateEntry(entryId, new UpdateObjectInput().Set(e => e.Note[ws], note)); - if (entry is Entry crdtEntry) - eventBus.NotifyEntryUpdated(crdtEntry); + var entry = await api.UpdateEntry(entryId, new UpdateObjectInput().Set(e => e.Note[ws], note)); + eventBus.NotifyEntryUpdated(entry); }); group.MapPost("/add-new-entry", - async (IMiniLcmApi api, ChangeEventBus eventBus, MiniLcm.Models.Entry entry) => + async (IMiniLcmApi api, ChangeEventBus eventBus, Entry entry) => { var createdEntry = await api.CreateEntry(entry); - if (createdEntry is Entry crdtEntry) - eventBus.NotifyEntryUpdated(crdtEntry); + eventBus.NotifyEntryUpdated(createdEntry); }); return group; } diff --git a/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs b/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs index 97954f5c1..06d331d8e 100644 --- a/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs +++ b/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs @@ -1,10 +1,10 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using LcmCrdt; -using LcmCrdt.Objects; using LocalWebApp.Hubs; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; +using MiniLcm.Models; namespace LocalWebApp.Services; diff --git a/backend/FwLite/LocalWebApp/SyncService.cs b/backend/FwLite/LocalWebApp/SyncService.cs index bfe5e708f..ac8925a00 100644 --- a/backend/FwLite/LocalWebApp/SyncService.cs +++ b/backend/FwLite/LocalWebApp/SyncService.cs @@ -3,10 +3,8 @@ using LocalWebApp.Auth; using LocalWebApp.Services; using MiniLcm; +using MiniLcm.Models; using SIL.Harmony.Entities; -using Entry = LcmCrdt.Objects.Entry; -using ExampleSentence = LcmCrdt.Objects.ExampleSentence; -using Sense = LcmCrdt.Objects.Sense; namespace LocalWebApp; @@ -33,25 +31,23 @@ private async Task SendNotifications(SyncResults syncResults) await foreach (var entryId in syncResults.MissingFromLocal .SelectMany(c => c.Snapshots, (commit, snapshot) => snapshot.Entity) .ToAsyncEnumerable() - .SelectAwait(async e => await GetEntryId(e)) + .SelectAwait(async e => await GetEntryId(e.DbObject as IObjectWithId)) .Distinct()) { if (entryId is null) continue; var entry = await lexboxApi.GetEntry(entryId.Value); - if (entry is Entry crdtEntry) + if (entry is not null) { - changeEventBus.NotifyEntryUpdated(crdtEntry); + changeEventBus.NotifyEntryUpdated(entry); } else { - logger.LogError("Failed to get entry {EntryId}, was not a crdt entry, was {Type}", - entryId, - entry?.GetType().FullName ?? "null"); + logger.LogError("Failed to get entry {EntryId}, was not found", entryId); } } } - private async ValueTask GetEntryId(IObjectBase entity) + private async ValueTask GetEntryId(IObjectWithId? entity) { return entity switch { diff --git a/backend/FwLite/MiniLcm/InMemoryApi.cs b/backend/FwLite/MiniLcm/InMemoryApi.cs index fc6bfe0e1..afebd814d 100644 --- a/backend/FwLite/MiniLcm/InMemoryApi.cs +++ b/backend/FwLite/MiniLcm/InMemoryApi.cs @@ -103,11 +103,11 @@ public class InMemoryApi : IMiniLcmApi private readonly WritingSystems _writingSystems = new WritingSystems{ Analysis = [ - new WritingSystem { Id = "en", Name = "English", Abbreviation = "en", Font = "Arial" }, + new WritingSystem { Id = Guid.NewGuid(), Type = WritingSystemType.Analysis, WsId = "en", Name = "English", Abbreviation = "en", Font = "Arial" }, ], Vernacular = [ - new WritingSystem { Id = "en", Name = "English", Abbreviation = "en", Font = "Arial" }, + new WritingSystem { Id = Guid.NewGuid(), Type = WritingSystemType.Vernacular, WsId = "en", Name = "English", Abbreviation = "en", Font = "Arial" }, ] }; @@ -133,8 +133,8 @@ public Task CreateWritingSystem(WritingSystemType type, WritingSy public Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) { var ws = type == WritingSystemType.Analysis - ? _writingSystems.Analysis.Single(w => w.Id == id) - : _writingSystems.Vernacular.Single(w => w.Id == id); + ? _writingSystems.Analysis.Single(w => w.WsId == id) + : _writingSystems.Vernacular.Single(w => w.WsId == id); if (ws is null) throw new KeyNotFoundException($"unable to find writing system with id {id}"); update.Apply(ws); return Task.FromResult(ws); diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs b/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs new file mode 100644 index 000000000..cbea10589 --- /dev/null +++ b/backend/FwLite/MiniLcm/Models/ComplexFormComponent.cs @@ -0,0 +1,61 @@ +namespace MiniLcm.Models; + +public record ComplexFormComponent : IObjectWithId +{ + public static ComplexFormComponent FromEntries(Entry complexFormEntry, + Entry componentEntry, + Guid? componentSenseId = null) + { + if (componentEntry.Id == default) throw new ArgumentException("componentEntry.Id is empty"); + if (complexFormEntry.Id == default) throw new ArgumentException("complexFormEntry.Id is empty"); + return new ComplexFormComponent + { + Id = Guid.NewGuid(), + ComplexFormEntryId = complexFormEntry.Id, + ComplexFormHeadword = complexFormEntry.Headword(), + ComponentEntryId = componentEntry.Id, + ComponentHeadword = componentEntry.Headword(), + ComponentSenseId = componentSenseId, + }; + } + + public Guid Id { get; set; } + public DateTimeOffset? DeletedAt { get; set; } + public virtual required Guid ComplexFormEntryId { get; set; } + public string? ComplexFormHeadword { get; set; } + public virtual required Guid ComponentEntryId { get; set; } + public virtual Guid? ComponentSenseId { get; set; } = null; + public string? ComponentHeadword { get; set; } + + + public Guid[] GetReferences() + { + Span senseId = (ComponentSenseId.HasValue ? [ComponentSenseId.Value] : []); + return + [ + ComplexFormEntryId, + ComponentEntryId, + ..senseId + ]; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + if (ComponentEntryId == id || ComplexFormEntryId == id || ComponentSenseId == id) + DeletedAt = time; + } + + public IObjectWithId Copy() + { + return new ComplexFormComponent + { + Id = Id, + ComplexFormEntryId = ComplexFormEntryId, + ComplexFormHeadword = ComplexFormHeadword, + ComponentEntryId = ComponentEntryId, + ComponentHeadword = ComponentHeadword, + ComponentSenseId = ComponentSenseId, + DeletedAt = DeletedAt, + }; + } +} diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs new file mode 100644 index 000000000..4e3268cba --- /dev/null +++ b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs @@ -0,0 +1,24 @@ +namespace MiniLcm.Models; + +//todo support an order for the complex form types, might be here, or on the entry +public class ComplexFormType : IObjectWithId +{ + public virtual Guid Id { get; set; } + public required MultiString Name { get; set; } + + public DateTimeOffset? DeletedAt { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + } + + public IObjectWithId Copy() + { + return new ComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt, }; + } +} diff --git a/backend/FwLite/MiniLcm/Models/Entry.cs b/backend/FwLite/MiniLcm/Models/Entry.cs index c8492deda..98dc6c9c9 100644 --- a/backend/FwLite/MiniLcm/Models/Entry.cs +++ b/backend/FwLite/MiniLcm/Models/Entry.cs @@ -3,6 +3,7 @@ public class Entry : IObjectWithId { public Guid Id { get; set; } + public DateTimeOffset? DeletedAt { get; set; } public virtual MultiString LexemeForm { get; set; } = new(); @@ -17,10 +18,12 @@ public class Entry : IObjectWithId /// Components making up this complex entry /// public virtual IList Components { get; set; } = []; + /// /// This entry is a part of these complex forms /// public virtual IList ComplexForms { get; set; } = []; + public virtual IList ComplexFormTypes { get; set; } = []; public string Headword() @@ -29,30 +32,42 @@ public string Headword() if (string.IsNullOrEmpty(word)) word = LexemeForm.Values.Values.FirstOrDefault(); return word?.Trim() ?? "(Unknown)"; } -} -public record ComplexFormComponent -{ - public static ComplexFormComponent FromEntries(Entry complexFormEntry, Entry componentEntry, Guid? componentSenseId = null) + + public IObjectWithId Copy() { - if (componentEntry.Id == default) throw new ArgumentException("componentEntry.Id is empty"); - if (complexFormEntry.Id == default) throw new ArgumentException("complexFormEntry.Id is empty"); - return new ComplexFormComponent + return new Entry { - Id = Guid.NewGuid(), - ComplexFormEntryId = complexFormEntry.Id, - ComplexFormHeadword = complexFormEntry.Headword(), - ComponentEntryId = componentEntry.Id, - ComponentHeadword = componentEntry.Headword(), - ComponentSenseId = componentSenseId, + Id = Id, + DeletedAt = DeletedAt, + LexemeForm = LexemeForm.Copy(), + CitationForm = CitationForm.Copy(), + LiteralMeaning = LiteralMeaning.Copy(), + Note = Note.Copy(), + Senses = [..Senses.Select(s => (Sense)s.Copy())], + Components = + [ + ..Components.Select(c => (ComplexFormComponent)c.Copy()) + ], + ComplexForms = + [ + ..ComplexForms.Select(c => (ComplexFormComponent)c.Copy()) + ], + ComplexFormTypes = + [ + ..ComplexFormTypes.Select(cft => (ComplexFormType)cft.Copy()) + ] }; } - public Guid Id { get; set; } - public virtual required Guid ComplexFormEntryId { get; set; } - public string? ComplexFormHeadword { get; set; } - public virtual required Guid ComponentEntryId { get; set; } - public virtual Guid? ComponentSenseId { get; set; } = null; - public string? ComponentHeadword { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + } } public class Variants @@ -62,12 +77,6 @@ public class Variants public IList Types { get; set; } = []; } -//todo support an order for the complex form types, might be here, or on the entry -public class ComplexFormType -{ - public virtual Guid Id { get; set; } - public required MultiString Name { get; set; } -} public class VariantType { diff --git a/backend/FwLite/MiniLcm/Models/ExampleSentence.cs b/backend/FwLite/MiniLcm/Models/ExampleSentence.cs index d05230acc..dde441994 100644 --- a/backend/FwLite/MiniLcm/Models/ExampleSentence.cs +++ b/backend/FwLite/MiniLcm/Models/ExampleSentence.cs @@ -6,4 +6,31 @@ public class ExampleSentence : IObjectWithId public virtual MultiString Sentence { get; set; } = new(); public virtual MultiString Translation { get; set; } = new(); public virtual string? Reference { get; set; } = null; + + public Guid SenseId { get; set; } + public DateTimeOffset? DeletedAt { get; set; } + + public Guid[] GetReferences() + { + return [SenseId]; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + if (id == SenseId) + DeletedAt = time; + } + + public IObjectWithId Copy() + { + return new ExampleSentence() + { + Id = Id, + DeletedAt = DeletedAt, + SenseId = SenseId, + Sentence = Sentence.Copy(), + Translation = Translation.Copy(), + Reference = Reference + }; + } } diff --git a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs index ff026a3f6..dd0cf269f 100644 --- a/backend/FwLite/MiniLcm/Models/IObjectWithId.cs +++ b/backend/FwLite/MiniLcm/Models/IObjectWithId.cs @@ -1,6 +1,24 @@ -namespace MiniLcm.Models; +using System.Text.Json.Serialization; +namespace MiniLcm.Models; + +[JsonPolymorphic] +[JsonDerivedType(typeof(Entry), nameof(Entry))] +[JsonDerivedType(typeof(Sense), nameof(Sense))] +[JsonDerivedType(typeof(ExampleSentence), nameof(ExampleSentence))] +[JsonDerivedType(typeof(WritingSystem), nameof(WritingSystem))] +[JsonDerivedType(typeof(PartOfSpeech), nameof(PartOfSpeech))] +[JsonDerivedType(typeof(SemanticDomain), nameof(SemanticDomain))] +[JsonDerivedType(typeof(ComplexFormType), nameof(ComplexFormType))] +[JsonDerivedType(typeof(ComplexFormComponent), nameof(ComplexFormComponent))] public interface IObjectWithId { public Guid Id { get; } + public DateTimeOffset? DeletedAt { get; set; } + + public Guid[] GetReferences(); + + public void RemoveReference(Guid id, DateTimeOffset time); + + public IObjectWithId Copy(); } diff --git a/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs b/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs index a7163ee1b..0c0ccd014 100644 --- a/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs +++ b/backend/FwLite/MiniLcm/Models/PartOfSpeech.cs @@ -4,4 +4,21 @@ public class PartOfSpeech : IObjectWithId { public Guid Id { get; set; } public MultiString Name { get; set; } = new(); + + public DateTimeOffset? DeletedAt { get; set; } + public bool Predefined { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + } + + public IObjectWithId Copy() + { + return new PartOfSpeech { Id = Id, Name = Name, DeletedAt = DeletedAt, Predefined = Predefined }; + } } diff --git a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs index 8d6c86061..616af0adb 100644 --- a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs +++ b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs @@ -5,4 +5,27 @@ public class SemanticDomain : IObjectWithId public virtual required Guid Id { get; set; } public virtual required MultiString Name { get; set; } public virtual required string Code { get; set; } + public DateTimeOffset? DeletedAt { get; set; } + public bool Predefined { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + } + + public IObjectWithId Copy() + { + return new SemanticDomain + { + Id = Id, + Code = Code, + Name = Name, + DeletedAt = DeletedAt, + Predefined = Predefined + }; + } } diff --git a/backend/FwLite/MiniLcm/Models/Sense.cs b/backend/FwLite/MiniLcm/Models/Sense.cs index f8fd22d22..2117e2738 100644 --- a/backend/FwLite/MiniLcm/Models/Sense.cs +++ b/backend/FwLite/MiniLcm/Models/Sense.cs @@ -3,10 +3,43 @@ public class Sense : IObjectWithId { public virtual Guid Id { get; set; } + public DateTimeOffset? DeletedAt { get; set; } + public Guid EntryId { get; set; } public virtual MultiString Definition { get; set; } = new(); public virtual MultiString Gloss { get; set; } = new(); public virtual string PartOfSpeech { get; set; } = string.Empty; public virtual Guid? PartOfSpeechId { get; set; } public virtual IList SemanticDomains { get; set; } = []; public virtual IList ExampleSentences { get; set; } = []; + + public Guid[] GetReferences() + { + ReadOnlySpan pos = PartOfSpeechId.HasValue ? [PartOfSpeechId.Value] : []; + return [EntryId, ..pos, ..SemanticDomains.Select(sd => sd.Id)]; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + if (id == EntryId) + DeletedAt = time; + if (id == PartOfSpeechId) + PartOfSpeechId = null; + SemanticDomains = [..SemanticDomains.Where(sd => sd.Id != id)]; + } + + public IObjectWithId Copy() + { + return new Sense + { + Id = Id, + EntryId = EntryId, + DeletedAt = DeletedAt, + Definition = Definition.Copy(), + Gloss = Gloss.Copy(), + PartOfSpeech = PartOfSpeech, + PartOfSpeechId = PartOfSpeechId, + SemanticDomains = [..SemanticDomains], + ExampleSentences = [..ExampleSentences.Select(s => (ExampleSentence)s.Copy())] + }; + } } diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index c36198600..ed96bcddc 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -1,16 +1,46 @@ namespace MiniLcm.Models; -public record WritingSystem +public record WritingSystem: IObjectWithId { - public required WritingSystemId Id { get; set; } + public required Guid Id { get; set; } + public required WritingSystemId WsId { get; set; } public required string Name { get; set; } public required string Abbreviation { get; set; } public required string Font { get; set; } + public DateTimeOffset? DeletedAt { get; set; } + public required WritingSystemType Type { get; set; } public string[] Exemplars { get; set; } = []; //todo probably need more stuff here, see wesay for ideas public static string[] LatinExemplars => Enumerable.Range('A', 'Z' - 'A' + 1).Select(c => ((char)c).ToString()).ToArray(); + + public double Order { get; set; } + + public Guid[] GetReferences() + { + return []; + } + + public void RemoveReference(Guid id, DateTimeOffset time) + { + } + + public IObjectWithId Copy() + { + return new WritingSystem + { + Id = Id, + WsId = WsId, + Name = Name, + Abbreviation = Abbreviation, + Font = Font, + Exemplars = Exemplars, + DeletedAt = DeletedAt, + Type = Type, + Order = Order + }; + } } public record WritingSystems diff --git a/backend/LfClassicData/LfClassicMiniLcmApi.cs b/backend/LfClassicData/LfClassicMiniLcmApi.cs index e3ba31d5f..fb25363fc 100644 --- a/backend/LfClassicData/LfClassicMiniLcmApi.cs +++ b/backend/LfClassicData/LfClassicMiniLcmApi.cs @@ -33,7 +33,9 @@ public async Task GetWritingSystems() { var writingSystem = new WritingSystem { - Id = ws, + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = ws, Font = "???", Name = inputSystem.LanguageName, Abbreviation = inputSystem.Abbreviation @@ -225,30 +227,32 @@ private static Entry ToEntry(Entities.Entry entry) LexemeForm = ToMultiString(entry.Lexeme), Note = ToMultiString(entry.Note), LiteralMeaning = ToMultiString(entry.LiteralMeaning), - Senses = entry.Senses?.OfType().Select(ToSense).ToList() ?? [], + Senses = entry.Senses?.OfType().Select(sense => ToSense(entry.Guid,sense)).ToList() ?? [], }; } - private static Sense ToSense(Entities.Sense sense) + private static Sense ToSense(Guid entryId, Entities.Sense sense) { return new Sense { Id = sense.Guid, + EntryId = entryId, Gloss = ToMultiString(sense.Gloss), Definition = ToMultiString(sense.Definition), PartOfSpeech = sense.PartOfSpeech?.Value ?? string.Empty, SemanticDomains = (sense.SemanticDomain?.Values ?? []) .Select(sd => new SemanticDomain { Id = Guid.Empty, Code = sd, Name = new MultiString { { "en", sd } } }) .ToList(), - ExampleSentences = sense.Examples?.OfType().Select(ToExampleSentence).ToList() ?? [], + ExampleSentences = sense.Examples?.OfType().Select(example => ToExampleSentence(sense.Guid, example)).ToList() ?? [], }; } - private static ExampleSentence ToExampleSentence(Example example) + private static ExampleSentence ToExampleSentence(Guid senseId, Example example) { return new ExampleSentence { Id = example.Guid, + SenseId = senseId, Reference = (example.Reference?.TryGetValue("en", out var value) == true) ? value.Value : string.Empty, Sentence = ToMultiString(example.Sentence), Translation = ToMultiString(example.Translation) diff --git a/backend/harmony b/backend/harmony index 0063f050e..4200fa131 160000 --- a/backend/harmony +++ b/backend/harmony @@ -1 +1 @@ -Subproject commit 0063f050e8ef3d79f64c81d89f36c8a5dceff489 +Subproject commit 4200fa131004ab509a16b6385d7b6cd0da459578