From effc25b7e34bdd4601a7e5e58e558969ec6734a7 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 4 Dec 2024 12:01:45 +0700 Subject: [PATCH] implement update and delete apis for ComplexFormTypes and sync them in CrdtFwdataProjectSyncService (#1295) * implement update and delete apis for ComplexFormTypes and sync them in CrdtFwdataProjectSyncService * also sync writing system changes * enable test which imports all data as a sync instead of the dedicated import code * test creating writing systems, make create WS throw if it already exists * fix issue with conflicts between the multiple sync tests running at once. Also fixes issues where previous projects would not be cleaned up --- .github/workflows/fw-lite.yaml | 2 +- .../Api/FwDataMiniLcmApi.cs | 52 +++++++++++++++++-- .../UpdateProxy/UpdateComplexFormTypeProxy.cs | 16 ++++-- .../Fixtures/Sena3SyncFixture.cs | 2 +- .../Fixtures/SyncFixture.cs | 12 ++--- .../FwLiteProjectSync.Tests/Sena3SyncTests.cs | 36 +++++++++---- .../FwLiteProjectSync.Tests/SyncTests.cs | 52 ++++++++++++------- .../CrdtFwdataProjectSyncService.cs | 32 +++++++++--- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 24 +++++++++ .../FwLiteProjectSync/MiniLcmExtensions.cs | 12 +++++ .../FwLite/FwLiteProjectSync/MiniLcmImport.cs | 31 ++++++----- ...pshotTests.VerifyChangeModels.verified.txt | 4 ++ ...elSnapshotTests.VerifyDbModel.verified.txt | 5 +- .../LcmCrdt.Tests/EntityCopyMethodTests.cs | 4 +- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 37 +++++++++++-- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 3 ++ .../FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 24 +-------- .../ComplexFormComponentTestsBase.cs | 19 +++++++ .../FwLite/MiniLcm.Tests/SortingTestsBase.cs | 27 ---------- .../MiniLcm.Tests/UpdateEntryTestsBase.cs | 22 -------- .../MiniLcm.Tests/WritingSystemTestsBase.cs | 49 ++++++++++++++++- .../Exceptions/DuplicateObjectException.cs | 12 +++++ backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 18 +++++++ backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 3 ++ .../FwLite/MiniLcm/Models/ComplexFormType.cs | 4 +- .../SyncHelpers/ComplexFormTypeSync.cs | 47 +++++++++++++++++ .../MiniLcm/SyncHelpers/WritingSystemSync.cs | 7 +++ 27 files changed, 405 insertions(+), 151 deletions(-) create mode 100644 backend/FwLite/FwLiteProjectSync/MiniLcmExtensions.cs create mode 100644 backend/FwLite/MiniLcm/Exceptions/DuplicateObjectException.cs create mode 100644 backend/FwLite/MiniLcm/SyncHelpers/ComplexFormTypeSync.cs diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index f36d825e6..d85d974a5 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -23,7 +23,7 @@ on: jobs: build-and-test: name: Build FW Lite and run tests - timeout-minutes: 20 + timeout-minutes: 30 runs-on: windows-latest outputs: version: ${{ steps.setVersion.outputs.VERSION }} diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 2fc63ed95..a60f206e2 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -163,6 +163,11 @@ internal void CompleteExemplars(WritingSystems writingSystems) public Task CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem) { + var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems; + if (exitingWs.Any(ws => ws.Id == writingSystem.WsId)) + { + throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists"); + } CoreWritingSystemDefinition? ws = null; UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Writing System", "Remove writing system", @@ -377,6 +382,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 +413,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 @@ -591,9 +638,8 @@ public IAsyncEnumerable GetEntries( string? text = e.CitationForm.get_String(sortWs).Text; text ??= e.LexemeFormOA.Form.get_String(sortWs).Text; return text?.Trim(LcmHelpers.WhitespaceChars); - }) - .Skip(options.Offset) - .Take(options.Count); + }); + entries = options.ApplyPaging(entries); return entries.ToAsyncEnumerable().Select(FromLexEntry); } 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/Fixtures/Sena3SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs index cc3551181..7435b5963 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs @@ -28,7 +28,7 @@ public async Task InitializeAsync() var crdtProjectsFolder = rootServiceProvider.GetRequiredService>().Value.ProjectPath; if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); - rootServiceProvider.Dispose(); + await rootServiceProvider.DisposeAsync(); Directory.CreateDirectory(crdtProjectsFolder); await DownloadSena3(); diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index 149adf76a..7a1279ba1 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -2,14 +2,10 @@ using FwDataMiniLcmBridge; using FwDataMiniLcmBridge.Api; using FwDataMiniLcmBridge.LcmUtils; -using FwDataMiniLcmBridge.Tests.Fixtures; using LcmCrdt; using LexCore.Utils; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MiniLcm; namespace FwLiteProjectSync.Tests.Fixtures; @@ -23,19 +19,19 @@ public class SyncFixture : IAsyncLifetime private readonly string _projectName; private readonly IDisposable _cleanup; - public static SyncFixture Create([CallerMemberName] string projectName = "") => new(projectName); + public static SyncFixture Create([CallerMemberName] string projectName = "", [CallerMemberName] string projectFolder = "") => new(projectName, projectFolder); - private SyncFixture(string projectName) + private SyncFixture(string projectName, string projectFolder) { _projectName = projectName; var crdtServices = new ServiceCollection() - .AddSyncServices(_projectName); + .AddSyncServices(projectFolder); var rootServiceProvider = crdtServices.BuildServiceProvider(); _cleanup = Defer.Action(() => rootServiceProvider.Dispose()); _services = rootServiceProvider.CreateAsyncScope(); } - public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N")) + public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N"), "FwLiteSyncFixture") { } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs index 77add42f5..54e991041 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs @@ -18,6 +18,7 @@ public class Sena3SyncTests : IClassFixture, IAsyncLifetime private CrdtMiniLcmApi _crdtApi = null!; private FwDataMiniLcmApi _fwDataApi = null!; private IDisposable? _cleanup; + private MiniLcmImport _miniLcmImport = null!; public Sena3SyncTests(Sena3Fixture fixture) @@ -29,6 +30,7 @@ public async Task InitializeAsync() { (_crdtApi, _fwDataApi, var services, _cleanup) = await _fixture.SetupProjects(); _syncService = services.GetRequiredService(); + _miniLcmImport = services.GetRequiredService(); _fwDataApi.EntryCount.Should().BeGreaterThan(100, "project should be loaded and have entries"); } @@ -56,9 +58,11 @@ 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() + private async Task BypassImport(bool wsImported = false) { - await _syncService.SaveProjectSnapshot(_fwDataApi.Project, new ([], [], [])); + var snapshot = CrdtFwdataProjectSyncService.ProjectSnapshot.Empty; + if (wsImported) snapshot = snapshot with { WritingSystems = await _fwDataApi.GetWritingSystems() }; + await _syncService.SaveProjectSnapshot(_fwDataApi.Project, snapshot); } //this lets us query entries when there is no writing system @@ -98,28 +102,37 @@ public async Task DryRunSync_MakesNoChanges() _crdtApi.GetEntries().ToBlockingEnumerable().Should().BeEmpty(); } - [Fact(Skip = "this test is waiting for syncing ComplexFormTypes and WritingSystems")] - public async Task DryRunSync_MakesTheSameChangesAsImport() + [Fact] + public async Task DryRunSync_MakesTheSameChangesAsSync() { - await BypassImport(); + //syncing requires querying entries, which fails if there are no writing systems, so we import those first + await _miniLcmImport.ImportWritingSystems(_crdtApi, _fwDataApi); + await BypassImport(true); + var dryRunSyncResult = await _syncService.SyncDryRun(_crdtApi, _fwDataApi); var syncResult = await _syncService.Sync(_crdtApi, _fwDataApi); - dryRunSyncResult.Should().BeEquivalentTo(syncResult); + dryRunSyncResult.CrdtChanges.Should().Be(syncResult.CrdtChanges); + //can't test fwdata changes as they will not work correctly since the sync code expects Crdts to contain data from FWData + //this throws off the algorithm and it will try to delete everything in fwdata since there's no data in the crdt since it was a dry run } [Fact] public async Task FirstSena3SyncJustDoesAnSync() { + _fwDataApi.EntryCount.Should().BeGreaterThan(1000, + "projects with less than 1000 entries don't trip over the default query limit"); + var results = await _syncService.Sync(_crdtApi, _fwDataApi); results.FwdataChanges.Should().Be(0); results.CrdtChanges.Should().BeGreaterThanOrEqualTo(_fwDataApi.EntryCount); - var crdtEntries = await _crdtApi.GetEntries().ToDictionaryAsync(e => e.Id); - var fwdataEntries = await _fwDataApi.GetEntries().ToDictionaryAsync(e => e.Id); + var crdtEntries = await _crdtApi.GetAllEntries().ToDictionaryAsync(e => e.Id); + var fwdataEntries = await _fwDataApi.GetAllEntries().ToDictionaryAsync(e => e.Id); + fwdataEntries.Count.Should().Be(_fwDataApi.EntryCount); ShouldAllBeEquivalentTo(crdtEntries, fwdataEntries); } - [Fact(Skip = "this test is waiting for syncing ComplexFormTypes and WritingSystems")] + [Fact] public async Task SyncWithoutImport_CrdtShouldMatchFwdata() { await BypassImport(); @@ -128,8 +141,9 @@ public async Task SyncWithoutImport_CrdtShouldMatchFwdata() results.FwdataChanges.Should().Be(0); results.CrdtChanges.Should().BeGreaterThan(_fwDataApi.EntryCount); - var crdtEntries = await _crdtApi.GetEntries().ToDictionaryAsync(e => e.Id); - var fwdataEntries = await _fwDataApi.GetEntries().ToDictionaryAsync(e => e.Id); + var crdtEntries = await _crdtApi.GetAllEntries().ToDictionaryAsync(e => e.Id); + var fwdataEntries = await _fwDataApi.GetAllEntries().ToDictionaryAsync(e => e.Id); + fwdataEntries.Count.Should().Be(_fwDataApi.EntryCount); ShouldAllBeEquivalentTo(crdtEntries, fwdataEntries); } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 36b4084f2..49a9fd860 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -57,16 +57,14 @@ await _fixture.FwDataApi.CreateEntry(new Entry() public async Task DisposeAsync() { - await foreach (var entry in _fixture.FwDataApi.GetEntries()) + await foreach (var entry in _fixture.FwDataApi.GetAllEntries()) { await _fixture.FwDataApi.DeleteEntry(entry.Id); } - foreach (var entry in await _fixture.CrdtApi.GetEntries().ToArrayAsync()) + foreach (var entry in await _fixture.CrdtApi.GetAllEntries().ToArrayAsync()) { await _fixture.CrdtApi.DeleteEntry(entry.Id); } - - _fixture.DeleteSyncSnapshot(); } public SyncTests(SyncFixture fixture) @@ -82,8 +80,8 @@ public async Task FirstSyncJustDoesAnImport() var fwdataApi = _fixture.FwDataApi; await _syncService.Sync(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); @@ -144,8 +142,8 @@ await crdtApi.CreateEntry(new Entry() }); await _syncService.Sync(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); @@ -180,8 +178,8 @@ await crdtApi.CreateEntry(new Entry() }); await _syncService.SyncDryRun(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Select(e => e.Id).Should().NotContain(fwDataEntryId); fwdataEntries.Select(e => e.Id).Should().NotContain(crdtEntryId); } @@ -222,8 +220,8 @@ public async Task CreatingAComplexEntryInFwDataSyncsWithoutIssue() hatstand.Components = [component1, component2]; await _syncService.Sync(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); @@ -304,8 +302,8 @@ await crdtApi.CreateEntry(new Entry() }); await _syncService.Sync(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); @@ -383,8 +381,8 @@ await crdtApi.CreateEntry(new Entry() }); await _syncService.Sync(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); @@ -406,8 +404,8 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() results.CrdtChanges.Should().Be(1); results.FwdataChanges.Should().Be(1); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options .For(e => e.Components).Exclude(c => c.Id) @@ -474,8 +472,8 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth() await _syncService.Sync(crdtApi, fwdataApi); - var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); - var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync(); crdtEntries.Should().BeEquivalentTo(fwdataEntries, options => options.For(e => e.Components).Exclude(c => c.Id) .For(e => e.ComplexForms).Exclude(c => c.Id)); @@ -500,4 +498,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..f167e2444 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -39,9 +39,11 @@ public async Task Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataA { await SaveProjectSnapshot(fwdataApi.Project, new ProjectSnapshot( - await fwdataApi.GetEntries().ToArrayAsync(), + await fwdataApi.GetAllEntries().ToArrayAsync(), await fwdataApi.GetPartsOfSpeech().ToArrayAsync(), - await fwdataApi.GetSemanticDomains().ToArrayAsync())); + await fwdataApi.GetSemanticDomains().ToArrayAsync(), + await fwdataApi.GetComplexFormTypes().ToArrayAsync(), + await fwdataApi.GetWritingSystems())); } return result; } @@ -62,21 +64,27 @@ private async Task Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, return new SyncResult(entryCount, 0); } - //todo sync complex form types, writing systems + var currentFwDataWritingSystems = await fwdataApi.GetWritingSystems(); + var crdtChanges = await WritingSystemSync.Sync(currentFwDataWritingSystems, projectSnapshot.WritingSystems, crdtApi); + var fwdataChanges = await WritingSystemSync.Sync(await crdtApi.GetWritingSystems(), currentFwDataWritingSystems, fwdataApi); var currentFwDataPartsOfSpeech = await fwdataApi.GetPartsOfSpeech().ToArrayAsync(); - var crdtChanges = await PartOfSpeechSync.Sync(currentFwDataPartsOfSpeech, projectSnapshot.PartsOfSpeech, crdtApi); - var fwdataChanges = await PartOfSpeechSync.Sync(await crdtApi.GetPartsOfSpeech().ToArrayAsync(), currentFwDataPartsOfSpeech, fwdataApi); + crdtChanges += await PartOfSpeechSync.Sync(currentFwDataPartsOfSpeech, projectSnapshot.PartsOfSpeech, crdtApi); + fwdataChanges += await PartOfSpeechSync.Sync(await crdtApi.GetPartsOfSpeech().ToArrayAsync(), currentFwDataPartsOfSpeech, fwdataApi); var currentFwDataSemanticDomains = await fwdataApi.GetSemanticDomains().ToArrayAsync(); crdtChanges += await SemanticDomainSync.Sync(currentFwDataSemanticDomains, projectSnapshot.SemanticDomains, crdtApi); fwdataChanges += await SemanticDomainSync.Sync(await crdtApi.GetSemanticDomains().ToArrayAsync(), currentFwDataSemanticDomains, fwdataApi); - var currentFwDataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + 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.GetAllEntries().ToArrayAsync(); crdtChanges += await EntrySync.Sync(currentFwDataEntries, projectSnapshot.Entries, crdtApi); LogDryRun(crdtApi, "crdt"); - fwdataChanges += await EntrySync.Sync(await crdtApi.GetEntries().ToArrayAsync(), currentFwDataEntries, fwdataApi); + fwdataChanges += await EntrySync.Sync(await crdtApi.GetAllEntries().ToArrayAsync(), currentFwDataEntries, fwdataApi); LogDryRun(fwdataApi, "fwdata"); //todo push crdt changes to lexbox @@ -100,7 +108,15 @@ 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, + WritingSystems WritingSystems) + { + internal static ProjectSnapshot Empty { get; } = new([], [], [], [], new WritingSystems()); + } 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/FwLiteProjectSync/MiniLcmExtensions.cs b/backend/FwLite/FwLiteProjectSync/MiniLcmExtensions.cs new file mode 100644 index 000000000..2518953e5 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync/MiniLcmExtensions.cs @@ -0,0 +1,12 @@ +using MiniLcm; +using MiniLcm.Models; + +namespace FwLiteProjectSync; + +public static class MiniLcmExtensions +{ + public static IAsyncEnumerable GetAllEntries(this IMiniLcmApi api) + { + return api.GetEntries(new QueryOptions(Count: QueryOptions.QueryAll)); + } +} diff --git a/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs b/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs index 40572b47d..45bb17968 100644 --- a/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs +++ b/backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs @@ -9,18 +9,7 @@ public class MiniLcmImport(ILogger logger) { public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, int entryCount) { - var writingSystems = await importFrom.GetWritingSystems(); - foreach (var ws in writingSystems.Analysis) - { - await importTo.CreateWritingSystem(WritingSystemType.Analysis, ws); - 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.WsId); - } + await ImportWritingSystems(importTo, importFrom); await foreach (var partOfSpeech in importFrom.GetPartsOfSpeech()) { @@ -36,7 +25,7 @@ public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, in var semanticDomains = importFrom.GetSemanticDomains(); - var entries = importFrom.GetEntries(new QueryOptions(Count: 100_000, Offset: 0)); + var entries = importFrom.GetAllEntries(); if (importTo is CrdtMiniLcmApi crdtLexboxApi) { logger.LogInformation("Importing semantic domains"); @@ -62,4 +51,20 @@ public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, in logger.LogInformation("Imported {Count} entries", entryCount); } + + internal async Task ImportWritingSystems(IMiniLcmApi importTo, IMiniLcmApi importFrom) + { + var writingSystems = await importFrom.GetWritingSystems(); + foreach (var ws in writingSystems.Analysis) + { + await importTo.CreateWritingSystem(WritingSystemType.Analysis, ws); + 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.WsId); + } + } } 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/DataModelSnapshotTests.VerifyDbModel.verified.txt b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt index 5e1e13fe7..b1ae8d40a 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt @@ -236,14 +236,15 @@ Name (string) Required Order (double) Required SnapshotId (no field, Guid?) Shadow FK Index - Type (WritingSystemType) Required - WsId (WritingSystemId) Required + Type (WritingSystemType) Required Index + WsId (WritingSystemId) Required Index Keys: Id PK Foreign keys: WritingSystem {'SnapshotId'} -> ObjectSnapshot {'Id'} Unique SetNull Indexes: SnapshotId Unique + WsId, Type Unique Annotations: DiscriminatorProperty: Relational:FunctionName: 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 a2c93c3da..387dd8a0e 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -8,6 +8,7 @@ using LcmCrdt.Objects; using LinqToDB; using LinqToDB.EntityFrameworkCore; +using Microsoft.Data.Sqlite; using MiniLcm.Exceptions; using MiniLcm.SyncHelpers; using MiniLcm.Validators; @@ -46,7 +47,14 @@ public async Task CreateWritingSystem(WritingSystemType type, Wri { var entityId = Guid.NewGuid(); var wsCount = await WritingSystems.CountAsync(ws => ws.Type == type); - await dataModel.AddChange(ClientId, new CreateWritingSystemChange(writingSystem, type, entityId, wsCount)); + try + { + await dataModel.AddChange(ClientId, new CreateWritingSystemChange(writingSystem, type, entityId, wsCount)); + } + catch (Microsoft.EntityFrameworkCore.DbUpdateException e) when (e.InnerException is SqliteException { SqliteErrorCode: 19 }) //19 is a unique constraint violation + { + throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists", e); + } return await dataModel.GetLatest(entityId) ?? throw new NullReferenceException(); } @@ -163,6 +171,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 +184,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 => @@ -242,9 +272,8 @@ private async IAsyncEnumerable GetEntries( .LoadWith(e => e.Components) .AsQueryable() .OrderBy(e => e.Headword(sortWs.WsId).CollateUnicode(sortWs)) - .ThenBy(e => e.Id) - .Skip(options.Offset) - .Take(options.Count); + .ThenBy(e => e.Id); + queryable = options.ApplyPaging(queryable); var entries = queryable.AsAsyncEnumerable(); await foreach (var entry in entries) { diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 151e08652..08557d6bc 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -63,6 +63,7 @@ private static void ConfigureDbOptions(IServiceProvider provider, DbContextOptio #if DEBUG builder.EnableSensitiveDataLogging(); #endif + builder.EnableDetailedErrors(); builder.UseSqlite($"Data Source={projectContext.Project.DbPath}") .UseLinqToDB(optionsBuilder => { @@ -128,6 +129,7 @@ public static void ConfigureCrdt(CrdtConfig config) }) .Add(builder => { + builder.HasIndex(ws => new { ws.WsId, ws.Type }).IsUnique(); builder.Property(w => w.Exemplars) .HasColumnType("jsonb") .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null), @@ -163,6 +165,7 @@ public static void ConfigureCrdt(CrdtConfig config) .Add>() .Add>() .Add>() + .Add>() .Add>() .Add>() .Add>() diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index 9b1f05144..ac0c6074b 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -8,28 +8,6 @@ public abstract class BasicApiTestsBase : MiniLcmTestBase public override async Task InitializeAsync() { await base.InitializeAsync(); - await Api.CreateWritingSystem(WritingSystemType.Analysis, - new WritingSystem() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Analysis, - WsId = "en", - Name = "English", - Abbreviation = "En", - Font = "Arial", - Exemplars = [] - }); - await Api.CreateWritingSystem(WritingSystemType.Vernacular, - new WritingSystem() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Vernacular, - WsId = "en", - Name = "English", - Abbreviation = "En", - Font = "Arial", - Exemplars = [] - }); await Api.CreateEntry(new Entry { Id = Entry1Id, @@ -141,7 +119,7 @@ await Api.CreateWritingSystem(WritingSystemType.Vernacular, { Id = Guid.NewGuid(), Type = WritingSystemType.Vernacular, - WsId = "en", + WsId = "es", Name = "test", Abbreviation = "test", Font = "Arial", 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.Tests/SortingTestsBase.cs b/backend/FwLite/MiniLcm.Tests/SortingTestsBase.cs index be58f4bee..0304daa92 100644 --- a/backend/FwLite/MiniLcm.Tests/SortingTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/SortingTestsBase.cs @@ -2,33 +2,6 @@ public abstract class SortingTestsBase : MiniLcmTestBase { - public override async Task InitializeAsync() - { - await base.InitializeAsync(); - await Api.CreateWritingSystem(WritingSystemType.Analysis, - new WritingSystem() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Analysis, - WsId = "en", - Name = "English", - Abbreviation = "En", - Font = "Arial", - Exemplars = [] - }); - await Api.CreateWritingSystem(WritingSystemType.Vernacular, - new WritingSystem() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Vernacular, - WsId = "en-US", - Name = "English", - Abbreviation = "En", - Font = "Arial", - Exemplars = [] - }); - } - private Task CreateEntry(string headword) { return Api.CreateEntry(new() { LexemeForm = { { "en", headword } }, }); diff --git a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs index 65913827b..73e978afd 100644 --- a/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/UpdateEntryTestsBase.cs @@ -8,28 +8,6 @@ public abstract class UpdateEntryTestsBase : MiniLcmTestBase public override async Task InitializeAsync() { await base.InitializeAsync(); - await Api.CreateWritingSystem(WritingSystemType.Analysis, - new WritingSystem() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Analysis, - WsId = "en", - Name = "English", - Abbreviation = "En", - Font = "Arial", - Exemplars = [] - }); - await Api.CreateWritingSystem(WritingSystemType.Vernacular, - new WritingSystem() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Vernacular, - WsId = "en", - Name = "English", - Abbreviation = "En", - Font = "Arial", - Exemplars = [] - }); await Api.CreateEntry(new Entry { Id = Entry1Id, diff --git a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs index d05e77fa5..3d740d65a 100644 --- a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs @@ -1,10 +1,10 @@ -using MiniLcm.Models; +using MiniLcm.Exceptions; +using MiniLcm.Models; namespace MiniLcm.Tests; public abstract class WritingSystemTestsBase : MiniLcmTestBase { - [Fact] public async Task GetWritingSystems_DoesNotReturnNullOrEmpty() { @@ -19,4 +19,49 @@ public async Task GetWritingSystems_ReturnsExemplars() var writingSystems = await Api.GetWritingSystems(); writingSystems.Vernacular.Should().Contain(ws => ws.Exemplars.Any()); } + + [Fact] + public async Task CreateWritingSystem_Works() + { + var ws = await Api.CreateWritingSystem(WritingSystemType.Vernacular, + new() + { + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = "es", + Name = "Spanish", + Abbreviation = "Es", + Font = "Arial" + }); + ws.Should().NotBeNull(); + var writingSystems = await Api.GetWritingSystems(); + writingSystems.Vernacular.Should().ContainEquivalentOf(ws); + } + + [Fact] + public async Task CreateWritingSystem_DoesNothingIfAlreadyExists() + { + WritingSystemId wsId = "es"; + await Api.CreateWritingSystem(WritingSystemType.Vernacular, + new() + { + Id = Guid.NewGuid(), + WsId = wsId, + Type = WritingSystemType.Vernacular, + Name = "Spanish", + Abbreviation = "Es", + Font = "Arial" + }); + var action = async () => await Api.CreateWritingSystem(WritingSystemType.Vernacular, + new() + { + Id = Guid.NewGuid(), + WsId = wsId, + Type = WritingSystemType.Vernacular, + Name = "Spanish", + Abbreviation = "Es", + Font = "Arial" + }); + await action.Should().ThrowAsync(); + } } diff --git a/backend/FwLite/MiniLcm/Exceptions/DuplicateObjectException.cs b/backend/FwLite/MiniLcm/Exceptions/DuplicateObjectException.cs new file mode 100644 index 000000000..3ccb810d5 --- /dev/null +++ b/backend/FwLite/MiniLcm/Exceptions/DuplicateObjectException.cs @@ -0,0 +1,12 @@ +namespace MiniLcm.Exceptions; + +public class DuplicateObjectException : Exception +{ + public DuplicateObjectException(string? message) : base(message) + { + } + + public DuplicateObjectException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index 242f45892..03d1aa440 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); @@ -24,7 +25,24 @@ public record QueryOptions( int Offset = 0) { public static QueryOptions Default { get; } = new(); + public const int QueryAll = -1; public SortOptions Order { get; init; } = Order ?? SortOptions.Default; + + public IEnumerable ApplyPaging(IEnumerable enumerable) + { + if (Offset > 0) + enumerable = enumerable.Skip(Offset); + if (Count == QueryAll) return enumerable; + return enumerable.Take(Count); + } + + public IQueryable ApplyPaging(IQueryable queryable) + { + if (Offset > 0) + queryable = queryable.Skip(Offset); + if (Count == QueryAll) return queryable; + return queryable.Take(Count); + } } public record SortOptions(SortField Field, WritingSystemId WritingSystem, bool Ascending = true) 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); + } +} diff --git a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs index 1930059ce..142ae97e7 100644 --- a/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs +++ b/backend/FwLite/MiniLcm/SyncHelpers/WritingSystemSync.cs @@ -5,6 +5,13 @@ namespace MiniLcm.SyncHelpers; public static class WritingSystemSync { + public static async Task Sync(WritingSystems currentWritingSystems, + WritingSystems previousWritingSystems, + IMiniLcmApi api) + { + return await Sync(currentWritingSystems.Vernacular, previousWritingSystems.Vernacular, api) + + await Sync(currentWritingSystems.Analysis, previousWritingSystems.Analysis, api); + } public static async Task Sync(WritingSystem[] currentWritingSystems, WritingSystem[] previousWritingSystems, IMiniLcmApi api)