From 367ab330eecb9e911a7c358b7aad6727ba4d25c6 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 29 Nov 2024 17:00:36 +0700 Subject: [PATCH] implement update and delete apis for ComplexFormTypes and sync them in CrdtFwdataProjectSyncService --- .../Api/FwDataMiniLcmApi.cs | 42 +++++++++++++++++ .../UpdateProxy/UpdateComplexFormTypeProxy.cs | 16 +++++-- .../FwLiteProjectSync.Tests/Sena3SyncTests.cs | 2 +- .../FwLiteProjectSync.Tests/SyncTests.cs | 14 ++++++ .../CrdtFwdataProjectSyncService.cs | 16 ++++++- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 24 ++++++++++ ...pshotTests.VerifyChangeModels.verified.txt | 4 ++ .../LcmCrdt.Tests/EntityCopyMethodTests.cs | 4 +- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 22 +++++++++ backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 1 + .../ComplexFormComponentTestsBase.cs | 19 ++++++++ backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 1 + backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 3 ++ .../FwLite/MiniLcm/Models/ComplexFormType.cs | 4 +- .../SyncHelpers/ComplexFormTypeSync.cs | 47 +++++++++++++++++++ 15 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 backend/FwLite/MiniLcm/SyncHelpers/ComplexFormTypeSync.cs diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 2fc63ed95..90cf38434 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -377,6 +377,14 @@ public IAsyncEnumerable GetComplexFormTypes() { return ComplexFormTypesFlattened.Select(ToComplexFormType).ToAsyncEnumerable(); } + + public Task GetComplexFormType(Guid id) + { + var lexEntryType = ComplexFormTypesFlattened.SingleOrDefault(c => c.Guid == id); + if (lexEntryType is null) return Task.FromResult(null); + return Task.FromResult(ToComplexFormType(lexEntryType)); + } + private ComplexFormType ToComplexFormType(ILexEntryType t) { return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) }; @@ -400,6 +408,40 @@ public async Task CreateComplexFormType(ComplexFormType complex return ToComplexFormType(ComplexFormTypesFlattened.Single(c => c.Guid == complexFormType.Id)); } + public Task UpdateComplexFormType(Guid id, UpdateObjectInput update) + { + var type = ComplexFormTypesFlattened.SingleOrDefault(c => c.Guid == id); + if (type is null) throw new NullReferenceException($"unable to find complex form type with id {id}"); + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Complex Form Type", + "Revert Complex Form Type", + Cache.ServiceLocator.ActionHandler, + () => + { + var updateProxy = new UpdateComplexFormTypeProxy(type, null, this); + update.Apply(updateProxy); + }); + return Task.FromResult(ToComplexFormType(type)); + } + + public async Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after) + { + await ComplexFormTypeSync.Sync(before, after, this); + return ToComplexFormType(ComplexFormTypesFlattened.Single(c => c.Guid == after.Id)); + } + + public async Task DeleteComplexFormType(Guid id) + { + var type = ComplexFormTypesFlattened.SingleOrDefault(c => c.Guid == id); + if (type is null) return; + await Cache.DoUsingNewOrCurrentUOW("Delete Complex Form Type", + "Revert delete", + () => + { + type.Delete(); + return ValueTask.CompletedTask; + }); + } + public IAsyncEnumerable GetVariantTypes() { return VariantTypes.PossibilitiesOS diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs index b3a6eeeb0..bbde58976 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateComplexFormTypeProxy.cs @@ -7,16 +7,16 @@ namespace FwDataMiniLcmBridge.Api.UpdateProxy; public record UpdateComplexFormTypeProxy : ComplexFormType { private readonly ILexEntryType _lexEntryType; - private readonly ILexEntry _lcmEntry; + private readonly ILexEntry? _lcmEntry; private readonly FwDataMiniLcmApi _lexboxLcmApi; [SetsRequiredMembers] - public UpdateComplexFormTypeProxy(ILexEntryType lexEntryType, ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi) + public UpdateComplexFormTypeProxy(ILexEntryType lexEntryType, ILexEntry? lcmEntry, FwDataMiniLcmApi lexboxLcmApi) { _lexEntryType = lexEntryType; _lcmEntry = lcmEntry; _lexboxLcmApi = lexboxLcmApi; - Name = new(); + Name = base.Name = new(); } public override Guid Id @@ -24,8 +24,18 @@ public override Guid Id get => _lexEntryType.Guid; set { + if (_lcmEntry is null) + throw new InvalidOperationException("Cannot update complex form type Id on a null entry"); _lexboxLcmApi.RemoveComplexFormType(_lcmEntry, _lexEntryType.Guid); _lexboxLcmApi.AddComplexFormType(_lcmEntry, value); } } + + public override required MultiString Name + { + get => new UpdateMultiStringProxy(_lexEntryType.Name, _lexboxLcmApi); + set + { + } + } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs index 77add42f5..b7381cae7 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs @@ -58,7 +58,7 @@ private void ShouldAllBeEquivalentTo(Dictionary crdtEntries, Dictio //by default the first sync is an import, this will skip that so that the sync will actually sync data private async Task BypassImport() { - await _syncService.SaveProjectSnapshot(_fwDataApi.Project, new ([], [], [])); + await _syncService.SaveProjectSnapshot(_fwDataApi.Project, CrdtFwdataProjectSyncService.ProjectSnapshot.Empty); } //this lets us query entries when there is no writing system diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 36b4084f2..8bd929f53 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -500,4 +500,18 @@ public async Task CanCreateAComplexFormAndItsComponentInOneSync() //one of the entries will be created first, it will try to create the reference to the other but it won't exist yet await _fixture.SyncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi); } + + [Fact] + public async Task CanCreateAComplexFormTypeAndSyncsIt() + { + //ensure they are synced so a real sync will happen when we want it to + await _fixture.SyncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi); + + var complexFormEntry = await _fixture.CrdtApi.CreateComplexFormType(new() { Name = new() { { "en", "complexFormType" } } }); + + //one of the entries will be created first, it will try to create the reference to the other but it won't exist yet + await _fixture.SyncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi); + + _fixture.FwDataApi.GetComplexFormTypes().ToBlockingEnumerable().Should().ContainEquivalentOf(complexFormEntry); + } } diff --git a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs index 58685043b..c71d2f902 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -41,7 +41,8 @@ await SaveProjectSnapshot(fwdataApi.Project, new ProjectSnapshot( await fwdataApi.GetEntries().ToArrayAsync(), await fwdataApi.GetPartsOfSpeech().ToArrayAsync(), - await fwdataApi.GetSemanticDomains().ToArrayAsync())); + await fwdataApi.GetSemanticDomains().ToArrayAsync(), + await fwdataApi.GetComplexFormTypes().ToArrayAsync())); } return result; } @@ -72,6 +73,10 @@ private async Task Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, crdtChanges += await SemanticDomainSync.Sync(currentFwDataSemanticDomains, projectSnapshot.SemanticDomains, crdtApi); fwdataChanges += await SemanticDomainSync.Sync(await crdtApi.GetSemanticDomains().ToArrayAsync(), currentFwDataSemanticDomains, fwdataApi); + var currentFwDataComplexFormTypes = await fwdataApi.GetComplexFormTypes().ToArrayAsync(); + crdtChanges += await ComplexFormTypeSync.Sync(currentFwDataComplexFormTypes, projectSnapshot.ComplexFormTypes, crdtApi); + fwdataChanges += await ComplexFormTypeSync.Sync(await crdtApi.GetComplexFormTypes().ToArrayAsync(), currentFwDataComplexFormTypes, fwdataApi); + var currentFwDataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtChanges += await EntrySync.Sync(currentFwDataEntries, projectSnapshot.Entries, crdtApi); LogDryRun(crdtApi, "crdt"); @@ -100,7 +105,14 @@ private void LogDryRun(IMiniLcmApi api, string type) return ((DryRunMiniLcmApi)api).DryRunRecords; } - public record ProjectSnapshot(Entry[] Entries, PartOfSpeech[] PartsOfSpeech, SemanticDomain[] SemanticDomains); + public record ProjectSnapshot( + Entry[] Entries, + PartOfSpeech[] PartsOfSpeech, + SemanticDomain[] SemanticDomains, + ComplexFormType[] ComplexFormTypes) + { + internal static ProjectSnapshot Empty { get; } = new([], [], [], []); + } private async Task GetProjectSnapshot(FwDataProject project) { diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index f23161649..ff925b4c5 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -114,6 +114,12 @@ public IAsyncEnumerable GetComplexFormTypes() return api.GetComplexFormTypes(); } + public Task GetComplexFormType(Guid id) + { + return api.GetComplexFormType(id); + } + + public Task CreateComplexFormType(ComplexFormType complexFormType) { DryRunRecords.Add(new DryRunRecord(nameof(CreateComplexFormType), @@ -121,6 +127,24 @@ public Task CreateComplexFormType(ComplexFormType complexFormTy return Task.FromResult(complexFormType); } + public async Task UpdateComplexFormType(Guid id, UpdateObjectInput update) + { + DryRunRecords.Add(new DryRunRecord(nameof(UpdateComplexFormType), $"Update complex form type {id}")); + return await GetComplexFormType(id) ?? throw new NullReferenceException($"unable to find complex form type with id {id}"); + } + + public Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after) + { + DryRunRecords.Add(new DryRunRecord(nameof(UpdateComplexFormType), $"Update complex form type {after.Id}")); + return Task.FromResult(after); + } + + public Task DeleteComplexFormType(Guid id) + { + DryRunRecords.Add(new DryRunRecord(nameof(DeleteComplexFormType), $"Delete complex form type {id}")); + return Task.CompletedTask; + } + public IAsyncEnumerable GetEntries(QueryOptions? options = null) { return api.GetEntries(options); diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt index 4622a2e58..685df4f41 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt @@ -24,6 +24,10 @@ DerivedType: JsonPatchChange, TypeDiscriminator: jsonPatch:SemanticDomain }, + { + DerivedType: JsonPatchChange, + TypeDiscriminator: jsonPatch:ComplexFormType + }, { DerivedType: DeleteChange, TypeDiscriminator: delete:Entry diff --git a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs index 3caf45d35..10387a6ef 100644 --- a/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs @@ -25,7 +25,8 @@ public static IEnumerable GetEntityTypes() typeof(ExampleSentence), typeof(WritingSystem), typeof(PartOfSpeech), - typeof(SemanticDomain) + typeof(SemanticDomain), + typeof(ComplexFormType) ]; return types.Select(t => new object[] { t }); } @@ -37,6 +38,7 @@ public void EntityCopyMethodShouldCopyAllFields(Type type) type.IsAssignableTo(typeof(IObjectWithId)).Should().BeTrue(); var entity = (IObjectWithId) AutoFaker.Generate(type); var copy = entity.Copy(); + //todo this does not detect a deep copy, but it should as that breaks stuff copy.Should().BeEquivalentTo(entity, options => options.IncludingAllRuntimeProperties()); } } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 04eb2d70d..ea03205c7 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -163,6 +163,11 @@ public IAsyncEnumerable GetComplexFormTypes() return ComplexFormTypes.AsAsyncEnumerable(); } + public async Task GetComplexFormType(Guid id) + { + return await ComplexFormTypes.SingleOrDefaultAsync(c => c.Id == id); + } + public async Task CreateComplexFormType(ComplexFormType complexFormType) { await validators.ValidateAndThrow(complexFormType); @@ -171,6 +176,23 @@ public async Task CreateComplexFormType(ComplexFormType complex return await ComplexFormTypes.SingleAsync(c => c.Id == complexFormType.Id); } + public async Task UpdateComplexFormType(Guid id, UpdateObjectInput update) + { + await dataModel.AddChange(ClientId, new JsonPatchChange(id, update.Patch)); + return await GetComplexFormType(id) ?? throw new NullReferenceException($"unable to find complex form type with id {id}"); + } + + public async Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after) + { + await ComplexFormTypeSync.Sync(before, after, this); + return await GetComplexFormType(after.Id) ?? throw new NullReferenceException($"unable to find complex form type with id {after.Id}"); + } + + public async Task DeleteComplexFormType(Guid id) + { + await dataModel.AddChange(ClientId, new DeleteChange(id)); + } + public async Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent) { var existing = await ComplexFormComponents.SingleOrDefaultAsync(c => diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index df5c9e5d0..ef6ec704e 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -161,6 +161,7 @@ public static void ConfigureCrdt(CrdtConfig config) .Add>() .Add>() .Add>() + .Add>() .Add>() .Add>() .Add>() diff --git a/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs b/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs index d6f646435..2f5906f9e 100644 --- a/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/ComplexFormComponentTestsBase.cs @@ -100,6 +100,25 @@ public async Task CreateComplexFormType_Works() types.Should().ContainSingle(t => t.Id == complexFormType.Id); } + [Fact] + public async Task UpdateComplexFormType_Works() + { + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new() { { "en", "test" } } }; + await Api.CreateComplexFormType(complexFormType); + var updatedComplexFormType = await Api.UpdateComplexFormType(complexFormType.Id, new UpdateObjectInput().Set(c => c.Name["en"], "updated")); + updatedComplexFormType.Name["en"].Should().Be("updated"); + } + + [Fact] + public async Task UpdateComplexFormTypeSync_Works() + { + var complexFormType = new ComplexFormType() { Id = Guid.NewGuid(), Name = new() { { "en", "test" } } }; + await Api.CreateComplexFormType(complexFormType); + var afterFormType = complexFormType with { Name = new() { { "en", "updated" } } }; + var actualFormType = await Api.UpdateComplexFormType(complexFormType, afterFormType); + actualFormType.Should().BeEquivalentTo(afterFormType, options => options.Excluding(c => c.Id)); + } + [Fact] public async Task AddComplexFormType_Works() { diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index 242f45892..5b3c24f18 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs @@ -9,6 +9,7 @@ public interface IMiniLcmReadApi IAsyncEnumerable GetPartsOfSpeech(); IAsyncEnumerable GetSemanticDomains(); IAsyncEnumerable GetComplexFormTypes(); + Task GetComplexFormType(Guid id); IAsyncEnumerable GetEntries(QueryOptions? options = null); IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null); Task GetEntry(Guid id); diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index 930b2bb75..1c5301fb8 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -29,6 +29,9 @@ Task UpdateWritingSystem(WritingSystemId id, #endregion Task CreateComplexFormType(ComplexFormType complexFormType); + Task UpdateComplexFormType(Guid id, UpdateObjectInput update); + Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after); + Task DeleteComplexFormType(Guid id); #region Entry Task CreateEntry(Entry entry); diff --git a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs index 5ba397094..892e35daf 100644 --- a/backend/FwLite/MiniLcm/Models/ComplexFormType.cs +++ b/backend/FwLite/MiniLcm/Models/ComplexFormType.cs @@ -4,7 +4,7 @@ public record ComplexFormType : IObjectWithId { public virtual Guid Id { get; set; } - public required MultiString Name { get; set; } + public virtual required MultiString Name { get; set; } public DateTimeOffset? DeletedAt { get; set; } @@ -19,6 +19,6 @@ public void RemoveReference(Guid id, DateTimeOffset time) public IObjectWithId Copy() { - return new ComplexFormType { Id = Id, Name = Name, DeletedAt = DeletedAt }; + return new ComplexFormType { Id = Id, Name = Name.Copy(), DeletedAt = DeletedAt }; } } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/ComplexFormTypeSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/ComplexFormTypeSync.cs new file mode 100644 index 000000000..e7ae771cf --- /dev/null +++ b/backend/FwLite/MiniLcm/SyncHelpers/ComplexFormTypeSync.cs @@ -0,0 +1,47 @@ +using MiniLcm.Models; +using SystemTextJsonPatch; + +namespace MiniLcm.SyncHelpers; + +public static class ComplexFormTypeSync +{ + public static async Task Sync(ComplexFormType[] afterComplexFormTypes, + ComplexFormType[] beforeComplexFormTypes, + IMiniLcmApi api) + { + return await DiffCollection.Diff(api, + beforeComplexFormTypes, + afterComplexFormTypes, + complexFormType => complexFormType.Id, + static async (api, afterComplexFormType) => + { + await api.CreateComplexFormType(afterComplexFormType); + return 1; + }, + static async (api, beforeComplexFormType) => + { + await api.DeleteComplexFormType(beforeComplexFormType.Id); + return 1; + }, + static (api, beforeComplexFormType, afterComplexFormType) => Sync(beforeComplexFormType, afterComplexFormType, api)); + } + + public static async Task Sync(ComplexFormType before, + ComplexFormType after, + IMiniLcmApi api) + { + var updateObjectInput = ComplexFormTypeDiffToUpdate(before, after); + if (updateObjectInput is not null) await api.UpdateComplexFormType(after.Id, updateObjectInput); + return updateObjectInput is null ? 0 : 1; + } + + public static UpdateObjectInput? ComplexFormTypeDiffToUpdate(ComplexFormType before, ComplexFormType after) + { + JsonPatchDocument patchDocument = new(); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(ComplexFormType.Name), + before.Name, + after.Name)); + if (patchDocument.Operations.Count == 0) return null; + return new UpdateObjectInput(patchDocument); + } +}