Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 31 additions & 89 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class CrdtMiniLcmApi(
CrdtMorphTypeApi morphTypeApi,
CrdtPartsOfSpeechApi partsOfSpeechApi,
CrdtComplexFormTypesApi complexFormTypesApi,
CrdtSenseApi senseApi,
CrdtPictureApi pictureApi,
CrdtMediaApi mediaApi,
CrdtCustomViewApi customViewApi,
Expand Down Expand Up @@ -113,6 +114,11 @@ public async Task DeletePartOfSpeech(Guid id)
{
await partsOfSpeechApi.DeletePartOfSpeech(id);
}

public async Task SetSensePartOfSpeech(Guid senseId, Guid? partOfSpeechId)
{
await partsOfSpeechApi.SetSensePartOfSpeech(senseId, partOfSpeechId);
}
#endregion

#region PublicationApi
Expand Down Expand Up @@ -202,6 +208,16 @@ public async Task BulkImportSemanticDomains(IAsyncEnumerable<SemanticDomain> sem
{
await semanticDomainsApi.BulkImportSemanticDomains(semanticDomains);
}

public async Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
{
await semanticDomainsApi.AddSemanticDomainToSense(senseId, semanticDomain);
}

public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId)
{
await semanticDomainsApi.RemoveSemanticDomainFromSense(senseId, semanticDomainId);
}
#endregion

#region ComplexFormTypeApi
Expand Down Expand Up @@ -460,7 +476,7 @@ ..await entry.Senses.ToAsyncEnumerable()
.SelectMany((s, i) =>
{
s.Order = i + 1;
return CreateSenseChanges(entry.Id, s, repo.SemanticDomains);
return CrdtSenseApi.CreateSenseChanges(entry.Id, s, repo.SemanticDomains);
})
.ToArrayAsync(),
..await ToPublications(entry.PublishIn).ToArrayAsync(),
Expand Down Expand Up @@ -583,133 +599,59 @@ public async Task DeleteEntry(Guid id)
await harmonyChangeWriter.AddChange(new DeleteChange<Entry>(id));
}

private async IAsyncEnumerable<IChange> CreateSenseChanges(Guid entryId,
Sense sense,
IQueryable<SemanticDomain> semanticDomains)
{
sense.SemanticDomains = await semanticDomains
.Where(sd => sense.SemanticDomains.Select(s => s.Id).Contains(sd.Id))
.ToListAsync();

yield return new CreateSenseChange(sense, entryId);
var exampleOrder = 1;
foreach (var exampleSentence in sense.ExampleSentences)
{
exampleSentence.Order = exampleOrder++;
yield return new CreateExampleSentenceChange(exampleSentence, sense.Id);
}
}

#region SenseApi
public async Task<Sense?> GetSense(Guid senseId)
{
await using var repo = await repoFactory.CreateRepoAsync();
return await repo.GetSense(senseId);
return await senseApi.GetSense(senseId);
}

public async Task<Sense?> GetSense(Guid entryId, Guid senseId)
{
await using var repo = await repoFactory.CreateRepoAsync();
var sense = await repo.GetSense(senseId);
if (sense is null) return null;
VerifySenseBelongsToEntry(entryId, sense);
return sense;
}

private static void VerifySenseBelongsToEntry(Guid entryId, Sense sense)
{
if (sense.EntryId != entryId) throw ParentMismatchException.ForType<Sense>(sense.Id, entryId, sense.EntryId);
return await senseApi.GetSense(entryId, senseId);
}

public async Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
await using var repo = await repoFactory.CreateRepoAsync();
sense.Order = await OrderPicker.PickOrder(repo.Senses.Where(s => s.EntryId == entryId), between);
await harmonyChangeWriter.AddChanges(await CreateSenseChanges(entryId, sense, repo.SemanticDomains).ToArrayAsync());
await senseApi.SubmitCreateSense(entryId, sense, between);
}

public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
if (sense.PartOfSpeechId.HasValue && await GetPartOfSpeech(sense.PartOfSpeechId.Value) is null)
throw new InvalidOperationException($"Part of speech must exist when creating a sense (could not find GUID {sense.PartOfSpeechId.Value})");

await SubmitCreateSense(entryId, sense, between);
return await GetSense(entryId, sense.Id) ?? throw NotFoundException.ForType<Sense>(sense.Id);
return await senseApi.CreateSense(entryId, sense, between);
}

public async Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sense> update)
{
await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(senseId));
await senseApi.SubmitUpdateSense(entryId, senseId, update);
}

public async Task<Sense> UpdateSense(Guid entryId,
Guid senseId,
UpdateObjectInput<Sense> update)
{
await SubmitUpdateSense(entryId, senseId, update);
return await GetSense(entryId, senseId) ?? throw NotFoundException.ForType<Sense>(senseId);
return await senseApi.UpdateSense(entryId, senseId, update);
}

public async Task<Sense> UpdateSense(Guid entryId, Sense before, Sense after, IMiniLcmApi? api = null)
{
await SenseSync.Sync(entryId, before, after, api ?? this,
SyncContext.For(before, after, deferDeletes: false));
return await GetSense(entryId, after.Id) ?? throw NotFoundException.ForType<Sense>(after.Id);
return await senseApi.UpdateSense(entryId, before, after, api ?? this);
}

public async Task MoveSense(Guid entryId, Guid senseId, BetweenPosition between, MoveKind kind = MoveKind.Reorder)
{
await using var repo = await repoFactory.CreateRepoAsync();
var sense = await repo.GetSense(senseId) ?? throw NotFoundException.ForType<Sense>(senseId);
if (kind == MoveKind.Reorder)
{
// SetOrder doesn't re-parent, so an order picked against another entry's senses would be silently wrong
VerifySenseBelongsToEntry(entryId, sense);
await harmonyChangeWriter.AddChange(new Changes.SetOrderChange<Sense>(senseId, await PickSenseOrder(repo, entryId, between)));
return;
}
if (!await repo.Entries.AnyAsyncEF(e => e.Id == entryId)) throw NotFoundException.ForType<Entry>(entryId);
await harmonyChangeWriter.AddChange(new MoveSenseToEntryChange(senseId, entryId, await PickSenseOrder(repo, entryId, between)));
await senseApi.MoveSense(entryId, senseId, between, kind);
}

public async Task SubmitMoveSense(Guid entryId, Guid senseId, BetweenPosition position, MoveKind kind = MoveKind.Reorder)
{
await using var repo = await repoFactory.CreateRepoAsync();
if (kind == MoveKind.Reorder)
{
// the sense is gone or was reparented on this side: the reorder is moot, skip it
var sense = await repo.GetSense(senseId);
if (sense is null || sense.EntryId != entryId) return;
await harmonyChangeWriter.AddChange(new Changes.SetOrderChange<Sense>(senseId, await PickSenseOrder(repo, entryId, position)));
return;
}
// no target check: a deleted target entry is fine, the move change then deletes the sense (delete wins)
await harmonyChangeWriter.AddChange(new MoveSenseToEntryChange(senseId, entryId, await PickSenseOrder(repo, entryId, position)));
}

private static async Task<double> PickSenseOrder(MiniLcmRepository repo, Guid entryId, BetweenPosition between)
{
return await OrderPicker.PickOrder(repo.Senses.Where(s => s.EntryId == entryId), between);
await senseApi.SubmitMoveSense(entryId, senseId, position, kind);
}

public async Task DeleteSense(Guid entryId, Guid senseId)
{
await harmonyChangeWriter.AddChange(new DeleteChange<Sense>(senseId));
}

public async Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
{
await harmonyChangeWriter.AddChange(new AddSemanticDomainChange(semanticDomain, senseId));
}

public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId)
{
await harmonyChangeWriter.AddChange(new RemoveSemanticDomainChange(semanticDomainId, senseId));
}

public async Task SetSensePartOfSpeech(Guid senseId, Guid? partOfSpeechId)
{
await harmonyChangeWriter.AddChange(new SetPartOfSpeechChange(senseId, partOfSpeechId));
await senseApi.DeleteSense(entryId, senseId);
}
#endregion

public async Task SubmitCreateExampleSentence(Guid entryId,
Guid senseId,
Expand Down Expand Up @@ -742,7 +684,7 @@ public async Task<ExampleSentence> CreateExampleSentence(Guid entryId,
var sense = await repo.GetSense(senseId);
if (sense is not null)
{
VerifySenseBelongsToEntry(entryId, sense);
CrdtSenseApi.VerifySenseBelongsToEntry(entryId, sense);
var owned = sense.ExampleSentences.FirstOrDefault(e => e.Id == id);
if (owned is not null) return owned;
}
Expand Down Expand Up @@ -791,7 +733,7 @@ public async Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleId
}
if (!await repo.ExampleSentences.AnyAsyncEF(e => e.Id == exampleId)) throw NotFoundException.ForType<ExampleSentence>(exampleId);
var targetSense = await repo.GetSense(senseId) ?? throw NotFoundException.ForType<Sense>(senseId);
VerifySenseBelongsToEntry(entryId, targetSense);
CrdtSenseApi.VerifySenseBelongsToEntry(entryId, targetSense);
await harmonyChangeWriter.AddChange(new MoveExampleSentenceToSenseChange(exampleId, senseId, await PickExampleOrder(repo, senseId, between)));
}

Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public static IServiceCollection AddLcmCrdtClientCore(this IServiceCollection se
services.AddScoped<CrdtMorphTypeApi>();
services.AddScoped<CrdtPartsOfSpeechApi>();
services.AddScoped<CrdtComplexFormTypesApi>();
services.AddScoped<CrdtSenseApi>();
services.AddScoped<CrdtPictureApi>();
services.AddScoped<CrdtMediaApi>();
services.AddScoped<CrdtCustomViewApi>();
Expand Down
5 changes: 5 additions & 0 deletions backend/FwLite/LcmCrdt/MiniLcmImp/CrdtPartsOfSpeechApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,9 @@ public async Task DeletePartOfSpeech(Guid id)
{
await harmonyChangeWriter.AddChange(new DeleteChange<PartOfSpeech>(id));
}

public async Task SetSensePartOfSpeech(Guid senseId, Guid? partOfSpeechId)
{
await harmonyChangeWriter.AddChange(new SetPartOfSpeechChange(senseId, partOfSpeechId));
}
}
10 changes: 10 additions & 0 deletions backend/FwLite/LcmCrdt/MiniLcmImp/CrdtSemanticDomainsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,14 @@ public async Task BulkImportSemanticDomains(IAsyncEnumerable<SemanticDomain> sem
{
await harmonyChangeWriter.AddChanges(await semanticDomains.Select(sd => new CreateSemanticDomainChange(sd)).ToArrayAsync());
}

public async Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
{
await harmonyChangeWriter.AddChange(new AddSemanticDomainChange(semanticDomain, senseId));
}

public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId)
{
await harmonyChangeWriter.AddChange(new RemoveSemanticDomainChange(semanticDomainId, senseId));
}
}
133 changes: 133 additions & 0 deletions backend/FwLite/LcmCrdt/MiniLcmImp/CrdtSenseApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using LcmCrdt.Changes;
using LcmCrdt.Data;
using LcmCrdt.Harmony;
using LcmCrdt.Objects;
using LinqToDB.Async;
using LinqToDB.EntityFrameworkCore;
using MiniLcm.Exceptions;
using MiniLcm.SyncHelpers;
using SIL.Harmony.Changes;

namespace LcmCrdt.MiniLcmImp;

public class CrdtSenseApi(MiniLcmRepositoryFactory repoFactory, HarmonyChangeWriter harmonyChangeWriter)
{
public async Task<Sense?> GetSense(Guid senseId)
{
await using var repo = await repoFactory.CreateRepoAsync();
return await repo.GetSense(senseId);
}

public async Task<Sense?> GetSense(Guid entryId, Guid senseId)
{
await using var repo = await repoFactory.CreateRepoAsync();
var sense = await repo.GetSense(senseId);
if (sense is null) return null;
VerifySenseBelongsToEntry(entryId, sense);
return sense;
}

internal static void VerifySenseBelongsToEntry(Guid entryId, Sense sense)
{
if (sense.EntryId != entryId) throw ParentMismatchException.ForType<Sense>(sense.Id, entryId, sense.EntryId);
}

public async Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
await using var repo = await repoFactory.CreateRepoAsync();
sense.Order = await OrderPicker.PickOrder(repo.Senses.Where(s => s.EntryId == entryId), between);
await harmonyChangeWriter.AddChanges(await CreateSenseChanges(entryId, sense, repo.SemanticDomains).ToArrayAsync());
}

public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
if (sense.PartOfSpeechId.HasValue && !await PartOfSpeechExists(sense.PartOfSpeechId.Value))
throw new InvalidOperationException($"Part of speech must exist when creating a sense (could not find GUID {sense.PartOfSpeechId.Value})");

await SubmitCreateSense(entryId, sense, between);
return await GetSense(entryId, sense.Id) ?? throw NotFoundException.ForType<Sense>(sense.Id);
}

private async Task<bool> PartOfSpeechExists(Guid partOfSpeechId)
{
await using var repo = await repoFactory.CreateRepoAsync();
return await repo.PartsOfSpeech.AnyAsync(pos => pos.Id == partOfSpeechId);
}

internal static async IAsyncEnumerable<IChange> CreateSenseChanges(Guid entryId,
Sense sense,
IQueryable<SemanticDomain> semanticDomains)
{
sense.SemanticDomains = await semanticDomains
.Where(sd => sense.SemanticDomains.Select(s => s.Id).Contains(sd.Id))
.ToListAsync();

yield return new CreateSenseChange(sense, entryId);
var exampleOrder = 1;
foreach (var exampleSentence in sense.ExampleSentences)
{
exampleSentence.Order = exampleOrder++;
yield return new CreateExampleSentenceChange(exampleSentence, sense.Id);
}
}

public async Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sense> update)
{
await harmonyChangeWriter.AddChanges(update.Patch.ToChanges(senseId));
}

public async Task<Sense> UpdateSense(Guid entryId,
Guid senseId,
UpdateObjectInput<Sense> update)
{
await SubmitUpdateSense(entryId, senseId, update);
return await GetSense(entryId, senseId) ?? throw NotFoundException.ForType<Sense>(senseId);
}

public async Task<Sense> UpdateSense(Guid entryId, Sense before, Sense after, IMiniLcmApi api)
{
await SenseSync.Sync(entryId, before, after, api,
SyncContext.For(before, after, deferDeletes: false));
return await GetSense(entryId, after.Id) ?? throw NotFoundException.ForType<Sense>(after.Id);
}

public async Task MoveSense(Guid entryId, Guid senseId, BetweenPosition between, MoveKind kind = MoveKind.Reorder)
{
await using var repo = await repoFactory.CreateRepoAsync();
var sense = await repo.GetSense(senseId) ?? throw NotFoundException.ForType<Sense>(senseId);
if (kind == MoveKind.Reorder)
{
// SetOrder doesn't re-parent, so an order picked against another entry's senses would be silently wrong
VerifySenseBelongsToEntry(entryId, sense);
await harmonyChangeWriter.AddChange(new Changes.SetOrderChange<Sense>(senseId, await PickSenseOrder(repo, entryId, between)));
return;
}
if (!await repo.Entries.AnyAsyncEF(e => e.Id == entryId)) throw NotFoundException.ForType<Entry>(entryId);
await harmonyChangeWriter.AddChange(new MoveSenseToEntryChange(senseId, entryId, await PickSenseOrder(repo, entryId, between)));
}

public async Task SubmitMoveSense(Guid entryId, Guid senseId, BetweenPosition position, MoveKind kind = MoveKind.Reorder)
{
await using var repo = await repoFactory.CreateRepoAsync();
if (kind == MoveKind.Reorder)
{
// the sense is gone or was reparented on this side: the reorder is moot, skip it
var sense = await repo.GetSense(senseId);
if (sense is null || sense.EntryId != entryId) return;
await harmonyChangeWriter.AddChange(new Changes.SetOrderChange<Sense>(senseId, await PickSenseOrder(repo, entryId, position)));
return;
}
// no target check: a deleted target entry is fine, the move change then deletes the sense (delete wins)
await harmonyChangeWriter.AddChange(new MoveSenseToEntryChange(senseId, entryId, await PickSenseOrder(repo, entryId, position)));
}

private static async Task<double> PickSenseOrder(MiniLcmRepository repo, Guid entryId, BetweenPosition between)
{
return await OrderPicker.PickOrder(repo.Senses.Where(s => s.EntryId == entryId), between);
}

public async Task DeleteSense(Guid entryId, Guid senseId)
{
await harmonyChangeWriter.AddChange(new DeleteChange<Sense>(senseId));
}
}
Loading